我正在学习汇编中的数据移动( 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 上加载字节的“正常”方式是使用 movzbl
或 movsbl
, 与 ARM 等 RISC 机器相同 ldrb
或 ldrsb
, 或 MIPS lbu
/lb
.
GCC通常避免的奇怪的CISC事情是与仅替换低位的旧值合并,例如movb (%rdi), %al
. Why doesn't GCC use partial registers? Clang 更加鲁莽,并且会更频繁地编写部分 regs,而不仅仅是为商店读取它们。您可能会看到 clang 加载到 %al
和存储时间 dst_t
是 signed char
.
如果您想知道为什么不movsbl (%rdi), %eax
(符号扩展)
源值是无符号的,因此零扩展名 (不是符号扩展)是根据 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
的值values >= 128。这与 C 语义冲突,用于扩大转换保留值。
Shouldn't it correspond to
dst_t
?
它必须至少加宽为
dst_t
.事实证明,使用 movzbl
扩展到 64 位(前 32 位由隐式零扩展写入 32 位 reg 处理)是最有效的加宽方式。存储到
*dp
是一个很好的演示,asm 适用于 dst_t
宽度不是 32 位。无论如何,请注意只有一次转换发生。您的
src_t
转换为 dst_t
在 al/ax/eax/rax 中使用加载指令,并存储到任何宽度的 dst_t 中。并且还留在那里作为返回值。即使您只是要读取该结果的低字节,零扩展负载也是正常的。
关于c - 将无符号字符转换为有符号数据类型时,为什么在汇编中使用 movzbl?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/58539038/