Linux C/C++调试:快速找出GCC O2优化编译的汇编与C源码的对应关系

相信很多C/C++程序员都有过线上gdb调试程序的经历,没有或者剥离了调试信息,线上运行的也是GCC -O2编译优化后的release版本,这时就要求快速看懂汇编指令和对应C代码的对应关系。进而,找到自己需要gdb print出的关键内存信息,或针对性的打断点以及观察点等,这些调试一切的前提都是要求快速看懂反汇编指令和对应源代码的对应关系。一个函数的反汇编短则数百,长则近万行甚至更多,如何一下子找到其与C源码的对应关系呢?我通常就是快速搜索关键指令,并结合其上下文来理解推断。当然,这一切的前提是对汇编常用指令以及x86 calling convention有一定的了解。

搜索关键指令

  • 找全局变量或函数等符号;
  • 找立即数,一些特定的宏或者枚举,AT&T汇编立即数是$开头的十六进制数;
  • 找位运算、逻辑运算、乘法、加锁等特殊指令;
  • callq ,那么其前面一定是入参操作,后面是判断返回值;
  • test %rax,%rax ,判断寄存器rax是否为0,大概率是判断前面的函数调用返回值;
  • lea Load effect address, 常用来(基址+offset*单位)算地址,本质是计算一个数值,有些数据计算也会优化成这个;
  • xor %reg, %reg, 把寄存器reg清零,异或操作,相同为0,相当于赋值0,常见的优化操作;

结合上下文推断

当快速定位了一些关键指令后,接下来就是快速根据上下文进行理解。从关键指令,马上就可以推断一些寄存器存的值的意义,是一个地址,还是一个中间计算的值,那么顺着这个指令往后走,如果是函数调用往前可以看入参操作,一般情况是往后看,前面大都无法确定哪里jump过来的。对着C/C++源码,还是相对比较容易推断的。当然,如果有调试信息,那么可以用disass /m <func>来进行查看,会显示源码和汇编指令的关系,当然自己要熟悉指令,有时候优化的厉害gdb /m看的也不太准。下面用一个例子来说明:

之前写过一般文章,按照页对齐来申请对应内存,里面的函数 malloc_align_page很短,源码如下:

struct mem_align {
    void *origin_start;  // for free
    void *start;         // data addr start, align page size
    void *end;           // data addr end,   align page size
    void *origin_end;
};

int malloc_align_page(size_t memsize, struct mem_align *mem)
{
    if (memsize == 0 || mem == NULL)
        return -1;

    memset(mem, 0, sizeof(*mem));
    long pagesize = sysconf(_SC_PAGE_SIZE);
    if (pagesize == -1) {
        perror("sysconf err");
        return -1;
    }

    size_t datasize = memsize + pagesize * 2;
    mem->origin_start = malloc(datasize);
    if(mem->origin_start == NULL)
        return -1;
    mem->origin_end = mem->origin_start + datasize;

    long mask = pagesize - 1;
    mem->start = (void *)((long)(mem->origin_start + pagesize) & ~mask);
    long pagenum = memsize / pagesize + 1;
    mem->end = mem->start + pagesize * pagenum;
    return 0;
}

上面函数GCC -02编译后,反汇编如下:

