linuxC一站式编程的函数调用的过程汇编理解

本文详细解析了C语言中函数调用的过程,包括参数传递、栈帧管理、返回值处理等核心内容。通过具体示例介绍了编译原理及运行时栈的使用方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

测试程序

#include<stdio.h>
int bar(int c,int d)
{       
        int e = c+d;
        return e;
}
int foo(int a,int b)
{       
        return bar(a,b);
}       
int main()
{       
        int a = 1;
        int b = 2;
        foo(a,b);
        return 0;
}

一些命令

readelf 显示ELF文件的信息
readelf -a file 显示所有的信息
hexdump -C file 使用16进制加ascii显示文件里的内容 
objdump -d file 反汇编文件
objdump -dS file 反汇编文件,可以把源码和汇编代码串联起来看,编译时必须得有-g选项才可以

这里使用反汇编命令

objdump -dS  memory.exe得到对应的汇编代码,下面只列出部分

int bar(int c,int d)
 8048404:	55                   	push   %ebp
 8048405:	89 e5                	mov    %esp,%ebp
 8048407:	83 ec 10             	sub    $0x10,%esp
{
	int e = c+d;
 804840a:	8b 45 0c             	mov    0xc(%ebp),%eax
 804840d:	03 45 08             	add    0x8(%ebp),%eax
 8048410:	89 45 fc             	mov    %eax,0xfffffffc(%ebp)
	return e;
 8048413:	8b 45 fc             	mov    0xfffffffc(%ebp),%eax
}
 8048416:	c9                   	leave  
 8048417:	c3                   	ret    

08048418 <_Z3fooii>:
int foo(int a,int b)
 8048418:	55                   	push   %ebp
 8048419:	89 e5                	mov    %esp,%ebp
 804841b:	83 ec 08             	sub    $0x8,%esp
{
	return bar(a,b);
 804841e:	8b 45 0c             	mov    0xc(%ebp),%eax
 8048421:	89 44 24 04          	mov    %eax,0x4(%esp)
 8048425:	8b 45 08             	mov    0x8(%ebp),%eax
 8048428:	89 04 24             	mov    %eax,(%esp)
 804842b:	e8 d4 ff ff ff       	call   8048404 <_Z3barii>
}
 8048430:	c9                   	leave  
 8048431:	c3                   	ret    

08048432 <main>:
int main()
 8048432:	8d 4c 24 04          	lea    0x4(%esp),%ecx
 8048436:	83 e4 f0             	and    $0xfffffff0,%esp
 8048439:	ff 71 fc             	pushl  0xfffffffc(%ecx)
 804843c:	55                   	push   %ebp
 804843d:	89 e5                	mov    %esp,%ebp
 804843f:	51                   	push   %ecx
 8048440:	83 ec 18             	sub    $0x18,%esp
{
	int a = 1;
 8048443:	c7 45 f4 01 00 00 00 	movl   $0x1,0xfffffff4(%ebp)
	int b = 2;
 804844a:	c7 45 f8 02 00 00 00 	movl   $0x2,0xfffffff8(%ebp)
	foo(a,b);
 8048451:	8b 45 f8             	mov    0xfffffff8(%ebp),%eax
 8048454:	89 44 24 04          	mov    %eax,0x4(%esp)
 8048458:	8b 45 f4             	mov    0xfffffff4(%ebp),%eax
 804845b:	89 04 24             	mov    %eax,(%esp)
 804845e:	e8 b5 ff ff ff       	call   8048418 <_Z3fooii>
	return 0;
 8048463:	b8 00 00 00 00       	mov    $0x0,%eax
}


然后使用gdb调试memory.exe,下面是部分gdb的内容

