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)

转载于:https://www.cnblogs.com/zhaojinxiang/p/linux_disassembling.html

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值