判断大小端
首先判断本台机器是大端还是小端
#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函数的栈帧
可以看出来,调用函数时要做的准备:
- 将参数入栈(如果大于6个)
- 保留当前函数的返回地址,即当前指令的下一条指令的地址
- 保存当前函数的栈帧指针(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函数做的操作:
- pop %rbp,恢复栈帧,rbp寄存器的值恢复为main函数栈帧中rbp的值
- retq,恢复函数现场,即将栈中main函数的返回地址赋值给rip,这样就可以继续执行main函数剩下的部分。
- 经过第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