Breakpoint 1, main () at memory.cpp:13
13  int a = 1;
(gdb) n
14  int b = 2;
(gdb) n
15  foo(a,b);
(gdb) p &a
$1 = (int *) 0xffd7e35c
(gdb) p &b
$2 = (int *) 0xffd7e360
(gdb) s
foo (a=1, b=2) at memory.cpp:9
9  return bar(a,b);
(gdb) s
bar (c=1, d=2) at memory.cpp:4
4  int e = c+d;
(gdb) p &c
$3 = (int *) 0xffd7e33c
(gdb) p &d
$4 = (int *) 0xffd7e340
(gdb) bt
#0  0x0804840d in bar (c=1, d=2) at memory.cpp:4
#1  0x08048430 in foo (a=1, b=2) at memory.cpp:9
#2  0x08048463 in main () at memory.cpp:15
(gdb) f 1
#1  0x08048430 in foo (a=1, b=2) at memory.cpp:9
9  return bar(a,b);
(gdb) p &a
$5 = (int *) 0xffd7e34c
(gdb) p &b
$6 = (int *) 0xffd7e350
(gdb) f 0
#0  0x0804840d in bar (c=1, d=2) at memory.cpp:4
4  int e = c+d;
(gdb) p &c
$7 = (int *) 0xffd7e33c
(gdb) x/100hb 0xffd7e32c
0xffd7e32c: 0x90 0x83 0x04 0x08 0x85 0x82 0x04 0x08
0xffd7e334: 0x44 0xe3 0xd7 0xff 0x30 0x84 0x04 0x08
0xffd7e33c: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00
0xffd7e344: 0x68 0xe3 0xd7 0xff 0x63 0x84 0x04 0x08
0xffd7e34c: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00
0xffd7e354: 0x03 0x00 0x00 0x00 0xa0 0x2c 0xe8 0xf7
0xffd7e35c: 0x01 0x00 0x00 0x00 0x02 0x00 0x00 0x00
0xffd7e364: 0x80 0xe3 0xd7 0xff 0xd8 0xe3 0xd7 0xff
0xffd7e36c: 0x7c 0x98 0xd7 0xf7 0xa0 0x9c 0xfd 0xf7
0xffd7e374: 0x90 0x84 0x04 0x08 0xd8 0xe3 0xd7 0xff
0xffd7e37c: 0x7c 0x98 0xd7 0xf7 0x01 0x00 0x00 0x00
0xffd7e384: 0x04 0xe4 0xd7 0xff 0x0c 0xe4 0xd7 0xff
0xffd7e38c: 0xbd 0xc2 0xfc 0xf7
(gdb) bt
#0  0x0804840d in bar (c=1, d=2) at memory.cpp:4
#1  0x08048430 in foo (a=1, b=2) at memory.cpp:9
#2  0x08048463 in main () at memory.cpp:15
(gdb) f 1
#1  0x08048430 in foo (a=1, b=2) at memory.cpp:9
9  return bar(a,b);
(gdb) f 0
#0  0x0804840d in bar (c=1, d=2) at memory.cpp:4
4  int e = c+d;
(gdb) f 2
#2  0x08048463 in main () at memory.cpp:15
15  foo(a,b);
(gdb) info registers
eax            0x2 2
ecx            0xffd7e380 -2628736
edx            0x1 1
ebx            0xf7e81ff4 -135782412
esp            0xffd7e34c 0xffd7e34c
ebp            0xffd7e368 0xffd7e368
esi            0xf7fd9ca0 -134374240
edi            0x0 0
eip            0x8048463 0x8048463 <main+49>
eflags         0x200286 [ PF SF IF ID ]
cs             0x23 35
ss             0x2b 43
ds             0x2b 43
es             0x2b 43
fs             0x0 0
gs             0x63 99

 

