读书频道 > 系统 > linux > 深度探索Linux操作系统系统构建和原理解析
2.1.2 编译
2013-09-28 11:52:04     我来说两句 
收藏    我要投稿   
全书一共8章:第1章介绍了如何准备工作环境。在第2章中构建了编译工具链,这是后面构建操作系统各个组件的基础。在这一章中,不仅详细讲解了工具链的构建过程,而且还通过对编译链接过程的探讨,深入讨论了工具链  立即去当当网订购

编译程序对预处理过的结果进行词法分析、语法分析、语义分析,然后生成中间代码, 并对中间代码进行优化,目标是使最终生成的可执行代码执行时间更短、占用的空间更小,最后生成相应的汇编代码。

以foo2.c为例,我们可以使用如下命令指定编译过程只进行编译,不进行汇编和链接。

root@baisheng:~/demo# gcc -S foo2.c

编译后产生的汇编文件为foo2.s,其内容如下:
    .file   "foo2.c"
    .globl  foo2
    .data
    .align 4
    .type   foo2, @object
    .size   foo2, 4
foo2:
    .long   20
    .text
    .globl  foo2_func
    .type   foo2_func, @function
foo2_func:
.LFB0:
    .cfi_startproc
    pushl   %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl    %esp, %ebp
    .cfi_def_cfa_register 5
    subl    $16, %esp
    movl    foo2, %eax
    movl    %eax, -4(%ebp)
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   foo2_func, .-foo2_func
    .ident  "GCC: (Ubuntu/Linaro 4.7.2-2ubuntu1) 4.7.2"
    .section    .note.GNU-stack,"",@progbits

在文件foo2.c中,除定义了一个全局变量foo2外,仅定义了一个函数foo2_func,而该函数体中也只有区区一行代码,但为什么产生的汇编代码如此之长?事实上,仔细观察可以发现,文件foo2.s中相当一部分是汇编器的伪指令。伪指令是不参与CPU运行的,只指导编译链接过程。比如,代码中以“.cfi”开头的伪指令是辅助汇编器创建栈帧(stack frame)信息的。

在终端上调试程序的程序员一般都会有这样的经历:某个程序出现Segment Fault了,然后终端中会输出回溯(backtrace)信息。或者,我们在调试程序时,也经常需要回溯,查找一些变量或查看函数调用信息。这个过程,就是所谓的栈的回卷(unwind stack)。事实上,在每个函数调用过程中,都会形成一个栈帧,以main函数调用foo2_func为例,形成的栈帧如图2-2所示。

 

frame pointer和base pointer均指向栈桢的底部,只是叫法不同,在IA32架构中,通常使用寄存器ebp保存这个位置。因为main并不是程序中第一个运行的函数,所以main也是一个被调函数,其也有栈帧。事实上,即使程序中第一个被调用的函数_start(该函数实现在启动代码中),也会自己模拟一个栈帧。

理论上,调试器或异常处理程序完全可以根据frame pointer来遍历调用过程中各个函数的栈帧,但是因为gcc的代码优化,可能导致调试器或异常处理很难甚至不能正常回溯栈帧,所以这些伪指令的目的就是辅助编译过程创建栈帧信息,并将它们保存在目标文件的段“.eh_frame”中,这样就不会被编译器优化影响了。

去掉这些伪指令后,函数foo2_func中CPU真正执行的代码如下:

foo2_func:
1    pushl   %ebp
2    movl    %esp, %ebp
3    subl    $16, %esp
4    movl    foo2, %eax
5    movl    %eax, -4(%ebp)
6    leave
7    ret

在汇编语言中,在函数的开头和结尾处分别会插入一小段代码,分别称为Prologue和Epilogue,如foo2_func中的第1、2、3行代码就是Prologue,第6、7行代码就是Epilogue。

Prologue保存主调函数的frame pointer,这是为了在子函数调用结束后,恢复主调函数的栈帧。同时为子函数准备栈帧。其主要操作包括:

保存主调函数的frame pointer,如第1行代码所示,就是将保存在寄存器ebp中的frame pointer压栈。在退出子函数时可以从栈中恢复主调函数的frame pointer。

将esp赋值给ebp,即将子函数的frame pointer指向主调函数的栈顶,如第2行代码所示。换句话说,这行代码的意义就是记录了子函数的栈帧的底部,从这里就开始了子函数的栈帧。

修改栈顶指针esp,为子函数的本地变量分配栈空间,如第3行代码。注意虽然这里的foo2_func中只有一个局部变量ret,占据4字节,但是还是预留了16字节的栈空间,这根据的是IA32的ABI(Application Binary Interface)的16字节的对齐要求。

Epilogue功能与Prologue恰恰相反,如果说Prologue相当于构造函数,那么Epilogue就相当于析构函数。其主要操作包括:

将栈指针esp指向当前子函数的栈帧的frame pointer,也就是说,指向当前栈桢的栈底,而在这个位置,恰恰是Prologue保存的主调函数的frame pointer。然后,通过指令pop将主调函数的frame pointer弹出到ebp中,如此,一方面释放了被调函数foo2_func的栈帧,同时也回到了主调函数main的栈帧。IA32提供了指令leave来完成这个功能,即第6行代码,这个指令相当于:
movl %ebp, %esp
pop %ebp

将调用子函数时call指令压栈的返回地址从栈顶pop到EIP中,并跳转到EIP处继续执行。如此,CPU就返回到主调函数继续执行。IA32提供了指令ret来完成这个功能,即第7行代码。

除了Prologue和Epilogue,foo2_func的核心代码就剩下第4行和第5行两行了。这两行代码对应的就是C语言中的赋值语句“int ret = foo2”。首先,即第4行代码,CPU从数据段中读取全局变量foo2的值到寄存器EAX中。然后,即第5行代码,将eax中的内容,即foo2的值,复制到栈中的局部变量ret的位置。代码中根据局部变量相对于栈的frame pointer(在ebp中保存)的偏移来访问局部变量,如变量ret位于相对于栈底偏移为–4的内存处。

点击复制链接 与好友分享!回本站首页
分享到: 更多
您对本文章有什么意见或着疑问吗?请到论坛讨论您的关注和建议是我们前行的参考和动力  
上一篇:2.1.1 预编译
下一篇:2.1.3 汇编
相关文章
图文推荐
3.3.6 GNOME的软件管
3.3.5 GNOME的文件管
3.3.4 GNOME的窗口管
3.3.3 收藏夹和快捷
排行
热门
文章
下载
读书

关于我们 | 联系我们 | 广告服务 | 投资合作 | 版权申明 | 在线帮助 | 网站地图 | 作品发布 | Vip技术培训
版权所有: 红黑联盟--致力于做最好的IT技术学习网站