当前位置: 首页 > 知识库问答 >
问题:

为x86-64 ABI的指针添加32bit偏移量时,是否需要符号扩展或零扩展?

胡利
2023-03-14

摘要:我在查看程序集代码以指导我的优化,并且在将int32添加到指针时看到了大量的符号或零扩展。

void Test(int *out, int offset)
{
    out[offset] = 1;
}
-------------------------------------
movslq  %esi, %rsi
movl    $1, (%rdi,%rsi,4)
ret

起初,我认为我的编译器在添加32bit到64bit的整数时遇到了挑战,但我已经在Intel ICC 11、ICC 14和GCC 5.3中证实了这种行为。

共有1个答案

祁和通
2023-03-14

是的,您必须假设arg或返回值寄存器的32位高位包含垃圾。另一方面,你允许在调用或返回自己时在高32中留下垃圾。即接收方负责忽略高位,而传递方负责清除高位。

您需要将符号或零扩展到64位才能使用64位有效地址中的值。在x32 ABI中,gcc经常使用32位有效地址,而不是使用64位操作数大小来修改用作数组索引的可能为负的整数的每条指令。

x86-64 SysV ABI只说明寄存器的哪些部分为_bool(又名bool)清零。第20页:

关于x32和x86-64 ABI文档的github页面上有一个关于这个问题的公开的github问题。

ABI没有对保存参数或返回值的整数或向量寄存器的高位部分的内容提出任何进一步的要求或保证,因此没有任何要求或保证。我从Michael Matz(ABI维护人员之一)的电子邮件中确认了这一事实:“通常,如果ABI没有说某些东西是指定的,您就不能依赖它。”

他还确认,e.g.clang>=3.6使用addps来减缓或引发高元素中垃圾的额外FP异常是一个bug(这提醒我应该报告这一点)。他补充说,在AMD实现glibc数学函数时,这曾经是一个问题。普通C代码在传递标量doublefloat参数时会在向量regs的高位元素中留下垃圾。

这不适用于返回值,只适用于args:gcc和clang都假定它们接收的返回值仅具有类型宽度范围内的有效数据。例如,gcc将使返回char的函数将垃圾留在%eax的高位24位。

ABI讨论组最近的一个线程是一个提议,它澄清了将8位和16位参数扩展到32位的规则,并且可能实际上修改ABI以要求这样做。主要编译器(ICC除外)已经这样做了,但这将改变调用者和被调用者之间的契约。

这里有一个示例(使用其他编译器查看它,或者在Godbolt编译器资源管理器上调整代码,我在这里包括了许多简单的示例,它们只演示了其中的一个部分,还有这个示例,它演示了很多):

extern short fshort(short a);
extern unsigned fuint(unsigned int a);

extern unsigned short array_us[];
unsigned short lookupu(unsigned short a) {
  unsigned int a_int = a + 1234;
  a_int += fshort(a);                 // NOTE: not the same calls as the signed lookup
  return array_us[a + fuint(a_int)];
}

# clang-3.8 -O3  for x86-64.    arg in %rdi.  (Actually in %di, zero-extended to %edi by our caller)
lookupu(unsigned short):
    pushq   %rbx                      # save a call-preserved reg for out own use.  (Also aligns the stack for another call)
    movl    %edi, %ebx                # If we didn't assume our arg was already zero-extended, this would be a movzwl (aka movzx)
    movswl  %bx, %edi                 # sign-extend to call a function that takes signed short instead of unsigned short.
    callq   fshort(short)
    cwtl                              # Don't trust the upper bits of the return value.  (This is cdqe, Intel syntax.  eax = sign_extend(ax))
    leal    1234(%rbx,%rax), %edi     # this is the point where we'd get a wrong answer if our arg wasn't zero-extended.  gcc doesn't assume this, but clang does.
    callq   fuint(unsigned int)
    addl    %ebx, %eax                # zero-extends eax to 64bits
    movzwl  array_us(%rax,%rax), %eax # This zero-extension (instead of just writing ax) is *not* for correctness, just for performance: avoid partial-register slowdowns if the caller reads eax
    popq    %rbx
    retq

执行32位操作时,忽略高电平32是自由的。32位操作将其结果零-免费扩展到64位,因此,如果您可以在64位寻址模式或64位操作中直接使用reg,您只需要额外的mov edx、edi或其他东西。

有些函数将无法节省任何insns的参数扩展到64位,因此调用方总是必须这样做是一种潜在的浪费。有些函数使用它们的参数时,需要与参数签名相反的扩展,因此让被调用者来决定要做什么很好。

尽管零扩展到64位(无论签名与否)对于大多数调用者来说都是免费的,而且可能是一个很好的设计选择。由于arg regs无论如何都被破坏了,如果调用方想要在只通过低32位的调用中保持完整的64位值,那么它已经需要做一些额外的事情。因此,通常只有在调用前需要64位的结果,然后将截断的版本传递给函数时,才需要额外的代价。在x86-64 SysV中,您可以在RDI中生成结果并使用它,然后调用只查看EDI的foo

