从汇编角度看函数调用

判断大小端

首先判断本台机器是大端还是小端

#include <stdio.h>

union {
    short a;
    char b[sizeof(short)];
} c;

int main() {
    c.a = 0x0102;
    if (c.b[0] == 0x1 && c.b[1] == 0x2) {
        printf("big endian\n");
    } else if (c.b[0] == 0x2 && c.b[1] == 0x1) {
        printf("small endian\n");
    } else {
        printf("unknown endian\n");
    }
 
    return 0;
}

// 输出:
// small endian

hello.c代码

hello.c内容如下,为了简介,不#include<stdio.h>

int add(int a, int b) {
    return a + b;
}
int main() {
    int a = 1, b = 2;
    int c = add(a, b);
    return 0;
}

预处理、编译、汇编、链接

# 预处理(生成.i文件:需要自己重定向到哪个文件)
gcc -E hello.c > hello.i

# 编译(生成.s文件,输入文件为 .i 或者 .c)
# gcc -S hello.i
gcc -S hello.c

# 汇编(生成.o文件,输入文件为 .s 或者 .c)
# gcc -c hello.s
gcc -c hello.c

# 链接(输出可执行文件)
gcc -o hello hello.o

hello.s(gcc -S hello.c生成的内容,其中有一些我们无需关注,下面呈现的是删除了这部分内容)

  • %rsp :堆栈指针寄存器,指向栈顶位置。pop操作通过增大rsp的值实现出栈,push操作通过减小rsp的值实现入栈。
  • %rbp :栈帧指针,标识当前栈帧的起始位置。
	.file	"hello.c"
	.text
	.globl	add
	.type	add, @function
add:
.LFB0:
	pushq	%rbp
	movq	%rsp, %rbp
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	-4(%rbp), %edx
	movl	-8(%rbp), %eax
	addl	%edx, %eax
	popq	%rbp
	.cfi_def_cfa 7, 8
	ret
	.cfi_endproc
.LFE0:
	.size	add, .-add
	.globl	main
	.type	main, @function
main:
.LFB1:
	.cfi_startproc
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$16, %rsp
	movl	$1, -12(%rbp)
	movl	$2, -8(%rbp)
	movl	-8(%rbp), %edx
	movl	-12(%rbp), %eax
	movl	%edx, %esi
	movl	%eax, %edi
	call	add
	movl	%eax, -4(%rbp)
	movl	$0, %eax
	leave
	ret
.LFE1:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
	.section	.note.GNU-stack,"",@progbits

也可以用 gcc -g -O0 -S hello.c 命令生成hello.s,这样会带有更多调试信息。

gdb调试

基础知识

栈的增长方向是地址减小的方向

  • rbp指向栈底

  • rsp指向栈顶

  • rip指向下一条即将执行的汇编指令的地址

  • eax存储函数返回值

  • callq addr 指令做:

    • 1、首先 call 指令把当前指令的下一条指令 push 入栈(先把这条指令的地址暂存下来,等后面被调用函数返回的时候才能找到它,并继续执行)
    • 2、将addr赋给rip(找到下一条需要执行的指令的地址,并跳转执行)
  • 在指令后面加上q代表8个字节,l代表4字节,w代表2字节,不加就看寄存器多少位

x86_64 registers

在这里插入图片描述

x86_64寄存器特性表

特性要点:

1)常用寄存器有16个,分为x86通用寄存器以及r8-r15寄存器。
  2)通用寄存器中,函数执行前后必须保持原始的寄存器有3个:是rbx、rbp、rsp。rx寄存器中,最后4个必须保持原值:r12、r13、r14、r15。
保持原值的意义是为了让当前函数有可信任的寄存器,减小在函数调用过程中的保存&恢复操作。除了rbp、rsp用于特定用途外,其余5个寄存器可随意使用。
  3)通用寄存器中,不必假设保存值可随意使用的寄存器有5个:是rax、rcx、rdx、rdi、rsi。其中rax用于第一个返回寄存器(当 然也可以用于其它用途),rdx用于第二个返回寄存器(在调用函数时也用于第三个参数寄存器)。rcx用于第四个参数。rdi用于第一个参数。rsi用于 第二个函数参数。
  4)r8、r9分配用于第5、第6个参数。

反汇编

gcc -g -O0 -o hello hello.c (开启-g -O0 不要优化,不然与上面的汇编指令对不上。注意:优化是位于编译阶段。)

# 开启汇编开关,每执行一条语句就使其
(gdb) set disassemble-next-line on

# main函数打断点
(gdb) b main

# 这里的反汇编出来的代码与上面hello.s的代码有点不一样。hello.s的立即数是十进制,下面的是十六进制。