(gdb) disass malloc_align_page
Dump of assembler code for function malloc_align_page:
   0x00000000004007b0 <+0>:     push   %r13
   0x00000000004007b2 <+2>:     push   %r12
   0x00000000004007b4 <+4>:     mov    %rdi,%r12
   0x00000000004007b7 <+7>:     push   %rbp
   0x00000000004007b8 <+8>:     push   %rbx
   0x00000000004007b9 <+9>:     sub    $0x8,%rsp
   0x00000000004007bd <+13>:    test   %rdi,%rdi
   0x00000000004007c0 <+16>:    je     0x400858 <malloc_align_page+168>
   0x00000000004007c6 <+22>:    test   %rsi,%rsi
   0x00000000004007c9 <+25>:    mov    %rsi,%rbx
   0x00000000004007cc <+28>:    je     0x400858 <malloc_align_page+168>
   0x00000000004007d2 <+34>:    movq   $0x0,(%rsi)
   0x00000000004007d9 <+41>:    movq   $0x0,0x8(%rsi)
   0x00000000004007e1 <+49>:    mov    $0x1e,%edi
   0x00000000004007e6 <+54>:    movq   $0x0,0x10(%rsi)
   0x00000000004007ee <+62>:    movq   $0x0,0x18(%rsi)
   0x00000000004007f6 <+70>:    callq  0x400600 <sysconf@plt>
   0x00000000004007fb <+75>:    cmp    $0xffffffffffffffff,%rax
   0x00000000004007ff <+79>:    mov    %rax,%rbp
   0x0000000000400802 <+82>:    je     0x400860 <malloc_align_page+176>
   0x0000000000400804 <+84>:    lea    (%r12,%rax,2),%r13
   0x0000000000400808 <+88>:    mov    %r13,%rdi
   0x000000000040080b <+91>:    callq  0x4005c0 <malloc@plt>
   0x0000000000400810 <+96>:    test   %rax,%rax
   0x0000000000400813 <+99>:    mov    %rax,(%rbx)
   0x0000000000400816 <+102>:   je     0x400858 <malloc_align_page+168>
   0x0000000000400818 <+104>:   lea    (%rax,%rbp,1),%rcx
   0x000000000040081c <+108>:   add    %rax,%r13
   0x000000000040081f <+111>:   mov    %rbp,%rax
   0x0000000000400822 <+114>:   neg    %rax
   0x0000000000400825 <+117>:   xor    %edx,%edx
   0x0000000000400827 <+119>:   mov    %r13,0x18(%rbx)
   0x000000000040082b <+123>:   and    %rax,%rcx
   0x000000000040082e <+126>:   mov    %r12,%rax
   0x0000000000400831 <+129>:   div    %rbp
   0x0000000000400834 <+132>:   mov    %rcx,0x8(%rbx)
   0x0000000000400838 <+136>:   add    $0x1,%rax
   0x000000000040083c <+140>:   imul   %rax,%rbp
   0x0000000000400840 <+144>:   xor    %eax,%eax
   0x0000000000400842 <+146>:   add    %rbp,%rcx
   0x0000000000400845 <+149>:   mov    %rcx,0x10(%rbx)
   0x0000000000400849 <+153>:   add    $0x8,%rsp
   0x000000000040084d <+157>:   pop    %rbx
   0x000000000040084e <+158>:   pop    %rbp
   0x000000000040084f <+159>:   pop    %r12
   0x0000000000400851 <+161>:   pop    %r13
   0x0000000000400853 <+163>:   retq
   0x0000000000400854 <+164>:   nopl   0x0(%rax)
   0x0000000000400858 <+168>:   mov    $0xffffffff,%eax
   0x000000000040085d <+173>:   jmp    0x400849 <malloc_align_page+153>
   0x000000000040085f <+175>:   nop
   0x0000000000400860 <+176>:   mov    $0x4008f4,%edi
   0x0000000000400865 <+181>:   callq  0x4005f0 <perror@plt>
   0x000000000040086a <+186>:   mov    %ebp,%eax
   0x000000000040086c <+188>:   jmp    0x400849 <malloc_align_page+153>
End of assembler dump.

根据 callq fun看一下调用函数的指令,上下文前后看,往前就是调用是入参,往后就是返回值:

0x04007e1 <+49>:    mov    $0x1e,%edi // 反推,这就是第一个入参, 立即数 _SC_PAGE_SIZE
0x04007e6 <+54>:    movq   $0x0,0x10(%rsi)
0x04007ee <+62>:    movq   $0x0,0x18(%rsi)
0x04007f6 <+70>:    callq  0x400600 <sysconf@plt> // 搜索 sysconf, 快速找到
0x04007fb <+75>:    cmp    $0xffffffffffffffff,%rax // 比较返回值是否是-1,-1就是全F
0x04007ff <+79>:    mov    %rax,%rbp
0x0400802 <+82>:    je     0x400860 <malloc_align_page+176>

根据 lea指令定位后,往下看,前面我们无法知道哪里跳过来的,但是,lea后没有无跳转,肯定接着顺序执行,往下看即可:

0x00400804 <+84>:    lea    (%r12,%rax,2),%r13 // %r13 = %r12 + %rax * 2
                     // 显然对应源代码: datasize = memsize + pagesize * 2;
0x0400808 <+88>:    mov    %r13,%rdi // 前面无跳转,往下执行,rdi传第一个参数
0x040080b <+91>:    callq  0x4005c0 <malloc@plt>
0x0400810 <+96>:    test   %rax,%rax // 判断返回值是否为0

找乘除指令, 同样往下看,前面无法知道哪儿jump过来的,但是后面肯定是顺序接着执行:

0x000000000040083c <+140>:   imul   %rax,%rbp // %rbp = %rbp * %rax
             // 源码只有一处乘法 mem->end = mem->start + pagesize * pagenum;
0x0000000000400840 <+144>:   xor    %eax,%eax
0x0000000000400842 <+146>:   add    %rbp,%rcx // %rcx = %rbp + %rcx 即 mem->end
0x0000000000400845 <+149>:   mov    %rcx,0x10(%rbx) // mem->end 地址 %rbx + 0x10
                    // 根据结构体,end就是struct mem_align偏移0x10字节的字段
                    // 那么rbx就是mem结构体首地址了
                    // 从头看,rdi第二个入参,赋给rbx,rbx就没被改写了,必然存的mem的值

就这样,关键指令定位,结合一些上下文,结合调用约定,就可以快速找出关键的指令和源码的对应关系了。

猜你喜欢

转载自blog.csdn.net/thisinnocence/article/details/80767776