-
-
[原创]从GDB中观察x86-64函数控制流
-
发表于: 3天前 853
-
测试代码
#include <stdio.h>
int test_if(int x) {
if (x > 10) {
return 1;
} else {
return 0;
}
}
int test_for(int n) {
int sum = 0;
for (int i = 0; i < n; i++) {
sum += i;
}
return sum;
}
int test_while(int x) {
while (x > 0) {
x--;
}
return x;
}
int test_switch(int x) {
switch (x) {
case 0: return 100;
case 1: return 200;
case 2: return 300;
default: return -1;
}
}
int main() {
printf("%d\n", test_if(15));
printf("%d\n", test_for(10));
printf("%d\n", test_while(5));
printf("%d\n", test_switch(1));
return 0;
}运行环境:
Ubuntu 20.04.6 LTS
编译指令:
gcc -g -O0 -o main main.c
gcc版本
gcc version 9.4.0
调试工具:
GDB
gdb ./main
启动调试:进入main函数内部
(gdb) x/10i $rip0x5555555551f7 <main>: endbr64
0x5555555551fb <main+4>: push %rbp
0x5555555551fc <main+5>: mov %rsp,%rbp
0x5555555551ff <main+8>: mov $0xf,%edi
0x555555555204 <main+13>: callq 0x555555555149 <test_if>
发现在调用test_if时,传递了一个参数,参数为0xf
单步调试进入test_if函数:
0x555555555149 <test_if>: endbr64
0x55555555514d <test_if+4>: push %rbp
0x55555555514e <test_if+5>: mov %rsp,%rbp
0x555555555151 <test_if+8>: mov %edi,-0x4(%rbp)
0x555555555154 <test_if+11>: cmpl $0xa,-0x4(%rbp)
0x555555555158 <test_if+15>: jle 0x555555555161 <test_if+24>
0x55555555515a <test_if+17>: mov $0x1,%eax
0x55555555515f <test_if+22>: jmp 0x555555555166 <test_if+29>
0x555555555161 <test_if+24>: mov $0x0,%eax
0x555555555166 <test_if+29>: pop %rbp
0x555555555167 <test_if+30>: retq
发现cmpl 指令,对比了 0xa,与参数0xf
jle 指令,判断0xf 是否小于等于 0xa 通过jle指令可以看出是有符号比较
0xa = 10, 0xf = 15
如果小于等于则跳转0x555555555161,将0赋值给eax,后返回函数,则函数返回值为0
如果大于0xa则继续向下执行
将eax赋值为1跳转到0x555555555166,函数返回。
即:
if (a <= b){
retrun 0;
}else{
return 1;
}
该函数返回值为1
0x555555555209 <main+18>: mov %eax,%esi
0x55555555520b <main+20>: lea 0xdf2(%rip),%rdi # 0x555555556004
0x555555555212 <main+27>: mov $0x0,%eax
0x555555555217 <main+32>: callq 0x555555555050 <printf@plt>
0x55555555521c <main+37>: mov $0xa,%edi
执行printf函数输出1
继续执行
0x55555555521c <main+37>: mov $0xa,%edi
0x555555555221 <main+42>: callq 0x555555555168 <test_for>
传递了一个参数0xa
进入test_for:
0x555555555168 <test_for>: endbr64
0x55555555516c <test_for+4>: push %rbp
0x55555555516d <test_for+5>: mov %rsp,%rbp
0x555555555170 <test_for+8>: mov %edi,-0x14(%rbp)
0x555555555173 <test_for+11>: movl $0x0,-0x8(%rbp)
0x55555555517a <test_for+18>: movl $0x0,-0x4(%rbp)
0x555555555181 <test_for+25>: jmp 0x55555555518d <test_for+37>
0x555555555183 <test_for+27>: mov -0x4(%rbp),%eax
0x555555555186 <test_for+30>: add %eax,-0x8(%rbp)
0x555555555189 <test_for+33>: addl $0x1,-0x4(%rbp)
0x55555555518d <test_for+37>: mov -0x4(%rbp),%eax
0x555555555190 <test_for+40>: cmp -0x14(%rbp),%eax
0x555555555193 <test_for+43>: jl 0x555555555183 <test_for+27>
0x555555555195 <test_for+45>: mov -0x8(%rbp),%eax
0x555555555198 <test_for+48>: pop %rbp
0x555555555199 <test_for+49>: retq
发现定义两个局部变量
rbp-4 = 0 rbp-8 = 0; 4字节,暂时把他当做int
直接跳转到jmp 0x55555555518d
观察0x55555555518d位置代码
0x55555555518d <test_for+37>: mov -0x4(%rbp),%eax
0x555555555190 <test_for+40>: cmp -0x14(%rbp),%eax
0x555555555193 <test_for+43>: jl 0x555555555183 <test_for+27>
判断rbp-4为变量i,即for(int i = 0; i < rbp-0x14 = 0xa)
如果小于,进入循环体
0x555555555183 <test_for+27>: mov -0x4(%rbp),%eax
0x555555555186 <test_for+30>: add %eax,-0x8(%rbp)
0x555555555189 <test_for+33>: addl $0x1,-0x4(%rbp)
0x55555555518d <test_for+37>: mov -0x4(%rbp),%eax
将i 赋值给eax 然后 用 j = i + j 即 j += i;
addl $0x1,-0x4(%rbp) ;即为 i自增1
补充循环递增以及循环体
int j = 0;
for(int i = 0; i < 10; i++){
j += i;
}
执行完毕 输出45
继续执行
传入 5进入test_while
1239: bf 05 00 00 00 mov $0x5,%edi
123e: e8 57 ff ff ff callq 119a <test_while>
进入 test_while:
0x55555555519a <test_while>: endbr64
0x55555555519e <test_while+4>: push %rbp
0x55555555519f <test_while+5>: mov %rsp,%rbp
0x5555555551a2 <test_while+8>: mov %edi,-0x4(%rbp)
0x5555555551a5 <test_while+11>: jmp 0x5555555551ab <test_while+17>
0x5555555551a7 <test_while+13>: subl $0x1,-0x4(%rbp)
0x5555555551ab <test_while+17>: cmpl $0x0,-0x4(%rbp)
0x5555555551af <test_while+21>: jg 0x5555555551a7 <test_while+13>
0x5555555551b1 <test_while+23>: mov -0x4(%rbp),%eax
0x5555555551b4 <test_while+26>: pop %rbp
0x5555555551b5 <test_while+27>: retq
mov %edi,-0x4(%rbp)保存传参在 rbp-4位置
后直接跳转0x5555555551ab
直接比较传入的5是否大于0;
cmpl $0x0,-0x4(%rbp)
jg 0x5555555551a7 <test_while+13>
如果大于进入循环体
0x5555555551a7 <test_while+13>: subl $0x1,-0x4(%rbp)
每次 传入的变量-1
while (num > 0){
num --;
}
比较 for 与 while发现,流程控制骨架都是相同的
执行完毕 输出0
继续执行
传入参数:1
0x555555555256 <main+95>: mov $0x1,%edi
0x55555555525b <main+100>: callq 0x5555555551b6 <test_switch>
进入test_switch函数:
0x5555555551b6 <test_switch>: endbr64
0x5555555551ba <test_switch+4>: push %rbp
0x5555555551bb <test_switch+5>: mov %rsp,%rbp
0x5555555551be <test_switch+8>: mov %edi,-0x4(%rbp)
0x5555555551c1 <test_switch+11>: cmpl $0x2,-0x4(%rbp)
0x5555555551c5 <test_switch+15>: je 0x5555555551e9 <test_switch+51>
0x5555555551c7 <test_switch+17>: cmpl $0x2,-0x4(%rbp) ;感觉冗余了,完全可以比较一次重复上一次的标志位
0x5555555551cb <test_switch+21>: jg 0x5555555551f0 <test_switch+58>
0x5555555551cd <test_switch+23>: cmpl $0x0,-0x4(%rbp)
0x5555555551d1 <test_switch+27>: je 0x5555555551db <test_switch+37>
0x5555555551d3 <test_switch+29>: cmpl $0x1,-0x4(%rbp)
0x5555555551d7 <test_switch+33>: je 0x5555555551e2 <test_switch+44>
0x5555555551d9 <test_switch+35>: jmp 0x5555555551f0 <test_switch+58>
0x5555555551db <test_switch+37>: mov $0x64,%eax
0x5555555551e0 <test_switch+42>: jmp 0x5555555551f5 <test_switch+63>
0x5555555551e2 <test_switch+44>: mov $0xc8,%eax
0x5555555551e7 <test_switch+49>: jmp 0x5555555551f5 <test_switch+63>
0x5555555551e9 <test_switch+51>: mov $0x12c,%eax
0x5555555551ee <test_switch+56>: jmp 0x5555555551f5 <test_switch+63>
0x5555555551f0 <test_switch+58>: mov $0xffffffff,%eax
0x5555555551f5 <test_switch+63>: pop %rbp
0x5555555551f6 <test_switch+64>: retq
先保存函数参数
判断 num == 2;如果等于 mov $0x12c,%eax 函数返回 0x12c
判断 num > 2 : 如果大于 mov $0xffffffff,%eax 函数返回 -1;
判断 num == 0; 如果等于 mov $0x64,%eax 函数返回0x64
判断 num == 1; 如果等于 mov $0xc8,%eax 函数返回0xc8
default return -1;
还原代码:
switch (num){
case 2: return 0x12c;
case 0: return 0x64;
case 1: return 0xc8;
default: return -1;
}
对比原代码,发现编译器不一定会按照源码的顺序去生成代码,可能偷偷改了
由此可见 是使用cmp+je 链实现的 而不是跳转表
最后应该输出 0xc8
总结: 在O0编译下:
if-else:
cmp → 条件跳转 → 一个分支的代码 → jmp 跳过另一个分支 → 另一个分支的代码
(条件跳转跳到哪个分支,取决于编译器怎么反转条件)
循环:
jmp 条件检查
循环体
条件检查:cmp + 条件跳转回到循环体
(不区分 for 和 while,汇编结构相同)
switch(少量case):
连续的 cmp + je,跳到对应 case
(case 顺序可能被编译器重排)