找回密码
 立即注册→加入我们

QQ登录

只需一步,快速开始

搜索
热搜: 下载 VB C 实现 编写
查看: 9965|回复: 6

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

[复制链接]

1112

主题

1653

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
245
威望
744 点
宅币
24257 个
贡献
46222 次
宅之契约
0 份
在线时间
2298 小时
注册时间
2014-1-26
发表于 2015-2-28 18:58:19 | 显示全部楼层 |阅读模式

欢迎访问技术宅的结界,请注册或者登录吧。

您需要 登录 才可以下载或查看,没有账号?立即注册→加入我们

×
个人觉得,编程的时候,到底哪个线程是主线程,取决于你怎么使用哪个线程,而不是它是不是第一个被创建的线程。
当一个进程的所有线程都退出的时候,这个进程才会退出。
为了验证这个观点,我写了一个简单的汇编程序。为什么是汇编呢?因为如果是C语言的话,CRT会在main返回后调用ExitProcess,导致进程退出。因此我写汇编语言,让入口点函数不调用ExitProcess。
为了完成验证,我们这样写:

1、入口点调用CreateThread创建子线程。
2、入口点调用ExitThread退出。
3、子线程Sleep一秒
4、子线程弹出对话框表示自己在运行。
5、子线程退出。

为了保证可信度,我们把子线程的入口点写在整个程序的入口点的前面。
写好以后的源码是这样的:
  1. global _Entry

  2. extern __imp__CreateThread@24
  3. extern __imp__Sleep@4

  4. extern __imp__MessageBoxA@16
  5. extern __imp__ExitThread@4
  6. extern __imp__ExitProcess@4

  7. segment .text
  8. SubThread:
  9. push 1000
  10. call dword[__imp__Sleep@4]

  11. push 0
  12. push g_szTitle
  13. push g_szPrompt
  14. push 0
  15. call dword[__imp__MessageBoxA@16]
  16. ret

  17. _Entry:
  18. push g_ulThreadID
  19. push 0
  20. push 0
  21. push SubThread
  22. push 0
  23. push 0
  24. call dword[__imp__CreateThread@24]

  25. push 0
  26. call dword[__imp__ExitThread@4]
  27. ret

  28. segment .data
  29. g_szTitle db "主线程已经退出",0
  30. g_szPrompt db "子线程仍在继续",0

  31. g_ulThreadID dd 0
复制代码
经过运行,我们看到了对话框:
20150228185530.png
也就是说,一个进程最终会在所有的线程结束后退出,而不是主线程结束后退出。主线程结束后退出的这个设定其实是CRT给的!CRT在main返回后执行了ExitProcess导致进程退出!
BIN: main.exe (2.5 KB, 下载次数: 5)
SRC: MT.7z (434.76 KB, 下载次数: 6)
回复

使用道具 举报

307

主题

228

回帖

7343

积分

用户组: 真·技术宅

UID
2
精华
76
威望
291 点
宅币
5593 个
贡献
253 次
宅之契约
0 份
在线时间
948 小时
注册时间
2014-1-25
发表于 2015-2-28 22:07:51 | 显示全部楼层
0xAA55 发表于 2015-2-28 19:06
其实也可以用C写,main在return前ExitThread即可!


我用vc6做了个实验,为了去掉CRT库,需要在工程设置里将入口函数重置,并使用Release编译,

  1. #include <windows.h>
  2. static bool mainexist=false;
  3. DWORD WINAPI callback(LPVOID param)
  4. {
  5.         while(mainexist)//保证主线程退出
  6.         {
  7.                 Sleep(100);
  8.         }
  9.         MessageBox(NULL,(LPCTSTR)param,(LPCTSTR)param,MB_OK);
  10.         MessageBox(NULL,(LPCTSTR)param,(LPCTSTR)param,MB_OK);
  11.         return 0;
  12. }

  13. void start()
  14. {
  15.         mainexist=true;
  16.         CreateThread(NULL,0,callback,"another thread",0,NULL);
  17.         mainexist=false;
  18. }
