[CSAPP-I] 过程(函数栈帧) C语句的机器级表示(gcc -S)

来源:互联网 发布:微商达人是什么软件 编辑:程序博客网 时间:2024/06/07 14:33

1 C和机器指令

1.1 电平和二进制

给计算机通电后,计算机内导通的器件“带电”。不同器件的“带电程度”可能不同。可能有些器件的某个输出端的电压为5V,有的器件的某个输出端电压为1V。


若将计算机内带5V电压的输出端的状态视为“1”,带1V电压的输出端的状态视为“0”。那么,计算机内一连串的输出端就可以被人视为一连串的0或1的序列。这一串0或1(bnbn-1 … b1 b0)的序列可以被解释为整数(bn*2n+ bn-1*2n-1 + … b1*21 + b0*20)、字符(串)、汉字或图像数据等的编码或其它,并将它们显示出来。


将只有0,1两个数字存在的集合作一些规定得到二进制集。由于编码这个抽象东西的存在,所以说存在计算机中的数据(东西)都是二进制(0,1序列)。[紫色的东西都需要对其额外的了解]


1.2 汇编指令和机器指令

指令集体系结构(汇编指令集)代表了机器级程序的格式和行为,对机器级编程来说是一种很重要的抽象。用汇编编写的程序经过汇编编译器后被转换为机器指令(二进制),且汇编指令与机器代码一一对应。汇编指令时机器指令的助记符,便于人理解程序。


Figure 1. 汇编指令和机器指令

1.3 C和汇编指令

C是在指令集体系结构基础之上的再次抽象,以方便程序员以接近于Americans的日常思维习惯编程。CPU只能识别机器指令(二进制),必须要将用C编写的程序转换为机器指令,过程为:C--> 汇编 -->机器码。C源程序的C语句和汇编指令非一一对应的关系,所以在C转换为汇编时,能得到怎么样的汇编指令,依靠具体的转换策略。这些转换策略被包含在“C编译器”中。

Figure 2. C语句到汇编指令

由于可以用不同的汇编指令来实现C中同一条语句或者数据结构,所以针对相同的C代码,不同平台的编译器(一般,编译器可能针对某个特定的平台如windowsx86、Linux来编写)产生的汇编指令不一定相同(选取汇编指令来实现C语句的机制和优化能力)。


2 平台

2.1 硬件

Table 1. 硬件(ls cpu)

Architecture:

i686(Intel 80386)

Byte Order:

Little Endian

 

 

2.2 操作系统

Table 2. 操作系统类型

操作系统(cat /proc/version)

位数(uname -a)

Linux version 3.2.0-4-686-pae

i686(32bit)

 

2.3 编译器

Table 3. 编译器信息

编译器(gcc -v)

gcc (Debian 4.7.2-5) 4.7.2

gcc有不同级别的优化能力,-O1,-O2等级。本笔记不使用任何优化选项(不同优化级别得到的汇编指令不同)。


3 C代码

3.1 main.c

#include "sub_fun.h"int main(void){        if_else_fun(1, 2);        return 0;}


3.2 sub_fun.c

//-------------------------------Data in C ----------------------------------////Array's mechanism in assemblyvoid array_fun(int n){int a[n];int b[2][3];a[n - 1]= 0;b[1][2]= 1;}//Pointer's mechanism in assemblyvoid pointer_fun(void){        int i           = 0;        int j           = 2;        int *p;        int **pp;        p       = &i;        *p      += 1;        pp      = &p;        *pp     = &j;        **pp    = 1;}//Struct's mechanism in assemblyvoid struct_fun(void){        struct e{                int     i;                char    c;                int     *p;        }evar;        evar.i  = 1;        evar.c  = 'a';        evar.p  = &evar.i;}//Union's mechanism in assemblychar union_fun(void){        union e{                char    c;                float   f;        }evar;        evar.f  = 1.0f;return evar.c;}//-------------------------------Data in C ----------------------------------////-------------------------------Control in C ----------------------------------////IF-ELSE in data control flowint if_else_fun(int x, int y){        if (x >= y) {                return x - y;        }else {                return y - x;        }}//SWITCH-caseint switch_case_fun(int x){        switch (x) {                case 0:                        x       = ~x;                        break;                case 1:                        x       += 1;                case 2:                        x       += 1;                        break;                case 99:                        x       += 10;                        break;                default:                        x       = 1;                        break;        }        return x;}//DO-WHILEvoid do_while_fun(int n){        do {                n--;        } while(n);}//WHILEvoid while_fun(int n){        while (n) {               n--;        }}//FORvoid for_fun(int n){        int i;        for (i = 0; i < n; ++i) {                i++;                i--;        }}//-------------------------------Control in C ----------------------------------//

