最近在学习《深入理解计算机系统这本书》。原来学的计算机系统的相关知识忘的差不多了,发现这是很好的一本教材,如果现在高校用这本书教学的话,效果一定不错。这本书总是在适当的时候弄个练习题出来,让你反思一下你现在学的东西。这里总结一下最近学习到的第三章的知识要点。
为什么我们还要花时间学习机器代码呢?即使编译器承担了产生汇编代码的大部分工作,对于严谨的程序员来说,能够阅读和理解汇编代码仍是一项很重要的技能。
试图最大化一段关键代码的性能的程序员,通常会尝试源代码的各种形式,每次编译并检查产生的汇编代码,从而了解程序将要运行的效率如何。高级语言提供的抽象层会隐藏我们需要的了解的有关程序运行时行为的信息。源代码和对应的汇编码的关系通常不太容易理解。
学习这一章节要努力去弄明白一个switch语句是否总比一系列的if-else高效的多?一个函数调用开销多大?while循环比for循环更有效么?指针引用比数组索引更有效么?为什么将循环求和的结果放到一个本地变量中,与将其放到一个通过引用传递过来的参数中相比,运行速度快多少呢?为什么我们只是简单滴重新排列一下一个算数表达式中的括号就能让一个函数运行的更快?怎样去避免缓冲区溢出错误?意识到高速缓存的存在可以良好的提高程序性能,如何提高呢?这都是在学习过程中需要探索的问题。
学习汇编语言首先要弄清楚什么是IA32架构?
英特尔32位元架构(英语:Intel Architecture, 32-bit,缩写为IA-32),常被称为i386、x86-32或是x86,由英特尔公司推出的指令集架构,至今英特尔最受欢迎的处理器仍然采用此架构。它是x86架构的32位元延伸版本,首次应用在Intel 80386芯片中,用来取代之前的x86 16位元架构(x86-16),包括8086、80186与80286芯片。IA-32属于复杂指令集。 复杂指令集,也称为CISC指令集,英文名是CISC,(Complex Instruction Set Computer的缩写)。在CISC微处理器中,程序的各条指令是按顺序串行执行的,每条指令中的各个操作也是按顺序串行执行的。顺序执行的优点是控制简单,但计算机各部分的利用率不高,执行速度慢。其实它是英特尔生产的x86系列(也就是IA-32架构)CPU及其兼容CPU,如AMD、VIA的。即使是现在新起的X86-64(也被称为AMD64)都是属于CISC的范畴。要知道什么是指令集还要从当今的X86架构的CPU说起。X86指令集是Intel为其第一块16位CPU(i8086)专门开发的,IBM1981年推出的世界第一台PC机中的CPU—i8088(i8086简化版)使用的也是X86指令,同时电脑中为提高浮点数据处理能力而增加了X87芯片,以后就将X86指令集和X87指令集统称为X86指令集。虽然随着CPU技术的不断发展,Intel陆续研制出更新型的i80386、i80486直到过去的PII至强、PIII至强、Pentium 3,最后到今天的Pentium 4系列、至强(不包括至强Nocona),但为了保证电脑能继续运行以往开发的各类应用程序以保护和继承丰富的软件资源,所以Intel公司所生产的所有CPU仍然继续使用X86指令集,所以它的CPU仍属于X86系列。由于Intel X86系列及其兼容CPU(如AMD Athlon MP、)都使用X86指令集,所以就形成了今天庞大的X86系列及兼容CPU阵容。x86CPU目前主要有intel的服务器CPU和AMD的服务器CPU两类。
在这我们了解到IA32架构师一个指令集合,这一章的所有指令都来自于这个集合。
在此再回顾一下什么是程序的编译过程:共分为四步,预编译、编译、汇编、链接。预处理(也称预编译,Preprocessing)、在预处理过程中,对源代码文件中的文件包含(include)、预编译语句(如宏定义define等)进行分析。编译(Compilation)、接着调用cc1进行编译,这个阶段根据输入文件生成以.s为后缀的汇编代码文件。汇编(Assembly),将汇编代码转化成二进制目标文件.o结尾。目标代码是机器代码的一种形式,他包含所有指令的二进制表示,但是还没有填入地址的全局值。链接(Linking),当所有的目标文件都生成之后,gcc就调用ld来完成最后的关键性工作,这个阶段就是连接。在连接阶段,所有的目标文件被安排在可执行程序中的恰当的位置,同时,该程序所调用到的库函数也从各自所在的档案库中连到合适的地方。
数据格式
16位的机器体系结构,Intel用术语"字(word)"表示16位数据类型。32位机器体系结构是从16位扩展过来,因此称32位数为“双字(double words)”,称64位数为“四字(quad words)”。大部分指令都是针对字节和双字操作的。
C声明 Intel数据类型 GAS后缀 大小(字节)
char 字节(byte) b 1
short 字(word) w 2
int 双字(double words) l 4
unsigned 双字(double words) l 4
long int 双字(double words) l 4
unsigned long 双字(double words) l 4
char * 双字(double words) l 4
float 单精度(signal) s 4
double 双精度(double) l 8
long double 扩展精度 t 10/12
GAS中的每个操作指令都有一个字符后缀,用于表明操作数的大小。例如,mov有三种形式: movb(传送字节)、movw(传送字)、movl(传送双字)。其中float的后缀也是l,这不会与整数的混淆,因为浮点数使用的一组完全不同的指令和寄存器(浮点数寄存器)。
在IA32中央处理单元(CPU)中,包含了8个32位整数寄存器(如下图)。从图中可以看到在每个32位寄存器的名字前面都会有一个%e,在这里可以把e理解成extended(扩展的),因为早期的8086寄存器是16位,所以加e之后就变成32位的了。
操作数指示符包括立即数,寄存器,存储器,下图是对应的操作数的格式及对应的寻址方式。
类型 | 格式 | 操作数值 | 名称 |
立即数 | $Imm | Imm | 立即数寻址 |
寄存器 | Ea | R[Ea] | 寄存器寻址 |
存储器 | Imm | M[Imm] | 绝对寻址 |
存储器 | (Ea) | M[R[Ea]] | 间接寻址 |
存储器 | Imm(Eb) | M[Imm + R[Eb]] | (基址+偏移量)寻址 |
存储器 | (Eb, Ei) | M[R[Eb] + R[Ei]] | 变址寻址 |
存储器 | Imm(Eb, Ei) | M[Imm + R[Eb] + R[Ei]] | 变址寻址 |
存储器 | (, Ei, s) | M[R[Ei] * s] | 比例变址寻址 |
存储器 | Imm(, Ei, s) | M[Imm + R[Ei] * s] | 比例变址寻址 |
存储器 | (Eb, Ei, s) | M[R[Eb] + R[Ei]*s] | 比例变址寻址 |
寄存器 | Imm (Eb, Ei, s) | M[Imm + R[Eb] + R[Ei]*s] | 比例变址寻址 |
数据传送指令:
move指令:将源操作符的值复制到目的操作数中。源操作数指定的值是一个立即数,存储在寄存器中或者存储器中。目的操作数指定一个位置,要么是一个寄存器,要么是一个存储器地址。。传送指令的链各个操作数不能都指向存储器位置。这些指令的寄存器操作数,对于movl来说,可以是8个32位寄存器(%eax~%ebp),对于movw来说,可以是8个16位寄存器(%ax~%bp),movb可以使单字节寄存器元素(%ah~%bh,%al~%bl)。
以上是最近的总结。