11.1.2 入口函数如何实现(1)
大部分程序员在平时都接触不到入口函数,为了对入口函数进行详细的了解,本节我们将深入剖析glibc和MSVC的入口函数实现。
GLIBC入口函数
glibc的启动过程在不同的情况下差别很大,比如静态的glibc和动态的glibc的差别,glibc用于可执行文件和用于共享库的差别,这样的差别可以组合出4种情况,这里只选取最简单的静态glibc用于可执行文件的时候作为例子,其他情况诸如共享库的全局对象构造和析构跟例子中稍有出入,我们在本书中不一一详述了,有兴趣的读者可以根据这里的介绍自己阅读glibc和gcc的源代码,相信能起到举一反三的效果。下面所有关于Glibc和MSVC CRT的相关代码分析在不额外说明的情况下,都默认为静态/可执行文件链接的情况。
读者可以免费下载到Linux下glibc的源代码,在其中的子目录libc/csu里,有关于程序启动的代码。glibc的程序入口为_start(这个入口是由ld链接器默认的链接脚本所指定的,我们也可以通过相关参数设定自己的入口)。_start由汇编实现,并且和平台相关,下面可以单独看i386的_start实现:
libc\sysdeps\i386\elf\Start.S: _start: xorl %ebp, %ebp popl %esi movl %esp, %ecx pushl %esp pushl %edx pushl $__libc_csu_fini pushl $__libc_csu_init pushl %ecx pushl %esi pushl main call __libc_start_main hlt |
这里省略了一些不重要的代码,可以看到_start函数最终调用了名为__lib_start_main的函数。加粗部分的代码是对该函数的完整调用过程,其中开始的7个压栈指令用于给函数传递参数。在最开始的地方还有3条指令,它们的作用分别为:
xor %ebp, %ebp:这其实是让ebp寄存器清零。xor的用处是把后面的两个操作数异或,结果存储在第一个操作数里。这样做的目的表明当前是程序的最外层函数。
ebp设为0正好可以体现出这个最外层函数的尊贵地位:。
pop %esi及mov %esp, %ecx:在调用_start前,装载器会把用户的参数和环境变量压入栈中,按照其压栈的方法,实际上栈顶的元素是argc,而接着其下就是argv和环境变量的数组。图11-1为此时的栈布局,其中虚线箭头是执行pop %esi之前的栈顶(%esp),而实线箭头是执行之后的栈顶(%esp)。
|
| (点击查看大图)图11-1 环境变量和参数数组 |
pop %esi将argc存入了esi,而mov %esp、%ecx将栈顶地址(此时就是argv和环境变量(env)数组的起始地址)传给%ecx。现在%esi指向argc,%ecx指向argv及环境变量数组。
综合以上分析,我们可以把_start改写为一段更具有可读性的伪代码:
void _start() { %ebp = 0; int argc = pop from stack char** argv = top of stack; __libc_start_main( main, argc, argv, __libc_csu_init, __libc_csu_fini, edx, top of stack ); }
|
其中argv除了指向参数表外,还隐含紧接着环境变量表。这个环境变量表要在__libc_start_main里从argv内提取出来。
环境变量
环境变量是存在于系统中的一些公用数据,任何程序都可以访问。通常来说,环境变量存储的都是一些系统的公共信息,例如系统搜索路径,当前OS版本等。环境变量的格式为key=value的字符串,C语言里可以使用getenv这个函数来获取环境变量信息。
在Windows里,可以直接在控制面板→系统→高级→环境变量查阅当前的环境变量,而在Linux下,直接在命令行里输入export即可。
实际执行代码的函数是__libc_start_main,由于代码很长,下面我们一段一段地看:
_start -> __libc_start_main: int __libc_start_main ( int (*main) (int, char **, char **), int argc, char * __unbounded *__unbounded ubp_av, __typeof (main) init, void (*fini) (void), void (*rtld_fini) (void), void * __unbounded stack_end) { #if __BOUNDED_POINTERS__ char **argv; #else # define argv ubp_av #endif int result;
|
这是__libc_start_main的函数头部,可见和_start函数里的调用一致,一共有7个参数,其中main由第一个参数传入,紧接着是argc和argv(这里称为ubp_av,因为其中还包含了环境变量表)。除了main的函数指针之外,外部还要传入3个函数指针,分别是:
init:main调用前的初始化工作。
fini:main结束后的收尾工作。
rtld_fini:和动态加载有关的收尾工作,rtld是runtime loader的缩写。
最后的stack_end标明了栈底的地址,即最高的栈地址。
bounded pointer
GCC支持bounded类型指针(bounded指针用__bounded关键字标出,若默认为bounded指针,则普通指针用__unbounded标出),这种指针占用3个指针的空间,在第一个空间里存储原指针的值,第二个空间里存储下限值,第三个空间里存储上限值。__ptrvalue、__ptrlow、__ptrhigh 分别返回这3个值,有了3个值以后,内存越界错误便很容易查出来了。 并且要定义__BOUNDED_POINTERS__这个宏才有作用,否则这3个宏定义是空的。
不过,尽管bounded指针看上去似乎很有用,但是这个功能却在2003年被去掉了。因此现在所有关于bounded指针的关键字其实都是一个空的宏。鉴于此,我们接下来在讨论libc代码时都默认不使用bounded指针(即不定义__BOUNDED_POINTERS__)。
接下来的代码如下:
char** ubp_ev = &ubp_av[argc + 1]; INIT_ARGV_and_ENVIRON; __libc_stack_end = stack_end; INIT_ARGV_and_ENVIRON这个宏定义于libc/sysdeps/ generic/bp-start.h,展开后本段代码变为: char** ubp_ev = &ubp_av[argc + 1]; __environ = ubp_ev; __libc_stack_end = stack_end; |
【责任编辑:
云霞 TEL:(010)68476606】