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
,所以 al
是 0x10
。结果应该是这样的,但是为什么会选择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 sr
或 mov r, sr
)。 pop sr
甚至有点慢。
我什至不打算为此编写代码,因为这是个糟糕的主意。确保使用 clobber 约束让编译器知道你踩到的每个寄存器,否则当周围的代码停止工作时你将遇到难以调试的错误。
有关 GNU C 内联汇编信息,请参阅 x86 标签 wiki。
当我测试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
,所以 al
是 0x10
。结果应该是这样的,但是为什么会选择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 sr
或 mov r, sr
)。 pop sr
甚至有点慢。
我什至不打算为此编写代码,因为这是个糟糕的主意。确保使用 clobber 约束让编译器知道你踩到的每个寄存器,否则当周围的代码停止工作时你将遇到难以调试的错误。
有关 GNU C 内联汇编信息,请参阅 x86 标签 wiki。