第一章 计算机工作原理
1 存储程序计算机工作模型
存储程序计算机的主要思想是将程序存放在计算机存储器中,然后按存储器中的存储程序的首地址执行程序的第一条指令,以后就按照该程序中编写好的指令执行,直至程序执行结束。
冯·诺依曼体系结构的核心是存储程序计算机。
2 x86-32汇编基础
8086CPU中总共有14个16位的寄存器(AX、BX、CX、DX、SP、BP、SI、DI、IP、FLAG、CS、DS、SS和ES),然后对应也有32位的寄存器(EAX、EBX、ECX、EDX、ESP、EBP、ESI、EDI、EIP、EFlags、CS、DS、SS、ES、FS和GS)32位寄存器只是把对应的16位寄存器扩展到了32位,并且所有开头为E的存储器,一般是32位的。
要注意的是在16位的CPU中,AX、BX、CX和DX不能作为基址和变址寄存器来存放存储单元的地址,但在32位的CPU中,32位寄存器EAX、EBX、ECX和EDX不仅可以传送数据、暂存数据保存算术逻辑运算结果,还可以作为指针寄存器,这些32位寄存器更具通用性。
以32位为主介绍一些常见的汇编指令:
寄存器寻址:movl %eax, %edx;
寄存器寻址就是操作的是寄存器,不和内存打交道,%eax,其中%开头后面跟一个寄存器名称。上述代码的含义为把寄存器%eax的内容放到%edx中。
立即寻址:movl $0x123,%edx
上述代码的意思是把0x123这个十六进制的数值直接放到EDX寄存器中。立即寻址也和内存没有关系。
直接寻址:movl 0x123, %edx
上述代码的意思是把0x123内存地址所指向的那块内存里存储的数据放到EDX寄存器里。直接寻址就是用内存地址直接访问内存中的数据。
间接寻址:movl (%ebx), %dx
间接寻址就是寄存器加个小括号。上述代码的意思是%ebx这个寄存器中存的值是一个内存地址,加个小括号表示这个内存地址所存储的数据,我们把它放到EDX寄存器中。
变址寻址: movl 4(%ebx), %edx
代码中“(%ebx)”前面出现了一个4,也就是在间接寻址的基础上,在原地址上加上一个立即数4。
还有一点需要注意,一般说来,全是大写字母的一般是Intel汇编,全是小写字母的一般是AT&T汇编。在这里代码用到的寄存器名称都遵守AT&T汇编格式采用全小写的方式。
接下来介绍几个很重要的指令:pushl/popl和call/ret。
在AT&T汇编中,pushl等效于以下汇编指令:
1 subl $4, %esp #把堆栈的栈顶ESP寄存器的值减4,因为堆栈是向下增长的
2 movl %eax, (%esp) #把EAX寄存器的值放到ESP寄存器所指向的地方
在AT&T汇编中,popl等效于以下汇编指令:
1 movl (%esp), %eax #把栈顶的数值放到EAX寄存器中
2 addl $4, %esp #栈顶加4,相当于栈向上回退了一个存储单元的位置
在AT&T汇编中,call指令是函数调用,调用一个地址,例如call 0x12345等效于以下汇编指令:
1 pushl %eip (*) #把当前的EIP寄存器压栈
2 movl $0x12345, %eip (*) #把0x12345这个立即数放到EIP寄存器里
然后在这里要特别注意,上面的两个动作并不存在实际对应的指令,这里用“(*)”来特别标记一下,这两个动作是由硬件一次性完成的,而且出于安全方面的原因,EIP寄存器不能被直接使用和修改。
在AT&T汇编中,ret指令跟call指令相对应,是函数返回指令,ret等效于以下汇编指令:
1 popl %eip (*) #把当前堆栈栈顶的一个存储单元放到EIP寄存器中
同样这里的动作并不存在实际对应的指令,这个动作也是硬件一次性完成的。
3 汇编一个简单的C语言程序并分析其汇编指令执行过程
输入ls命令可看到“Code”和“LInuxKernel”,因为目录“Code”按照“实验楼”的使用约定是保存用户编写代码的目录,所以进入“Code”目录下进行操作。
本次实验中用到的C语言代码为:
1 // main.c
2 int g(int x)
3 {
4 return x + 4;
5 }
6
7 int f(int x)
8 {
9 return g(x);
10 }
11
12 int main(void)
13 {
14 return f(6) + 1;
15 }
在命令行下输入" vi main.c"命令打开文本编辑main.c文件,按"i"键进入输入状态,如下图所示:
接下来在文本编辑器VIM中按“Shift” + “:”进行文本编辑的命令模式,输入“wq”就可把代码保存到main.c中,并退出VIM编辑器,但是我遇到了一个问题就是在插入模式下按照前边操作是无效没有反应的,我的解决方案是:按ESC退出编辑模式,此时的模式为“NORMAL”模式,然后再输入冒号,接着输入“wq”,再按回车键即可,如下图所示:
然后使用gcc main.c命令编译main.c这个代码文件,这时会生成一个目标文件a.out,它是可执行的,但此时看不到任何信息,可以通过echo $?命令查看这个程序的返回值,该C语言程序的结果为11,如下图所示:
接着可以把main.c编译成一个汇编代码,可使用gcc -S -o main.s main.c -m32这个命令,如下图所示:
此时我们可以看到main.s汇编文件还有一些“.cfi_”打头的字符串以及其他以“.”打头的字符串,这些都是编译器在链接阶段所需的辅助信息,可以通过在VIM中输入“g/.s*/d”命令删除所有以“.”打头的字符串简化main.s里的汇编代码,注意在这里VIM编辑器要在“NORMAL”模式下输入“:”,再输入“g/.s*/d”命令,按回车键即可,如下图所示:
接着我介绍一下在上面main.s文件中新出现的汇编指令leave指令,还有与leave指令相对应的enter指令。
leave指令用来撤销函数堆栈,等价于下面两条指令:
1 movl %ebp, %esp
2 popl %ebp
enter指令用来建立函数堆栈,等价于下面两条指令:
1 pushl %ebp
2 movl %esp, %ebp
最后对该C语言程序的汇编代码进行分析:
首先假定堆栈为空栈的情况下EBP和ESP寄存器都指向栈底,而且为了简化起见,我们为栈空间的存储单元进行标号,压栈时标号加1,出栈时标号减1,如下图所示:
下面是我的分析过程:
我在分析汇编代码时movl 8(%ebp), %eax中的间接寻址产生了一个误区:我以为使用ESP寄存器变址寻址,是ESP寄存器向上移动两个标号,导致我后边的分析出了差错。后来翻看课本对间接寻址的定义,才知道是EBP寄存器存储的数值加8,ESP寄存器不移动。
4 总结
在本次学习中,我通过课本,再一次回顾了存储程序计算机工作模型以及基本的一些的汇编语言,重点是再一次深入理解了函数调用堆栈相关汇编指令,如call/ret和pushl/popl,还有leave和enter,并通过自己动手实践将一个C语言代码程序在Linux环境下运行,同时反汇编C语言程序,最后自己分析了汇编代码是如何在存储程序计算机工作模型上一步步执行的。