您现在的位置是:首页 >其他 >【CSAPP 3.3~3.4】x86-64访问数据网站首页其他

【CSAPP 3.3~3.4】x86-64访问数据

拉车看路 2023-05-12 20:15:03
简介一个x86-64的CPU包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针,它们的名字都以%r开头。

1. 数据格式

在汇编语言层面,Intel用术语word表示16位数据类型,双字double words表示32位数据类型,四字quad words表示64位数据类型。

这里的概念和字长不一样,要注意区分。

下表给出了x86-64环境(64位机器+64位编译)下C语言的基本数据类型表示。

C声明Intel数据类型汇编代码后缀字节大小
char字节b1
shortw2
int双字l4
long四字q8
char *四字q8
float单精度s4
double双精度l8

此外还有不太常用的数据类型,如long longlong double

大多数gcc生成的汇编代码指令都有一个字符的后缀,表面操作数的大小。例如:传送字节movb、传送字movw、传送双字movl、传送四字movq
用后缀l表示双字,因为32位数被看成是长字long word。汇编代码也使用后缀l表示8字节双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器

2. 访问信息

一个x86-64CPU包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针,它们的名字都以%r开头。
在这里插入图片描述
最初的8086中有816位寄存器,即%ax ~ %sp。扩展到IA32架构时,为了兼容旧架构,这些寄存器的标号扩展为%eax ~ %esp。扩展到64位时,原来的8个寄存器标号为%rax ~ %rsp,此外还增加了8个新的寄存器%r8 ~ %r15

指令可以对这16个寄存器的低位字节中存放的不同大小的数据进行操作。字节级操作可以访问最低的字节,16位操作可以访问最低的2个字节,32位操作可以访问最低的4个字节,64位操作可以访问整个寄存器。

对于操作小于8字节数据的指令,寄存器中剩下的字节会怎样,有以下规则:

  • 仅操作最低的1字节或2字节时,寄存器中其他的字节不变。
  • 仅操作低4字节时,高4字节会被置0

在常见的程序里,不同的寄存器扮演不同的角色。如%rsp用来指明运行时栈的结束位置。
有一组标准的编程规范控制着如何使用寄存器来管理栈、传递函数参数、存储函数的返回值,存储局部和临时数据。

2.1. 操作数指示符

大多数指令有一个或多个操作数,指示执行一个操作要使用的源数据值、目的位置。源数据可以以常数形式给出,或从寄存器或内存中读出。目的位置可以是寄存器或内存。因此操作数的类型有三种:

  • 立即数,用来表示常数值。在ATT格式的汇编代码中,立即数的书写方式是$后面跟一个标准C表示法表示的整数,如$-577$0xff$010等。不同的指令允许的立即数值范围不同,汇编器会自动选择最紧凑的方式进行数值编码。
  • 寄存器,表示某个寄存器的内容。使用16个寄存器的低1248字节作为操作数。我们使用 r a r_a ra表示任意寄存器a,使用 R [ r a ] R[r_a] R[ra]表示该寄存器的值。这里将寄存器集合看成数组R,寄存器标号是数组下标。
  • 内存引用,它会根据计算出来的地址访问某个内存位置。将内存看成一个很大的字节数组,我们用符号 M b [ A d d r ] M_b[Addr] Mb[Addr]表示对存储在内存中从地址Addr开始的b的字节的访问。通常省略下标b
