《汇编程序设计与计算机体系结构:软件工程师教程》 —1.4 数据的表示
662
2022-05-30
2.3 处理器
CPU 或者说处理器可以视为计算机的大脑。在计算机系统中,主要的算术运算与逻辑运算都靠这个部件来处理。宏观地来看,CPU 由 4 个主要的部分组成,它们是:算术逻辑单元(Arithmetic Logic Unit,ALU)、控制单元(Control Unit,CU)、CPU 时钟(CPU clock)及存储器(包括缓存和寄存器),参见图2-6。
ALU 是 CPU 中执行数学运算的子组件,它所执行的算术与逻辑运算针对的是整数型操作数(要注意操作数的类型是整数,浮点数由另一个组件处理)。CU 负责指挥 CPU 中的数据流,以确保 CPU 里的其他子组件能够在适当的时机接收到正确的数据,并做出相应的处理。CU 还必须把指令执行周期(Instruction Execution Cycle)安排好,使得 CPU 指令能够据此得以执行(此外,为了正确执行这些指令,计算机还需要完成其他一些子任务)。CPU 时钟与系统时钟不同,它是 CPU 本身的时钟,用来为 CPU 的操作计时。
有一个和 CPU 时钟相关的术语叫作时钟频率,它的单位是赫兹(Hertz,Hz)。说得简单一些:频率为 1 Hz 的处理器其时钟每秒震荡 1 次。这里所说的震荡一次,是指像图 2-4 那样完整地经历高电平期与低电平期。当前的处理器都是以 MHz(megahertz,兆赫) 或 GHz(gigahertz,千兆赫)来描述频率的,1 MHz 意味着每秒震荡一百万次,1 GHz 意味着每秒震荡十亿次。如果要用时钟周期而不是频率来描述处理器的快慢,就给频率取倒数。例如, 1GHz 的 CPU其时钟每秒钟脉冲十亿次,因此每个时钟周期的长度就是十亿分之一秒。有一个重要的现象要注意:CPU的时钟频率是系统时钟频率的倍数,具体是几倍由倍频系数(multiplier)决定。比方说,如果系统时钟运行在 800 MHz 的频率上,而倍频系数是 4,CPU 的频率就是 3.2 GHz。由于 CPU 的时钟运行速度为系统时钟的 4 倍,因此它在同样长的时间内所能完成的操作数量也是后者的 4 倍。
2.3.1 缓存与寄存器
由于处理器要对数据执行操作,因此必须要有地方来保存这些用作操作数的数据,此外,操作结果以及操作所涉及的地址也得有地方保存。前面讲解存储器的层次结构时曾经说过,离 ALU 越远,存储器中的数据访问起来越慢。于是,为了令 CPU 能够尽快执行操作,必须想办法把指令与数据放到它能够迅速访问的存储器中,也就是说,存放这些指令与数据的存储器离 ALU 及 CU 越近越好。这种紧邻 CPU 的存储器就叫作cache(高速缓存,简称缓存),实际上,它跟逻辑电路一起位于 CPU 芯片中。缓存在存储器的层次结构中有其地位,然而它内部还有一套自己的层次结构。
当前的处理器缓存通常分为三个级别,分别是 L1 缓存(一级缓存)、L2 缓存(二级缓存)与 L3 缓存(三级缓存)。缓存本身的层次结构与存储器的层次结构都遵循同一条原则:距离 ALU 越远容量越大,价格也越便宜。L1 与 L2 缓存都离 ALU 很近,不过 L2 要比 L1 稍远一些,因此其容量也大一些。L3 缓存一般出现在多核处理器中,它为所有 CPU 核心所共享,而 L1 与 L2 缓存则每个 CPU 核心都配有一套,如图2-7所示。L3 缓存是静态存储器中的最后一层,如果数据不保存在该层及其上方的各层中,那就只好放到它下方的 RAM 中了。
现在以 Intel Core i7 系列的处理器为例来说明缓存的层级:i7 处理器的每个核心都配有 64KB的 L1 缓存,其中 32KB保存指令,32KB保存数据。此外,还配有 256KB的 L2缓存。L3 缓存的容量是4MB~24MB,具体要看你买的是便宜一些的型号还是贵一些的型号。
CPU 提供了一些与缓存有关的指令,然而开发者一般都不用专门编写代码去访问或操作缓存,因为缓存是由复杂的算法来控制的,以确保程序所需的数据能够尽量出现在缓存中,所以开发者通常不需要干预这套机制。比方说,如果程序频繁引用某个变量,那么处理器就有可能认为这份数据相当重要,从而将其预先获取(prefetch,简称预取)出来并放入缓存,使得程序以后访问该数据时能够快一些。CPU 会在执行程序的过程中随时根据情况来做出这种处理。
除了缓存之外,CPU 中还有一种存储器叫作寄存器(register),它的内容可以通过明确的地址来访问,而且在此类存储器中它是最快的一种。它位于整个存储器层级的最顶端,其容量比缓存更小,速度也比缓存更快。寄存器是最贴近 ALU 的一小块存储区域,用来保存执行指令时所涉及的操作数、地址及结果。
提示:处理器的每个核心都有自己的一套 L1 与 L2 缓存,与之类似,每个核心也都有自己的一套寄存器。然而,编写汇编代码的时候你不用指出当前操作的寄存器究竟处在哪个核心上,你只需要写出寄存器的名字就可以了,至于这个寄存器到底指的是哪个核心上的寄存器则由 CPU决定。表 2-4 列出了这些名称。
表 2-4 x86 与 x86_64 的寄存器
表 2-4 中的寄存器可以分成 4 类:通用目的寄存器(General Purpose Register)、段寄存器(Segment Register)、标志寄存器(Flags Register)及指令指针寄存器(Instruction Pointer Register)。汇编程序所操作的基本上都是通用目的寄存器,其中,32 位的通用寄存器有 8 个,64 位的有 16 个。通用寄存器用来执行计算或移动数据。由于 64 位处理器是在 32 位设计方案的基础上构建的,而 32 位处理器又是在 16 位设计方案的基础上构建的,因此新式处理器不仅可以通过寄存器本身的名字来使用该寄存器,而且还能通过旧式处理器所用的名字将其当成旧式的寄存器来使用。此外,如果你要使用的数据或是你要执行的运算只需占据 8 个二进制位,那么可以把 16 位的寄存器想象成两个 8 位的寄存器,这样就可以用它来保存两份数据了。图 2-8 以 64 位的rax寄存器为例来演示怎样以 8 位、16 位、32 位及 64 位的方式使用它。
图 2-8 寄存器寻址
由于上述寄存器之间有所重叠,因此会引发一个问题:以不同的名义来操作寄存器会不会使其中的数据受到影响?比方说,如果以 eax 的名义保存了一个 32 位的值,然后又以 ax 的名义保存了一个 16 位的值,那么 eax 的低 16 位是得以保留,还是遭到覆盖?图 2-9 给出了解答。由该图可知,无论以什么名义来使用寄存器操作的都是同一套二进制位,具体到本例来说,这意味着无论你是用 rax 来操作,还是用 eax、ax、ah或 al 来操作,你所操作的二进制位其实都位于 rax 寄存器本身的那 64 个二进制位中,而不是说总共有 128 个二进制位可以使用,其中 64 个划拨给 rax ,32 个划拨给 eax,16 个划拨给 ax,8 个划拨给 ah,8 个划拨给al。同样的规律也适用于其他类似的情况:凡是以小寄存器的名义来操作大寄存器的,其实操作的都是大寄存器中与这个小寄存器相对应的那一部分二进制位。
图 2-9 以 ax 的名义操作 eax,会破坏 eax 中已有的一部分数据
从图 2-9 中可以看出,如果把值复制到 ax 中,那么实际上会把 eax 的低 16 位给覆盖掉,导致 eax 中原有的值遭到破坏。之所以出现这种问题是因为 eax 与 ax 共用这 16 个二进制位。不过并非所有的寄存器都像 rax 这样可以通过 rax、eax、ax、ah 及 al 等不同的名义来使用,即便可以这样用,其二进制位的共享情况也未必和此处所举的例子相同。表 2-5 列出了 rax、rbx、rcx 及 rdx 这 4 个 64 位寄存器与其 32 位、16 位、8 位子寄存器之间的共用情况。另外 4 个 64 位寄存器,也就是 rsi、rdi、rbp 及 rsp 与其 32 位及16 位子寄存器之间的共用情况参见表 2-6。
表 2-5 rax、rbx、rcx 及 rdx 与其子寄存器之间的重叠情况
表 2-6 rsi、rdi、rbp 及 rsp 与其子寄存器之间的重叠情况
尽管刚才提到的那些寄存器都可以用在汇编程序里,但是必须注意,有些寄存器是有特殊用途的。如果不加注意,那么保存于某个寄存器(例如 rax/eax)中的数据就有可能在执行完下一项操作之后遭到修改,从而产生违背开发者意图的效果。下面列出某些 64 位及 32 位寄存器的特殊用法,以提醒大家避免相关的编程错误。
rax/eax 通常是默认的累加寄存器。乘法等操作会将其中一部分结果自动存放到 rax/eax 中,调用函数的时候也需要把返回值保存在 rax/eax 中。因此,执行这些操作时不要用 rax/eax 保存一般的数据。
rcx/ecx 用来在执行循环的过程中记录循环计数器的值。因此,在循环内部不要用 rcx/ecx 保存一般的数据。
rbp/ebp 用作栈帧中的帧指针,这会在第 6 章讲解。该寄存器用来指向栈中的数据,笔者建议只把它当作专门的寄存器来用。
rsp/esp 是栈指针寄存器,这也是个与栈管理有关的寄存器,它一般指向活动栈帧的顶部。与前一个寄存器一样,这个寄存器也只应该当成专门的寄存器来用。
rsi/esi 与 rdi/edi 是索引寄存器(index register,也称为变址寄存器),它和STOSB、MOVSB 与 SCASB 这样的字符串操作结合起来使用,以便保存、加载或扫描大量的数据。这些操作实际上会把 CPU 置于一种自动循环模式中,这要比开发者手工编写循环更有效率。
rip/eip 是扩展版的指令指针寄存器。这个寄存器用来指向内存中的地址,以表示接下来应该获取、解码并执行的指令,它是在程序运行过程中自动调整的,不应该通过编程的手段修改。
rflags/eflags 是状态与控制寄存器,这会在接下来的内容里详细讲解。LAHF 与 SAHF 等特殊指令可以把 CPU 的一些状态标志载入 ah 寄存器,或是将 ah 寄存器里的值保存到状态寄存器中。除此以外,不应该用其他手段直接修改 rflags/eflags。该寄存器里的二进制位是在执行完算术运算之后根据一套布尔规则自动设置的。尽管 rflags 是 64 位,但其中能够用到的只有低 32 位,因此,x86 与 x86_64 处理器用的是同一套状态标志。
CPU 标志(CPU flag)是一些二进制位的统称,这些二进制位分别用来以某种方式控制 CPU 操作或反映 CPU 操作的状态。表2-7列出了大多数开发环境中值得关注的 8 个标志位,其中某些标志可以由开发者通过 LAHF 及 SAHF 指令来操作。表 2-8 列出了可以由这些指令所编辑的标志位。对于标志位来说,set(设置)的意思是将其设为 1(也称为置 1),clear(清除)的意思是将其设为 0(也称为置 0)。
表2-7 值得关注的标志位
Overflow(溢出) OF 11 如果运算结果以补码的形式来表示时所需的二进制位个数超过了该运算所采取的个数,那么该标志就会设置。与 Carry 标志不同的是,此标志是针对带符号的整数而言的,如果运算结果的符号与操作数的符号相反,那么它就会得到设置,比方说,在 8 位环境中,127 + 127 的结果用补码表示需要 9 位(0 1111 1110),如果只看后 8 位(1111 1110),那么由于其中的最高有效位是1,因此这是个负值(相当于十进制的 -2),于是执行完这次加法之后OF标志就会得到设置
编程知识:表 2-8 中,1、3、5 号二进制位是用 U 来表示的,意思是说,这些二进制位用不到(Unused)或是予以保留(reserved)。在通过 SAHF 指令把 ah 的内容保存到标志寄存器时,这些二进制位的值不应当受到影响,3、5 号二进制位始终应该是 0,1 号二进制位始终应该是 1。
表 2-8 可以通过 LAHF/SAHF 指令编辑的标志位及其序号
软件开发
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。