# add函数反汇编(/r:当指定此选项时,反汇编命令将显示所有反汇编指令的原始字节值。)
(gdb) disassemble /r add
Dump of assembler code for function add:
   0x00005555555545fa <+0>:     55      		push   %rbp
   0x00005555555545fb <+1>:     48 89 e5        mov    %rsp,%rbp
   0x00005555555545fe <+4>:     89 7d fc        mov    %edi,-0x4(%rbp)
   0x0000555555554601 <+7>:     89 75 f8        mov    %esi,-0x8(%rbp)
   0x0000555555554604 <+10>:    8b 55 fc        mov    -0x4(%rbp),%edx
   0x0000555555554607 <+13>:    8b 45 f8        mov    -0x8(%rbp),%eax
   0x000055555555460a <+16>:    01 d0   		add    %edx,%eax
   0x000055555555460c <+18>:    5d      		pop    %rbp
   0x000055555555460d <+19>:    c3      		retq   
End of assembler dump.

# main函数反汇编(/r:当指定此选项时,反汇编命令将显示所有反汇编指令的原始字节值。)
(gdb) disassemble /r main
Dump of assembler code for function main:
   0x000055555555460e <+0>:     55      				push   %rbp
   0x000055555555460f <+1>:     48 89 e5        		mov    %rsp,%rbp
   0x0000555555554612 <+4>:     48 83 ec 10     		sub    $0x10,%rsp
=> 0x0000555555554616 <+8>:     c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)
   0x000055555555461d <+15>:    c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)
   0x0000555555554624 <+22>:    8b 55 f8        		mov    -0x8(%rbp),%edx
   0x0000555555554627 <+25>:    8b 45 f4        		mov    -0xc(%rbp),%eax
   0x000055555555462a <+28>:    89 d6   				mov    %edx,%esi
   0x000055555555462c <+30>:    89 c7   				mov    %eax,%edi
   0x000055555555462e <+32>:    e8 c7 ff ff ff  		callq  0x5555555545fa <add>
   0x0000555555554633 <+37>:    89 45 fc        		mov    	%eax,-0x4(%rbp)
   0x0000555555554636 <+40>:    b8 00 00 00 00  		mov    	$0x0,%eax
   0x000055555555463b <+45>:    c9      				leaveq 
   0x000055555555463c <+46>:    c3      				retq   
End of assembler dump.


# 查看寄存器:由于现在程序还没启动,没有寄存器
(gdb) info register
The program has no registers now.


调试

在函数里定义局部变量,相当于在函数中的栈帧给变量分配一些内存空间,用于存储这些变量的值。

例如:本例中的int a = 1, b = 2。就是将其分别存在rbp-0xc的位置,rbp-0x8的位置。

movl $0x1,-0xc(%rbp)

movl $0x2,-0x8(%rbp)

函数的栈帧就是[rsp, rbp]这段空间

出栈、入栈就是移动rsp指针(改变rsp寄存器的值)

# 启动程序
(gdb) start
Temporary breakpoint 2 at 0x616: file hello.c, line 5.
Starting program: /root/code/hello 

Breakpoint 1, main () at hello.c:5   # 这里输出的是<main+8>开始后的语句,前面的语句是建立栈帧的汇编指令,没有对应的c代码
5           int a = 1, b = 2;
=> 0x0000555555554616 <main+8>:         c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)
   0x000055555555461d <main+15>:        c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)



(gdb) info register # 这条指令会输出一堆寄存器,重点关注rbp、rsp、rip
rbp            0x7fffffffe660   0x7fffffffe660
rsp            0x7fffffffe650   0x7fffffffe650
rip            0x555555554616   0x555555554616 <main+8>

0x000000000000060e <+0>:     push   %rbp
0x000000000000060f <+1>:     mov    %rsp,%rbp
0x0000000000000612 <+4>:     sub    $0x10,%rsp

# 查看rbp指向的四个字节的内容(0x00005555 + 0x55554640 = 0x0000555555554640)
(gdb) x /2xw 0x7fffffffe660
0x7fffffffe660: 0x55554640      0x00005555

可见 rsp与rbp的值确实相差0x10,上面三条汇编建立main函数的栈帧。此时的栈帧是


                   	----------------------------- 0x7fffffffe650 <-- rsp
                   	|							|	
  					----------------------------- 
                   	|							|
                   	-----------------------------
                   	|							|	
  					----------------------------- 
                   	|							|
                   	----------------------------- 0x7fffffffe660 <-- rbp
                   	|			0x55554640	   	|	
  					----------------------------- 
                   	|			0x00005555		|
                   	----------------------------- 0x7fffffffe668

调用函数,其实就是一句call语句。

callq的指令格式: call + addr (addr为函数地址)

callq addr 指令做:

  • 首先 call 指令把当前指令的下一条指令 push 入栈(先把这条指令的地址暂存下来,等后面被调用函数返回的时候才能找到它,并继续执行)
  • 将addr赋给rip(找到下一条需要执行的指令的地址,并跳转执行)

