文章目录
- 3.4 访问信息
- 3.4.1 操作数指示符
- 3.4.2 数据传送指令
- 3.4.3 数据传送示例
3.4 访问信息
一个 IA32 中央处理单元(CPU)包含一组八个存储 32 位值的寄存器,这些寄存器用来存储整数数据和指针。
下图显示了这八个寄存器。它们的名字都是以 %e
开头的,不过它们都有特殊的名字。
在最初的 8086 中,寄存器是 16 位的,每个都有特殊的用途。选择的名字就是用来反映各种用途的。在平面寻址中,对特殊寄存器的需求已经大为降低了。
在大多数情况中,前六个寄存器都可以看成通用寄存器,对它们的使用没有限制。之所以说“在大多数情况中”,是因为有些指令是以固定的寄存器作为源和/或目的的。
另外,在过程(procedures)处理中,对前三个寄存器(%eax
、%ecx
和 %edx
)的保存和恢复惯例将不同于接下来的三个寄存器(%ebx
、%edi
和 %esi
)。
最后两个寄存器(%ebp
和 %esp
)保存着指向程序栈中重要位置的指针,只有根据栈管理的标准惯例才能修改这两个寄存器中的值。
如上图所示,字节操作指令可以独立地读或者写前四个寄存器的两个低位字节。8086 中提供这样的特性是为了向后兼容 8008 和 8080,8008 和 8080 是两款可以追溯到 1974 年的微处理器。当一条字节指令更新这些单字节 “寄存器元素” 中的一个时,该寄存器余下的三个字节不会被改变。类似地,字操作指令可以读或者写每个寄存器的低 16 位。这个特性源自 IA32 是从 16 位微处理器演化而来的。
3.4.1 操作数指示符
大多数指令有一个或多个操作数(operand),指示出执行一个操作中要引用的源数据值,以及放置结果的目的位置。
IA32 支持多种操作数格式,如下图所示:
源数据值可以以常数形式给出,或是从寄存器或存储器中读出,结果可以存放在寄存器或存储器中。因此,各种操作数的可能性被分为三种类型。
- 第一种是立即数(immediate),也就是常数值。在GAS中,采用标准 C 的表示方法,立即数的书写方式是 “ $ ” 后面跟一个整数,比如,$-577 或 $0x1F。任何32位的字都可以用作立即数,不过汇编器在可能时会使用一个或两个字节的编码。
- 第二种类型是寄存器(register),它表示某个寄存器的内容。对双字操作来说,可以是八个 32 位寄存器中的一个(如
%eax
);对字节操作来说,可以是八个单字节寄存器元素中的一个(如%al
)。在上图中,用符号 E a E_a Ea 来表示任意寄存器 a a a,用引用 R [ E a ] R[E_a] R[Ea] 来表示它的值,这是将寄存器集合看成一个数组 R R R,用寄存器标识符作为索引。 - 第三类操作数是存储器引用,它会根据计算出来的地址(通常称为有效地址)访问某个存储器位置。因为将存储器看成一个很大的字节数组,用符号 M b [ A d d r ] M_b[Addr] Mb[Addr] 表示对存储在存储器中从地址 Addr 开始的 b b b 字节值的引用。为了简便,通常省去写在下方的 b b b。
如上图所示,有多种不同的寻址模式,允许不同形式的存储器引用。表中底部的 I m m ( E b , E i , s ) Imm(E_b, E_i, s) Imm(Eb,Ei,s) 是最通常的形式。这样的引用有四个部分:一个立即数偏移 I m m Imm Imm,一个基址寄存器 E b E_b Eb,一个变址或索引寄存器 E i E_i Ei 和一个伸缩因子(scale factor) s s s,这里 s s s 必须是 1、2、4 或者 8。然后,有效地址被计算为 I m m + R [ E b ] + R [ E i ] ⋅ s Imm + R[E_b] + R[E_i] · s Imm+R[Eb]+R[Ei]⋅s。引用数组元素时,会用到这种通用形式。其他形式只是这种通用形式的特殊情况,省略了某些部分。正如即将看到的,当引用数组和结构元素时,比较复杂的寻址模式是很有用的。
3.4.2 数据传送指令
最频繁使用的指令是执行数据传送的指令。操作数符号的通用性使得一条简单的传送指令能够完成许多机器中要好几条指令才能完成的功能。
下图列出的是一些重要的数据传送指令,最常用的是传送双字的 movl
指令。
源操作数指定一个值,它可以是立即数,可以存放在寄存器中,也可以存放在存储器中。
目的操作数指定一个位置,它可以是寄存器,也可以是存储器地址。
IA32 加了一条限制,传送指令的两个操作数不能都指向存储器位置。将一个值从一个存储器位置拷到另一个存储器位置需要两条指令——第一条指令将源值加载到寄存器中,第二条将该寄存器值写入目的位置。
下面这个 movl
指令示例给出了源和目的类型的五种可能组合。回想一下,第一个是源操作数,第二个是目的操作数:
movb
指令是类似的,除了它只传送一个字节。当一个操作数是寄存器时,它必须是图3.2中所示的八个单字节寄存器元素中的一个。
类似地,movw
指令传送两个字节。当它的一个操作数为寄存器时,它必须是上图3.2中所示的八个两字节寄存器元素中的一个。
movsbl
和 movzbl
指令负责拷贝一个字节,并设置目的操作数中其余的位。movsbl
指令的源操作数是单字节的,它执行符号扩展到 32 位(也就是,将高 24 位设置为源字节的最高位),然后拷贝到双字的目的中。类似地,movzbl
指令的源操作数是单字节的,在前面加 24 个 0 扩展到 32 位,并将结果拷贝到双字的目的中。
字节传送指令比较
三个字节传送指令 movb
、movsbl
和 movzbl
之间有细微的差别,这里有一个示例:
在这些例子中,都是将寄存器 %eax
的低位字节设置成 %ebx
的第二个字节。movb
指令不改变其他三个字节。根据源字节的最高位,movsbl
指令将其他三个字节设为全1或全0。movzbl
指令无论如何都是将其他三个字节设为全0。
最后两个数据传送操作是用来将数据压入栈中和从栈中弹出数据的。正如即将看到的,栈在处理过程调用中起到至关重要的作用。pushl
和 popl
指令都只有一个操作数——用于压入的源数据和用于弹出的目的数据。
程序栈存放在存储器中某个区域。如下图3.5 所示,栈向下增长,这样一来,栈顶元素的地址是所有栈中元素地址中最低的。(根据惯例,栈是倒过来画的,栈“顶” 在图的底部。)
栈指针 %esp
保存着栈顶元素的地址。
将一个双字值压入栈中,首先要将栈指针减4,然后将值写到新的栈顶地址。因此,指令 pushl %ebp
的行为等价于下面这样两条指令:
subl $4, %esp
movl %ebp, (%esp)
它们之间的区别是在目标代码中 pushl
指令是编码为 1 个字节的,而上面那两条指令一共需要 6 个字节。图中前两栏给出的是当 %esp
为 0x108 和 %eax
为 0x123 时,执行指令 pushl %eax
的效果。首先 %esp 会减4,得到 0x104,然后会将 0x123 存放到存储器地址 0x104 处。
弹出一个双字这样的操作将包括从栈顶位置读出数据,然后将栈指针加 4。因此,指令 popl %eax
等价于下面这样两条指令:
movl (%esp), %eax
addl $4, %esp
图3.5 的第三栏说明的是在执行完 pushl
后立即执行指令 popl %edx
的效果。先从存储器中读出值 0x123,再写到寄存器 %edx 中,然后,寄存器 %esp 的值将增加为 0x108。如图中所示,值 0x123 仍然会保持在存储器位置 0x104 中,直到被另一条入栈操作覆盖。无论如何,%esp 指向的地址总是栈顶。
因为栈和程序代码以及其他形式的程序数据都是放在同样的存储器中,所以程序可以用标准的存储器寻址方法访问栈内任意位置。例如,假设栈顶元素是双字,指令 movl 4(%esp),%edx
会将第二个双字从栈中拷贝到寄存器 %edx
。
3.4.3 数据传送示例
一些指针的示例
函数 exchange
提供了一个关于 C 中指针使用的很好说明。
参数 xp
是一个指向整数的指针,而 y
是一个整数。
语句
int x = *xp;
表示我们将读存储在 xp
所指位置中的值,并将它存放到名字为 x
的局部变量中。这个读操作称为指针的间接引用(pointer dereferencing),C 操作符 *
执行指针的间接引用。
语句
*xp = y;
正好相反——它将参数 y
的值写到 xp
所指的位置。这也是一种间接引用的形式(所以有操作符 *
),但是它表明的是一个写操作,因为它是在赋值语句的左边。
下面是一个使用 exchange
的例子:
int a = 4;
int b = exchange(&a, 3);
printf("a = %d, b = %d\n", a, b);
这段代码会打印出:
a = 3, b = 4
C操作符 &
(称为 “取址” 操作符)创建一个指针,在本例中,该指针指向保存局部变量 a
的位置。然后,函数 exchange
将用 3 覆盖存储在 a
中的值,但是返回 4 作为函数的值。注意如何将指针传递给 exchange,它能修改存在某个远处位置的数据。
作为一个使用数据传送指令的代码示例,考虑图 3.6 中所示的数据交换函数,既有 C 代码,也有 GCC 产生的汇编代码。省略了过程入口处的汇编代码,这些代码用来为运行时栈分配空间,以及在过程返回前回收栈空间的代码。除此之外剩下的代码,我们称之为 “过程体(body)”。
当过程体开始执行时,过程参数 xp
和 y
存储在相对于寄存器 %ebp
中地址值的偏移 8 和 12 的地方。
- 指令 1 和 2 会将这些参数传送寄存器
%eax
和%edx
。 - 指令 3 间接引用
xp
,并将值存储在寄存器%ecx
中,对应于程序值x
。 - 指令 4 将
y
存储在xp
。 - 指令 5 将 x 传送到寄存器
%eax
。根据惯例,所有返回整数或指针值的函数都是通过将结果放在寄存器%eax
中来达到目的的,因此这条指令实现了 C 代码中第 6 行的功能。
这个例子说明 movl
执行是如何用于从存储器中读值到寄存器的(指令 1 ~ 3),如何从寄存器写到存储器的(指令 4),以及如何从一个寄存器拷贝到另一个寄存器的(指令 5)。
关于这段汇编代码有两点需要注意:
- 首先,我们看到 C 中所谓的“指针” 其实就是地址。间接引用指针就是将该指针放在一个寄存器中,然后在间接存储器引用中使用这个寄存器。
- 其次,像
x
这样的局部变量通常是保存在寄存器中,而不是存储器中。寄存器访问比存储器访问要快得多。