设为首页 加入收藏

TOP

11.1.2 入口函数如何实现(1)
2013-10-07 00:43:34 来源: 作者: 【 】 浏览:73
Tags:11.1.2 入口 函数 如何 实现

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】

回书目   上一节   下一节

】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇11.1.1 程序从main开始吗 下一篇11.1.4 MSVC CRT的入口函数初始化..

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容: