通过汇编简单C程序来理解计算机如何工作

试验截图

小小吐槽

汇编代码长度太长,不利于截图(主要是一页放不下整个代码);且在实验楼环境中,毕竟虚拟机中,即便是阅读代码也不如本机来得自如方便。所以,试验截图仅仅为了说明我是确确实实在实验楼中做过这个试验,具体汇编代码分析,会有更具体的非截图显示的代码。

具体截图

644558-20160227102352271-541009983.jpg

644558-20160227102403646-776652867.jpg

644558-20160227102423896-113411927.jpg

汇编代码工作中的堆栈变化分析

以下代码都是在实验楼环境下试验之后,下载到本地的代码。

main.c文件

与老师代码不同的是,我加入了一些局部变量,同时f函数使其传入两个参数。

int f(int x, int y){
    int i = 1;
    int j = 2;
    int k = 3;
    return i + j + k + x + y;
}

int g(int x){
    return f(1, 2) + x;
}

int main(){
    int i = 1;
    return g(i) + 2;
}
去除干扰项后干净的main.o文件

先说下生成main.o的命令:

gcc –S –o main.s main.c -m32

-m32是为了在64位环境下生成生成32位的中间代码。

把前缀是.的无关项都去掉,就生成了下面所示的汇编代码了:

f:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $16, %esp
    movl    $1, -12(%ebp)
    movl    $2, -8(%ebp)
    movl    $3, -4(%ebp)
    movl    -8(%ebp), %eax
    movl    -12(%ebp), %edx
    addl    %eax, %edx
    movl    -4(%ebp), %eax
    addl    %eax, %edx
    movl    8(%ebp), %eax
    addl    %eax, %edx
    movl    12(%ebp), %eax
    addl    %edx, %eax
    leave
    ret
g:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $8, %esp
    movl    $2, 4(%esp)
    movl    $1, (%esp)
    call    f
    movl    8(%ebp), %edx
    addl    %edx, %eax
    leave
    ret
main:
    pushl   %ebp
    movl    %esp, %ebp
    subl    $20, %esp
    movl    $1, -4(%ebp)
    movl    -4(%ebp), %eax
    movl    %eax, (%esp)
    call    g
    addl    $2, %eax
    leave
    ret
程序执行过程中栈变化图示

644558-20160227133447052-1278996092.jpg

644558-20160227133500646-1123017056.jpg

644558-20160227133509099-516082672.jpg

644558-20160227133518099-435649498.jpg

644558-20160227133526271-1237830782.jpg

644558-20160227133533911-1586877576.jpg

程序执行过程中栈变化的分析

大体的程序执行流程,通过上述栈的示意图能看得很清楚。

执行call指令后,CPU把返回地址压入栈,再转移到被调用函数执行,esp在执行过程中随时会用到,用esp来存取局部变量是不现实的,所以用ebp来作为局部变量指针。

pushl %ebp
movl %esp, %ebp

leave

两相配合,使得esp可以更安全、自由的使用,同时借助ebp保证了每次都能返回正确的地方。

以ebp为中间分界线,ebp本身指向原ebp值,ebp+4指向由call指令推入的返回地址,ebp+8及以上则为调用函数传入被调用函数的实参值;ebp以下即为局部变量。

ebp所存放的就是在刚进入被调用函数时候的esp的值,需要在堆栈中预留空间来存放局部变量,使用subl $预留空间, %esp。这个预留空间是如何确定的,这是目前面临的问题。

堆栈预留空间

通过一些小的试验来猜测该预留多大的栈空间。

测试代码
void a(char c){}
void b(){
    char c = 1;
}
void c(){
    int i = 1;
    int j = 2;
    int k = 3;
    int n = 4;
    char c = 1;
}
void d(){
    a(1);
}
void e(){
    char c = 1;
    a(c);
}
所得汇编代码

只写出相关部分

a:
    ...
    subl    $4, %esp
    movl    8(%ebp), %eax
    movb    %al, -4(%ebp)
    ...
b:
    ...
    subl    $16, %esp
    movb    $1, -1(%ebp)
    ...
c:
    ...
    subl    $32, %esp
    movl    $1, -16(%ebp)
    movl    $2, -12(%ebp)
    movl    $3, -8(%ebp)
    movl    $4, -4(%ebp)
    movb    $1, -17(%ebp)
    ...
d:
    ...
    subl    $4, %esp
    movl    $1, (%esp)
    ...
e:
    ...
    subl    $20, %esp
    movb    $1, -1(%ebp)
    movsbl  -1(%ebp), %eax
    movl    %eax, (%esp)
    ...
汇编代码分析

通过b的汇编代码可以看出:

尽管实际上movb $1, -1(%ebp),只用了1,系统却给预留出了16的栈空间;

通过c的汇编代码可以看出:

局部变量需要超过16的栈空间,即便只是超出1,也会额外分配16的栈空间;

从以上可得出结论,gcc每次分配局部变量栈空间是以16为单位。

通过e的汇编代码可以看出:

movl $1, (%esp),调用被调用函数的时候,实参是放在esp所指向的栈空间内的,即栈顶;如若有多个参数,则按照正增长的顺序从栈顶开始依次往下排。传入的实参,在汇编代码中可以很容易看出来,使用X(%esp)的即为实参。

同时还可以看出,立即数作为参数的时候,分配最小栈空间为4.

通过上述汇编代码的认知,再看e就清楚很多:

一个局部变量,栈空间为16;一个需要传入一个char的函数,参数个数为1,类型为char,需要4栈空间。一共需要20个。

同时,由于传参的栈空间可以重复利用,所以说参数所占用的栈空间大小是,一个函数内部所需要调用的所有函数的参数需要栈空间的最大值。这个可以通过更详尽的试验来验证,考虑到篇幅,就不再贴相关代码。

总结

通过一步步的分析,可以看出计算机的工作流程大致如下:

  • 以冯诺依曼体系为基础,依据存储程序的模型,一步步取出指令,自动执行;

  • eip(32位机器为例)存放当前指令地址,通过eip来寻找指令并执行;

  • 一般情况下机器指令执行是顺序执行的,call、jmp等指令可以改变eip值,实现跳转;

  • 高级语言(比如C)在执行的时候会被翻译成更贴近机器的汇编,之后再是机器语言,交与机器执行;

  • 栈很重要,通过跟踪栈的变化,能够了解到函数调用被调用函数时候自身数据如何保存的,数据是如何传递的,以及从被调用函数返回调用函数时,如何找到调用函数相关数据;

  • esp、ebp使得栈更具灵活性。

参考文献

《Windows环境下32位汇编语言程序设计 第二版》 p66

转载于:https://www.cnblogs.com/newc/p/5222403.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值