Linux内核分析 作业1 反汇编

  • 李奇 原创作品转载请注明出处《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000

实验内容:反汇编如下C代码, 分析汇编代码的工作过程中堆栈的变化

int g(int x)
{
    return x + 41;
}

int f(int x)
{
    return g(x);
}

int main(void)
{
    return f(0) + 1;
}

实验环境 x86_64-linux-gnu,Ubuntu 12.04.4 LTS, gcc version 4.6.3, gdb 7.4

这次实验,我使用了与gcc –S –o main.s main.c -m32略有不同的办法:结合gdb对这段代码进行分析,使用了gdb的disassemble与step命令,因此在编译时,我使用编译选项-g生成调试符号,并使用-o直接link和load生成目标文件:gcc –g –o main.o main.c -m32,然后直接使用gdb直接查看反汇编和单步执行,这样可以对代码的实际执行过程有更清晰的认识,并可在gdb中实时查看堆栈和寄存器信息。

第一步: 命令行中输入gdb ./main.o 开始调试:

root@:~# gdb ./main.o
GNU gdb (Ubuntu/Linaro 7.4-2012.04-0ubuntu2.1) 7.4-2012.04
Copyright (C) 2012 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later 
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
...
Reading symbols from /root/main.o...done.
(gdb)

第二步,在执行main函数前, 让我们用 disas 命令分别观察 main, f, g函数源码的反汇编代码。

main

(gdb) disas /m main
Dump of assembler code for function main:
12  {
   0x080483d2 <+0>: push   %ebp
   0x080483d3 <+1>: mov    %esp,%ebp
   0x080483d5 <+3>: sub    $0x4,%esp

13      return f(0) + 1;
   0x080483d8 <+6>: movl   $0x0,(%esp)
   0x080483df <+13>:    call   0x80483bf 
   0x080483e4 <+18>:    add    $0x1,%eax

14  }
   0x080483e7 <+21>:    leave
   0x080483e8 <+22>:    ret

End of assembler dump.

f

(gdb) disas /m f
Dump of assembler code for function f:
7   {
   0x080483bf <+0>: push   %ebp
   0x080483c0 <+1>: mov    %esp,%ebp
   0x080483c2 <+3>: sub    $0x4,%esp

8       return g(x);
   0x080483c5 <+6>: mov    0x8(%ebp),%eax
   0x080483c8 <+9>: mov    %eax,(%esp)
   0x080483cb <+12>:    call   0x80483b4 

9   }
   0x080483d0 <+17>:    leave
   0x080483d1 <+18>:    ret

End of assembler dump.

g

(gdb) disas /m g
Dump of assembler code for function g:
2   {
   0x080483b4 <+0>: push   %ebp
   0x080483b5 <+1>: mov    %esp,%ebp

3       return x + 42;
   0x080483b7 <+3>: mov    0x8(%ebp),%eax
   0x080483ba <+6>: add    $0x2a,%eax

4   }
   0x080483bd <+9>: pop    %ebp
   0x080483be <+10>:    ret

End of assembler dump.

首先不难发现,main f g 三个函数反汇编的共同特点:头两行均为

      push   %ebp
      mov    %esp,%ebp

这两行的目的是保存'stack frame',简要介绍如下:
入栈调用函数(caller)的ebp, 将ebp设为当前栈寄存器的值; 这样一来,嵌套函数调用时,被调用函数(callee)的ebp指向调用函数(caller)的栈,而通过caller的栈又能找到caller的ebp, 这样层层往上,就可以生成stack trace, 可以方便调试和异常处理(stack unwinding),另外由于参数和局部变量相对于ebp的偏移固定,编译器产生代码更易懂(在开启了FPO编译优化选项后,这两行就没有了,另外在x86-64中通过入栈ebp来保存stack frame的做法不再被鼓励, 原因可参考一篇blog)。

不妨通过gdb来验证一下,在函数main f g中下断点, 并在遇到断点时分别观察ebp, esp的值

(gdb) break main
Breakpoint 1 at 0x80483d8: file main.c, line 13.
(gdb) break f
Breakpoint 2 at 0x80483c5: file main.c, line 8.
(gdb) break g
Breakpoint 3 at 0x80483b7: file main.c, line 3.
(gdb) run
Starting program: /root/main.o

Breakpoint 1, main () at main.c:13
13      return f(0) + 1;
(gdb) i r ebp esp
ebp            0xffffd388   0xffffd388
esp            0xffffd384   0xffffd384
(gdb) c
Continuing.

Breakpoint 2, f (x=0) at main.c:8
8       return g(x);
(gdb) i r ebp esp
ebp            0xffffd37c   0xffffd37c
esp            0xffffd378   0xffffd378
(gdb) c
Continuing.

Breakpoint 3, g (x=0) at main.c:3
3       return x + 42;
(gdb) i r ebp esp
ebp            0xffffd370   0xffffd370
esp            0xffffd370   0xffffd370

现在观察ebp所指向的内存的值

(gdb) x 0xffffd370
0xffffd370: 0xffffd37c
(gdb) x 0xffffd37c
0xffffd37c: 0xffffd388

g函数中的ebp值指向f中的ebp值, f中的ebp值又指向main的ebp值

然后,通过理解C语言的calling convention(cdecl), 不难理解main f g中剩余代码的意义。

如果callee有参数,则在caller中压栈参数。例如在main和f中callee都有一个参数,则sub $0x4,%esp 为该参数分配了空间,movl $0x0,(%esp)mov %eax,(%esp)分别为参数赋值; callee将返回值放入$eax后,由caller负责恢复栈,所以main和f中均有leave指令(等价于mov $ebp,$esppop $ebp 两条指令), 恢复esp为caller自己的ebp, 从而将分配给callee的栈空间释放

你可能感兴趣的:(Linux内核分析 作业1 反汇编)