首先,我知道这是一种unholy的事情,但我试图通过用众所周知的常量内存位置替换一些解引用来判断可能的性能yield ,从而用TCC来减少jitC代码中的指令开销.由于我对x86/x64内存分段了解不多,这个问题可能无法修复,也可能是TCC编译器的限制.

简而言之,该代码如下:

// Two 64 bits integer
U64 a, b;

// ADD_CODE is just a macro to add C source that will be compiled
ADD_CODE("if ((*((volatile U64*)(%p)) += %d) >= *((volatile U64*)(%p))) {\n", &a, 1, &b);

将我带到此程序集(由于错误地址导致的段错误(SIGSEGV)):

    0x1a6ac56 movabs rax, 0x7ffff5e1b020
    0x1a6ac60 mov    rax, QWORD PTR [rax]
    0x1a6ac63 add    rax, 0x1
 →  0x1a6ac67 mov    QWORD PTR ds:0xfffffffff5e1b020, rax
    0x1a6ac6f movabs rcx, 0x7ffff5e1b028

a&b变量是C++对象实例的一部分,该实例在任何编译之前就在那里,并在执行jited代码时保持不变.我们可以看到a变量位于0x7ffff5e1b020,我确实可以通过gdb访问这个地址上的变量.

但由于我目前的知识库之外的一个原因,这个变量的回写似乎意味着DS个寄存器,而内存地址现在看起来像是生成SIGSEGV的内核空间地址.

我的猜测倾向于对具有增量的直接内存访问的回写的限制为TCC(这个增量在示例之外并不总是1).令我惊讶的是,TCC没有重写RAX以外的另一个寄存器中的地址,至少是为了写回.

在研究了大约DS个之后,我try 在强制转换中添加一个Volatile关键字,但在生成的程序集中没有任何区别.我还试图在"if"语句之外获得递增操作,但同样,汇编仍然是相同的.

有没有人有建议试一试?也许有一个关键字或什么东西指定了这种访问的目标内存空间?(我猜在访问IO时可能会发生这种情况,但在这种情况下没有那么多操作系统的魔力障碍)

推荐答案

看起来您(或TCC)截断了一个指向32位的指针,并将其符号扩展到64位.它对存储使用了[disp32]寻址模式,因为这是mov而不是movabs moffs, rax.

64-位模式意味着CS/DS/ES/SS段基数都为零,主流OS下的32位代码已经做到了这一点.ds:0x...是GAS .intel_syntax noprefix反汇编语法(如objdump -drwC -Mintel)如何显示[disp32]个寻址模式以将其与立即数区分开来,而不是仅使用方括号(这在asm源代码中确实适用于裸数字,而不像在实际的MASM中).例如add rax, 1添加常数1,而不是来自绝对地址1的加载.

movabs rax, 0x7ffff5e1b020是地址的立即移位,mov rax, [rax]也使用DS作为段基数,只是不会在反汇编中显示它.


