本文共 5757 字,大约阅读时间需要 19 分钟。
相信很多C/C++程序员都有过线上gdb调试程序的经历,没有或者剥离了调试信息,线上运行的也是GCC -O2
编译优化后的release版本,这时就要求快速看懂汇编指令和对应C代码的对应关系。进而,找到自己需要gdb print出的关键内存信息,或针对性的打断点以及观察点等,这些调试一切的前提都是要求快速看懂反汇编指令和对应源代码的对应关系。一个函数的反汇编短则数百,长则近万行甚至更多,如何一下子找到其与C源码的对应关系呢?我通常就是快速搜索关键指令,并结合其上下文来理解推断。当然,这一切的前提是对汇编常用指令以及x86 calling convention
有一定的了解。
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_pageDump 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 0x4008580x00000000004007c6 <+22>: test %rsi,%rsi 0x00000000004007c9 <+25>: mov %rsi,%rbx 0x00000000004007cc <+28>: je 0x400858 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 0x00000000004007fb <+75>: cmp $0xffffffffffffffff,%rax 0x00000000004007ff <+79>: mov %rax,%rbp 0x0000000000400802 <+82>: je 0x400860 0x0000000000400804 <+84>: lea (%r12,%rax,2),%r13 0x0000000000400808 <+88>: mov %r13,%rdi 0x000000000040080b <+91>: callq 0x4005c0 0x0000000000400810 <+96>: test %rax,%rax 0x0000000000400813 <+99>: mov %rax,(%rbx) 0x0000000000400816 <+102>: je 0x400858 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 0x000000000040085f <+175>: nop 0x0000000000400860 <+176>: mov $0x4008f4,%edi 0x0000000000400865 <+181>: callq 0x4005f0 0x000000000040086a <+186>: mov %ebp,%eax 0x000000000040086c <+188>: jmp 0x400849 End of assembler dump.
根据 callq fun
看一下调用函数的指令,上下文前后看,往前就是调用是入参,往后就是返回值:
0x04007e1 <+49>: mov $0x1e,%edi // 反推,这就是第一个入参, 立即数 _SC_PAGE_SIZE0x04007e6 <+54>: movq $0x0,0x10(%rsi)0x04007ee <+62>: movq $0x0,0x18(%rsi)0x04007f6 <+70>: callq 0x400600// 搜索 sysconf, 快速找到0x04007fb <+75>: cmp $0xffffffffffffffff,%rax // 比较返回值是否是-1,-1就是全F0x04007ff <+79>: mov %rax,%rbp0x0400802 <+82>: je 0x400860
根据 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 0x4005c00x0400810 <+96>: test %rax,%rax // 判断返回值是否为0
找乘除指令, 同样往下看,前面无法知道哪儿jump过来的,但是后面肯定是顺序接着执行:
0x000000000040083c <+140>: imul %rax,%rbp // %rbp = %rbp * %rax // 源码只有一处乘法 mem->end = mem->start + pagesize * pagenum;0x0000000000400840 <+144>: xor %eax,%eax0x0000000000400842 <+146>: add %rbp,%rcx // %rcx = %rbp + %rcx 即 mem->end0x0000000000400845 <+149>: mov %rcx,0x10(%rbx) // mem->end 地址 %rbx + 0x10 // 根据结构体,end就是struct mem_align偏移0x10字节的字段 // 那么rbx就是mem结构体首地址了 // 从头看,rdi第二个入参,赋给rbx,rbx就没被改写了,必然存的mem的值
就这样,关键指令定位,结合一些上下文,结合调用约定,就可以快速找出关键的指令和源码的对应关系了。
转载地址:http://cbqli.baihongyu.com/