调用函数时,如何传递参数:

6个以内的函数参数是通过寄存器传递的,超过6个的参数是通过栈传递的。

当参数小于7时,参数传递从右往左,传入esi、edi等等其他寄存器,

当参数大于等于于7个时, 前6个参数与参数小于7个的情况一样。多余的参数会通过栈来传递,即先将多余的参数压入栈中,然后再将栈的地址作为第7个参数传递给函数。函数在执行时会从栈中读取多余的参数。这种方式称为"栈上传参"。

参数传递顺序:从右往左,在本例中,是先传递b,再传递a。

由上面可以知道,变量a存在rbp-0xc的位置,变量b存在rbp-0x8的位置。

0x0000555555554624 <main+22>: 8b 55 f8 mov -0x8(%rbp),%edx

0x0000555555554627 <main+25>: 8b 45 f4 mov -0xc(%rbp), %eax

0x000055555555462a <main+28>: 89 d6 mov %edx,%esi

0x000055555555462c <main+30>: 89 c7 mov %eax,%edi

(gdb) s
6           int c = add(a, b);
=> 0x0000555555554624 <main+22>:        8b 55 f8        mov    -0x8(%rbp),%edx
   0x0000555555554627 <main+25>:        8b 45 f4        mov    -0xc(%rbp),%eax
   0x000055555555462a <main+28>:        89 d6   		mov    %edx,%esi
   0x000055555555462c <main+30>:        89 c7   		mov    %eax,%edi
   0x000055555555462e <main+32>:        e8 c7 ff ff ff  callq  0x5555555545fa <add>
   0x0000555555554633 <main+37>:        89 45 fc        mov    %eax,-0x4(%rbp)

(gdb) x /1xw 0x7fffffffe658
0x7fffffffe658: 0x00000002
(gdb) x /1xw 0x7fffffffe654
0x7fffffffe654: 0x00000001

/* 对应内容
5           int a = 1, b = 2;
=> 0x0000555555554616 <main+8>:         c7 45 f4 01 00 00 00    movl   $0x1,-0xc(%rbp)
   0x000055555555461d <main+15>:        c7 45 f8 02 00 00 00    movl   $0x2,-0x8(%rbp)
*/
  					----------------------------- 
                   	|							|
                   	----------------------------- 0x7fffffffe650 <-- rsp
                   	|							|	
  					----------------------------- 0x7fffffffe654 <-- -0xc(%rbp)
                   	|	a:0x00000001		   	|
                   	----------------------------- 0x7fffffffe658 <-- -0x8(%rbp)
                   	|   b:0x00000002		 	|	
  					----------------------------- 0x7fffffffe65c
                   	|							|
                   	----------------------------- 0x7fffffffe660 <-- rbp
                   	|			0x55554640	   	|	
  					----------------------------- 
                   	|			0x00005555		|
                   	----------------------------- 0x7fffffffe668

这里已经进入到add函数的栈帧

可以看出来,调用函数时要做的准备:

  1. 将参数入栈(如果大于6个)
  2. 保留当前函数的返回地址,即当前指令的下一条指令的地址
  3. 保存当前函数的栈帧指针(rbp)
(gdb) s
add (a=1, b=2) at hello.c:2
2           return a + b;
=> 0x0000555555554604 <add+10>: 8b 55 fc        mov    -0x4(%rbp),%edx
   0x0000555555554607 <add+13>: 8b 45 f8        mov    -0x8(%rbp),%eax
   0x000055555555460a <add+16>: 01 d0   		add    %edx,%eax


/* 对应内容
6           int c = add(a, b);
=> 0x0000555555554624 <main+22>:        8b 55 f8        mov    -0x8(%rbp),%edx	
   0x0000555555554627 <main+25>:        8b 45 f4        mov    -0xc(%rbp),%eax
   0x000055555555462a <main+28>:        89 d6   		mov     %edx,%esi
   0x000055555555462c <main+30>:        89 c7   		mov     %eax,%edi
   0x000055555555462e <main+32>:        e8 c7 ff ff ff  callq  0x5555555545fa <add>
   0x0000555555554633 <main+37>:        89 45 fc        mov    %eax,-0x4(%rbp)
*/

# 可见a存入了edi寄存器,b存入了esi
(gdb) info register edx esi eax edi
edx            0x2      2
esi            0x2      2
eax            0x1      1
edi            0x1      1


# callq 0x5555555545fa <add> 指令做了什么呢?我们首先抛出结论:
# 首先 call 指令把当前指令的下一条指令 push 入栈(先把这条指令的地址暂存下来,等后面被调用函数返回的时候才能找到它,并继续执行)
# 找到下一条需要执行的指令的地址(即 empty 的地址),并跳转执行。