请注意,TCC已经很旧了,而且x86-64支持可能是在它被设计为为32位x86编译之后添加的.This is probably a TCC bug,如果该代码(在mov rcx之前)都来自*((volatile uint64_t*)(0x7ffff5e1b020) += 1,因为它从正确的地址加载,但截断存储地址.32位x86可以使用任何有效地址作为绝对寻址模式.X86-64只有在加载/存储累加器时才能做到这一点,具有mov moffs又称为movabs操作码(https://www.felixcloutier.com/x86/mov).


对于静态存储,您通常需要RIP相对寻址模式,例如mov rax, [RIP + rel32],因为这是一个7字节指令,而不是10用于movab,并且更有效地适合uop缓存. (或者RIP相关的lea到不同的寄存器中,以便它可以在加载和存储之间重用地址,同时仍然将+=结果留在寄存器中作为同一表达式的一部分进行比较.

TCC正在使用可能最糟糕的策略,先将64位立即数写入寄存器,然后再用值覆盖地址.但它需要将+=存储回相同的地址,所以如果它只是使用了其他寄存器中的任何一个,它仍然有mov [rdx], rax或其他地址可用.为了生成正确的代码,它需要另一个movabs rcx, imm64来重新实现mov [rcx], rax之前或更早的寄存器中的地址.

或者因为这是累加器,movabs rax, ds:0x7ffff5e1b020 / inc rax / movabs ds:0x7ffff5e1b020, rax将是可编码的. (mov-imm 64到任何寄存器都是可用的,但是从64位绝对地址加载/存储仅适用于AL/AX/EAX/RAX. 但这是TCC,所以它不会寻找RAX的特殊情况.)

不知道为什么它知道如何将mov-imm64写入寄存器以加载而不是存储.也许是因为加载不需要任何额外的寄存器,因为它已经加载到一个寄存器中,因此该寄存器可以作为地址的暂存空间.截断store 的64位地址显然是一个问题.

非PIE可执行文件的静态存储在虚拟地址空间的低32位,mov [disp32], reg可以对其进行寻址,但mov [RIP+rel32]仍然更高效,这就是为什么像GCC和clang这样的主流编译器即使在非PIE可执行文件中也使用RIP相对寻址来处理全局/静态变量.100(但mov r32, imm32用于非PIE中的静态地址与PIE或其他共享对象中的RIP相对LEA.101)

顺便说一句,它可能会对<=比较的另一边造成同样的低效混乱,因为你有一个相似的表达式.这只是一个加载,但在您的例子中,b的地址非常接近a的地址,所以好的代码生成程序将地址放入+=的寄存器一次,就可以使用具有小偏移量的地址,例如

    movabs rcx, 0x7ffff5e1b020
    mov    rax, [rcx]
    add    rax, 0x1
    mov    [rcx], rax
    cmp    rax, [rcx+8]      # 0x7ffff5e1b020+8 = 0x7ffff5e1b028

这是一个低效的10字节指令,其余的每个都是3或4字节.


如果您将TCC用作带有64位地址常量的JIT(使用&a的printf %p使C for TCC编译成您dlopen的库),它将不能使用RIP相对寻址.使用像uint64_t *anchor = 0x7ffff5e1b020;这样的本地变量将为您的全局变量提供一个具有+-2GiB范围的参考点.(或者char*uint32_t*,然后在指针数学之后对其进行强制转换.)例如,如果将anchor[1]定义为uint64_t*,则在您的例子中anchor[1]就是b.

TCC可能必须在每个使用它的表达式中至少从堆栈加载一次,但是mov rax, [rbp+8]只有4字节长,并且可以在现有CPU上高效运行.(每个时钟周期加载2次,或者在Alder Lake上加载3次,标量-整数加载使用Zen 3或更高版本.)如果 Select movabs r64, imm64,那么movabs-imm64可能会更好,即使movabs-imm64可以在任何ALU端口上运行,而不是大量运行.

我希望TCC会将指针本地变量加载到寄存器中,然后在C源代码看起来像anchor[0]anchor[1]时使用像[rcx][rcx+8]这样的寻址模式,但如果它浪费add上的指令,情况可能会更糟.

C++相关问答推荐

自定义malloc实现上奇怪的操作系统依赖行为

segfault在C中使用getline()函数

如何判断宏参数是否为C语言中的整型文字

如何在不使用其他数组或字符串的情况下交换字符串中的两个单词?

以下声明和定义之间的区别

平均程序编译,但结果不好

getline()从c中的外部函数传递指针时输出null

初始成员、公共初始序列、匿名联合和严格别名如何在C中交互?

将非连续物理内存映射到用户空间

浮动目标文件,数据段

意外的C并集结果

在列表中查找素数

C struct 中的冒泡排序

C 程序不显示任何输出,但它接受 CS50 Lab1 的输入问题

无法将字符串文字分配给 C 中的字符数组

中位数和众数不正确

cs50拼写器分配中的无限循环

malloc 属性不带参数

Codewars Kata 掷骰子的不稳定行为

为什么这里的符号没有解析?