搞清楚ebp指向栈底,esp指向栈顶
压栈时,先修改esp即减4个字节,再把esp指向的内存赋值
出栈时,先把esp指向的内存返回,再修改esp即加4个字节
在main函数中时ebp为0xffd7e368,esp为0xffd7e34c
定义变量a时
movl   $0x1,0xfffffff4(%ebp)                       //定义变量时用栈底的地址减去一定偏移量得到变量的地址,这个一定的偏移量是怎么算出来的?
定义变量b时
movl   $0x2,0xfffffff8(%ebp)
所以a的地址为0xffd7e35c,值为0x01,b的地址为0xffd7e360,值为0x02
局部变量是在栈上分配内存,先定义的变量地址小,后定义的变量地址大
调用foo(a,b)函数时
要从右向左向栈中压入参数,这里的压栈操作没有修改esp,且确保所有的参数保存在栈上后,最左边的参数是保存在栈顶上
mov    0xfffffff8(%ebp),%eax    把b取出放到eax
mov    %eax,0x4(%esp)      把eax中的值放到栈顶加4的内存上
mov    0xfffffff4(%ebp),%eax  把a取出放到eax
mov    %eax,(%esp)       把eax中的值放到栈顶的内存上

调用call指令即调用foo()
call指令会执行下列操作
1.把call指令的下一条指令的地址保存起来即压入栈中,用于在call指令返回时继续执行
压栈会修改esp
此时esp由0xffd7e34c减小成0xffd7e348
2.修改程序计数器eip,即eip指向foo的地址0x8048418

调用foo(a,b)
int foo(int a,int b)
 8048418: 55                    push   %ebp
 8048419: 89 e5                 mov    %esp,%ebp
 804841b: 83 ec 08              sub    $0x8,%esp
{
 return bar(a,b);
 804841e: 8b 45 0c              mov    0xc(%ebp),%eax
 8048421: 89 44 24 04           mov    %eax,0x4(%esp)
 8048425: 8b 45 08              mov    0x8(%ebp),%eax
 8048428: 89 04 24              mov    %eax,(%esp)
 804842b: e8 d4 ff ff ff        call   8048404 <_Z3barii>
}
 8048430: c9                    leave 
 8048431: c3                    ret 
 
首先把ebp压栈,此时esp减4变为0xffd7e344,相应的内存值为0xffd7e368即保存之前的栈底到栈上
然后把当前的栈顶赋值给栈底,即ebp由0xffd7e368变为0xffd7e344,
再减小栈顶用于存放调用bar(a,b)函数时要压栈的两个参数,即esp变成0xffd7e33c

下面就是压栈两个参数了,再调用call指令调用bar函数,下面的流程和上面的分析基本一致。
注意在取参数时都是通过ebp加偏移来取参数的
int bar(int c,int d)
 8048404: 55                    push   %ebp
 8048405: 89 e5                 mov    %esp,%ebp
 8048407: 83 ec 10              sub    $0x10,%esp
{
 int e = c+d;
 804840a: 8b 45 0c              mov    0xc(%ebp),%eax
 804840d: 03 45 08              add    0x8(%ebp),%eax
 8048410: 89 45 fc              mov    %eax,0xfffffffc(%ebp)
 return e;
 8048413: 8b 45 fc              mov    0xfffffffc(%ebp),%eax
}
 8048416: c9                    leave 
 8048417: c3                    ret
 
最后的结果存在eax中
执行leave指令
该指令是开关的push %ebp和mov %esp,%ebp的逆操作
1.把ebp的值赋给esp,恢复之前的栈顶
2.出栈赋值给ebp,恢复之前的栈底

执行ret指令
该指令是call指令的逆操作
1.现在esp所指向的栈顶保存着返回地址,把这个值恢复给eip,同时esp增加4
2.修改了程序计数器eip后,跳转到eip所指的地址继续执行。

最终就执行到main函数结束

注意函数调用和返回过程中的这些原则:
1.参数压栈传递,并且是从右向左依次压栈
2.ebp问题指向栈底
3.返回值是通过eax寄存器传递

操作系统和编译器选择了这样的方式实现c代码中的函数调用,这称为
calling convention,除此之外,操作系统还需要规定许多c代码和二进制指令
间的接口规范,统称为ABI(Application Binary Interface).

 


 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值