您的位置:首页 > 理论基础

《Linux 内核分析》课程作业(1)——计算机基本原理和汇编基础

2015-03-06 00:00 731 查看
摘要: 《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000 作业一:1. 计算机工作基本原理(存储程序);2. x86 汇编基础(AT&T);3. 实际例程分析。

1. 计算机是如何工作的?——存储程序

计算机工作的基本方式:取指执行。即,从内存中指定位置取出指令,然后在 CPU 中执行该指令。

IP 寄存器: IP (Instruction Pointer) 总指向计算机即将执行的指令。计算机在工作的时候,首先到 IP 所指向的内存地址获取将要执行的指令,然后 IP 自加 1(条指令)指向下一条指令。注意;每条指令所占据的内存空间可能是不同的。

在 x86 架构中,实际由 EIP 寄存器作为指令指针(32位寄存器)。EIP 寄存器可被 CALL, RET, JMP 以及条件跳转语句修改。

2. x86 汇编基础

x86 CPU 寄存器

通用寄存器 低16位 低8位 全名
Accumulator AX AL EAX
Base Register BX BL EBX
Count Register CX CL ECX
Data Register DX DL EDX
注:EAX 寄存器通常用于函数返回值的默认寄存器

特殊寄存器 对应 16-bit 寄存器 32-bit 寄存器
Base Pointer BP EBP
Stack Pointer SP ESP
Source Index SI ESI
Destination Index DI EDI
说明:EBP 和 ESP 分别是程序运行栈底和栈顶指针。
段寄存器: CS (Code Segment), SS (Stack Segment) 等。

CPU 在取指令时,根据 CS 和 EIP 查找指令。

在 32 位汇编中,寄存器缩写一般以 E 开头,意为 Extended;64 位汇编中,寄存器缩写以 R 开头。

常用命令

MOV, PUSH, POP, CALL, RET

MOV - 赋值命令。用法:movl a, b 将 a 的值赋给 b

PUSH - 压栈。用法: pushl a,将 a 的值压栈。

等价命令: addl 4, %esp

movl a, (%esp)

POP - 出栈。用法:popl a,将栈中弹出的值赋给 a

等价命令: movl (%esp), a

subl 4, %esp

CALL - 调用函数。用法:call func

实际操作:pushl %eip

movl func, %eip

RET - 返回。

实际操作:popl %eip

[注]CALL 和 RET 不能用对应实际操作的语句来替换,原因:eip 寄存器只能通过指定语句赋值。

[注]MOV, PUSH, POP 后可以加 b(8-bit), w(16-bit), l(32-bit), g(64-bit)。

寻址方式

寻址方式 实例 说明
Register mode movl %eax, %ebx 直接将 eax 的内容赋值给 ebx
Immediate movl $0x123, %eax 将数 0x123 赋给 eax
Direct movl 0x123, %eax 将地址 0x123 所存放内容赋给 eax
Indirect movl (%ebx), %eax 将地址为 ebx 的值赋给 eax
Displace movl 4(%ebx), %eax 将地址为 ebx + 4 的值赋给 eax


3. 实际例程分析

实验截图





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

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

int main(void)
{
return f(11) + 9;
}
上面是一段简单的 C 语言代码,使用 gcc -S test.c -o test.s -m32 将 C 语言编译成为汇编语言,得到如下的汇编代码(只保留指令内容)
g:
pushl	%ebp
movl	%esp, %ebp
movl	8(%ebp), %eax
addl	$7, %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	$11, (%esp)
call	f
addl	$9, %eax
leave
ret

由于 main 函数是整个程序的入口,我们从 main 标签开始分析整个汇编程序。

main: 18, 19行

pushl %ebp - 将栈底指针压栈
movl %esp, %ebp - 将 esp 赋值给 ebp

以上两个语句实质是将函数运行栈置空,两个指令完成之后,ebp 和 esp 相等,表现为空栈。而 ebp 上一个位置存放的即是之前 ebp 的值。这个过程也被定义为宏指令 enter。对应的逆过程指令为 leave(main: 24行),可以展开写作:

movl %ebp, %esp
popl %ebp

通过 enter 和 leave 的配合,保证了栈状态在函数调用前后的一致性。

main: 20, 21行

subl $4, %esp
movl $11, (%esp)

可以看到这实质上就是 pushl $11 指令的展开,即将函数调用参数压栈。将立即数 11 而是赋给了 esp 作为地址所指向的内存单元。

call f

调用 f 函数,此指令实际由两个指令组成:首先是将当前 eip 压栈(注意!!),然后将 f 函数起始指令地址赋给 eip,从而实现指令执行的跳转。回顾一下,到目前为止执行了三次压栈操作,依次压入了 ebp (main 函数调用前状态),立即数11,以及 eip (指向程序第 23 行)。

现在,程序执行跳转到 f 部分的第一条指令(f: 9行)

pushl %ebp
接下来就是在 main 部分已经解释的函数调用栈的初始化动作,首先将 ebp (f 函数调用前状态)压栈,接着第 10 行指令将当前 esp 赋值给 ebp。然后接下来三行,
subl	$4, %esp
movl	8(%ebp), %eax
movl	%eax, (%esp)
如果排列成这样的顺序则更容易理解,
movl	8(%ebp), %eax
subl	$4, %esp
movl	%eax, (%esp)
观察代码,我们可以很安全地交换第一二行。交换后,第二、三行执行的调用函数时参数的压栈动作。那么第一行呢?可以看到是从 ebp + 8 这个地址取数并复制给 eax,那么这里 ebp + 8 这个地址存放的是什么呢?
我们回顾一下程序执行过程,可以发现,在第10行,ebp 被赋值成为了 f 函数的栈底指针;第9行
处,压入了 main 这个函数的栈底指针,此刻 ebp 指向的地址存放的正是这个指针的值。再往上存在跳转,在 main 函数调用 f 函数的的22行压入了当时的 eip 指针,这里的位置就是此刻的 4(%ebp)。再往上,在 main 函数第20,21行,程序将立即数 11 压栈,因此 8(%ebp) 存放的内容正是立即数 11。

在 32 位汇编中,函数调用参数通过栈进行传递,按照参数列表逆序压栈;紧接着压入 eip 指针和 ebp 栈底指针,并将 ebp 的值置为 esp。所以要访问第一个参数,需访问地址 ebp + 8。

之后的代码和上面大同小异,就不逐个分析了。

总结

总体说来,程序在执行的时候,如果不存在函数调用或者其他跳转语句时,CPU 将依照指令顺序线性地进行取指执行,这个过程中扮演者关键地位的是 EIP 寄存器,EIP 总指向即将执行的语句,并且取指后自动指向下一条指令。如果存在函数调用,如果存在函数调用,则需要在栈内维护以下信息;1. 函数调用参数;2. [b]函数返回位置;3. 函数运行栈指针。[/b]

具体说来,在函数调用前,将参数压入栈内;接下来调用函数时将 EIP 寄存器的值压栈(正是函数返回地址);然后在被调函数中将主调函数的栈底指针压栈,并更新 EBP 为此刻栈顶指针的值(亦即置为空栈)。

在函数调用完毕后,进行逆操作:首先,在被调用函数中将 EBP 赋给 ESP(栈顶指针还原),弹出主调函数的栈底指针赋给 EBP(栈底指针还原);然后,弹出函数返回地址赋给 EIP,让程序可以从调用处接着往下执行。

=========================================

真实姓名:姚思远 原创作品转载请注明出处
《Linux内核分析》MOOC课程 http://mooc.study.163.com/course/USTC-1000029000
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  Linux 内核 x86 汇编
相关文章推荐