在线学习了Mooc的《计算机内核分析》课程,为了探究计算机运行过程,现做博文记录实验过程。
首先打开虚拟机中的linux环境,输入C语言代码:
int g(int x) { return x + 3; } int f(int x) { return g(x); } int main(void) { return f(8) + 1; }
保存为main.c文件
使用反汇编命令
gcc -S -o main.s main,c -m32
-S : 编译为汇编语言程序
-o : 指定目标程序名称
-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 $3, %eax popl %ebp .cfi_def_cfa 4, 4 .cfi_restore 5 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 subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp) call g 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 subl $4, %esp movl $8, (%esp) call f addl $1, %eax leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE2: .size main, .-main .ident "GCC: (GNU) 4.6.2 20111027 (Red Hat 4.6.2-1)" .section .note.GNU-stack,"",@progbits
除去代码中的标志符号后:
g: pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $3, %eax popl %ebp ret f: pushl %ebp movl %esp, %ebp subl $4, %esp movl 8(%ebp), %eax movl %eax, (%esp) call g leave ret main: pushl %ebp movl %esp, %ebp subl $4, %esp movl $8, (%esp) call f addl $1, %eax leave ret
下面对此段汇编代码进行分析,首先要熟悉一些基本的汇编知识:
为了计算方便,加快计算机运行速度,计算机中配置了很多寄存器,这些寄存器在计算机工作中起到各种不同的重要作用:
eax默认作为函数返回值保存寄存器
ebp为栈低指针寄存器
esp为栈顶指针寄存器(运行中的每个程序都有一段堆栈空间,用来存放程序运行时数据)
eip为当前指令位置寄存器
push为压栈指令
pop为出栈指令
mov为转移指令,通过该指令有七种寻址方式:
mov eax,0x1234
//立即寻址
mov eax,ebx //寄存器寻址
mov eax,[ebx]
//寄存器间接寻址
mov eax,[0x1234]
//直接寻址
mov eax,[ebx+0x1234]
//寄存器相对寻址
mov eax,[esi+edi]
//基址变址寻址
mov eax,[esi+edi+0x1234]
//基址变址相对寻址
add为加法指令
此外还有一些宏指令,它们对应一行或多行汇编代码:
ret : pop %eip(实则返回指令)
leave : mov %ebp %esp
pop %ebp
enter : push %ebp
mov %esp %ebp
call 0x12345 : push %eip
mov $0x12345 , %eip
在指令后加上b、l、w、q分别代表8位、16位、32位、64位操作
下面直接通过分析代码来熟悉这些指令:
首先程序从main函数开始执行,
pushl %ebp
将基址寄存器的值进栈,用于系统控制main函数返回,此时堆栈情况如图:
movl %esp, %ebp
此行代码将esp寄存器的内容赋值给ebp,即ebp和esp都指向地址1的位置
subl $4, %esp
将esp的指针指向下面一个位置
movl $8, (%esp)
将立即数8存放在当前esp所指位置,堆栈的情况如图:
call f
调用f函数,即执行下面的指令
push %eip //将当前的eip入栈,即将下一条指令[addl $1, %eax]的地址入栈
mov f, %eip //函数f的入口地址赋值到eip中,即程序跳转到f函数入口处
pushl %ebp
f函数的第一条指令,同main函数,如图:
movl %esp, %ebp
初始化f函数的堆栈,如图:
subl $4, %esp<span style="white-space:pre"> </span>//栈顶下移
movl 8(%ebp), %eax<span style="white-space:pre"> </span>//栈低加8对应地址的数据存入eax中 movl %eax, (%esp)<span style="white-space:pre"> </span>//eax中数据存入esp所指空间如图:
call g
调用g函数
pushl %ebp movl %esp, %ebp movl 8(%ebp), %eax addl $3, %eax
如图:
popl %ebp
出栈,如图:
ret<span style="white-space:pre"> </span>//pop %eip
eip指向f函数中leave指令的地址,开始继续执行
leave
mov %ebp %esp
pop %ebp
ret
返回main函数
addl $1, %eax<span style="white-space:pre"> </span>//eax = 12 leave<span style="white-space:pre"> </span>// ret
如图:
程序执行完毕,main函数返回交给系统处理。
计算机执行就是顺序执行的过程,借助着寄存器和堆栈实现程序的跳转执行。