Gcc内联汇编:输入操作数中动态分配的寄存器`r`有什么问题?

Gcc inline assembly: what's wrong with the dynamic allocated register `r` in input operand?

当我测试GCC内联汇编时,我使用test函数在BOCHS模拟器的屏幕上显示一个字符。此代码在 32 位保护模式下为 运行。代码如下:

test() {
    char ch = 'B';
    __asm__ ("mov [=10=]x10, %%ax\n\t" 
                "mov %%ax, %%es\n\t"
                "movl [=10=]xb8000, %%ebx\n\t"
                "mov [=10=]x04, %%ah\n\t" 
                "mov %0, %%al\n\t" 
                "mov %%ax, %%es: ((80 * 3 + 40) * 2)(%%ebx)\n\t" 
                ::"r"(ch):);
}

我得到的结果是:

屏幕上的红色字符显示不正确B。但是,当我把输入寄存器r改成c时:::"c"(ch):);,也就是上面代码的最后一行,字符'B'正常显示:

有什么不同?我是在电脑进入保护模式后直接通过数据段访问显存的。

I have trace the assembly code, I have found that the code has been assembled to mov al, al when the r register is chosen and the value of ax is 0x0010,所以 al0x10。结果应该是这样的,但是为什么会选择al这个寄存器呢。不是应该选择以前没用过的寄存器吗?当我添加 clobbers 列表时,我已经解决了问题。

就像@MichaelPetch 评论的那样,您可以使用 32 位地址从 C 访问您想要的任何内存。asm gcc 发出的将假设一个平坦的内存 space,并假设它可以复制 esp例如,到 edi 并使用 rep stos 将一些堆栈内存归零(这要求 %es%ss 具有相同的基数)。

我猜最好的解决方案是不使用任何内联汇编,而只是使用全局常量作为指向 char 的指针。例如

// pointer is constant, but points to non-const memory
uint16_t *const vga_base = (uint16_t*)0xb8000;   // + whatever was in your segment

// offsets are scaled by 2.  Do some casting if you want the address math to treat offsets as byte offsets
void store_in_flat_memory(unsigned char c, uint32_t offset) {
  vga_base[offset] = 0x0400U | c;            // it matters that c is unsigned, so it zero-extends instead of sign-extending
}
    movzbl  4(%esp), %eax       # c, c
    movl    8(%esp), %edx       # offset, offset
    orb     , %ah   #, tmp95         # Super-weird, wtf gcc.  We get this even for -mtune=core2, where it causes a partial-register stall
    movw    %ax, 753664(%edx,%edx)  # tmp95, *_3   # the addressing mode scales the offset by two (sizeof(uint16_t)), by using it as base and index
    ret

来自 Godbolt 上的 gcc6.1(下方 link),-O3 -m32

如果没有 const,像 vga_base[10] = 0x4 << 8 | 'A'; 这样的代码将不得不加载 vga_base 全局变量,然后从中偏移。对于 const&vga_base[10] 是编译时常量。


如果您真的想要细分:

由于您不能离开 %es 修改,您需要 save/restore 它。这是首先避免使用它的另一个原因。如果你真的想要一个特殊的段,设置一次 %fs%gs 并保留它们,所以它不会影响任何不使用段覆盖的指令的正常操作。

对于线程局部变量,有内置语法可以使用 %fs%gs 而无需内联汇编。

如果您使用的是自定义段,您可以将其基地址设置为非零,这样您就不需要自己添加 0xb8000。但是,Intel CPU 针对平面内存情况进行了优化,因此使用非零段基址的地址生成要慢几个周期,IIRC。

我确实找到了 request for gcc to allow segment overrides without inline asm, and a question about adding segment support to gcc。目前您不能这样做。


在 asm 中使用专用段手动完成

为了查看 asm 输出,我将其放在 Godbolt with the -mx32 ABI 上,因此参数在寄存器中传递,但地址不需要符号扩展到 64 位。 (我想避免为 -m32 代码从堆栈加载参数的噪音。保护模式的 -m32 asm 看起来很相似)

void store_in_special_segment(unsigned char c, uint32_t offset) {
    char *base = (char*)0xb8000;               // sizeof(char) = 1, so address math isn't scaled by anything

    // let the compiler do the address math at compile time, instead of forcing one 32bit constant into a register, and another into a disp32
    char *dst = base+offset;               // not a real address, because it's relative to a special segment.  We're using a C pointer so gcc can take advantage of whatever addressing mode it wants.
    uint16_t val = (uint32_t)c | 0x0400U;  // it matters that c is unsigned, so it zero-extends

    asm volatile ("movw  %[val], %%fs: %[dest]\n"
         : 
         : [val] "ri" (val),  // register or immediate
           [dest] "m" (*dst)
         : "memory"   // we write to something that isn't an output operand
    );
}
    movzbl  %dil, %edi        # dil is the low 8 of %edi (AMD64-only, but 32bit code prob. wouldn't put a char there in the first place)
    orw     24, %di        #, val   # gcc causes an LCP stall, even with -mtune=haswell, and with gcc 6.1
    movw  %di, %fs: 753664(%esi)    # val, *dst_2

void test_const_args(void) {
    uint32_t offset = (80 * 3 + 40) * 2;
    store_in_special_segment('B', offset);
}
    movw  90, %fs: 754224        #, MEM[(char *)754224B]

void test_const_offset(char ch) {
    uint32_t offset = (80 * 3 + 40) * 2;
    store_in_special_segment(ch, offset);
}
    movzbl  %dil, %edi  # ch, ch
    orw     24, %di        #, val
    movw  %di, %fs: 754224  # val, MEM[(char *)754224B]

void test_const_char(uint32_t offset) {
    store_in_special_segment('B', offset);
}
    movw  90, %fs: 753664(%edi)  #, *dst_4

所以这段代码让 gcc 在使用寻址模式进行地址数学运算方面做得非常出色,并在编译时尽可能多地进行。


段寄存器

如果您确实想为每个存储修改段寄存器,请记住它很慢:Agner Fog's insn tables 停止在 Nehalem 之后包含 mov sr, r,但在 Nehalem 上它是一个 6 uop 指令,其中包括3 个加载微指令(我假设来自 GDT)。它的吞吐量为每 13 个周期一个。读取段寄存器没问题(例如 push srmov r, sr)。 pop sr 甚至有点慢。

我什至不打算为此编写代码,因为这是个糟糕的主意。确保使用 clobber 约束让编译器知道你踩到的每个寄存器,否则当周围的代码停止工作时你将遇到难以调试的错误。

有关 GNU C 内联汇编信息,请参阅 标签 wiki。