复制代码

运行后,可以看到弹出对话框,只要弹出对话框就说明确实是“主线程退出,而子线程运行”的情况
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系统函数执行情况:


  1. ntdll:772CB580 ntdll___RtlUserThreadStart proc near
  2. ntdll:772CB580
  3. ntdll:772CB580 ; FUNCTION CHUNK AT ntdll:7731F3C3 SIZE 00000011 BYTES
  4. ntdll:772CB580
  5. ntdll:772CB580                 push    1Ch
  6. ntdll:772CB582                 push    offset unk_772CB5C0
  7. ntdll:772CB587                 call    near ptr ntdll__SEH_prolog4_GS
  8. ntdll:772CB58C                 mov     edi, ecx
  9. ntdll:772CB58E                 and     dword ptr [ebp-4], 0
  10. ntdll:772CB592                 mov     esi, ntdll_Kernel32ThreadInitThunkFunction
  11. ntdll:772CB598                 push    edx
  12. ntdll:772CB599                 test    esi, esi
  13. ntdll:772CB59B                 jz      loc_7731F3C3
  14. ntdll:772CB5A1                 mov     ecx, esi
  15. ntdll:772CB5A3                 call    ntdll___guard_check_icall_fptr
  16. ntdll:772CB5A9                 mov     edx, edi
  17. ntdll:772CB5AB                 xor     ecx, ecx
  18. ntdll:772CB5AD                 call    esi ; kernel32_BaseThreadInitThunk
  19. ntdll:772CB5AF                 mov     dword ptr [ebp-4], 0FFFFFFFEh
  20. ntdll:772CB5B6                 call    near ptr ntdll__SEH_epilog4_GS
  21. ntdll:772CB5BB                 retn
  22. ntdll:772CB5BB ntdll___RtlUserThreadStart endp

  23. ntdll:772CB55F ntdll__RtlUserThreadStart proc near
  24. ntdll:772CB55F
  25. ntdll:772CB55F var_8           = byte ptr -8
  26. ntdll:772CB55F arg_0           = dword ptr  8
  27. ntdll:772CB55F arg_4           = dword ptr  0Ch
  28. ntdll:772CB55F
  29. ntdll:772CB55F                 mov     edi, edi
  30. ntdll:772CB561                 push    ebp
  31. ntdll:772CB562                 mov     ebp, esp
  32. ntdll:772CB564                 push    ecx
  33. ntdll:772CB565                 push    ecx
  34. ntdll:772CB566                 lea     eax, [ebp+var_8]
  35. ntdll:772CB569                 push    eax
  36. ntdll:772CB56A                 call    near ptr ntdll_RtlInitializeExceptionChain
  37. ntdll:772CB56F                 mov     edx, [ebp+arg_4]
  38. ntdll:772CB572                 mov     ecx, [ebp+arg_0]
  39. ntdll:772CB575                 call    ntdll___RtlUserThreadStart
  40. ntdll:772CB57A                 int     3               ; Trap to Debugger
  41. ntdll:772CB57A ntdll__RtlUserThreadStart endp
复制代码