# 这里rsp和rbp的变化是由于:
# 1、callq 0x5555555545fa <add>指令将main函数的返回地址(0x0000555555554633 <main+37>)入栈,rsp减8
# 2、add函数中前两条建立栈帧的指令:
# 0x00005555555545fa <+0>:     55      			push   %rbp
# 0x00005555555545fb <+1>:     48 89 e5        	mov    %rsp,%rbp
# 导致rsp再减8,然后rsp的值赋给rbp,由下面的info register可以看出来

# 查看两个栈指针
(gdb) info register rsp rbp
rsp            0x7fffffffe640   0x7fffffffe640
rbp            0x7fffffffe640   0x7fffffffe640


# 查看入栈的16个字节的内容(先是main函数返回地址入栈,再是rbp入栈)
# main函数返回地址是指add函数返回后紧接着执行的main函数中的下一条汇编指令的地址:
# 0x0000555555554633 <main+37>:        89 45 fc        mov    %eax,-0x4(%rbp)
(gdb) x /4xw 0x7fffffffe640
0x7fffffffe640: 0xffffe660      0x00007fff      0x55554633      0x00005555
					
					
					----------------------------- 0x7fffffffe640 <-- rbp rsp
                   	|	0xffffe660|	main函数	   |
  					                              0x7fffffffe644
                   	|	0x00007fff|	rbp的内容	  |
					----------------------------- 0x7fffffffe648
                   	|	0x55554633|	main函数	   |
  												  0x7fffffffe64c
                   	|	0x00005555|	返回地址	  |
                   	----------------------------- 0x7fffffffe650 
                   	|							|	
  					----------------------------- 0x7fffffffe654 
                   	|	a:0x00000001		   	|
                   	----------------------------- 0x7fffffffe658
                   	|   b:0x00000002		 	|	
  					----------------------------- 0x7fffffffe65c
                   	|							|
                   	----------------------------- 0x7fffffffe660 
                   	|			0x55554640	   	|	
  					----------------------------- 0x7fffffffe664
                   	|			0x00005555		|
                   	----------------------------- 0x7fffffffe668
                   	

进入到add函数,有些隐藏操作:将参数入栈

(gdb) s
3       }
=> 0x000055555555460c <add+18>: 5d      pop    %rbp
   0x000055555555460d <add+19>: c3      retq


/* 对应内容
由于add函数有两个参数,而且将函数入栈这个操作是隐藏的,没有对应某一条c语言语句,所以gdb调试时是没有显示这两条语句的,但实际上已经执行。
int add(int a, int b) {
    return a + b;
}
0x00005555555545fe <+4>:     89 7d fc        mov    %edi,-0x4(%rbp)
0x0000555555554601 <+7>:     89 75 f8        mov    %esi,-0x8(%rbp)

gdb显示的内容
add (a=1, b=2) at hello.c:2
2           return a + b;
=> 0x0000555555554604 <add+10>: 8b 55 fc        mov    -0x4(%rbp),%edx
   0x0000555555554607 <add+13>: 8b 45 f8        mov    -0x8(%rbp),%eax
   0x000055555555460a <add+16>: 01 d0   		add    %edx,%eax
*/


# 查看两个栈指针(注意:这里rbp和rsp的值依然是相同的,代表并没有为add函数访分配栈帧)
(gdb) info register rsp rbp
rsp            0x7fffffffe640   0x7fffffffe640
rbp            0x7fffffffe640   0x7fffffffe640

# 查看-0x4(%rbp)和-0x8(%rbp)
(gdb) p $rbp-0x4
$3 = (void *) 0x7fffffffe63c
(gdb) x /1xw $rbp-0x4
0x7fffffffe63c: 0x00000001
(gdb) p $rbp-0x8
$4 = (void *) 0x7fffffffe638
(gdb) x /1xw $rbp-0x8
0x7fffffffe638: 0x00000002

# 查看edx和eax两个寄存器的值
# 过程:
#	mov    -0x4(%rbp),%edx  将参数a的值1移入edx寄存器
#	mov    -0x8(%rbp),%eax	将参数b的值2移入eax寄存器
#	add    %edx,%eax		将两个寄存器的值相加,将结果存到eax寄存器
(gdb) info register edx eax
edx            0x1      1
eax            0x3      3
					----------------------------- 
                   	|							|
					----------------------------- 0x7fffffffe638
                   	|   参数b的值:0x00000002	  |
					----------------------------- 0x7fffffffe63c
                   	|	参数a的值:0x00000001	  |
					----------------------------- 0x7fffffffe640 <-- rbp rsp
                   	|	0xffffe660|	main函数	   |
  					                              0x7fffffffe644
                   	|	0x00007fff|	rbp的内容	  |
					----------------------------- 0x7fffffffe648
                   	|	0x55554633|	main函数	   |
  												  0x7fffffffe64c
                   	|	0x00005555|	返回地址	  |
                   	----------------------------- 0x7fffffffe650 
                   	|							|	
  					----------------------------- 0x7fffffffe654 
                   	|	a:0x00000001		   	|
                   	----------------------------- 0x7fffffffe658
                   	|   b:0x00000002		 	|	
  					----------------------------- 0x7fffffffe65c
                   	|							|
                   	----------------------------- 0x7fffffffe660 
                   	|			0x55554640	   	|	
  					----------------------------- 0x7fffffffe664
                   	|			0x00005555		|
                   	----------------------------- 0x7fffffffe668