16位和8位操作数大小通常会导致错误依赖(AMD、P4或Silvermont以及更高的SnB系列),或部分寄存器停顿(pre SnB)或轻微减速(Sandybridge),因此要求将8和16B类型扩展到32B以进行参数传递的未记录行为是有意义的。看看为什么GCC不使用部分寄存器?有关这些微架构的更多细节。

对于实际代码中的代码大小来说,这可能不是什么大问题,因为微小的函数是/应该是静态内联,而处理参数的INSN是较大函数的一小部分。当编译器可以看到两个定义时,过程间优化可以消除调用之间的开销,即使没有内联。(在实践中编译器在这方面做得有多好。)

我不确定更改函数签名以使用uintptr_t会帮助还是损害64位指针的总体性能。我不担心标量的堆栈空间。在大多数函数中,编译器会推送/弹出足够多的调用保留寄存器(如%RBX%RBP),以保持自己的变量在寄存器中生存。为8B溢出而不是4B溢出提供一个微小的额外空间是可以忽略不计的。

就代码大小而言,使用64位值需要在一些INSN上使用REX前缀,否则这些INSN不需要REX前缀。如果在32位值用作数组索引之前需要对其进行任何操作,则零扩展到64位是免费的。如果需要的话,符号扩展总是需要一个额外的指令。但是编译器可以对其进行符号扩展,并从一开始就将其作为64位符号值来使用,以保存指令,代价是需要更多的REX前缀。(带符号溢出是UB,不是为了环绕而定义的,因此编译器通常可以避免在使用arr[i]int i循环中重做符号扩展。)

现代CPU通常更关心insn计数,而不是insn大小,这是合理的。热代码通常会从拥有热代码的CPU的uop缓存中运行。但是,更小的代码可以提高uop缓存的密度。如果您可以节省代码大小,而不使用更多或更慢的INSN,那么这是一个胜利,但通常不值得牺牲任何其他东西,除非它是一个很大的代码大小。

比如一个额外的LEA指令,允许[reg+disp8]为后面十几个指令寻址,而不是disp32。或异或eax、eax之前的多个MOV[RDI+N]、0指令将IMM32=0替换为寄存器源。(特别是在允许微融合的情况下,使用RIP-relative+immediate是不可能的,因为真正重要的是前端uop计数,而不是指令计数。)

 类似资料:
  • 我阅读了Kip IRVINE的《x86处理器的汇编语言》一书,他写道: 将较小的值复制到较大的值 虽然MOV不能直接将数据从较小的操作数复制到较大的操作数,但程序员可以创建变通方法。假设计数(无符号,16位)必须移动到ECX(32位)。我们可以将ECX设置为零,并将计数移动到CX: 如果我们用一个等于-16的有符号整数尝试相同的方法,会发生什么? ECX(65,520)中的值与-16完全不同。另一

  • 我安装laravel通过作曲家,但当运行的网站它给一个错误 我也安装了php5-mcrypt,但错误仍然存在。

  • 在《计算机系统程序员的观点》(2.3.5)一书中,计算二的补码乘法的方法描述如下: C中的有符号乘法通常是通过将2w位乘积截断为w位来执行的。将二补码数截断为w位相当于首先计算其取模2w的值,然后从无符号转换为二补码。 因此,对于相似的位级操作数,为什么无符号乘法不同于二的补乘法?为什么两个补乘法需要进行符号扩展? 为了计算无符号和二补加法的相同位级表示,我们可以转换二补的参数,然后执行无符号加法

  • 我已经安装了家园,我试图在Laravel项目版本4.2上工作,但是我一直收到以下错误: 需要PHP扩展。 我已经在服务器上安装了Mcypt,并在PHP7INI文件中启用了它。由于某种原因,这个问题不想消失。 有什么建议,我可以做什么,以启用我的虚拟框的密码?

  • 问题内容: 我的要求是这样的: 我在数据库和时区中以毫秒为单位保存时间,例如以毫秒为单位的时间 很长,而时区为。我必须将其转换为时区。 要检查我引用此:链接 我的问题是: 如果时区只是不同格式的时间表示(地理区域偏离GMT),为什么我需要从实际时间中减去偏移时间? 为什么我不能忽略保存在数据库中的时区,而只考虑要转换日期的时区? 就像是: 我的系统时区是,我想将时区中的日期转换为时区。以上代码在这

  • 例如,在VS2008中,我有一个构建为x86(32bit)的exe,因为我希望它即使在64 bit机器上也能以32bit运行。此exe将通过反射和调用动态加载其他DLL。 现在我的问题是,如果我将我的dll构建为“所有CPU”,而不是x86,当我在64位机器上运行exe主机时,一旦我的dll被加载或调用,该dll将被视为64位或32位?(我的测试显示它被视为32bit,但我想和大家确认一下。我想一