【CSAPP笔记】4. 汇编语言——基础

程序的机器级表示

计算机能读懂是机器代码(machine code)—— 用字节序列编码的低级操作 —— 也就是0和1。编译器基于编程语言的规则、目标机器的指令集和操作系统的规则,经过一系列阶段产生机器代码。由于机器语言全是由0和1组成的,所以对于编程人员来说编写机器代码十分困难,也不容易学习。汇编语言(assembly language)就是机器语言的可读形式,学习汇编语言有很多的好处。

An assembly (or assembler) language,[1] often abbreviated asm, is a low-level programming language for a computer, or other programmable device, in which there is a very strong (generally one-to-one) correspondence between the language and the architecture’s machine code instructions.

Machine code or machine language is a set of instructions executed directly by a computer’s central processing unit (CPU). Each instruction performs a very specific task, such as a load, a jump, or an ALU operation on a unit of data in a CPU register or memory. Every program directly executed by a CPU is made up of a series of such instructions……Numerical machine code (i.e., not assembly code) may be regarded as the lowest-level representation of a compiled or assembled computer program or as a primitive and hardware-dependent programming language.

我们在用高级语言编程时(例如C语言),高级语言本身提供的抽象级别比较高,因此有“程序设计”的概念,方便我们编程。大多数情况下,抽象级别比较高,工作效率就会提高,也更高效可靠。高级语言相较汇编语言的一个最大优点是:用高级语言编写的程序可以在很多不同的机器上运行,而汇编代码与硬件本身有关联,例如指令集不同那么汇编代码就不同。

Each assembly language is specific to a particular computer architecture. In contrast, most high-level programming languages are generally portable across multiple architectures but require interpreting or compiling.

那我们为什么要花时间学习汇编和机器代码呢?

纵观计算机的发展历程,有一个很重要的观点就是用分层的思路来构造整个系统,每个下层都对其上层隐藏本层的细节。这个抽象原理对于理解计算机很多方面的知识都有帮助,例如网络四层模型就是一个很典型的例子。扯回编程语言这块,由于语言是“高级”的,自然屏蔽了很多机器级实现过程的细节。例如,如果只是单纯的学习C语言,那对于寄存器、程序计数器、系统的栈、缓冲区溢出等知识点没有办法很好的理解,因为它们对于C语言程序员来说都是被隐藏的处理器状态。C语言的指针通常是引起错误和不理解的来源,但在汇编中访存、取地址都是很常见的操作。学习汇编,能让你学到一些在C语言学习中由于粒度不够细而学不到的东西。除此之外,读完这一章之后,你还能领略到C语言的伟大之处。

抽象和分层是很重要的思想,但对于严谨的学习者来说,并不意味只要懂得抽象原理就足够了。能够阅读和理解汇编代码是仍是一项很重要的技能。精通细节是理解更深和更基本概念的先决条件。随着时间的推移,对于汇编代码的学习需求也有变化,我们现在不需要直接用汇编语言编写程序,只需要能够阅读并理解编译器产生的代码就可以了。

处理器执行的指令,被编码成二进制形式。一个处理器支持的指令及其编码集称为指令集体系结构(Instruction-Set Architecture, ISA)。

An instruction set, with its instruction set architecture (ISA), is the interface between a computer’s software and its hardware, and thereby enables the independent development of these two computing realms; it defines the valid instructions that a machine may execute.

ISA就是机器级程序的格式和行为,这种类型的指令,每一条完成的工作都是非常基本的,例如把两个数相加。因此学习汇编代码就有助于我们了解编译器在把C代码转换成机器代码时所做的转换。相对于C代码,编译器重新排列执行顺序、消除不必要的计算和变量、用基本的、快速的操作代替慢速操作,甚至用迭代替代了递归。学习逆向工程(reverse engineering),来研究高级语言,甚至研究系统创建的过程。(好书啊好书)

可以说,汇编语言很有其特殊性,高级语言的一些抽象的概念,都被归一和具象化了,C语言的指针,在汇编的世界里简直不值一提,因为地址,对地址的引用再平常不过了。另一方面,内存,缓存,寄存器,CPU,又构成了一个奇妙的世界,它们屏息恭立,静待主公的每一条喻令。
我不相信,一个人用纯手工一条条指令去雕琢他的程序,用手指感受计算机的呼吸时会无动于衷。……

作者:匿名用户
链接:https://www.zhihu.com/question/23088538/answer/23631875
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

Architecture

汇编内容比较多,部分涉及具体细节的内容有删减,仅做简单介绍,详情请参考书本。

