为什么在将 unsigned char 转换为有符号数据类型时在汇编中使用 movzbl?

Sea*_*ean 5 c assembly casting mov zero-extension

我正在学习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;
}
Run Code Online (Sandbox Code Playgroud)

哪里。src_tunsigned char至于,dst_t我试过了char,,shortintlong结果如下所示:

// 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)
Run Code Online (Sandbox Code Playgroud)

我想知道为什么movzbl在每种情况下都使用?不应该对应吗dst_t?谢谢!

Pet*_*des 5

如果您想知道为什么不使用movzbw (%rdi), %axforshort,那是因为写入 8 位和 16 位部分寄存器必须与之前的高字节合并。

写入像 EAX 这样的 32 位寄存器会隐式地零扩展到完整的 RAX,从而避免对 RAX 的旧值或任何 ALU 合并 uop 的错误依赖。(为什么 32 位寄存器上的 x86-64 指令会将整个 64 位寄存器的上部清零?

在 x86 上加载字节的“正常”方式是使用movzblmovsblldrb,与 ARM或ldrsb或 MIPS lbu/等 RISC 机器上的方式相同lb

GCC 通常避免的奇怪的 CISC 事情是与仅替换低位的旧值合并,例如movb (%rdi), %al. 为什么 GCC 不使用部分寄存器? Clang 更加鲁莽,并且会更频繁地写入部分寄存器,而不仅仅是为存储读取它们。您可能会看到 clang 加载到 just%al并存储 when dst_tis signed char


如果您想知道为什么不movsbl (%rdi), %eax(符号扩展)

源值是无符号的,因此零扩展(不是符号扩展)是根据 C 语义扩展它的正确方法为了得到movsbl,你需要return (int)(signed char)c.

*dp = (dst_t)*sp;to 的转换中dst_t,已经隐含了 to 的赋值*dp


的值范围unsigned char是 0..255(在 x86 上,其中 CHAR_BIT = 8)。

将其零扩展到signed int可以产生从 开始的值范围0..255,即将每个值保留为有符号非负整数。

将其符号扩展为signed int会产生一个从 开始的值范围-128..+127,从而更改值unsigned char>= 128 的值。这与扩大转换保留值的 C 语义相冲突。


不应该对应吗dst_t

它必须至少加宽为dst_t。事实证明,通过使用movzbl(通过隐式零扩展写入 32 位寄存器来处理前 32 位)扩展至 64 位是最有效的扩展方法。

存储到*dp是一个很好的演示,asm 适用于dst_t宽度不是 32 位的。

无论如何,请注意,只发生了一次转换。您将使用加载指令src_t转换为al/ax/eax/rax 格式,并存储到任意宽度的 dst_t 中。dst_t并且还留在那里作为返回值。

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