为什么在将 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_tunsigned char。至于dst_t,我试过charshortintlong。 结果如下图:

// 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" 方法是使用 movzblmovsbl,与在像 ARM 这样的 RISC 机器上一样ldrbldrsb,或 MIPS lbu / lb.

GCC 通常避免的奇怪的 CISC 事情是与仅替换低位的旧值合并,如 movb (%rdi), %al Clang 更鲁莽,更经常编写部分 reg,而不仅仅是读取它们以供存储。您可能会看到当 dst_tsigned 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 值。

即使您只是要读取该结果的低字节,零扩展加载也是正常的。