处理器

Intel 处理器系列俗称 x86,经历了一个长期的、不断发展过程。1965年,Intel公司的创始人Gordon Moore,根据当时的芯片技术(那时他们能够在一个芯片上制造大概有64个晶体管的电路)做出推断:未来10年内,芯片上晶体管数量每年都能翻一番。这个预测就是著名的摩尔定律。正如事实所证明的那样,他的预测有点乐观,还有点短视。在超过45年中,半导体工业一直能够让晶体管数量每18个月翻一番。

指令集

就像上面讲到的那样,计算机系统用到了许多不同形式的抽象,利用简单的模型隐藏各种细节。其中之一就是指令集,也就是ISA。在学习汇编时,ISA模型给人的感觉好像看上去应该是顺序指令执行,也就是先取出一条指令,等到它执行完毕,再开始下一条。然而处理器的实际工作方式远比这种模式精细复杂,因为处理器可以并发执行多条指令,因此需要做很多的额外工作。在计算机科学中,用巧妙的方法在提高性能,并完成更多更复杂的功能的同时,又同时保持了一个更简单、更抽象的底层模型功能,这种想法是无处不在的。

IA32和x86-64就是两种ISA,也是着重介绍的重点。这两者是当今大多数计算机的主导语言,后者是前者在64位机器上的扩展版本。

IA32的机器代码和原始C代码差别非常大。下面这些对于C语言程序员隐藏的概念在IA32中是可见的:

  • 程序计数器(Program Counter,简称PC),用来指示将要执行的下一条指令的地址。

  • 整数寄存器文件(Register File),非常重要的概念。用来存储地址(对应C语言的指针)或者整数数据,或者用来记录程序的状态,或者保持临时数据,例如局部变量、函数的返回值等等。

  • 条件码(Condition Code),保持最近执行过的算术or逻辑指令的状态信息。用来实现控制或数据流条件变化。(对应C语言的if等等)

对于机器级编程来说,有第另一种极为重要的抽象,那就是把存储器地址抽象成虚拟地址(Virtual Memory),提供的模型,让机器代码只是简单地把存储器看成是一个非常大的字节数组。所以,C语言中的各种数据类型,和自定义数据类型(struct),在机器代码中都是用一组字节来表示。汇编代码不区分有符号、无符号常数,不区分各种类型的指针,甚至不区分指针和整数。

  • 程序存储器(Program Memory)包含程序的可执行机器代码,操作系统需要的一些信息,例如支持系统运行的栈,以及为用户分配的存储器块(例如malloc)。程序存储器用虚拟地址来寻址(Addressable),操作系统负责管理虚拟地址空间,将虚拟地址翻译成实际处理器中的物理地址。

处理器能够执行的操作其实是非常有限的,简单来说只有三种:存取数据、计算和传输控制。存取数据是在内存和寄存器之间传输数据,进行计算则是对寄存器或者内存中的数据执行算术运算,传输控制主要指非条件跳转和条件分支。

代码示例

下面给出一个C语言代码以及其汇编代码中的一些关键行。一开始看不懂没关系,我们只需要对其有一个感性认知。

1
2
3
4
5
6
7
8
int accum = 0;

int sum(int x, int y)
{
int t = x + y;
accum += t;
return t;
}

对应的由编译器产生的汇编代码(部分行)如下:

1
2
3
4
5
6
7
8
sum:
pushl %ebp
movl %esp, %ebp
movl 12(%ebp), %eax
addl 8(%ebp), %eax
addl %eax, accum
popl %ebp
ret

观察汇编语言代码,我们可以看到第一个字符串就是操作符,后面可能跟着几个操作数,由逗号分开。操作符就被唯一解码成对应的机器指令,但汇编语言的操作符是能让我们看得懂的。

在汇编代码中,每一行都代表一条机器指令。例如pushl就是把寄存器%ebp的内容压入栈。这段代码已经不存在任何关于局部变量名和数据类型的信息(除了全局变量accum,还没有确定会放在哪里)。

如果使用汇编器(Assembler)将汇编语言翻译为机器语言,机器语言是二进制格式,无法直接查看。但其中有一列17个字节的字节序列:

1
55 89 e5 8b 45 0c 03 45 08 01 05 00 00 00 00 5d c3

这就是上面列出的汇编指令对应的机器代码。我们可以看出来,机器实际执行的就是对一系列指令进行编码的字节序列。机器对于我们写的C语言代码可以说是一无所知的。


See Also