从add函数返回main函数做的操作:

  1. pop %rbp,恢复栈帧,rbp寄存器的值恢复为main函数栈帧中rbp的值
  2. retq,恢复函数现场,即将栈中main函数的返回地址赋值给rip,这样就可以继续执行main函数剩下的部分。
  3. 经过第1和2步,(如果add函数有申请栈空间,也会释放)rsp也恢复到main函数栈帧中的值
(gdb) s
main () at hello.c:7
7           return 0;
=> 0x0000555555554636 <main+40>:        b8 00 00 00 00  mov    $0x0,%eax


/* 对应内容
3       }
=> 0x000055555555460c <add+18>: 5d      pop    %rbp
   0x000055555555460d <add+19>: c3      retq
   
pop    %rbp可以分解为两步 
1、将栈顶的值赋给rbp寄存器	:  	rbp寄存器的值变为main函数栈帧中rbp的值,变为0x00007fffffffe660
2、rsp指针移动			:	rsp+8 变为 0x7fffffffe648

popq、popl、pop都是汇编指令中的栈操作指令,它们的功能都是弹出栈顶元素到指定寄存器中。
popq指令是64位架构下的操作,用于将64位数据弹出栈顶并存入指定的64位寄存器;
popl指令是32位架构下的操作,用于将32位数据弹出栈顶并存入指定的32位寄存器;
pop指令则是通用的弹出操作指令,它既可以弹出32位数据(在32位架构下),也可以弹出64位数据(在64位架构下),而具体是32位还是64位则取决于对应的寄存器的大小

retq中的q是指64位操作数。
retq相当于:
popq %rip   同样可以分为两步
1、rsp指向的8个字节赋给rip,rip的值变为0x0000555555554633。指向0x0000555555554633 <+37>:    89 45 fc  mov    %eax,-0x4(%rbp)
2、同时rsp+8,变为0x7fffffffe650

由于现在已经从add函数返回到main函数,这个将add返回值赋给c的操作会隐式执行(gdb调的时候看不到),
所以rip的值为下一条returnn 0 对应的:0x0000555555554636 <main+40>:        	b8 00 00 00 00  	mov    $0x0,%eax。
int c = add(a, b);
0x0000555555554633 <+37>:    			89 45 fc        	mov    %eax,-0x4(%rbp)
0x0000555555554636 <main+40>:        	b8 00 00 00 00  	mov    $0x0,%eax
*/


# 查看两个栈指针(注意:已经退回到原先main函数的栈帧)
(gdb) info register rbp rsp
rbp            0x7fffffffe660   0x7fffffffe660
rsp            0x7fffffffe650   0x7fffffffe650

# 查看rip寄存器的值(rip存储的是下一次即将执行的汇编指令的地址)
# 已经指回main函数的指令
(gdb) info register rip
rip            0x555555554636   0x555555554636 <main+40>

# add函数的返回值
(gdb) info register eax
eax            0x3      3

# add函数的返回值放在main函数的栈帧:mov    %eax,-0x4(%rbp)
(gdb) x /1xw $rbp-0x4
0x7fffffffe65c: 0x00000003
					----------------------------- 
                   	|					     	|
                   	----------------------------- 0x7fffffffe650 <-- rsp
                   	|							|	
  					----------------------------- 0x7fffffffe654 
                   	|	a:0x00000001		   	|
                   	----------------------------- 0x7fffffffe658
                   	|   b:0x00000002		 	|	
  					----------------------------- 0x7fffffffe65c
                   	|c:0x00000003,add函数的返回值	|
                   	----------------------------- 0x7fffffffe660 <-- rbp
                   	|			0x55554640	   	|	
  					----------------------------- 0x7fffffffe664
                   	|			0x00005555		|
                   	----------------------------- 0x7fffffffe668
(gdb) s
8       }=> 0x000055555555463b <main+45>:       c9      leaveq 
   0x000055555555463c <main+46>:        		c3      retq
   
/*对应内容:
main () at hello.c:7
7           return 0;
=> 0x0000555555554636 <main+40>:        b8 00 00 00 00  mov    $0x0,%eax
*/