4 将C转换为汇编指令

在Linux shell命令行下“man gcc”了解关于gcc的用法。将sub_fun.c和main.c放在同一个目录下。


使用gcc编译器的-S参数来得到C源文件对应的汇编文件:

gcc  -S  sub_fun.c

gcc  -S  main.c

经过此gcc编译器的-S后,会在当前目录生成sub_fun.s和main.s两个文件。它们分别是sub_fun.c和main.c对应的汇编代码。在汇编文件中,所有以'.'开头的行都是指导汇编器和链接器的命令。目前只关心C到汇编中的一个(gcc (Debian 4.7.2-5) 4.7.2)机制,所以只在汇编文件中保留了汇编指令。


4.1 过程

《Linux C 编程一站式学习》第18,19章也有关于“函数栈帧”的讲解,笔记有“一个C源文件到可执行文件 [反汇编-函数栈帧 编译 链接]”。在此,再笔记一下C语言中调用函数的过程。


IA32用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧。最顶端的栈帧以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。[《CSAPP》2e  P.149]


以%ebp和%esp在程序中所描述的现象来理解这两个寄存器的作用。可在《汇编语言》—王爽一书中打基础。


(1) 查看%ss, %esp,%ebp的虚拟地址值

将sub_fun.c和main.c编译链接成可执行程序:

gcc  sub_fun.c  main.c  –g

gdb  a.out

Figure3. 程序运行时%esp和%ebp的值


(2) 函数栈帧

Figure 3中所查得到的%ebp和%esp是在未执行main程序中的任何语句时的值,故,我们可以从main()函数调用子函数开始了解函数调用过程(无call main)。图中绿色部分表示标绿指令的功能:

Figure4. C主函数main的汇编代码

_start调用main函数执行时,会自动完成红框中的汇编指令(andl $-16,%esp保证栈空间为16的整数倍,此平台采用补码表示有符号数,-16的补码为0xfffffff0):

Figure5. 进入main函数为main函数分配栈空间

完成main函数栈空间分配后,再执行main函数内的语句即调用if_else_fun函数:

Figure6. 为if_else函数传递参数

给子函数传递的参数保存在调用者的栈空间内。

Figure7. call if_else_fun函数的操作(main函数栈帧形成)

执行完call语句后(call指令将下一条指令的地址入栈,并跳到被调用过程的起始处),将转到if_else_fun函数内运行,同理,查看进入if_else_fun函数后的%esp和%ebp中的虚拟地址值:

Figure8. 进入子函数时%esp和%ebp值

从Figure 8可以看出,进入if_else_fun连续为if_else_fun分配栈空间,首先将%ebp寄存器入栈保存起来,然后再将%esp的值赋给%ebp,这个过程为下图红色方框内容:

Figure9. 进入if_else_fun

这个过程栈发生的变化为:

Figure10. 两个函数的栈帧

因为if_else_fun内没有其它数据,所以栈帧只有4个字节。当执行指令执行到Figure 10的.L7处时(其它指令会在“C语句实现机制”中分析),本程序的栈帧开始被回收。如下图所示:

Figure11. if_else_fun栈帧回收


