目录
3.4 访问信息
3.4.1 两大数据类型
1、整型
一个 x86-64 的 CPU 拥有一组包含 16 个存储 64 位值的通用目的寄存器,用来存储整数数据和指针。
指令可以对这 16 个寄存器的低位字节中存放的不同大小的数据进行操作。
- 字节级操作可以访问最低的字节;
- 16位操作可以访问最低的 2 个字节;
- 32位操作可以访问最低的 4 个字节;
- 64位操作可以访问整个 8 字节大的寄存器。
那么剩余字节会怎么样?对此有两条规则:
- 生成 1 个字节或 2 个字节数字的指令会保持剩下的高位字节不变;
- 生成 4 个字节数字的指令会把剩下的高位 4 个字节设置为0。
Besides,由上图也可以看出,上图中直接可以支持函数有6个参数,超过6个则需要堆栈来传递函数,影响调用函数的速度。
2、浮点型
x86-64 浮点数是基于 SSE 或 AVX 的,包括传递过程参数和返回值的规则。
SSE 中的寄存器称为“XMM”;
AVX 中的寄存器称为“YMM”。
AVX 浮点体系结构允许数据存储在 16 个 YMM 寄存器中,它们的名字为 %ymm0
~ %ymm15
,每个 YMM 寄存器都是 256 位(32 字节)的。
当对标量数据操作时,这些寄存器只保存浮点类型数据,而且只使用低 32 位(对于 float)或低 64 位(对于 double)。在汇编代码中,一般使用 SSE XMM 寄存器名字 %xmm0
~ %xmm15
来引用它们,每个 XMM 寄存器都是对应的 YMM 寄存器的低 128 位(16 字节),如👇图。
算术类型(Arithmetic Type):可以做算术运算的类型。包括整型、浮点型。算术类型可以表示为 0 和非 0,作为控制表达式。
标量类型(Scalar Type):可以参与逻辑运算(与或非),或者做控制表达式的类型。包括算术类型和指针类型。
3.4.2 操作数指示符
操作数(operand)
指示出执行一个操作中要使用的源数据值,以及放置结果的目的位置。
- 源数据值:常数形式直接给出,或从寄存器读出,或从内存中读出;
- 目的位置:寄存器,或内存。
1、立即数(immediate)
表示常数值。
【格式】‘$$$’ 后面跟一个用标准 C 表示法的整数,如 $$-577,{\space}$0x2F$
不同指令允许的立即数范围不同,汇编器会自动选择最紧凑的方式进行数值编码。
2、寄存器(register)
表示某个寄存器的内容,16个寄存器的低位1字节、2字节、4字节或8字节中的一个作为操作数,分别对应8位、16位、32位、64位。
【符号表示】 r a r_a ra 表示寄存器 a a a ,用引用 R [ r a ] R[r_a] R[ra] 表示其值。(将寄存器集合看成一个数组 R ,用寄存器标识符作为索引)
3、内存引用
根据地址访问某个内存位置。内存被视为一个很大的字节数组(虚拟内存)。
【符号表示】 M b [ A d d r ] M_b[Addr] Mb[Addr] 表示对存储在内存中从地址 A d d r Addr Addr 开始的 b b b 个字节值的引用( b b b 通常省略)。
类型 | 格式 | 操作数值 | 寻址模式 |
---|---|---|---|
立即数 | $$Imm$ | I m m Imm Imm | 立即数寻址, I m m Imm Imm 表示的是常数值 |
寄存器 | 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] | 绝对寻址, I m m Imm 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 i ) Imm(r_b,r_i) Imm(rb,ri) | 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 a , s ) Imm(r_b,r_a,s) Imm(rb,ra,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] | 比例变址寻址 |
【注意】比例因子 s s s 必须是1、2、4或者8.
3.4.3 数据传送 MOV 和类型转换
把数据从源位置复制到目的位置,只会更新指定寄存器中的字长度或内存位置,其他不做任何变化(除了 4 字节时,会将寄存器高位 4 字节设置为 0,这是个例外。)。
【注意】
- 传送指令的两个操作数,不可以都是内存位置,即从一个内存位置复制到另一个内存位置,需要两条指令:内存1到寄存器,寄存器再到内存2。
- x86-64 的内存引用总是用 4 字长的。
- MOV 后缀取决于寄存器的字长。(?取决于哪个寄存器?)
- % e b x \%ebx %ebx 是基址寄存器,不可以作为目的操作数
- MOV 指令的寄存器操作数可以是 16 个寄存器有标号部分中的任意一个,寄存器部分的大小必须与指令最后一个字符(‘b’,‘w’,‘l’ 或 ‘q’)指定的大小匹配。
- 源操作数和目的操作数都是寄存器时,其宽度必须相同,否则必须通过内存,两次使用 MOV。
1、MOV 类
指令 | 效果 | 描述 |
---|---|---|
MOV S, D |
D ← S D←S D←S | 传送 |
movb |
传送字节 | |
movw |
传送字 | |
movl |
传送 2 字,若目的位置是寄存器,则高位 2 字设置为 0 | |
movq |
传送 4 字 | |
movabsq I, R |
R ← I R←I R←I | 传送绝对的 4 字 |
# MOV指令五种可能的组合👇
movl $0x4050, %eax # 立即数->寄存器,2字
movw %bp, %sp # 寄存器->寄存器,1字
movb (%rdi, %rcx) %al # 内 存->寄存器,半字
movb $-17, (%rsp) # 立即数->内 存,半字
movq %rax, -12(%rbp) # 寄存器->内 存,4字
【注意】
常规的 movq
指令只能以 32 位补码数字的立即数作为源操作数,然后通过符号扩展得到 64 位的值,放到目的位置。
movabsq
指令能以任意 64 位的立即数值作为源操作数,并且只能以寄存器作为目的操作数。
2、MOVZ 类 和MOVS 类
【应用】
将较小的源值复制到较大的目的时使用。
MOVZ,零扩展:把目的中剩余的所有字节填充为 0。
MOVS,符号扩展:把目的中剩余的所有字节用源操作数的最高位进行填充。
- MOVS/Z 指令以寄存器或内存地址作为源操作数,不接受以立即数作为源操作数;
- MOVS/Z 指令只以寄存器作为目的操作数。
指令 | 描述 | 指令 | 描述 |
---|---|---|---|
MOVZ S, R |
R ← R← R←零扩展 ( S ) (S) (S) | MOVS S, R |
R ← R← R←符号扩展 ( S ) (S) (S) |
movzbw |
将做了零扩展的字节传送到 1 字 | movsbw |
将做了符号扩展的字节传送到 1 字 |
movzbl |
将做了零扩展的字节传送到 2 字 | movsbl |
将做了符号扩展的字节传送到 2 字 |
movzwl |
将做了零扩展的 1 字传送到 2 字 | movswl |
将做了符号扩展的 1 字传送到 2 字 |
movzbq |
将做了零扩展的字节传送到 4 字 | movsbq |
将做了符号扩展的字节传送到 4 字 |
movzwq |
将做了零扩展的 1 字传送到 4 字 | movswq |
将做了符号扩展的 1 字传送到 4 字 |
没有movzlq |
movl 实现了对高4位零扩展 |
movslq |
将做了符号扩展的 2 字传送到 4 字 |
cltq |
%rax ← 符号扩展 %eax |
【注意】cltq
指令没有操作数,总是以寄存器 %rax
作为源操作数,以 %eax
作为符号扩展结果的目的操作数。
3、浮点传送 VMOV
【应用】
从内存传送到一个 XMM 寄存器(可改变类型,即 float → double 或 double → float);
从一个 XMM 寄存器传送到内存(可改变类型,即 float → double 或 double → float);
从一个 XMM 寄存器传送到另一个 XMM 寄存器(不改变类型)。
指令 | 源操作数(S) | 目的操作数(D) | 描述(S→D) |
---|---|---|---|
vmovss |
M 32 M_{32} M32 | X | 传送单精度数 |
vmovss |
X | M 32 M_{32} M32 | 传送单精度数 |
vmovsd |
M 64 M_{64} M64 | X | 传送双精度数 |
vmovsd |
X | M 64 M_{64} M64 | 传送双精度数 |
vmovaps |
X | X | 传送对齐的封装好的单精度数 |
vmovapd |
X | X | 传送对齐的封装好的双精度数 |
【注意】
涉及到引用内存的指令,均是标量指令——其只对相应的低字节长度进行操作;
在两个 XMM 寄存器之间传送数据,GCC 会使用 vmovaps
传送单精度数,使用 vmovapd
传送双精度数;
程序复制整个寄存器,亦或是只复制低位值,并不会影响程序功能和执行速度。
vmovaps
和 vmovapd
中的字母 a 表示 aligned,表示对齐的:在两个寄存器之间传送数据,绝对不会出现错误对齐的状况。
🌰不同浮点数传送操作
float float_mov(float v1, float *src, float *dst) {
float v2 = *src;
*dst = v1;
return v2;
}
# float float_mov(float v1, float *src, float *dst)
# v1 in %xmm0, src in %rdi, dst in %rsi
float_mov:
vmovaps %xmm0, %xmm1 # Copy v1
vmovss (%rdi), %xmm0 # Read v2 from src
vmovss %xmm1, (%rsi) # Write v1 to dst
ret # Return v2 in %xmm0
4、浮点型与整型转换
都是标量指令。
R:表示通用寄存器;X:表示 XMM 寄存器;M:表示内存。
【双操作数浮点-整型转换指令】
指令 | 源(S) | 目的(D) | 描述(S→D) |
---|---|---|---|
vcvttss2si |
X X X 或 M 32 M_{32} M32 | R 32 R_{32} R32 | 用截断的方法把单精度转换成整数 |
vcvttsd2si |
X X X 或 M 64 M_{64} M64 | R 32 R_{32} R32 | 用截断的方法把双精度转换成整数 |
vcvttss2siq |
X X X 或 M 32 M_{32} M32 | R 64 R_{64} R64 | 用截断的方法把单精度转换成四字整数 |
vcvttsd2siq |
X X X 或 M 64 M_{64} M64 | R 64 R_{64} R64 | 用截断的方法把双精度转换成四字整数 |
【三操作数浮点-整型转换指令】
指令 | 源(S1) | 源(S2) | 目的(D) | 描述(S1→D) |
---|---|---|---|---|
vcvtsi2ss |
M 32 M_{32} M32 或 R 32 R_{32} R32 | X X X | X X X | 把整数转换成单精度数 |
vcvtsi2sd |
M 32 M_{32} M32 或 R 32 R_{32} R32 | X X X | X X X | 把整数转换成双精度数 |
vcvtsi2ssq |
M 64 M_{64} M64 或 R 64 R_{64} R64 | X X X | X X X | 把四字整数转换成单精度数 |
vcvtsi2sdq |
M 64 M_{64} M64 或 R 64 R_{64} R64 | X X X | X X X | 把四字整数转换成双精度数 |
第一个源操作数 S1 读取自内存或一个通用寄存器;
目标操作数 D 必须是 XMM 寄存器;
因为这个转换指令是标量指令,第二个源操作数 S2 可忽略,其只会影响结果的高位字节(128位以上的位),因此在常见的使用场景中,第二个源操作数 S2 和目的操作数都是一样的:就像:vcvtsi2sdq %rax, %xmm1, %xmm1
。
【double 转 float】
vmovddup %xmm0, %xmm0 # Replicate first vector element
vcvtpd2psx %xmm0, %xmm0 # Convert two vector elements to single
假设指令开始执行前:寄存器 %xmm0 的值为 [ x 1 , x 0 ] 8 B [x_1,x_0]_{8B} [x1,x0]8B;
vmovddup
指令把 %xmm0 设置为 [ x 0 , x 0 ] 8 B [x_0,x_0]_{8B} [x0,x0]8B;
vcvtpd2psx
指令把这两个值转换成单精度,再存放到该寄存器的低位一半中,并将高位一半设置为 0,得到结果 [ 0.0 , 0.0 , x 0 , x 0 ] 4 B [0.0,0.0,x_0,x_0]_{4B} [0.0,0.0,x0,x0]4B (0.0 是由位模式全 0 表示的)。
【float 转 double】
vunpcklps %xmm0, %xmm0, %xmm0 # Replicate firstvector element
vcvtps2pd %xmm0, %xmm0 # Convert two vector elements to double
vunpcklps
指令通常用来交叉放置来自前两个 XMM 寄存器的值,把它们存储到第三个寄存器中:
如果第一个寄存器为 [ s 3 , s 2 , s 1 , s 0 ] 4 B [s_3,s_2,s_1,s_0]_{4B} [s3,s2,s1,s0]4B,第二个寄存器为 [ d 3 , d 2 , d 1 , d 0 ] 4 B [d_3,d_2,d_1,d_0]_{4B} [d3,d2,d1,d0]4B,那么第三个寄存器(目的寄存器)的值会是 [ s 1 , d 1 , s 0 , d 0 ] 4 B [s_1,d_1,s_0,d_0]_{4B} [s1,d1,s0,d0]4B;上面的代码中,使用三个操作数使用同一个操作数,如果起初寄存器为 [ x 3 , x 2 , x 1 , x 0 ] 4 B [x_3,x_2,x_1,x_0]_{4B} [x3,x2,x1,x0]4B,那么指令该指令后,寄存器的值变为 [ x 1 , x 1 , x 0 , x 0 ] 4 B [x_1,x_1,x_0,x_0]_{4B} [x1,x1,x0,x0]4B。
vcvtps2pd
指令把源操作数寄存器中的两个低位单精度值扩展成目的寄存器中的两个双精度值:
对于寄存器的值 [ x 1 , x 1 , x 0 , x 0 ] 4 B [x_1,x_1,x_0,x_0]_{4B} [x1,x1,x0,x0]4B,执行完该指令后,得到的结果为 [ d x 1 , d x 0 ] 8 B [dx_1,dx_0]_{8B} [dx1,dx0]8B,这里 d x 0 dx_0 dx0 是将 x x x 转换成双精度后的结果。
👆两条指令的最终结果:将原始的 %xmm0
低位 4 字节中的单精度值转换成双精度值,再将其两个副本保存到 %xmm0
中。
3.4.4 数据传送示例
long exchange(long *xp, long y){
long x = *xp;
*xp = y;
return x;
}
exchange: # xp in %rdi, y in %rsi
movq (%rdi), %rax # long x = *xp;
movq %rsi, (%rdi) # *xp = y;
ret # return x;
- 参数通过寄存器传递给函数。
- 函数通过把值存储在寄存器
%rax
或该寄存器的某个低位部分中返回。
【过程】这个例子说明了如何用 MOV 指令从内存中读值到寄存器(第2行),如何从寄存器写到内存(第3行)。
- 过程参数
xp
和y
分别存储在寄存器%rdi
和%rsi
中。- 然后,第 2 行指令从内存中读出
x
,把它存放到寄存器%rax
中,直接实现了 C 程序中的操作x=*xp
。- 稍后,用寄存器
%rax
从这个函数返回一个值,因而返回值就是x
。- 第 3 行指令将
y
写入到寄存器%rdi
中的xp
指向的内存位置,直接实现了操作*xp=y
。
【练习】
假设变量 sp
和 dp
被声明为类型 src_t
和 dest_t
。使用适当的数据传送指令实现一条强制转换语句:*dp=(dest_t)*sp
。假设 sp
和 dp
的值分别存储在寄存器 %rdi
和 %rsi
中。
分析:sp
和 dp
的值都是地址。
- 扩展位时,有符号类型使用符号扩展,无符号类型使用零扩展;
- 截取时,先从内存读取到寄存器,再截取低位。
3.4.5 压入(push)和弹出(pop)栈数据
栈:由高地址向低地址方向增长。
指令 | 效果 | 描述 | 等价于 |
---|---|---|---|
pushq S |
R[%rsp]←R[%rsp]-8; M[R[%rsp]]←S |
将四字压入栈 | subq $8, %rsp; movq %rbp, (%rsp) |
popq D |
D←M[R[%rsp]]; R[%rsp]←R[%rsp]+8 |
将四字弹出栈 | movq (%rsp), %rax; addq $8, %rsp |
mov
和 pushq
指令的区别在于:pushq
指令编码为 1 个字节,等价的两条指令一共需要 8 个字节。
- 将一个四字值压入栈中,首先要将栈指针减 8,然后将值写到新的栈顶地址。
- 弹出一个四字的操作包括从栈顶位置读出数据,然后将栈指针加 8。
今天的文章CSPP学习笔记-Ch3.4 访问、传送、转换数据分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/82371.html