程序的机器级表示
- 程序编码
- 数据格式
- 访问信息的方式
所有的高级语言,都会被计算机翻译为机器代码,然后再根据汇编代码生成可执行的机器代码。二进制的机器代码我们人类肯定是读不懂了,但是汇编代码还是可以简单了解一下的。CPU 的 PC、寄存器、缓存都是怎么工作的,计算机是如何寻址的等等问题,都可以通过汇编代码去了解
程序编码
将高级语言编译成机器语言,计算机才可以根据自身的指令集来执行命令
机器执行的程序只是一个字节序列,是对一系列指令的编码。机器对产生这些指令的源代码几乎一无所知。在 Linux 上,汇编代码文件的后缀名是 .s 文件,二进制的目标代码文件后缀为 .o。对汇编文件会调用汇编器以产生目标代码文件,之后再对目标代码文件调用链接器,为其中的函数调用找到匹配的函数的可执行代码的位置
为了读取上面的 .o 文件,计算机对于使用机器级代码的机器级编程而言,有两种重要抽象:
- ISA:指令集体系结构或指令集架构,来定义机器级程序的格式和行为。它定义了处理器状态、指令的格式、以及每条指令对状态的影响
- 虚拟地址:机器级程序使用的内存地址是虚拟地址,所提供的内存模型看起来像是一个非常大的字节数组
ISA 中,指令由操作码和操作数两部分组成。CPU 在设计好之后,其指令集就确定了(硬件厂商处理),CPU 对每条指令都规定了相应的机器码,不同寻址方式的指令,不同运算的指令,它们的机器码都不相同。CPU 刚开始读取指令时并不知道将会执行什么指令,它将指令地址发到地址总线,然后指令将逐字节地通过数据总线传送到 CPU 中,当 CPU 读取到指令中的操作码(前几个字节)时,就知道了当前指令的长度,于是就知道接下来应该读取多少字节的数据作为一条指令和下一条指令的位置
数据格式
数据格式方面,由于是 16bit 体系结构扩展至 32bit,Intel 用术语字表示 16bit 数据类型。进而 32bit 称为双字,64bit 称为四字,字节、字、双字、四字分别对应的汇编代码后缀为 b,w,l,q,特别的 float 类型对应的是 s,而 double 对应的则是 l。他们的大小我们在学习 c 语言或者 java 时已经了解过了,就不再叙述了
大多数汇编代码都有这样一个后缀,表面操作数的大小,例如数据传输指令有四个变种,movb(传输字节)、movw(传输字)、movl(传输双字)、movq(传输四字),计算机知道了后面数字的大小,就知道什么位置是下一个指令的开头
我们可能想到一些常见的计算机处理文件大小的方法,例如 TCP 报文用差错校验、特殊的字符作为报文的开头和结尾,并且还有数字表示该报文有多长。CPU 没有采取这些方法,是因为 CPU 读取的指令多而且小,用特殊的字符以此来提高性能
访问信息的方式
一个 x86_64 的中央处理器单元 CPU 包含一组16个存储64位值的通用目的寄存器,以用来存储整数数据和指针。16个寄存器的命名有历史原因,其中有一重要的寄存器为 %rsp(用作栈指针,用来指明运行时栈的结束位置),%rax(返回值寄存器,ret 指令返回的即是 %rax 寄存器中的值)
此外对于寄存器,有一组标准的变成规范控制着如何使用寄存器来管理栈、传递返回值,以及存储局部和临时数据。以下表示的是16个存储寄存器分别的类别:
对于操作数指示符,也即是目的操作数的概念。其作用是指出执行一个操作中要使用的源数据值,以及放置结果的目的位置。各种不同的操作数可以分为四种:
- 立即数:用来表示常数值
- 寄存器数:表示某个寄存器的内容,16个寄存器的低位1字节、2字节、4字节或8字节中的一个作为一个操作数。访问寄存器比访问内存要快得多
- 内存引用:根据计算得到的地址(通常是有效的物理内存地址而不是虚拟内存地址)访问某个内存位置
- 以上混合使用:我们可以使用立即数当作寄存器的指针,因此获取寄存器中的数据,也可以使用寄存器中的数据或者立即数当作指针,来访问内存中对应地址的数据