打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
C语言过程(函数)的机器级表示
一个函数调用包括将数据(包括参数和返回值)和控制从代码一部分传到另一部分。还包括对函数内局部变量分配空间,并在退出时释放空间。  其中,转移控制到过程 和 从过程转移出控制——使用指令;局部变量的分配和释放通过 程序栈 来实现。
1.栈帧结构
栈由高地址向低地址方向增长。对单个过程分配的栈称为 栈帧。以两个指针来界定:帧指针%ebp和栈指针%esp.栈指针是不断变化的,所以大多数信息基于帧指针%ebp.(注意在我的电脑上,帧指针是%esp,所以在汇编时总是由 movl 8(%esp) %eax来得到参数)。
从上图的栈帧结构中看到,假设P调用Q。
P栈帧部分参数为传入Q的参数;
P的返回地址形成P栈帧的末尾;
Q在%ebp+4+4i的位置上得到其参数;
栈向低地址增长,故栈指针减小——分配空间;栈指针增大——释放空间。
2.转移控制
一个简单的sum调用函数:
1
2
3
4
5
6
7
8
9
10
11
12
13
int accum = 0;
int sum(int x, int y)
{
int t = x + y;
accum += t;
return t;
}
int main()
{
return sum(1,3);
}
该函数使用gcc –o sum sum.c –O1得到可执行文件sum。对该可执行文件使用objdump –d sum 得到反汇编代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
080483b4 <sum>:
80483b4:   8b 44 24 08             mov    0x8(%esp),%eax
80483b8:   03 44 24 04             add    0x4(%esp),%eax
80483bc:   01 05 18 a0 04 08       add    %eax,0x804a018
80483c2:   c3                      ret
080483c3 <main>:
80483c3:   83 ec 08                sub    $0x8,%esp
80483c6:   c7 44 24 04 03 00 00    movl   $0x3,0x4(%esp)
80483cd:   00
80483ce:   c7 04 24 01 00 00 00    movl   $0x1,(%esp)
80483d5:   e8 da ff ff ff          call   80483b4 <sum>
80483da:   83 c4 08                add    $0x8,%esp
80483dd:   c3                      ret
【汇编的一个习惯技巧】
对下面的一个代码片段:
1
2
3
call next
next:
popl %eax
这是一个汇编代码的习惯用法,结果是把popl指令地址放入%eax中——将程序计数器值放入整数寄存器的唯一方法。
这不是一个过程调用,因为跳转的结果与指令顺序相同:
执行call next前,PC设为popl指令的地址(PC始终设置为下条指令地址);
执行call next后,%esp被设置为popl指令地址(因为它是调用者的返回地址);程序同时也跳转到了popl指令,(此时帧指针和栈指针是重合的,栈指针%esp只有在声明新局部变量时会增大,所以%esp还存储的是popl的地址),根据: %eax = M[R[%esp]],所以popl指令地址放入了%eax中
3.寄存器使用惯例
寄存器组是被所有过程共享的资源,但同一时刻只有一个过程激活,所以需要保证被调用者不会影响调用者在寄存器中的值。
将%eax, %edx, %ecx作为调用者保存寄存器,被调用者可以覆盖这些寄存器;
将%ebx, %ebi, %edi作为被调用者保存寄存器,需要在过程开始前pushl,在过程结束后popl到寄存器。
4.过程示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int swap_add(int *xp, int *yp)
{
int x = *xp;
int y = *yp;
*xp = y;
*yp = x;
return x+y;
}
int caller()
{
int arg1 = 534;
int arg2 = 1057;
int sum = swap_add(&arg1, &arg2);
return sum;
}
汇编代码为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
swap_add:
.LFB0:
.cfi_startproc
pushl   %ebx
.cfi_def_cfa_offset 8
.cfi_offset 3, -8
movl    8(%esp), %ebx
movl    12(%esp), %ecx
movl    (%ebx), %edx
movl    (%ecx), %eax
movl    %eax, (%ebx)
movl    %edx, (%ecx)
addl    %edx, %eax
popl    %ebx
.cfi_def_cfa_offset 4
.cfi_restore 3
ret
.cfi_endproc
caller:
.LFB1:
.cfi_startproc
subl    $24, %esp
.cfi_def_cfa_offset 28
movl    $534, 16(%esp)
movl    $1057, 20(%esp)
leal    20(%esp), %eax
movl    %eax, 4(%esp)
leal    16(%esp), %eax
movl    %eax, (%esp)
call    swap_add
addl    $24, %esp
.cfi_def_cfa_offset 4
ret
.cfi_endproc
对caller栈帧,分配如下:
共32个字节:
+24  (一个空闲位,在书上该位用来存放了“保存的%ebp")
+20 arg2
+16 arg1
……
+8
+4 &arg2
&arg1(%esp保存该地址)
返回地址(from ret)
对swap_add栈帧,%esp从上面的返回地址后面的地址开始,由于首先pushl %edx, %esp变为下一个地址:
……
&arg1
返回地址(call调用会存储该地址)
Initial %ebx(%esp保存该地址,由于没有”保存的%ebp“,所以该位置就类似于书上的%ebp位置。)
所以可以从%esp+8处取得参数值,注意此处与书上有所不同,因为一直没有用到%ebp帧地址,书上使用%edp帧地址,所以在每次初始部分多了:
pushl %ebp
movl %esp %ebp  (这一步结束,就将%ebp指向了”保存的%ebp“的地址。)
在结束部分,要恢复%ebp的值:
popl %ebp
ret
练习3.32: 不论从书上还是实际试验都可以看到,前面位置的参数里%ebp越近(%ebp+4+4i为第i个参数的地址)。
练习3.33:不要犯下面三个错误:(1)注意地址相减表示由高位到低位的扩展;(2)注意做十六进制相加(每个寄存器4字节);(3)C对应12,E对应14.
5.递归过程
栈规则的关键是:每次函数调用它都会保存自己的私有信息(包括返回地址、栈指针%ebp、被调用者保存寄存器值%ebp等,如果需要还有其他局部变量)。
本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
对gcc编译汇编码解析
栈帧&溢出
数据传送指令详解
xcode反汇编调试iOS模拟器程序
C文件汇编后函数参数传递的不同之处
C语言的那些小秘密之变参函数的实现
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服