类型格式操作数值
立即数$ I m m m Immm Immm I m m m Immm Immm
寄存器 r a r_a ra R [ r a ] R[r_a] R[ra]
存储器 I m m Imm Imm M [ I m m ] M[Imm] M[Imm]
存储器( r a r_a ra) M [ R [ r a ] ] M[R[r_a]] M[R[ra]]
存储器 I m m ( r b ) Imm(r_b) Imm(rb) M [ I m m + R [ r b ] ] M[Imm+R[r_b]] M[Imm+R[rb]]
存储器 ( r b , r i ) (r_b, r_i) (rb,ri) M [ R [ r b ] + R [ r i ] ] M[R[r_b]+R[r_i]] M[R[rb]+R[ri]]
存储器 I m m ( r b , r t ) Imm(r_b, r_t) Imm(rb,rt) M [ I m m + R [ r b ] + R [ r i ] M[Imm+R[r_b]+R[r_i] M[Imm+R[rb]+R[ri]]
存储器 ( , r i , s ) (,r_i,s) (,ri,s) M [ R [ r i ] ∗ s ] M[R[r_i]*s] M[R[ri]s]
存储器 I m m ( , r i , s ) Imm(,r_i,s) Imm(,ri,s) M [ I m m + R [ r i ] ∗ s ] M[Imm+R[r_i]*s] M[Imm+R[ri]s]
存储器 ( r b , r i , s ) (r_b, r_i, s) (rb,ri,s) M [ R [ r b ] + R [ r i ] ∗ s ] M[R[r_b]+R[r_i]*s] M[R[rb]+R[ri]s]
存储器 I m m ( r b , r i , s ) Imm(r_b, r_i, s) Imm(rb,ri,s) M [ I m m + R [ r b ] + R [ r i ] ∗ s ] M[Imm+R[r_b]+R[r_i]*s] M[Imm+R[rb]+R[ri]s]

2.1.1. 练习

假设下面的值存放在指明的内存地址和寄存器中。

地址
0x1000xFF
0x1040xAB
0x1080x13
0x10c0x11
寄存器
%rax0x100
%rcx0x1
%rdx0x3

填写下表,给出操作数的值:

操作数
%rax0x100
0x1040xAB
$0x1080x108
(%rax)0xFF
4(%rax)0xAB
9(%rax, %rdx)0x11
260(%rcx, %rdx)0x13
0xFC(, %rcx, 4)0xFF
(%rax, %rdx, 4)0x11

2.2. 数据传送指令

最频繁使用的指令是将数据从一个位置复制到另一个位置的指令。下表列出来的是最简单的数据传送指令——MOV类,由四条指令组成:movbmovwmovlmovq,这些指令都执行同样的操作,区别在于所操作的数据大小不同。

指令效果描述
MOV S,DS → o D传送
movb传送字节
movw传送字
movl传送双字
movq传送四字
movabsq I,RI → o R传送绝对的四字

源操作数指定的值是一个立即数,存储在寄存器或内存中;目的操作数指定一个位置,要么是一个寄存器,要么是一个内存地址。x86-64两个操作数不能都指向内存位置。寄存器操作数可以是16个寄存器有标号部分中的任意一个,寄存器部分的大小必须与指令最后一个字符(bwlq)指定的大小匹配。通常情况下,MOV指令只会更新目的操作数指定的那些寄存器字节或内存位置。唯一例外的是movl指令以寄存器作为目的操作数时,它会把寄存器的高4字节设置为0

下面的MOV指令示例给出了源和目的类型的5种可能的组合。第一个是源操作数,第二个是目的操作数。

movl $0x4050, %eax
movw %bp, %sp
movb (%rdi, %rcx), %al
movb $-17, (%rsp)
movq %rax, -12(%rbp)

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

在将较小的源值复制到较大的目的时,需要使用MOVZMOVS做数据扩展。MOVZ类中的指令把目的中剩余的字节填充为0MOVS类中的指令通过符号扩展填充。每条指令名字的最后两个字符分别表示源大小和目的大小。如:

movzbw S, R  // 将做了零扩展的字节传送到字
movsbw S, R  // 将做了符号扩展的字节传送到字
cltq  // 把%eax符号扩展到%rax

MOVZMOVS只能以寄存器或内存地址作为源,以寄存器作为目的
movl %ebx, %raxmovl默认做了4字节传送到8字节的零扩展,等价于movzlq %ebx, %rax

2.2.1. 练习1

对于下面汇编代码的每一行,根据操作数,确定适当的指令后缀。

movl %eax, (%rsp)
movw (%rax), %dx
movb $0xFF, %bl
movb (%rsp, %rdx, 4), %dl
movq (%rdx), %rax
movw %dx, (%rax)

2.2.2. 练习2

请解释一下如下的每行代码分别有什么问题。

movb $0xF, (%ebx)  // x86-64环境下,内存地址是64位而不是32位,应该使用(%rbx)而不是(%ebx)
movl %rax, (%rsp)  // 传送8字节数据应该使用movq
movw (%rax), 4(%rsp)  // 两个操作数不能都为内存地址
movb %al, %sl  // 不存在寄存器%sl
movq %rax, $0x123  // 立即数不能作为目的操作数
movl %eax, %rdx  // 目的操作数大小大于源操作数大小,逻辑上应该使用`movzlq`或`movslq`
movb %si, 8(%rbp)  // %si是2字节大小,应该用movw

不存在movzlq这个指令,movzlq %eax, %rdx等价于movl %eax, %edx%rdx的高4字节会被自动置0

2.3. 数据传送示例

有如下C代码,exchange.c

long exchange(long *xp, long y)
{
    long x = *xp;
    *xp = y;
    return x;
}

使用gcc产生的汇编代码:

[root@localhost2 3]# gcc -Og -S exchange.c
[root@localhost2 3]# cat exchange.s
	.file	"exchange.c"
	.text
	.globl	exchange
	.type	exchange, @function
exchange:
.LFB0:
	.cfi_startproc
	movq	(%rdi), %rax
	movq	%rsi, (%rdi)
	ret
	.cfi_endproc
.LFE0:
	.size	exchange, .-exchange
	.ident	"GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-44)"
	.section	.note.GNU-stack,"",@progbits

核心部分代码是:

exchange:
	movq	(%rdi), %rax  // long x = *xp;
	movq	%rsi, (%rdi)  // *xp = y;
	ret  // return x;

函数调用的参数通过寄存器传递,第一个参数xp保存在%rdi,第二个参数y保存在%rsi%rax保存函数的返回值。

可以看到C语言中所谓的“指针”其实就是地址,引用指针就是将该指针放在一个寄存器中,然后在内存引用中使用这个寄存器。像x这样的局部变量通常保存在寄存器,而不是内存中。CPU访问寄存器比访问内存快得多。如果程序中有对x取地址的操作&x,那一定要在内存中为x分配空间。

2.3.1. 练习

假设变量spdp的声明如下:

src_t *sp;
dest_t *dp;

这里的src_tdest_t是用typedef声明的数据类型。我们想使用适当的数据传送指令来实现下面的操作:

*dp = (dest_t)*sp;

假设spdp的值分别存储在寄存器%rdi%rsi中。对于下表中每个表项,给出实现指定数据传送的两条指令。其中第一条指令应该从内存中读数,做适当的转换,并设置寄存器%rax的适当部分。第二条指令要把%rax的适当部分写到内存,在这两种情况中,寄存器的部分可以是%rax%eax%ax%al

当执行强制类型转换既涉及大小变化又涉及C语言中符号变化时,操作应该先改变大小。

src_tdest_t指令
longlongmovq (%rdi), %rax
movq %rax, (%rsi)
charintmovsbl (%rdi), %eax
movl %eax, (%rsi)
charunsignedmovsbl (%rdi), %eax
movl %eax, (%rsi)
unsigned charlongmovzbl (%rdi), %eax
movq %rax, (%rsi)
intcharmovl (%rdi), %eax
movb %al, (%rsi)
unsignedunsigned charmovl (%rdi), %eax
movb %al, (%rsi)
charshortmovsbw (%rdi), %ax
movw %ax, (%rsi)

数据扩展时,第一条指令读源数据并扩展,第二条指令存放数据。(带扩展的mov指令只能以寄存器为目的操作数)
数据截断时,第一条指令读完整的源数据,第二条指令获取低位字节。

数据截断时,能不能把截断操作放在第一条指令,即从内存中只读低几个字节的数据?跟上述方案有啥区别?

2.3.2. 练习

已知某个函数的原型如下:

void decode1(long *xp, long *yp, long *zp);

它的编码代码表示如下:

decode1:
	movq	(%rdi), %r8
	movq	(%rsi), %rcx
	movq	(%rdx), %rax
	movq	%r8, (%rsi)
	movq	%rcx, (%rdx)
	movq	%rax, (%rdi)
	ret

其中xpypzp分别存储在%rdi%rsi%rdx中。
请写出等价于上面汇编代码的decode1C代码。

void decode1(long *xp, long *yp, long *zp)
{
        long x = *xp;
        long y = *yp;
        long z = *zp;
        *yp = x;
        *zp = y;
        *xp = z;
}

2.4. 压入和弹出栈数据

是一种数据结构,可以添加或者删除值,不过要遵循“后进先出”的原则。通过push操作把数据压入栈中,通过pop操作删除数据。它具有一个属性:弹出的值永远是最近被压入而且仍然在栈中的值。栈可以实现为一个数组,总是从数组的一端插入和删除元素,这一端被称为栈顶

指令效果描述
pushq SR[%rsp] - 8 → o R[%rsp]; S → o M[R[%rsp]]将四字压入栈
pop DM[R[%rsp]] → o D; R[%rsp] + 8 → o R[%rsp]将四字弹出栈

x86-64中,程序栈存放在内存中某个区域,栈向下增长,栈顶元素的地址是所有栈中元素地址中最低的。栈指针%rsp保存着栈顶元素的地址

pushq %rbp等价于

subq $8, %rsp
movq %rbp, (%rsp)

popq %rax等价于

movq (%rsp), %rax
add $8, %rsp

pop时不会主动清理原来栈顶位置的数据。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。