Linux下反汇编分析C语⾔源代码
Linux下反汇编分析C语⾔源代码
by 赵缙翔
原创作品转载请注明出处
这是我第⼀次写的博客,如有疏漏,还请指教。
在上完孟宁⽼师的软件⼯程课程后,觉得这⽼师的课真⼼不错,就⼜选了他的Linux内核分析。因为Linux内核代码中还是有⼀些C语⾔没法做的事情需要At&T汇编代码来帮忙,所以我们需要了解⼀些汇编的常识。
汇编基础
命名习惯的历史由来
最先开始,Intel 8086和8088有⼗四个16位寄存器,⽐如AX, BX, CX, DX等等。然后Intel出了32位处理器,相对于16位处理器是是扩展的(extended),于是在16位的寄存器基础上加上E前缀,⽐如AX变成了EAX,在后来,AMD出了64位处理器,采⽤的R前缀,具体为什么⽤R,我也不造啊,求告诉。
常⽤的寄存器
(有汇编基础的应该很好懂……我学过单⽚机的汇编,竟然也看懂了⼤部分。so,我就不赘述了,摘抄⾃)
Although the main registers (with the exception of the instruction pointer) are "general-purpose" in the 32-bit and 64-bit versions of the instruction set and can be used for anything, it was originally envisioned that they be used for the following purposes: AL/AH/AX/EAX/RAX: Accumulator
BL/BH/BX/EBX/RBX: Base index (for use with arrays)
CL/CH/CX/ECX/RCX: Counter (for use with loops and strings)
DL/DH/DX/EDX/RDX: Extend the precision of the accumulator (e.g. combine 32-bit EAX and EDX for 64-bit integer
operations in 32-bit code)
SI/ESI/RSI: Source index for string operations.
DI/EDI/RDI: Destination index for string operations.
SP/ESP/RSP: Stack pointer for top address of the stack.
BP/EBP/RBP: Stack base pointer for holding the address of the current stack frame.
IP/EIP/RIP: Instruction pointer. Holds the program counter, the current instruction address.
Segment registers:
CS: Code
DS: Data
SS: Stack
ES: Extra data
FS: Extra data #2
汇编指令
由于是我们使⽤的32位的汇编指令,所以有个l前缀,还有,和51单⽚机的堆栈不同,这⾥的堆栈是从⾼向低⼊栈的……还有⼀个问题就摘抄吧,他说得很好
AT&T格式和intel格式,这两种格式GCC是都可以⽣成的,如果要⽣成intel格式的汇编代码,只需要加上-masm=intel选项即可,但是Linux下默认是使⽤AT&T格式来书写汇编代码,Linux Kernel代码中也是AT&T格式,我们要慢慢习惯使⽤AT&T格式书写汇编代码。这⾥最需要注意的AT&T和intel汇编格式不同点是:
AT&T格式的汇编指令是“源操作数在前,⽬的操作数在后”,⽽intel格式是反过来的,即如下:
AT&T格式:movl %eax, %edx
Intel格式:mov edx, eax
表⽰同⼀个意思,即把eax寄存器的内容放⼊edx寄存器。这⾥需要注意的是AT&T格式的movl⾥的l表⽰指令的操作数都是32
位,类似的还是有movb,movw,movq,分别表⽰8位,16位和64位的操作数。更具体的AT&T汇编语法请执⾏Google或者查阅相关书籍。
反汇编
下⾯,我们开始反汇编⼀个C语⾔的程序,来分析⼀下它的汇编代码:
⾸先,我们先写⼀个C语⾔的程序main.c
int g(int x)
{
return x + 6;
}
int f(int x)
{
return g(x);
}
int main(void)
{
return f(2333)+666;
}
在ubuntu平台下,使⽤gcc -S -o main.s main.c -m32将它反汇编成main.s。注意,我是在AMD64(或者说X86-64)的操作系统,所以为了产⽣32位的汇编代码,我使⽤了-m32选项让它⽣成32位汇编指令
.file "main.c"
.text
.globl g
.type g, @function
g:
.LFB0:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
movl 8(%ebp), %eax
addl $6, %eax
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.
cfi_endproc
.LFE0:
.size g, .-g
.globl f
.type f, @function
f:
.LFB1:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl 8(%ebp)
call g
addl $4, %esp
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE1:
.
size f, .-f
.globl main
.type main, @function
linux下gcc编译的四个步骤
main:
.LFB2:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
pushl $2333
call f
addl $4, %esp
addl $666, %eax
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE2:
.size main, .-main
.
ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
.section .note.GNU-stack,"",@progbits
代码中有许多以.开头的代码⾏,属于链接时候的辅助信息,在实际中不会执⾏,把它删除,得到下列的代码就是纯汇编代码了:
g:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl $6, %eax
popl %ebp
ret
f:
pushl %ebp
movl %esp, %ebp
pushl 8(%ebp)
call g
addl $4, %esp
leave
ret
main:
pushl %ebp
movl %esp, %ebp
pushl $2333
call f
addl $4, %esp
addl $666, %eax
leave
ret
汇编代码分析
下⾯,我们开始分析⼀下上⾯的汇编代码。
注意观察,每⼀个函数(在汇编,函数就是个代码段)的开头都是下⾯格式
函数名:
pushl %ebp
movl %esp, %ebp
;函数中间过程
leave(或者popl    %ebp)
ret
注意,leave和下⾯代码等价
movl %ebp, %esp
popl %ebp
也有时候,我们把下⾯代码写成enter
函数名:
pushl %ebp
movl %esp, %ebp
函数执⾏
我们先分析⼀下这个函数执⾏的过程。
每次call⼀个函数,函数总是先把当前的栈底指针压⼊堆栈,然后把栈底指针移动到当前的栈顶,这样⼦做,相当于在旧的栈上新起了⼀个栈。然后在新栈上执⾏函数。
结束函数执⾏的时候,如果有堆栈变化,我们在写单⽚机汇编的时候,我们的习惯是⼀个函数有多少push就写多少pop,但是,由于我们新引进了⼀个寄存器,我们可以⽤movl %ebp, %esp来瞬间恢复堆栈。当然,如果没有堆栈的变化,我们当然可以优化编译器把这句话去了。
这时候,马上就要ret飞回调⽤它的函数了……别急,我们还需要恢复栈底指针,否则回去的⽇⼦就难过了。于是popl %ebp。然后如果可以的话,我们会⽤leave来代替刚刚的两⾏代码。
函数调⽤
函数执⾏⼀定得是有函数调⽤了。
pushl $2333
call f
addl    $4, %esp
这是调⽤f(2333)函数的过程。
我们可以看到,我们把2333压栈,然后调⽤了f函数。
等到ret后,返回了现在的call的下⼀⾏汇编代码。这时候,esp和ebp是⼀个值,所以这以后如果压栈的时候,会覆盖了栈底指针,把esp往栈顶上移动1个单位也就是4个字节,这时候就完美解决了调⽤后的问题,才是真正调⽤完成了。
那么,怎么取得参数呢?
函数参数取得
这时候,得回头看⼀下f函数了。这时候,我们发现它⽤了
pushl 8(%ebp)
call g
addl    $4, %esp
它把增加了8个字节的地址压栈了,然后调⽤了g函数。
分析⼀下为什么是8个字节,我们可以⽤sizeof关键字来测试得到int占4个字节……所以,它却加了8个字节取值,那么必然是有什么怪东西⼜⼊栈了。pushl %ebp是每次函数执⾏的时候使⽤的,o(∩_∩)o 哈哈,到了,就是ebp寄存器还占⽤了4个字节,想想,32位芯⽚,寄存器$32位=8位/字节\times 4字节$。符合啦。
所以,⼜发现了ebp寄存器的⼀个好处,能够让我们⽅便取得函数的参数……否则后⾯再去参数,栈位置变了好多,就不⽅便了。
其它
addl    $6, %eax
之类的基本汇编指令,就不细说了,具体还是看看汇编的资料吧。知道⼀门汇编,就能很轻松看懂了。(上⾯的意思是把eax寄存器存的值加6)
总结
每次都是各种取指针执⾏,在程序中各种跳转。
函数执⾏前要enter,函数执⾏后要leave(如果没有改变esp就可以省去把ebp赋值给esp的步骤了),ret
函数取值可以靠ebp很⽅便做到
函数调⽤结束后要记住恢复堆栈指针(esp)