0xAA55 发表于 2015-2-28 18:58:19

【实验】主线程退出了,子线程仍然能继续运行吗?

个人觉得,编程的时候,到底哪个线程是主线程,取决于你怎么使用哪个线程,而不是它是不是第一个被创建的线程。
当一个进程的所有线程都退出的时候,这个进程才会退出。
为了验证这个观点,我写了一个简单的汇编程序。为什么是汇编呢?因为如果是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:

元始天尊 发表于 2015-2-28 22:07:51

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的地址空间。)
    因此经常说的:主线程结束,其他子线程也会结束,这句话是有道理的。

0xAA55 发表于 2015-2-28 19:06:07

其实也可以用C写,main在return前ExitThread即可!

0xAA55 发表于 2015-3-1 12:50:12

原来如此!
不过我觉得这个也可以理解成,所有线程结束后,进程结束。因为效果就是这样的。

0x01810 发表于 2015-3-8 09:39:57

原来如此.. 学习了.. 话说为什么汇编代码API前缀都要加"__imp__" 跟编译器有关吗?

0xAA55 发表于 2015-3-8 14:57:59

0x01810 发表于 2015-3-8 09:39
原来如此.. 学习了.. 话说为什么汇编代码API前缀都要加"__imp__" 跟编译器有关吗? ...

因为那个符号是从kernel32.lib和user32.lib中导入的,lib导出的符号都是要加__imp_前缀的,然后是C语言的符号所以还有个_,因此就是__imp__了。

0x01810 发表于 2015-3-9 10:53:22

0xAA55 发表于 2015-3-8 14:57
因为那个符号是从kernel32.lib和user32.lib中导入的,lib导出的符号都是要加__imp_前缀的,然后是C语言的 ...

原来是个thunk怪不得要加前缀。 thx
页: [1]
查看完整版本: 【实验】主线程退出了,子线程仍然能继续运行吗?