[关闭]
@xiaoxiang 2015-04-09T23:35:07.000000Z 字数 4203 阅读 6566

Linux下反汇编分析C语言源代码

Linux内核分析


by 赵缙翔

原创作品转载请注明出处

《Linux内核分析》MOOC课程——孟宁

这是我第一次写的博客,如有疏漏,还请指教。

在上完孟宁老师的软件工程课程后,觉得这老师的课真心不错,就又选了他的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,我就不赘述了,摘抄自wiki百科

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

  1. int g(int x)
  2. {
  3. return x + 6;
  4. }
  5. int f(int x)
  6. {
  7. return g(x);
  8. }
  9. int main(void)
  10. {
  11. return f(2333)+666;
  12. }

在ubuntu平台下,使用 gcc -S -o main.s main.c -m32将它反汇编成main.s。注意,我是在AMD64(或者说X86-64)的操作系统,所以为了产生32位的汇编代码,我使用了-m32选项让它生成32位汇编指令

  1. .file "main.c"
  2. .text
  3. .globl g
  4. .type g, @function
  5. g:
  6. .LFB0:
  7. .cfi_startproc
  8. pushl %ebp
  9. .cfi_def_cfa_offset 8
  10. .cfi_offset 5, -8
  11. movl %esp, %ebp
  12. .cfi_def_cfa_register 5
  13. movl 8(%ebp), %eax
  14. addl $6, %eax
  15. popl %ebp
  16. .cfi_restore 5
  17. .cfi_def_cfa 4, 4
  18. ret
  19. .cfi_endproc
  20. .LFE0:
  21. .size g, .-g
  22. .globl f
  23. .type f, @function
  24. f:
  25. .LFB1:
  26. .cfi_startproc
  27. pushl %ebp
  28. .cfi_def_cfa_offset 8
  29. .cfi_offset 5, -8
  30. movl %esp, %ebp
  31. .cfi_def_cfa_register 5
  32. pushl 8(%ebp)
  33. call g
  34. addl $4, %esp
  35. leave
  36. .cfi_restore 5
  37. .cfi_def_cfa 4, 4
  38. ret
  39. .cfi_endproc
  40. .LFE1:
  41. .size f, .-f
  42. .globl main
  43. .type main, @function
  44. main:
  45. .LFB2:
  46. .cfi_startproc
  47. pushl %ebp
  48. .cfi_def_cfa_offset 8
  49. .cfi_offset 5, -8
  50. movl %esp, %ebp
  51. .cfi_def_cfa_register 5
  52. pushl $2333
  53. call f
  54. addl $4, %esp
  55. addl $666, %eax
  56. leave
  57. .cfi_restore 5
  58. .cfi_def_cfa 4, 4
  59. ret
  60. .cfi_endproc
  61. .LFE2:
  62. .size main, .-main
  63. .ident "GCC: (Ubuntu 4.9.1-16ubuntu6) 4.9.1"
  64. .section .note.GNU-stack,"",@progbits

代码中有许多以.开头的代码行,属于链接时候的辅助信息,在实际中不会执行,把它删除,得到下列的代码就是纯汇编代码了:

  1. g:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. movl 8(%ebp), %eax
  5. addl $6, %eax
  6. popl %ebp
  7. ret
  8. f:
  9. pushl %ebp
  10. movl %esp, %ebp
  11. pushl 8(%ebp)
  12. call g
  13. addl $4, %esp
  14. leave
  15. ret
  16. main:
  17. pushl %ebp
  18. movl %esp, %ebp
  19. pushl $2333
  20. call f
  21. addl $4, %esp
  22. addl $666, %eax
  23. leave
  24. ret

截图

汇编代码分析

下面,我们开始分析一下上面的汇编代码。
注意观察,每一个函数(在汇编,函数就是个代码段)的开头都是下面格式

  1. 函数名:
  2. pushl %ebp
  3. movl %esp, %ebp
  4. ;函数中间过程
  5. leave(或者popl %ebp
  6. ret

注意,leave和下面代码等价

  1. movl %ebp, %esp
  2. popl %ebp

也有时候,我们把下面代码写成enter

  1. 函数名:
  2. pushl %ebp
  3. movl %esp, %ebp

函数执行

我们先分析一下这个函数执行的过程。
每次call一个函数,函数总是先把当前的栈底指针压入堆栈,然后把栈底指针移动到当前的栈顶,这样子做,相当于在旧的栈上新起了一个栈。然后在新栈上执行函数。
结束函数执行的时候,如果有堆栈变化,我们在写单片机汇编的时候,我们的习惯是一个函数有多少push就写多少pop,但是,由于我们新引进了一个寄存器,我们可以用movl %ebp, %esp来瞬间恢复堆栈。当然,如果没有堆栈的变化,我们当然可以优化编译器把这句话去了。
这时候,马上就要ret飞回调用它的函数了……别急,我们还需要恢复栈底指针,否则回去的日子就难过了。于是popl %ebp。然后如果可以的话,我们会用leave来代替刚刚的两行代码。

函数调用

函数执行一定得是有函数调用了。

  1. pushl $2333
  2. call f
  3. addl $4, %esp

这是调用f(2333)函数的过程。
我们可以看到,我们把2333压栈,然后调用了f函数。
等到ret后,返回了现在的call的下一行汇编代码。这时候,esp和ebp是一个值,所以这以后如果压栈的时候,会覆盖了栈底指针,把esp往栈顶上移动1个单位也就是4个字节,这时候就完美解决了调用后的问题,才是真正调用完成了。
那么,怎么取得参数呢?

函数参数取得

这时候,得回头看一下f函数了。这时候,我们发现它用了

  1. pushl 8(%ebp)
  2. call g
  3. addl $4, %esp

它把增加了8个字节的地址压栈了,然后调用了g函数。
分析一下为什么是8个字节,我们可以用sizeof关键字来测试得到int占4个字节……所以,它却加了8个字节取值,那么必然是有什么怪东西又入栈了。pushl %ebp是每次函数执行的时候使用的,o(∩_∩)o 哈哈,找到了,就是ebp寄存器还占用了4个字节,想想,32位芯片,寄存器32=8/×4。符合啦。
所以,又发现了ebp寄存器的一个好处,能够让我们方便取得函数的参数……否则后面再去参数,栈位置变了好多,就不方便了。

其它

  1. addl $6, %eax

之类的基本汇编指令,就不细说了,具体还是看看汇编的资料吧。知道一门汇编,就能很轻松看懂了。(上面的意思是把eax寄存器存的值加6)

总结

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注