3

我正在学习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和. 结果如下所示:intlong

// 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吗?谢谢!

4

1 回答 1

3

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

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

在 x86 上加载字节的“正常”方式是使用movzblormovsbl,这与在 RISC 机器上(如 ARMldrbldrsb, 或 MIPS lbu/ )相同lb

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


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

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

*dp = (dst_t)*sp;强制转换中 todst_t已经从分配中隐含了 to *dp


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

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

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


不应该对应dst_t吗?

它必须至少与 一样宽dst_t。事实证明,通过使用movzbl(使用隐式零扩展处理的前 32 位写入 32 位 reg)扩大到 64 位是最有效的扩大方式。

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

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

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

于 2019-10-24T10:22:27.917 回答