1、linux下glibc运行库运行逻辑
正常可执行文件用glibc运行的函数调用顺序
_start -> __libc_start_main -> exit -> _exit;
_start:
xorl %ebp, %ebp //异或操作,将结果赋值到第一个参数上, ebp设置为0正好可以体现出_start作为最外围函数的地位
popl %esi //将当前堆栈中argc弹出赋值给esi
movl %esp, %ecx //将argv和env数组指针赋值给ecx
pushl %esp
pushl %edx
pushl $__libc_csu_fini
pushl $__libc_csu_init
pushl %ecx
pushl %esi
pushl main
call __libc_start_main
hlt
int __libc_start_main(
int (*main) (int, char **, char **),
int argc, //main输入形参个数
char * __unbounded *__unbounded ubp_av, //输入形参数组和环境变量数组联合指针
__typeof (main) init, //main函数调用前的初始化函数指针
void (*fini) (void), //main函数调用后的收尾函数指针
void (*rtld_fini) (void),//和动态加载有关的收尾工作,rtld是runtime loader的缩写
void * __bounded stack_end) //栈底
{
#if __BOUNDED_POINTERS__
char **argv;
#else
#define argv ubp_av
#endif
int result;
char** ubp_ev = &ubp_av[argc+1]; //定位到env环境变量数组
INIT_ARGV_and_ENVIRON; //宏定义,展开为_environ = ubp_ev;但之所以分成两步看着略显多余的间接赋值,是为了兼容bounded机制,这种情况是默认不支持bounded的版本
__libc_stack_end = stack_end;//将栈底地址存储在一个全局变量,留待后续使用
DL_SYSDEP_OSCHECK (__libc_fata1); //检查操作系统版本的宏,不进行展开了
/*接下来代码繁杂,省去大量信息,只保留核心的函数调用*/
__pthread_initialize_minimal();
__cxa_atexit(rtld_fini, NULL, NULL); //等同于atexit()函数,用于将参数指定的函数在main结束之后调用,所以参数传入的rtld_fini和fini均会在main结束之后调用
__libc_init_first(argc, argv, __environ);
__cxa_atexit(fini, NULL, NULL);
(*init) (argc, argv, __environ);
result = main (argc, argv, __environ);
exit(result);
}
看下exit的实现
void exit(int status)
{
while(__exit_funcs != NULL)
{
...
__exit_funcs = __exit_funcs -> next; //__exit_funcs是存储着__cxa_atexit和atexit注册的善后函数的指针链表,这个while循环便是用来逐个调用这些注册的函数。
}
...
_exit(status);//最后由系统函数_exit完成输出返回,该函数由汇编实现,且与平台无关
}
_exit:
movl 4(%esp), %ebx
movl $__NR_exit, %eax
int $0x80
hlt
脚注
可以看到_start和_exit末尾都有一个hlt指令,这个指令是起到冗余保护作用。一般一旦exit被调用,即意味着程序的运行被终止(eix程序寄存器换到了别的函数了),因此_exit末尾的hlt不会执行,从而__libc_start_main永远不会再返回了,也就意味着_start末尾的hlt也不会运行,而_exit末尾的hlt指令则是为了检测exit系统调用是否成功,如果失败,程序将永远不会停止,所以hlt强制终止命令可以强行把程序停下来,_start中的hlt指令也是这种冗余保护机制的一部分。
2、Windows下MSVC-CRT运行库的运行逻辑
MSVC-CRT默认的入口函数名为mainCRTStartup,这个mainCRTStartup的总体流程是:
1.初始化和OS版本有关的全局变量
2.初始化堆
3.初始化I/O
4.获取命令行参数和环境变量
5.初始化C库的一些数据
6.调用main并记录返回值
7.检查错误并将main的返回值返回
int mainCRTStartup(void)
{
...
posvi = (OSVERSIONINFOA *) _alloca(sizeof(OSVERSIONINFOA));//这里不用malloc,是因为堆还没有初始化,这时只有alloca是唯一可以不使用堆的动态分配机制,alloca可以在栈上分配任意大小的空间(栈允许就可以),并且在函数返回的时候自动释放,就像局部变量一样。
posvi -> dwOSVersionInforSize = sizeof(OSVERSIONINFOA);
GetVersionExA(posvi);
_osplatform = posvi -> dwPlatformId;
_winmajor = posvi -> dwMajorVersion;
_winminor = posvi -> dwMinorVersion;
_osver = (posvi -> dwBuildNumber) & 0x07fff;
if ( _osplatform != VER_PLATFORM_WIN32_NT )
_osver |= 0x08000;
_winver = (_winmajor << 8) + _winminor;
/*被赋值的这些变量是VC7预定义的一些全局变量,其中_osver和_winver表示操作系统的版本,_winmajor是主版本号,这段代码通过调用GetVersionExA()来获取当前操作系统的版本信息,并且赋值给各个全局变量。 */
/*赶紧初始化堆空间,不然很多事情没法做*/
if (!_heap_init(0))
fast_error_exit(RT_HEAPINIT);//使用_heap_init()函数对堆进行初始化,如果初始化失败,则通过fast_error_exit()直接退出
__try {
if(_ioinit() < 0) //_ioinit函数初始化I/O
_amsg_exit(_RT_LOWIOINIT);
_acmdln = (char *)GetCommandLineA();
_aenvptr = (char *)_crtGetEnvironmentStringsA();
if(_setargv() < 0) //初始化main函数的argv参数
_amsg_exit(_RT_SPACEARG);
if(_setenvp() < 0)//设置环境变量数组指针
_amsg_exit(_RT_SPACEENV);
initret = _cinit(TRUE);//其他C库的这只
if(initret != 0)
_amsg_exit(initret);
__initenv = _environ;
mainret = main(__argc, __argv, _environ);
_cexit();
}
__except(_XcptFilter(GetExceptionCode(), GetExceptionInformation()) )
{
mainret = GetExceptionCode();
_c_exit();
}/*end of try-except*/
return mainret;
}
MSVC _heap_init()堆初始化代码
HANDLE _crtheap = NULL;
int _heap_init(int mtflag)
{
if( (_crtheap = HeapCreate(mtflag ? 0: HEAP_NO_SERIALIZE,
BYTES_PER_PAGE, 0)) == NULL)
return 0;
return 1;
}
/*32位编译环境的MSVC堆初始化异常简单,仅是调用了HeapCreate这个API来创建一个系统堆,因此不难想象,MSVC的malloc函数必然是调用了HeapAlloca这个API,将堆管理的过程直接交给了操作系统。*/
I/O初始化代码
I/O初始化主要是用户控件中建立stdin\stdout\stderr及其对应的FILE结构,使得程序进入main之后可以直接使用printf、scanf等函数。FILE结构实际定义在C语言标准中并没有明确指出,故而不同的版本可能有不同的实现:
struct _iobuf {
char *_ptr;
int _cnt;
char *_base;
int _flag;
int _file; //每个进程都有一个打开文件列表数组用来管理该进程当前使用的文件,_file值其实便是fd(file descriptor)项,即打开文件列表中的下标,用来指向某个进程打开的文件
int _charbuf;
int _bufsiz;
char *_tmpfname;
};
typedef struct _iobuf FILE;
typedef struct{
intptr_t osfhnd;//打开文件的句柄,使用8字节整数类型intptr_t来存储
char osfile;//文件的打开属性
/*osfile的值可以通过一系列值按位或的方式得出:
FOPEN(0x01)句柄被打开
FEOFLAG(0x02)已到达文件末尾
FCRLF(0x04)在文本模式中,行缓冲已遇到回车符
FPIPE(0x08)管道文件
FNOINHERIT(0x10)句柄打开时具有属性_O_NOINHERIT(不遗传给子进程)
FAPPEND(0x20)句柄打开时具有属性O_APPEND(在文件末尾追加数据)
FDEV(0x40)设备文件
FTEXT(0x80)文件以文本模式打开
*/
char pipch;//用于管道的单字符缓冲
}ioinfo;//MSVC CRT中,已经打开的文件句柄信息使用数据结构ioinfo来表示
在crt/src/ioinit.c中有一个数组
int _nhandle;
ioinfo * _pioinfo[64]; //等效于ioinfo _pioinfo[64][32]
这个二维数组便是进程用户态的打开文件表,所以进程可容纳的句柄是64*32 = 2048,验证了句柄资源的有限性,而n_handle则记录了该表的实际元素个数。只所以使用而不是二维数组的原因是使用指针数组更加节省空间,而如果使用二维数组,则不论程序里打开了几个文件都必须始终消耗2048个ioinfo的空间。
总结:无论是Linux还是Windows,用户程序的入口都绝非惯常所认为的main()函数。任何程序的运行是建立在多层抽象封装的基础上才能得以进行逻辑操作:1、操作系统的软硬件提供的虚拟空间、CPU调度等机制;2、运行库实现的,包括参数入栈、环境参数入栈、I/O初始化、堆初始化等操作。这几层背后的逻辑完成后,搭好了舞台,终于可以进入应用层,即程序员眼中所有逻辑操作入口–main()函数。