(gdb) info register rbp rsp eax
rbp            0x7fffffffe660   0x7fffffffe660 
rsp            0x7fffffffe650   0x7fffffffe650
eax            0x0      0  # main函数的返回值

# main函数rbp下的依次是调用main函数的函数的rbp地址以及其返回地址
(gdb) x /4xw 0x7fffffffe660
0x7fffffffe660: 0x55554640      0x00005555      0xf7a03c87      0x00007fff

					----------------------------- 
                   	|					     	|
                   	----------------------------- 0x7fffffffe650 <-- rsp
                   	|							|	
  					----------------------------- 0x7fffffffe654 
                   	|	a:0x00000001		   	|
                   	----------------------------- 0x7fffffffe658
                   	|   b:0x00000002		 	|	
  					----------------------------- 0x7fffffffe65c
                   	|c:0x00000003,add函数的返回值	|
                   	----------------------------- 0x7fffffffe660 <-- rbp
                   	|    0x55554640 main函数     |	
  					                              0x7fffffffe664
                   	|    0x00005555 调用者的rbp   |
                   	----------------------------- 0x7fffffffe668
                   	|	0xf7a03c87	main函数调用者 |	
  												  0x7fffffffe66c
                   	|	0x00007fff	的返回地址     |
                   	----------------------------- 0x7fffffffe670

hello.c之外的汇编,暂时看不懂

(gdb) s
__libc_start_main (main=0x55555555460e <main>, argc=1, argv=0x7fffffffe748, 
    init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7fffffffe738) at ../csu/libc-start.c:344
344     ../csu/libc-start.c: No such file or directory.
=> 0x00007ffff7a03c87 <__libc_start_main+231>:  89 c7   		mov    %eax,%edi
   0x00007ffff7a03c89 <__libc_start_main+233>:  e8 82 14 02 00  callq  0x7ffff7a25110 <__GI_exit>
/*
对应内容:
8       }=> 0x000055555555463b <main+45>:       c9      leaveq 
   0x000055555555463c <main+46>:        		c3      retq

leaveq用于函数的退出和栈帧的清理。
这个指令通常在函数的结尾处执行,以完成函数的退出操作,包括释放局部变量所占用的栈空间和返回到调用函数的位置。leaveq 的作用如下:
恢复栈帧基址指针 %rbp:
leaveq 指令会将栈帧基址指针 %rbp 的值恢复为之前保存在栈上的值。这个值通常是在函数开始时通过 push %rbp 指令保存的。
恢复 %rbp 的值可以将当前函数的栈帧的基址重新设置为调用函数的栈帧,从而回到了调用函数的上下文

retq中的q是指64位操作数。
retq相当于:
popq %rip   同样可以分为两步
1、rsp指向的8个字节赋给rip,rip的值变为0x0000555555554633。指向0x0000555555554633 <+37>:    89 45 fc  mov    %eax,-0x4(%rbp)
2、同时rsp+8,变为0x7fffffffe650
*/

# 注意此时(rbp位于rsp上面,此时已经不符合栈帧了)
(gdb) info register rbp rsp
rbp            0x555555554640   0x555555554640 <__libc_csu_init>
rsp            0x7fffffffe670   0x7fffffffe670

					-----------------------------
                   	|					     	|
                   	-----------------------------
                   	|					     	|
                   	----------------------------- 0x555555554640 <-- rbp
                   	
							...........
							
					----------------------------- 
                   	|					     	|
                   	----------------------------- 0x7fffffffe650
                   	|							|	
  					----------------------------- 0x7fffffffe654 
                   	|	a:0x00000001		   	|
                   	----------------------------- 0x7fffffffe658
                   	|   b:0x00000002		 	|	
  					----------------------------- 0x7fffffffe65c
                   	|c:0x00000003,add函数的返回值	|
                   	----------------------------- 0x7fffffffe660
                   	|    0x55554640 main函数     |	
  					                              0x7fffffffe664
                   	|    0x00005555 调用者的rbp   |
                   	----------------------------- 0x7fffffffe668
                   	|	0xf7a03c87	main函数调用者 |	
  												  0x7fffffffe66c
                   	|	0x00007fff	的返回地址     |
                   	----------------------------- 0x7fffffffe670 <-- rsp
                  
                   	
                   	
                   	
                   	
(gdb) s
__libc_start_main (main=0x55555555460e <main>, argc=1, argv=0x7fffffffe748, 
    init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7fffffffe738) at ../csu/libc-start.c:344
344     ../csu/libc-start.c: No such file or directory.
=> 0x00007ffff7a03c87 <__libc_start_main+231>:  89 c7   		mov    %eax,%edi
   0x00007ffff7a03c89 <__libc_start_main+233>:  e8 82 14 02 00  callq  0x7ffff7a25110 <__GI_exit>
