本文主要是介绍深入理解计算机系统阅读笔记-第三章,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
第三章 程序的机器级表示
本章通过对比C语言程序代码和汇编程序代码了解程序的机器级表示
3.1 历史的观点
Intel处理器的发展历史,后文基于IA32指令集。
3.2 程序编码
基于unix系统gcc编译器;
在linux系统使用如下命令编译c文件,它会调用一些列程序(参考1.2),将c转化为可执行代码。
gcc -O2 -o helloworld helloworld.c
gcc是linux默认编译器
-O2 表示使用第二级优化,优化级别越高,程序运行的越快,但编译时间会更久。
3.2.1 机器级代码
汇编代码非常接近机器代码,但可读性更高,能够理解汇编代码以及它是如何与c代码对应的,是理解计算机如何执行程序非常关键的一步。
汇编程序可以看到如下c程序中无法提现的内容:
1. 程序计数器(%eip)
2. 正数寄存器:8个,用于存储32位值,可以存贮地址(对应c的指针),整数数据。比如程序状态,或临时数据(如局部变量)。
3. 条形码寄存器:保存最近执行的算数指令的状态信息。主要用来实现控制流中的条件变化(如实现if,while语句)
4. 浮点寄存器:8个,用于存储浮点数据
c可以在存储器中声明和分配各种类型的数据,但汇编只把存储器看成一个按字节寻址的大数组,不区分数据类型。
一条机器指令只能执行非常简单的基本操作。如将两个寄存器的数据相加,在存储器和寄存器之间传递数据,条件分支转移到新的指令等,编译器就是把c转换成这种简单的序列来实现c中复杂的代码。
3.2.2 代码示例
这本书太老了,所有的示例,在12代i5,ubuntu18的系统上运行和本书都对不上,所以强行理解吧。
int accum = 0;int sum(int x, int y)
{int t = x + y;accum += t;return t;
}
通过-S参数编译汇编代码code.s
gcc -O2 -S code.c
GCC是用过GAS(Gnu ASsembler) 格式产生汇编代码的,这种格式和intel文档和微软编译器使用的格式差异很大。
生成的汇编代码如下
sum:pushl %ebpmovl %esp,%ebpmovl 12(%ebp), %eaxaddl 8(%ebp), %eaxaddl %eax, accummovl %ebp,%esppopl %ebpret
使用-c参数可以生成目标文件code.o
gcc -O2 -c code.c
使用反汇编器objdump可以将目标文件反汇编成一种类似于汇编代码的格式,输入如下命令
objdump -d code.o
直接输出结果
通过这个结果可以发现以下特性:
1. IA32指令长度从1~15个字节不等。指令编码被设计成常用的指令以及操作数较少的指令所需的字节数少,而那些不常用的或操作数较多的指令所需字节数较多。
2. 指令格式:从某个给定位置开始,可以将字节唯一地解码成机器指令。如只有指令push1 %ebp是以字节值55开头的。
3. 反汇编器只是根据目标文件中的字节序列来确定汇编代码的。它不需要访问程序的源代码或汇编代码。
4. 反汇编器使用的指令命名规则与GAS有细微差别。上面结果中省略了很多指令结尾的“l”。
5. 与code.s中的汇编代码相比,结尾多了nop指令。它根本不会被执行(它在过程返回指令之后),即使执行也不会有任何影响(nop,即no operation)。编译器插入这条指令的目的是为了填充存储该过程的空间。
实际的可执行代码必须包含main函数
int main()
{return sum(1, 3);
}
使用如下命令生成可执行文件
gcc -O2 -o prog code.o main.c
3.2.3 关于格式的注释
GCC产生的汇编包含一些程序员不需要关系的信息,比如以“.”开头的行都是指导汇编器和链接器的命令(directive),后面的代码示例将会添加行号和注释。
3.3 数据格式
C基本数据类型的机器表示,GAS的每个操作都有一个字符后缀,表面操作数的大小。例如movb传送字节,movw传送字,movl传送双字。
3.4 访问信息
IA32的cpu包含8个32位寄存器大多数情况,前六个寄存器可作为通用寄存器,最后两个寄存器保存着指向程序栈中重要位置的指针,只有根据栈管理的标准惯例才能修改这两个寄存器的值。
如图所示,字节操作指令可以独立地读写前四个寄存器的两个低位字节,类似ah和al被称为寄存器的元素
3.4.1 操作数指令符
IA32支持多种操作数格式
操作数可以分为三种类型:
1. 立即数(immediate):在GAS中用$接整数,如$-577或$0xF;
2. 寄存器(register):表中用Ea表示任意寄存器a,用R[Ea]表示它的值,相当于将寄存器集看成数组R,Ea看做索引。
3. 存储器引用:根据计算出来的地址访问存储器的位置,用Mb[Addr]表示对存储在存储器中从地址Addr开始的b字节值的引用。为简便,通常省去b。
表中最下方是最通常的形式,由4部分组成,一个立即数便宜Imm,一个基址寄存器Eb,一个变址或索引寄存器Ei,一个伸缩因子s(scale factor),s必须是1、2、4、8;其他形式是这种通用形式的特殊情况,省略了某些部分。
练习题和答案
3.4.2 数据传送指令
数据传送指令注意点:
源操作数:可以使立即数,寄存器,存储器地址;
目的操作数:寄存器,存储器
源操作数和目的操作数不能同时是存储器地址,所以如果要实现从存储器的一个地址传输数据到另一个地址的功能,需要拆分成两条指令实现。
movsbl:单字节传输,将前面24位设置为源字节的最高位扩展成32位,然后传输到目的操作数。
movzbl:单字节传输,将前面加24个0,扩展成32位,然后传输到目的 操作数中。
pushl和popl用来压栈和出栈。栈指针是前面的8个寄存器中的倒数第二个%esp。栈向下增长,即栈顶地址是最低的。
push1 %ebp
等同于
subl $4, %esp
movl %ebp, (%esp)
流程图
3.4.3 数据传送示例
从上面的代码可知:
1. 过程参数xp和y存储在寄存器%ebp中地址偏移8和12的地方。
3.5 算数和逻辑操作
下表列出一些双字整数操作。
3.5.1 加载有效地址
加载有效地址(Load Effective Address) 指令leal实际是movl指令的变形,它是将有效地址写入目的地址。假设%edx的值是x,则下面指令的作用是把%eax的值设为7+5x
leal 7(%edx, %edx,4), %eax
练习题3.3 假设%eax值为x,%ecx值为y,填写结果。
3.5.2 一元和二元操作
一元操作:只有一个操作数,既做源,又做目的。这个操作数可以是寄存器和存储器地址。比如incl(%esp)会使栈顶元素加1。
二元操作:第二个操作数既是源,又是目的,如subl %eax,%edx
3.5.3 移位操作
移位量用单个字节编码,只允许0~31位的移位,移位量可以是立即数,也可以放在单字节寄存器元素%cl中。
算数左移和逻辑左移一致,都是低位补0。
sarl执行算数右移 ,高位补符号位,shrl执行逻辑右移,高位补0
3.5.4 讨论
3.5.5 特殊的算数操作
3.6 控制
3.7 过程
3.8 数据分配和访问
3.9 异类的数据结构
3.10 对齐(alignment)
3.11 综合:理解指针
3.12 现实生活:使用GDB调试
3.13 存储器的月结引用和缓冲区溢出
3.14 *浮点代码
3.15 *在C程序中嵌入汇编代码
3.16 小结
这篇关于深入理解计算机系统阅读笔记-第三章的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!