深入理解计算机系统(第3章 程序的机器级表示②)

3.3 数据格式

由于是从16位体系结构扩展成32位的,Intel用术语“字(word)”表示16位数据类型。因此,称32位数为“双字(double words/long word)”,称64位数为“四字(quad words)”。
image
大多数GCC生成的汇编代码指令都有一个字符的后缀(b/w/l/q),表明操作数的大小。

注意:汇编代码同时使用后缀'l'来表示4字节整数和8字节双精度浮点数。这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。

3.4 访问信息

一个x86-64的中央处理单元(CPU)包含一组16个存储64位值的通用目的寄存器,用来存储整数数据和指针。
image

指令集的历史演化造成了不同的命名规则:最初的8086中有8个16位寄存器(%ax~%sp),每个寄存器都有特殊的用途。扩展到IA32架构时,寄存器扩展成32位寄存器(%eax~%esp)。扩展到x86-64后,原来的8个寄存器扩展成64位(%rax~%rsp),还增加了8个新的寄存器(%r8~%r15)。

两条规则:

  • 生成1字节和2字节数字的指令会保持剩下的字节不变。
  • 生成4字节数字的指令会把高位4个字节置为0。

%rsp是栈顶指针,用来指明运行时栈的结束位置。(一般不在其它情况下使用)
注意参数的使用顺序:%rdi->%rsi->%rdx->%rcx。
返回值存入%rax中。

3.4.1 操作数指示符

大多数指令有一个或多个操作数(operand),指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。有以下三种类型:

  • 立即数(immediate),表示常数值。书写方式:'$'后面跟一个用标准C表示法表示的整数。(不同指令允许的立即数值范围不同,汇编器会自动选择最紧凑的方式进行数值编码。)
  • 寄存器(register),表示某个寄存器的内容。书写方式:用符号\(r_a\)表示任意寄存器\(a\),用引用\(R[r_a]\)来表示它的值。(这是将寄存器集合看成一个数组R,用寄存器标识符作为索引。)
  • 内存(memory)引用,根据计算出来的有效地址访问某个内存位置。书写方式:用符号\(M_{b}[Addr]\)表示对存储在内存中从地址\(Addr\)开始的\(b\)个字节值的引用,通常省去下标\(b\)。(同样将内存看成一个很大的字节数组。)

如下图所示,有多种不同的寻址模式,允许不同形式的内存引用。
\(Imm(r_b, r_i, s)\)是最常用的表示形式,有四个部分:一个立即数偏移\(Imm\),一个基址寄存器\(r_b\),一个变址寄存器\(r_i\)和一个比例因子\(s\)(1/2/4/8)。有效地址被计算为\(Imm+R[r_b]+R[r_i]·s\)
image
image

3.4.2 数据传送指令

将数据从一个位置复制到另一个位置。

MOV类

把数据从源位置复制到目的位置。
image

常规的movq指令只能以表示为32位补码数字的立即数作为源操作数,然后把这个值符号扩展得到64位的值并放到目的位置,movabsq指令能够以任意64位立即数值作为源操作数,并且只能以寄存器作为目的。

MOVZ类

把目的中剩余的字节填充为0。
image

MOVS类

通过符号扩展填充,把源操作数的最高位进行复制。
image

cltq没有操作数,效果与movslq %eax, %rax完全一致,不过编码更紧凑。

x86-64采用的规则:

  • 规则1:任何为寄存器生成32位值的指令都会把该寄存器的高位置0。
  • 规则2:传送指令的两个操作数不能都指向内存位置。将一个值从内存位置复制到另一个内存位置需要两条指令。
    image

3.4.3 数据传送示例

image

image

image

image

image

image

image

image

C语言中所谓的“指针”其实就是地址。间接引用指针就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器。

像t0、t1这样的局部变量通常是保存在寄存器中,而不是内存中。

3.4.4 压入和弹出栈数据

有关栈ADT的描述可参考文章数据结构与算法分析——C语言描述(第3章 表、栈和队列②)https://www.cnblogs.com/kirin-dev/p/Data-Structures_Chapter-3-2.html
image

pushq和popq指令都只有一个操作数——压入的数据源和弹出的数据目的。

指令pushq %rax等价于下面两条指令(将栈指针减8,再将值写到新的栈顶地址):
subq $8, %rsp
movq %rax, (%rsp)

同理,指令popq %rdx等价于:
movq (%rsp), %rdx
addq $8, %rsp

image
在x86-64中,程序栈存放在内存中的某个区域。如上图所示,栈向下增长,栈顶元素的地址是所有栈中元素地址中最低的。(根据惯例,这里的栈是倒过来画的,栈“顶”在图的底部。)栈指针%rsp保存栈顶元素的地址。

因为栈和程序代码以及其他形式的程序数据都存放再同一内存中,所以程序可以用标准的内存寻址方法访问站内的任意位置。
eg:movq 8(%rsp), %rdx将第二个四字从栈中复制到寄存器%rdx。

3.5 算术和逻辑操作

image
这些操作被分为四组:加载有效地址、一元操作、二元操作和移位。

3.5.1 加载有效地址

加载有效地址(load effective address)指令lea实际上是mov指令的变形。
lea的指令形式是从内存读数据到寄存器,但实际上它根本就没有引用内存。第一个操作数看上去是一个内存引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。(目的操作数必须是一个寄存器)
image

image

3.5.2 一元和二元操作

  • 一元操作:只有一个操作数,既是源又是目的。(操作数可以是寄存器也可以是内存位置)
  • 二元操作:第二个操作数既是源又是目的。(源操作数是第一个,目的操作数是第二个)(第一个操作数可以是立即数/寄存器/内存位置,第二个操作数可以是寄存器/内存位置。当第二个操作数为内存地址时,处理器必须从内存读出值,执行操作,再把结果写回内存)

3.5.3 移位操作

移位操作:先给出移位量,再给出要移位的数。(移位量可以是一个立即数,或者放在单字节寄存器%cl中——这些指令很特别,只允许以这个特定的寄存器作为操作数)
SAR算术右移:\(>>_A\)
SHR逻辑右移:\(>>_L\)
image

3.5.4 讨论

上述大多操作都既可以用于无符号运算,也可以用于补码运算。只有右移操作要求区分有符号和无符号数。这个特性使得补码运算成为实现有符号整数运算的一种比较好的方法的原因之一。

3.5.5 特殊的算术操作

image
补码乘法imulq指令有两种不同的形式:

  • 双操作数。实现补码乘法深入理解计算机系统(第2章 信息的表示和处理②)https://www.cnblogs.com/kirin-dev/p/Computer-Systems_Chapter-2-2.html (回想一下,当将乘积截取到64位时,无符号乘和补码乘的位级行为是一样的)
  • 单操作数。(与无符号数乘法类似)要求一个参数必须在寄存器%rax中,而另一个作为指令的源操作数给出。

有符号除法指令idivl将寄存器%rdx和%rax中的128位数作为被除数,而除数作为指令的操作数给出。指令将商存储在寄存器%rax中,将余数存储在%rdx中。

对于大多数64位除法应用来说,被除数也常常是一个64位的值。这个值应该存储在%rax中,%rdx的位应该设置为全0(无符号运算)或%rax的符号位(有符号运算)。后面这个操作可以用指令cqto来完成。这条指令不需要操作数——它隐含读出%rax的符号位,并将它复制到%rdx的所有位。

posted @ 2022-10-01 21:31  kirin-dev  阅读(102)  评论(0编辑  收藏  举报