【实验】主线程退出了,子线程仍然能继续运行吗?
个人觉得,编程的时候,到底哪个线程是主线程,取决于你怎么使用哪个线程,而不是它是不是第一个被创建的线程。当一个进程的所有线程都退出的时候,这个进程才会退出。
为了验证这个观点,我写了一个简单的汇编程序。为什么是汇编呢?因为如果是C语言的话,CRT会在main返回后调用ExitProcess,导致进程退出。因此我写汇编语言,让入口点函数不调用ExitProcess。
为了完成验证,我们这样写:
1、入口点调用CreateThread创建子线程。
2、入口点调用ExitThread退出。
3、子线程Sleep一秒
4、子线程弹出对话框表示自己在运行。
5、子线程退出。
为了保证可信度,我们把子线程的入口点写在整个程序的入口点的前面。
写好以后的源码是这样的:global _Entry
extern __imp__CreateThread@24
extern __imp__Sleep@4
extern __imp__MessageBoxA@16
extern __imp__ExitThread@4
extern __imp__ExitProcess@4
segment .text
SubThread:
push 1000
call dword
push 0
push g_szTitle
push g_szPrompt
push 0
call dword
ret
_Entry:
push g_ulThreadID
push 0
push 0
push SubThread
push 0
push 0
call dword
push 0
call dword
ret
segment .data
g_szTitle db "主线程已经退出",0
g_szPrompt db "子线程仍在继续",0
g_ulThreadID dd 0经过运行,我们看到了对话框:
也就是说,一个进程最终会在所有的线程结束后退出,而不是主线程结束后退出。主线程结束后退出的这个设定其实是CRT给的!CRT在main返回后执行了ExitProcess导致进程退出!
BIN:
SRC: 0xAA55 发表于 2015-2-28 19:06
其实也可以用C写,main在return前ExitThread即可!
我用vc6做了个实验,为了去掉CRT库,需要在工程设置里将入口函数重置,并使用Release编译,
#include <windows.h>
static bool mainexist=false;
DWORD WINAPI callback(LPVOID param)
{
while(mainexist)//保证主线程退出
{
Sleep(100);
}
MessageBox(NULL,(LPCTSTR)param,(LPCTSTR)param,MB_OK);
MessageBox(NULL,(LPCTSTR)param,(LPCTSTR)param,MB_OK);
return 0;
}
void start()
{
mainexist=true;
CreateThread(NULL,0,callback,"another thread",0,NULL);
mainexist=false;
}
运行后,可以看到弹出对话框,只要弹出对话框就说明确实是“主线程退出,而子线程运行”的情况
msdn上介绍的有关进程线程的基础知识:https://msdn.microsoft.com/en-us/library/windows/desktop/ms681917(v=vs.85).aspx
进程拥有:虚拟地址空间、执行代码、系统资源句柄、安全context、进程id、环境变量、优先级、最大最小工作集,且最少有一个在执行的线程
线程拥有:异常处理、调度优先级、tls、线程id、一些用于保存context的系统结构
我上面vc6的程序,用windbg载入并在入口下断点,停下后看调用栈,得到:
0018ff94 772cb5af image00400000+0x1040
0018ffdc 772cb57a ntdll!__RtlUserThreadStart+0x2f
0018ffec 00000000 ntdll!_RtlUserThreadStart+0x1b
我们在最后函数里下断点,重新来过,现在进去看看这2系统函数执行情况:
ntdll:772CB580 ntdll___RtlUserThreadStart proc near
ntdll:772CB580
ntdll:772CB580 ; FUNCTION CHUNK AT ntdll:7731F3C3 SIZE 00000011 BYTES
ntdll:772CB580
ntdll:772CB580 push 1Ch
ntdll:772CB582 push offset unk_772CB5C0
ntdll:772CB587 call near ptr ntdll__SEH_prolog4_GS
ntdll:772CB58C mov edi, ecx
ntdll:772CB58E and dword ptr , 0
ntdll:772CB592 mov esi, ntdll_Kernel32ThreadInitThunkFunction
ntdll:772CB598 push edx
ntdll:772CB599 test esi, esi
ntdll:772CB59B jz loc_7731F3C3
ntdll:772CB5A1 mov ecx, esi
ntdll:772CB5A3 call ntdll___guard_check_icall_fptr
ntdll:772CB5A9 mov edx, edi
ntdll:772CB5AB xor ecx, ecx
ntdll:772CB5AD call esi ; kernel32_BaseThreadInitThunk
ntdll:772CB5AF mov dword ptr , 0FFFFFFFEh
ntdll:772CB5B6 call near ptr ntdll__SEH_epilog4_GS
ntdll:772CB5BB retn
ntdll:772CB5BB ntdll___RtlUserThreadStart endp
ntdll:772CB55F ntdll__RtlUserThreadStart proc near
ntdll:772CB55F
ntdll:772CB55F var_8 = byte ptr -8
ntdll:772CB55F arg_0 = dword ptr8
ntdll:772CB55F arg_4 = dword ptr0Ch
ntdll:772CB55F
ntdll:772CB55F mov edi, edi
ntdll:772CB561 push ebp
ntdll:772CB562 mov ebp, esp
ntdll:772CB564 push ecx
ntdll:772CB565 push ecx
ntdll:772CB566 lea eax,
ntdll:772CB569 push eax
ntdll:772CB56A call near ptr ntdll_RtlInitializeExceptionChain
ntdll:772CB56F mov edx,
ntdll:772CB572 mov ecx,
ntdll:772CB575 call ntdll___RtlUserThreadStart
ntdll:772CB57A int 3 ; Trap to Debugger
ntdll:772CB57A ntdll__RtlUserThreadStart endp
分析出的代码大概如下:
struct _EXCEPTION_REGISTRATION
{
struc EXCEPTION_REGISTRATION *Prev; //前一个_EXCEPTION_REGISTRATION结构
DWORD Handler; //异常处理过程地址
};
void _stdcall _RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam)
{
ExceptionChain chain;
RtlInitializeExceptionChain(&chain);
RtlUserThreadStart(pfnStartAddr,pvParam);
}
void _fastcall RtlInitializeExceptionChain(ExceptionChain* chain)
{
if(RtlpProcessECVPolicy == 1)
return;
_TEB* teb=getTeb();
chain->Prev = -1;
chain->Handler = RtlpFinalExceptionHandler;
if(teb->NtTib->Self->ExceptionList != -1)
return;
teb->NtTib->ExceptionList=chain;
teb->NtTib->SameTebFlags |= 0x200;
}
void _fastcall RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam)
{
__try
{
if(!Kernel32ThreadInitThunkFunction)
{
pfnStartAddr(pvParam);//同步调用用户态入口函数,我们的程序分支走到这里
}
else
{
Kernel32ThreadInitThunkFunction(0,pfn);
}
}
__finally
{
RtlExitUserThread();//最终主线程运行到这里,用户入口的部分运行结束,而用户态代码仍处主线程中,在等待所有子线程结束后才执行完毕。
}
}
经过试验后发现,在RtlUserThreadStart执行完之前,会等待子线程运行完毕。
根据实验结果,可以总结出出如下结论:
1.进程启动时,系统会为之创建一个线程,该线程通常称主线程,所有其他线程都通过主线程创建
2.主线程结束前,如果用默认的crt库,会结束所有其他子线程
3.系统启动进程的方式是使用ntdll!RtlUserThreadStart调用程序入口,调用入口的线程是主线程
4.主线程结束后,系统调用者ntdll!RtlUserThreadStart最终会调用RtlExitUserThread等待所有线程结束,所有线程结束之后,进程便结束,该函数执行完毕。
在某种程度上说,ntdll!RtlUserThreadStart才是主线程,因为他调用exe入口是个同步过程,而不是异步过程,举例来说,如果exe用户总入口是start(),那么RtlUserThreadStart中的调用方式是call start,等到start执行完毕,他才能继续运行,而此时的确不是用户空间了,但是此时主线程却不应该认为“结束”,我所认为的主线程结束,应该是RtlUserThreadStart结束,而从上面系统汇编代码可以看出,该函数结束前必然会等待所有线程结束。(另外RtlUserThreadStart在ntdll里,其地址<0x80000000,应该也算“用户态”,ntdll.dll也是该exe的地址空间。)
因此经常说的:主线程结束,其他子线程也会结束,这句话是有道理的。 其实也可以用C写,main在return前ExitThread即可! 原来如此!
不过我觉得这个也可以理解成,所有线程结束后,进程结束。因为效果就是这样的。 原来如此.. 学习了.. 话说为什么汇编代码API前缀都要加"__imp__" 跟编译器有关吗? 0x01810 发表于 2015-3-8 09:39
原来如此.. 学习了.. 话说为什么汇编代码API前缀都要加"__imp__" 跟编译器有关吗? ...
因为那个符号是从kernel32.lib和user32.lib中导入的,lib导出的符号都是要加__imp_前缀的,然后是C语言的符号所以还有个_,因此就是__imp__了。 0xAA55 发表于 2015-3-8 14:57
因为那个符号是从kernel32.lib和user32.lib中导入的,lib导出的符号都是要加__imp_前缀的,然后是C语言的 ...
原来是个thunk怪不得要加前缀。 thx
页:
[1]