为什么在将 unsigned char 转换为有符号数据类型时在汇编中使用 movzbl?
Why movzbl is used in assembly when casting unsigned char to signed data types?
我正在学习汇编中的数据移动(MOV
)。
我试图编译一些代码以查看 x86_64 Ubuntu 18.04 机器中的程序集:
typedef unsigned char src_t;
typedef xxx dst_t;
dst_t cast(src_t *sp, dst_t *dp) {
*dp = (dst_t)*sp;
return *dp;
}
其中 src_t
是 unsigned char
。至于dst_t
,我试过char
、short
、int
和long
。
结果如下图:
// typedef unsigned char src_t;
// typedef char dst_t;
// movzbl (%rdi), %eax
// movb %al, (%rsi)
// typedef unsigned char src_t;
// typedef short dst_t;
// movzbl (%rdi), %eax
// movw %ax, (%rsi)
// typedef unsigned char src_t;
// typedef int dst_t;
// movzbl (%rdi), %eax
// movl %eax, (%rsi)
// typedef unsigned char src_t;
// typedef long dst_t;
// movzbl (%rdi), %eax
// movq %rax, (%rsi)
我想知道为什么在所有情况下都使用 movzbl
?不应该对应dst_t
吗?
谢谢!
如果你想知道为什么 movzbw (%rdi), %ax
不是 short
,那是因为写入 8 位和 16 位部分寄存器必须合并与前面的高字节。
写入像 EAX 这样的 32 位寄存器隐式零扩展到完整的 RAX,避免对 RAX 的旧值或任何 ALU 合并 uop 的错误依赖。 (Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?)
在 x86 上加载字节的 "normal" 方法是使用 movzbl
或 movsbl
,与在像 ARM 这样的 RISC 机器上一样ldrb
或 ldrsb
,或 MIPS lbu
/ lb
.
GCC 通常避免的奇怪的 CISC 事情是与仅替换低位的旧值合并,如 movb (%rdi), %al
。 Clang 更鲁莽,更经常编写部分 reg,而不仅仅是读取它们以供存储。您可能会看到当 dst_t
为 signed char
.
时,clang 仅加载到 %al
并存储
如果您想知道为什么不 movsbl (%rdi), %eax
(符号扩展)
source 值是无符号的,因此零扩展(不是符号扩展)是根据以下内容加宽它的正确方法C语义。要获得 movsbl
,您需要 return (int)(signed char)c
.
在 *dp = (dst_t)*sp;
中,对 dst_t
的转换已经从对 *dp
.
的赋值中隐含了
unsigned char
的值范围是 0..255(在 x86 上 CHAR_BIT = 8)。
将其零扩展到 signed int
可以产生 0..255
范围内的值,即将每个值保留为带符号的非负整数。
将其符号扩展到 signed int
会产生一个从 -128..+127
开始的值范围,更改 unsigned char
值的值 >= 128。这与 C 语义冲突以扩大转换保留值。
Shouldn't it correspond to dst_t
?
它必须加宽 至少 和 dst_t
一样宽。事实证明,使用 movzbl
扩展到 64 位(前 32 位由隐式零扩展写入 32 位 reg 处理)是最有效的扩展方式。
存储到 *dp
是一个很好的演示,asm 适用于宽度不是 32 位的 dst_t
。
无论如何,请注意只有一次转换发生。您的 src_t
通过加载指令在 al/ax/eax/rax 中转换为 dst_t
,并存储到任何宽度的 dst_t。并且还留在那里作为 return 值。
即使您只是要读取该结果的低字节,零扩展加载也是正常的。
我正在学习汇编中的数据移动(MOV
)。
我试图编译一些代码以查看 x86_64 Ubuntu 18.04 机器中的程序集:
typedef unsigned char src_t;
typedef xxx dst_t;
dst_t cast(src_t *sp, dst_t *dp) {
*dp = (dst_t)*sp;
return *dp;
}
其中 src_t
是 unsigned char
。至于dst_t
,我试过char
、short
、int
和long
。
结果如下图:
// typedef unsigned char src_t;
// typedef char dst_t;
// movzbl (%rdi), %eax
// movb %al, (%rsi)
// typedef unsigned char src_t;
// typedef short dst_t;
// movzbl (%rdi), %eax
// movw %ax, (%rsi)
// typedef unsigned char src_t;
// typedef int dst_t;
// movzbl (%rdi), %eax
// movl %eax, (%rsi)
// typedef unsigned char src_t;
// typedef long dst_t;
// movzbl (%rdi), %eax
// movq %rax, (%rsi)
我想知道为什么在所有情况下都使用 movzbl
?不应该对应dst_t
吗?
谢谢!
如果你想知道为什么 movzbw (%rdi), %ax
不是 short
,那是因为写入 8 位和 16 位部分寄存器必须合并与前面的高字节。
写入像 EAX 这样的 32 位寄存器隐式零扩展到完整的 RAX,避免对 RAX 的旧值或任何 ALU 合并 uop 的错误依赖。 (Why do x86-64 instructions on 32-bit registers zero the upper part of the full 64-bit register?)
在 x86 上加载字节的 "normal" 方法是使用 movzbl
或 movsbl
,与在像 ARM 这样的 RISC 机器上一样ldrb
或 ldrsb
,或 MIPS lbu
/ lb
.
GCC 通常避免的奇怪的 CISC 事情是与仅替换低位的旧值合并,如 movb (%rdi), %al
。 dst_t
为 signed char
.
%al
并存储
如果您想知道为什么不 movsbl (%rdi), %eax
(符号扩展)
source 值是无符号的,因此零扩展(不是符号扩展)是根据以下内容加宽它的正确方法C语义。要获得 movsbl
,您需要 return (int)(signed char)c
.
在 *dp = (dst_t)*sp;
中,对 dst_t
的转换已经从对 *dp
.
unsigned char
的值范围是 0..255(在 x86 上 CHAR_BIT = 8)。
将其零扩展到 signed int
可以产生 0..255
范围内的值,即将每个值保留为带符号的非负整数。
将其符号扩展到 signed int
会产生一个从 -128..+127
开始的值范围,更改 unsigned char
值的值 >= 128。这与 C 语义冲突以扩大转换保留值。
Shouldn't it correspond to
dst_t
?
它必须加宽 至少 和 dst_t
一样宽。事实证明,使用 movzbl
扩展到 64 位(前 32 位由隐式零扩展写入 32 位 reg 处理)是最有效的扩展方式。
存储到 *dp
是一个很好的演示,asm 适用于宽度不是 32 位的 dst_t
。
无论如何,请注意只有一次转换发生。您的 src_t
通过加载指令在 al/ax/eax/rax 中转换为 dst_t
,并存储到任何宽度的 dst_t。并且还留在那里作为 return 值。
即使您只是要读取该结果的低字节,零扩展加载也是正常的。