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

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

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

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
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)

posted @ 2015-03-21 20:40  小小翔  阅读(3989)  评论(0编辑  收藏  举报