/*
对应内容:
__libc_start_main (main=0x55555555460e <main>, argc=1, argv=0x7fffffffe748, 
    init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, 
    stack_end=0x7fffffffe738) at ../csu/libc-start.c:344
344     ../csu/libc-start.c: No such file or directory.
=> 0x00007ffff7a03c87 <__libc_start_main+231>:  89 c7   		mov    %eax,%edi
   0x00007ffff7a03c89 <__libc_start_main+233>:  e8 82 14 02 00  callq  0x7ffff7a25110 <__GI_exit>
*/

(gdb) info register rsp rbp
rsp            0x7fffffffe668   0x7fffffffe668
rbp            0x555555554640   0x555555554640 <__libc_csu_init>
(gdb) info register rip
rip            0x7ffff7a25110   0x7ffff7a25110 <__GI_exit>
					-----------------------------
                   	|					     	|
                   	-----------------------------
                   	|					     	|
                   	----------------------------- 0x555555554640 <-- rbp
                   	
							...........
							
					----------------------------- 
                   	|					     	|
                   	----------------------------- 0x7fffffffe650
                   	|							|	
  					----------------------------- 0x7fffffffe654 
                   	|	a:0x00000001		   	|
                   	----------------------------- 0x7fffffffe658
                   	|   b:0x00000002		 	|	
  					----------------------------- 0x7fffffffe65c
                   	|c:0x00000003,add函数的返回值	|
                   	----------------------------- 0x7fffffffe660
                   	|    0x55554640 main函数     |	
  					                              0x7fffffffe664
                   	|    0x00005555 调用者的rbp   |
                   	----------------------------- 0x7fffffffe668 <-- rsp
                   	|	0xf7a03c87	main函数调用者 |	
  												  0x7fffffffe66c
                   	|	0x00007fff	的返回地址     |
                   	----------------------------- 0x7fffffffe670 
                  
                   	
                   	
                   	
                   	

总结

  • 调用函数:

    • (参数入栈)参数传递:参数按照从右往左顺序。当参数在6个及以内,就都用寄存器传递。当参数大于等于7个,从第7个参数起,将剩余的参数压入栈中(压入调用者的栈帧中),这样被调用函数(callee)就能取出剩下的参数。
    • 保存现场:
      • 保存调用者(caller)的返回地址(即调用者当前指令下一条指令的地址),这样后面被调用函数(callee)返回,可以继续执行
      • 保存调用者(caller)的栈帧,其实就是rbp寄存器。这样后面就能恢复caller的栈帧。
  • 函数返回

参数大于6个的情况

more_args.c

这里一个有a-i一共9个参数。

int add(int a, int b, int c, int d, int e, int f, int g, int h, int i) {
    return a+b+c+d+e+f+g+h+i;
}
int main() {
    int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6, g = 7, h = 8, i = 9;
    int j = add(a,b,c,d,e,f,g,h,i);
    return 0;
}

gcc -S more_args.c (下面删掉了一些无需关注的内容)

main函数:

  • 一堆 movl $xx, -xx(%rbp) 指令,在main的栈帧中先存下这9个参数。从左往右,按照定义顺序来。
  • 先将前6个参数放到寄存器。从右往左,从f到a。
  • 再将剩余的3个参数入栈。从右往左,从i到g。
main:
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$48, %rsp
	# 在main的栈帧中先存下这9个参数
	movl	$1, -40(%rbp)  	# a
	movl	$2, -36(%rbp)  	# b
	movl	$3, -32(%rbp)	# c
	movl	$4, -28(%rbp)	# d
	movl	$5, -24(%rbp)	# e
	movl	$6, -20(%rbp)	# f
	movl	$7, -16(%rbp)	# g
	movl	$8, -12(%rbp)	# h 
	movl	$9, -8(%rbp)  	# i
	# 前6个参数存入寄存器
	movl	-20(%rbp), %r9d # f 
	movl	-24(%rbp), %r8d	# e
	movl	-28(%rbp), %ecx	# d
	movl	-32(%rbp), %edx # c
	movl	-36(%rbp), %esi # b
	movl	-40(%rbp), %eax	# a
	# 剩下的3个参数存入栈
	movl	-8(%rbp), %edi 	# i
	pushq	%rdi
	movl	-12(%rbp), %edi	# h
	pushq	%rdi
	movl	-16(%rbp), %edi	# g
	pushq	%rdi
	
	movl	%eax, %edi
	call	add
	addq	$24, %rsp
	movl	%eax, -4(%rbp)
	movl	$0, %eax
	leave
	ret