Figure12. if_else_fun函数返回

ret指令从栈中弹出地址,并跳转到这个位置。此时,%eip的值为main函数中的return 0语句的地址。即Figure 4中第9行语句处(将整型的返回值给%eax)。然后再执行leave指令(相当于movl %ebp, %esp 加popl %ebp的功能):

Figure13. main函数执行leave,main函数栈帧完全释放

执行完leave指令后,还要执行ret指令,正确跳到调用main函数的下一个语句处继续执行程序:

Figure14. main函数返回

关于函数栈帧,可以在看过像《汇编函数》—王爽这一类书后再根据《Linux C编程一站式学习》自己画画。


4.2 C语句实现机制

(1) 数组

array_fun函数内定义了一个定长数组,一个变长数组。此函数在gcc (Debian 4.7.2-5) 4.7.2下的汇编代码为:

Figure15. arrar_fun函数在gcc –O1级下对应的汇编指令

上图中1中的代码是在为array_fun函数开辟栈空间(不包括数组的栈空间)。%ebp - %esp = 56 bytes。


%ebp+ 8是父函数的栈空间,所以%ebp + 8是父函数传递给array_fun函数的实参数n。


2中的代码是在为变长数组int a[n]开辟空间,红线标注的语句让array_fun函数的栈空间得到扩展,其大小跟参数n有关。经过对%eax进行计算(保证内存对齐等)后,将%eax的值作为数组a[n]的起始地址,并把它保存在-16(%ebp)中。


3中的代码是在计算a[n-1]的下标值n – 1,并将其保存在%edx中。


4中的代码是在给指定的数组元素赋值:movl $0, (%eax, %edx, 4)这是访问数组元素的机器级指令,代表访问数组元素的过程,表示给地址为4 * %edx + %eax的内存赋值为1,%eax就是数组a的起始地址值,4 * %edx为相对数组起始地址的偏移值;编译器发现只对数组b[2][3]的b[1][2]进行访问,干脆将整个数组优化为b[1][2]这个整型变量,将其保存在-20(%ebp)中。


然后回收给变长数组a分配的栈空间(movl %ecx, %esp),恢复%ebx的值(但从这个程序看,此句也显得多余),再返回到父函数中。


在array_fun函数的过程大概为下图所示:

Figure16. array_fun运行时的栈帧

传递给子函数实参的地址在父函数栈帧内,子函数内的局部变量在运行时分配在子函数栈帧内。


(2) 指针

pointer_fun的C代码和汇编代码如下:

Figure17. 实现C语言指针的机制


根据汇编代码,首先找到在pointer_fun内各变量的栈地址:

Figure18. pointer_fun局部变量的栈地址

求栈地址用“leal指令”;求栈地址内的内容用“()”。“指针”其实就是地址。间接引用指针就是将该指针放在一个寄存器中,然后在存储器引用中使用这个寄存器。


(3) 结构体

struct_fun的C代码和汇编源码如下:

Figure 19. struct_fun结构体实现机制

struct_fun的栈空间情况如下:

Figure20. struct_fun函数的栈空间分配

其中,evar.c是char类型变量,本只占一个字节,但因“内存对齐”的缘故,会空三个字节不用(内存间隙,上图灰色部分)。


(4) 联合

使用联合的函数union_fun的C代码和汇编代码如下:

Figure21. union实现机制

其中.LC0的内容为:


1065353216的十六进制表示为0x3f800000,它是IEEE浮点标准下1.0的编码。

movzbl指令将做了零扩展的字节传送到双字中。evar.f共占4个字节,evar.c占这四个字节中的一个字节(最高(大端)或最低字节(小端))。


(5) if-else(数据控制流)

使用if-else语句的函数if_else_fun的C代码和汇编指令如下:

Figure22. if-else实现机制

