首页
社区
课程
招聘
[原创]从GDB中观察x86-64函数控制流
发表于: 3天前 853

[原创]从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 $rip

       0x5555555551f7 <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 顺序可能被编译器重排)



传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

收藏
免费 0
支持
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回