add:
	# 建立栈帧
	pushq	%rbp
	movq	%rsp, %rbp
	# 将前6个参数存到当前的栈帧中
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	%edx, -12(%rbp)
	movl	%ecx, -16(%rbp)
	movl	%r8d, -20(%rbp)
	movl	%r9d, -24(%rbp)
	# 将前6个参数相加
	movl	-4(%rbp), %edx
	movl	-8(%rbp), %eax
	addl	%eax, %edx
	movl	-12(%rbp), %eax
	addl	%eax, %edx
	movl	-16(%rbp), %eax
	addl	%eax, %edx
	movl	-20(%rbp), %eax
	addl	%eax, %edx
	movl	-24(%rbp), %eax
	addl	%eax, %edx
	# 从栈中取出剩下的三个参数相加
	movl	16(%rbp), %eax
	addl	%eax, %edx
	movl	24(%rbp), %eax
	addl	%eax, %edx
	movl	32(%rbp), %eax
	addl	%edx, %eax
	# 返回
	popq	%rbp
	ret


参数为6的情况

int add(int a, int b, int c, int d, int e, int f) {
    return a+b+c+d+e+f;
}
int main() {
    int a = 1, b = 2, c = 3, d = 4, e = 5, f = 6;
    int g = add(a,b,c,d,e,f);
    return 0;
}
add:
	pushq	%rbp
	movq	%rsp, %rbp
	movl	%edi, -4(%rbp)
	movl	%esi, -8(%rbp)
	movl	%edx, -12(%rbp)
	movl	%ecx, -16(%rbp)
	movl	%r8d, -20(%rbp)
	movl	%r9d, -24(%rbp)
	movl	-4(%rbp), %edx
	movl	-8(%rbp), %eax
	addl	%eax, %edx
	movl	-12(%rbp), %eax
	addl	%eax, %edx
	movl	-16(%rbp), %eax
	addl	%eax, %edx
	movl	-20(%rbp), %eax
	addl	%eax, %edx
	movl	-24(%rbp), %eax
	addl	%edx, %eax
	popq	%rbp
	ret

main:
	pushq	%rbp
	movq	%rsp, %rbp
	subq	$32, %rsp
	# 6个参数放入main函数的栈帧中
	movl	$1, -28(%rbp) 	# a
	movl	$2, -24(%rbp)	# b
	movl	$3, -20(%rbp)	# c
	movl	$4, -16(%rbp)	# d
	movl	$5, -12(%rbp)	# e
	movl	$6, -8(%rbp)	# f
	# 6个参数全部通过寄存器传递给add函数
	# a->eax, b->esi, c->edx
	# d->ecx, e->edi, f->r8d
	movl	-8(%rbp), %r8d	# f
	movl	-12(%rbp), %edi	# e
	movl	-16(%rbp), %ecx	# d
	movl	-20(%rbp), %edx	# c
	movl	-24(%rbp), %esi	# b
	movl	-28(%rbp), %eax	# a
	movl	%r8d, %r9d
	movl	%edi, %r8d
	movl	%eax, %edi
	call	add
	movl	%eax, -4(%rbp)
	movl	$0, %eax
	leave
	ret

  • 16
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
函数调用过程是程序中常见的一种操作,它通常涉及到参数传递、栈帧的建立与销毁、返回值的传递等多个方面。从汇编的角度来看,函数调用过程可以分为以下几个步骤: 1. 将函数的参数压入栈中。在调用函数时,需要将函数所需的参数传递给它。这些参数通常以一定的顺序压入栈中,以便在函数内部使用。在 x86 架构中,参数的传递是通过将参数压入栈顶实现的。 2. 调用函数。函数调用的指令通常是 CALL 指令。在调用函数前,需要将函数的入口地址压入栈中,以便在函数执行完毕后返回到调用位置。CALL 指令会将当前的程序计数器(PC)压入栈中,并将函数的入口地址作为新的 PC。 3. 建立栈帧。在函数被调用时,需要为函数建立一个独立的栈帧,以便在函数内部使用局部变量和临时变量。栈帧通常包括以下几个部分:返回地址、旧的基址指针、局部变量和临时变量。在 x86 架构中,栈帧的建立是通过将 ESP 寄存器减去一个固定的值实现的。 4. 执行函数。在函数被调用后,CPU 会跳转到函数的入口地址并开始执行函数。函数内部可以通过栈中的参数和局部变量完成相应的计算和操作。 5. 返回值传递。在函数执行完毕后,需要将函数的返回值传递给调用者。在 x86 架构中,函数的返回值通常通过 EAX 寄存器传递。 6. 销毁栈帧。在函数执行完毕后,需要将栈帧销毁,以便释放栈空间。栈帧的销毁通常是通过将 ESP 寄存器还原到旧的基址指针处实现的。 7. 返回到调用位置。在函数执行完毕后,需要返回到函数被调用的位置。在 x86 架构中,返回指令通常是 RET 指令。RET 指令会将栈顶的返回地址弹出,并将其作为新的 PC。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值