此版本的gcc编译器采用条件跳转指令(jl)和无条件跳转指令(jmp)来实现C语句的if-else分支。测试数据值,然后根据测试的结果来改变控制流或者数据流的方式是gcc实现控制的主要策略。见(6)条件数据传输中gcc采用的另一种策略。


(6) switch-case

sub_fun.c中switch_fun函数使用了switch-case语句,其C代码和对应的汇编指令如下(gcc无指定优化级别的情况下没有使用跳转表机制来实现switch-case):

Figure23. switch-case实现机制

(7) do-while

Figure24. do-while实现机制

(8) while

Figure25. while实现机制

实现while时,将其转化为do-while,然后按照do-while的实现机制实现。


(9) for

Figure26. for实现机制

实现for循环时,也是先将其转化为do-while,然后按照do-while的实现机制实现。


5 总结

5.1 定长数组和变长数组

变长数组必须使用乘法来对下标伸展n倍,不能用一系列的移位和加法(对n未知)。乘法在一些处理器中会导致严重的性能处罚,但在这种情况下不可避免。


5.2 数组和指针访问过程区别(汇编指令层)

访问数组元素a[i]的过程为:

  • 取a的值:movl %esp, %eax;
  • 取i:movl  -4(%ebp), %ebx;
  • 读a[i]:movl ( %eax, %ebx, sizeof(a[0]) ),%ecx。


访问指针元素p[i]的过程为:

  •  取p内的值:movl  -8(%ebp), %eax;
  • 取i:movl  -4(%ebp), %ebx;
  • 读p[i]:movl( %eax, %ebx, sizeof(a[0]) ), %ecx。

其中,a存在%esp中,i存在-4(%ebp)中,p存在-8(%ebp)中,sizeof(a[0])表数组类型大小。


5.3 数据控制和条件控制

现在的处理器支持条件传送指令,实现分支的的策略就有了两种:数据转移和条件转移。


因为现代处理器采用流水线技术(可读《大话处理器》),所以数据转移比条件转移要好(当分支中的各表达式都容易计算时)。即使许多分支预测错误的开销会超过更复杂的计算,但gcc还是会使用条件控制转移。


5.4 栈空间分配策略

gcc坚持x86编程指导方针,也就是一个函数使用的所有栈空间必须是16的整数倍(4.2中为函数分配的栈都是16的整数倍)。


[2015.01.13–01.19]

Read 《CSAPP》 Note Over.

0 0
原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 孩子小学数学学不懂怎么办 入学籍系统提交了没分班怎么办 被监考老师抓了作弊怎么办 作弊被老师抓到了怎么办? 中考作弊给抓到怎么办 考试作弊证据没得监控怎么办 考试作弊被领导发现了怎么办 黄冈讲课视频看不了怎么办 高中学校不给转学籍怎么办 兴山香溪大酒店欠钱不还怎么办 黑坑青鱼滑口怎么办 不交物业费物业怎么办 车牌刮了一点漆怎么办 电脑光驱线坏了怎么办 做系统不读光驱怎么办 光盘放进电脑没反应怎么办 不服省高院裁定维持原判怎么办 咖啡和酒一起喝怎么办 跟法官联系不上怎么办 四维没有预约到怎么办 钥匙锁在车里怎么办 如果孩子很叛逆骂人打人怎么办 错过了今年规培怎么办 枣木怎么办才能搞直了 高中生和家里闹意见离家出走怎么办 校长信箱实名举报了怎么办 枣子吃多了胀气怎么办 红枣吃多了会怎么办 宁波南苑e家会员卡怎么办 宁波社保卡丢了怎么办 奶茶汉堡店经营不好改怎么办 军人保障卡丢了怎么办 军人保障卡丢失了怎么办 军人保障卡掉了怎么办 椎基底动脉供血不足怎么办 颈椎压迫神经脑供血不足怎么办 脑部基底动脉轻度狭窄怎么办 胸壁疼痛我该怎么办 厂房面积小于泄压面积怎么办 江苏海门农村自建房房产证怎么办 颈总动脉斑块形成怎么办