分析出的代码大概如下:

  1. struct _EXCEPTION_REGISTRATION
  2. {
  3.     struc EXCEPTION_REGISTRATION    *Prev;      //前一个_EXCEPTION_REGISTRATION结构
  4.     DWORD                           Handler;    //异常处理过程地址
  5. };

  6. void _stdcall _RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam)
  7. {
  8.         ExceptionChain chain;
  9.         RtlInitializeExceptionChain(&chain);
  10.         RtlUserThreadStart(pfnStartAddr,pvParam);
  11. }

  12. void _fastcall RtlInitializeExceptionChain(ExceptionChain* chain)
  13. {
  14.         if(RtlpProcessECVPolicy == 1)
  15.                 return;
  16.         _TEB* teb=getTeb();
  17.         chain->Prev = -1;
  18.         chain->Handler = RtlpFinalExceptionHandler;
  19.         if(teb->NtTib->Self->ExceptionList != -1)
  20.                 return;
  21.         teb->NtTib->ExceptionList=chain;
  22.         teb->NtTib->SameTebFlags |= 0x200;
  23. }

  24. void _fastcall RtlUserThreadStart(PTHREAD_START_ROUTINE pfnStartAddr,PVOID pvParam)
  25. {
  26.         __try
  27.         {
  28.                 if(!Kernel32ThreadInitThunkFunction)
  29.                 {
  30.                         pfnStartAddr(pvParam);//同步调用用户态入口函数,我们的程序分支走到这里
  31.                 }
  32.                 else
  33.                 {
  34.                         Kernel32ThreadInitThunkFunction(0,pfn);
  35.                 }
  36.                        
  37.         }
  38.         __finally
  39.         {
  40.                 RtlExitUserThread();//最终主线程运行到这里,用户入口的部分运行结束,而用户态代码仍处主线程中,在等待所有子线程结束后才执行完毕。
  41.         }
  42. }
复制代码

经过试验后发现,在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的地址空间。)
    因此经常说的:主线程结束,其他子线程也会结束,这句话是有道理的。
回复 赞! 1 靠! 0

使用道具 举报

1112

主题

1653

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
245
威望
744 点
宅币
24257 个
贡献
46222 次
宅之契约
0 份
在线时间
2298 小时
注册时间
2014-1-26
 楼主| 发表于 2015-2-28 19:06:07 | 显示全部楼层
其实也可以用C写,main在return前ExitThread即可!
回复 赞! 靠!

使用道具 举报

1112

主题

1653

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
245
威望
744 点
宅币
24257 个
贡献
46222 次
宅之契约
0 份
在线时间
2298 小时
注册时间
2014-1-26
 楼主| 发表于 2015-3-1 12:50:12 | 显示全部楼层
原来如此!
不过我觉得这个也可以理解成,所有线程结束后,进程结束。因为效果就是这样的。
回复 赞! 靠!

使用道具 举报

0

主题

76

回帖

6758

积分

用户组: 真·技术宅

UID
604
精华
0
威望
2 点
宅币
825 个
贡献
5853 次
宅之契约
0 份
在线时间
101 小时
注册时间
2014-12-20
发表于 2015-3-8 09:39:57 | 显示全部楼层
原来如此.. 学习了.. 话说为什么汇编代码API前缀都要加"__imp__" 跟编译器有关吗?
回复 赞! 靠!

使用道具 举报

1112

主题

1653

回帖

7万

积分

用户组: 管理员

一只技术宅

UID
1
精华
245
威望
744 点
宅币
24257 个
贡献
46222 次
宅之契约
0 份
在线时间
2298 小时
注册时间
2014-1-26
 楼主| 发表于 2015-3-8 14:57:59 | 显示全部楼层
0x01810 发表于 2015-3-8 09:39
原来如此.. 学习了.. 话说为什么汇编代码API前缀都要加"__imp__" 跟编译器有关吗? ...

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

使用道具 举报

0

主题

76

回帖

6758

积分

用户组: 真·技术宅

UID
604
精华
0
威望
2 点
宅币
825 个
贡献
5853 次
宅之契约
0 份
在线时间
101 小时
注册时间
2014-12-20
发表于 2015-3-9 10:53:22 | 显示全部楼层
0xAA55 发表于 2015-3-8 14:57
因为那个符号是从kernel32.lib和user32.lib中导入的,lib导出的符号都是要加__imp_前缀的,然后是C语言的 ...

原来是个thunk怪不得要加前缀。 thx
回复 赞! 靠!

使用道具 举报

QQ|Archiver|小黑屋|技术宅的结界 ( 滇ICP备16008837号 )|网站地图

GMT+8, 2024-4-29 05:56 , Processed in 0.045887 second(s), 36 queries , Gzip On.

Powered by Discuz! X3.5

© 2001-2024 Discuz! Team.

快速回复 返回顶部 返回列表