GCC内联汇编未将数据复制到输入寄存器

3

我正在尝试使用BIOS中断来读取硬盘扇区,使用以下代码:

int readSec(char sector, unsigned char out[]) {
  char error = 0;
  int address = (int)out;
  __asm__ __volatile__("xorw %ax, %ax;" //clear all registers
    "xorw %bx, %bx;"
    "xorw %cx, %cx;"
    "xorw %dx, %dx;");
  __asm__ __volatile__("movw $0x00, %%ax; movw %%ax,%%es;" //clear the memory location that the sector will be read into
    "movw $0x0000, %%ax;"
    "movw $512, %%cx;"
    "movw %w0, %%di;"
    "rep stosw;"::"b" (address));
  __asm__ __volatile__(
    "movb $0x02, %%ah;" //read the sector
    "movb $0x01, %%al;"
    "movw %w2, %%bx;"
    "movb %b1, %%cl;"
    "int $0x13;"
    "sbb %w0, %w0;": "=r" (error): "m" (sector), "m" (address));
  return error;
}

运行此代码时,没有任何反应。
为了调试代码,我查看了反汇编代码,令我惊讶的是,“address”变量从未被复制到di - 而是复制了一个空寄存器。以下是函数的反汇编(右侧)和函数本身(左侧):

enter image description here

如图所示,bx寄存器被清零(就像在第一个汇编块中一样),然后在到达movw %w0, %%di行时,它只是使用了被清零的bx寄存器mov %bx,%di。为什么GCC要这样做呢?显然,将空寄存器的值复制到di中不会复制address到其中...
编辑:我正在使用GCC编译此代码,并在虚拟机中运行它。这不是所有的代码,我只想知道为什么要将空寄存器复制到%di中。
编辑2:如nos建议,我已从(int) & out;中删除了&符号。

1
@FUZxxl 我知道...我只是使用Visual Studio作为代码编辑器。可执行文件在虚拟机中运行。 - DividedByZero
2
@EOF gcc使用GNU汇编器。如果您不知道自己在说什么,请在发表误导性和错误的评论之前进行一些研究。 - fuz
2
@EOF gold是一个链接器,而不是汇编器。 - fuz
3
@FUZxxl @EOF,我们可以不吵架吗?至少在我的情况下,生成的代码清楚地表明我的汇编器理解了我的代码。 - DividedByZero
1
@MichaelBurr: 从反汇编结果来看,他可能是用 gcc -m32 -S 命令输出的代码,然后在16位模式下进行了组装。(32位指令有操作数大小和/或地址大小前缀,而16位指令则没有。)这种方法可能仍然有效,但默认栈宽度将无法匹配,因此函数将无法在正确的位置找到它们的参数。 - Peter Cordes
显示剩余13条评论
4个回答

3

好的,让我们看看它在你指定的内容下生成了什么。你有:

:
"movw %w0, %%di;"
...::"b" (address));

这告诉编译器“将地址中的值复制到%ebx(约束为“b”),并生成movew %bx,% di 指令将其复制到%di。如果您查看生成的代码,它确实会执行此操作:

7e62:                       lea   0xc(%ebp),%ebx

7e83:                       mov   %bx,%di

它正在加载out指针的地址到%ebx中,作为初始化address的一部分,然后在稍后使用。

问题是你在这两个点之间放入了另一个asm代码块来清除ebx,但没有告诉编译器。要解决这个问题,你需要在第一个代码块上设置'clobber'——添加:

:::"eax","ebx","ecx","edx".

一旦你这样做了,第一个代码块本质上是一个空操作,告诉编译器保存和恢复该代码块周围那4个寄存器的值,除此之外什么也不做。所以你应该考虑完全删除它。

此外,第二个汇编块等同于:

memset(&out, 0, 1024);

除了效率低下外,它还会覆盖堆栈的一大块(因为out只是堆栈上的一个指针),这可能导致代码在尝试返回时崩溃(返回地址已被覆盖为0)。
总体来说,最好将代码写成C语言,并仅在不能用C语言表达的代码(如int $0x13指令和将进位标志转换为错误代码)中使用内联汇编。
像这样:
int readSec(char sector, unsigned char out[]) {
    int error;
    memset(out, 0, 1024);  // clear buffer pointed at by out, rather than stack!
    __asm__ volatile(
        "int $0x13;"
        "sbb %0,%0"
        : "=r"(error) : "a"(0x201), "c"(sector), "b"(out))
    return error;
}

2
除了没有正确使用 clobbers 外,这种嵌入式汇编的用法是不好的。你应该尽量使用 C 语言来实现除了实际的 int 0x13 指令以外的所有操作。此外,请注意 asm 块之间的寄存器没有被保留,不清楚你是否期望这种行为。
PS:清空缓冲区的原因是什么?
还要注意你需要设置 dldh。可以尝试像这样设置:
int readSec(unsigned sector, void* out) {
  int error = 0;
  unsigned sector_and_track = sectpr + xxxx; 
  unsigned drive_and_head = xxxx; 
  memset(out, 0, 512);
  __asm__ __volatile__(
    "int 0x13; sbb %0, %0"
    : "=r" (error) : "a" (0x0201), "b" (out), "c" (sector_and_track), "d" (drive_and_head));
  return error;
}

我清空缓冲区是为了预防现有数据出现异常行为。 - DividedByZero

2
你没有告诉编译器你清除了哪些寄存器,所以它不知道哪些可以安全使用。很有可能,从编译器的角度来看,你想要访问的地址在bx寄存器中。你清除该寄存器的事实对编译器不可见。
因此... 你可以尝试在清除寄存器的代码块的asm之后使用Clobbers参数(在输出之后)。

我尝试清空第三个汇编块中的 bx 寄存器,但它对生成的代码没有任何影响。 - DividedByZero
1
第一个代码块会破坏寄存器。你需要告诉编译器这个事实。 - Bahbar
好的,所以我将"xorw %dx, %dx;");更改为"xorw %%dx, %%dx;":::"bx");,但是代码仍然不幸地没有改变,结果也没有改变。 - DividedByZero

0

这里有两种可能出现了问题。根据您提供的信息,我无法确定是哪一种,但其中任何一种都会阻止其正常工作。

  1. 您的CPU处于实模式(16位),并且期望16位指令。GCC编译代码时假定它将在32位模式下运行;在16位模式下运行它将导致某些指令被错误解释。

    明确一点,GCC不是用于16位x86的编译器,不能用于编译要在实模式下运行的代码。虽然GCC编译的某些代码将部分在实模式下工作,但它不可靠。

  2. 您的CPU处于保护模式(32位)。BIOS调用在保护模式下不起作用。

从上下文来看,您似乎正在尝试使用C语言编写操作系统内核。如果您想这样做,您需要:

  • 尽快使用汇编将CPU置于保护模式或长模式下,然后在安全时刻进入C代码。

  • 针对另一种架构进行目标设置,该架构不会像x86那样遭受某些初始化“怪癖”的困扰。ARM在这方面表现出色。


1
不错的想法,但是根据他在“vmware player”窗口中的反汇编结果,“xor %ax,%ax”是“31 C0”,而使用32位地址和操作数大小的指令则使用“67”和/或“66”前缀字节。因此,看起来他以某种方式成功地将此代码编译为16位。 - Peter Cordes
对于内联汇编,也许可以。但由 C 编译器生成的代码则完全是另一回事。 - user149341
不,汇编器在同一文件中看到所有指令。 "inline asm" 指令不会分开汇编。 无论如何,即使是堆栈帧设置 mov %esp, %ebp 也是带有操作数大小前缀的 66 89 E5。 这只是一些混乱的代码。 int address = (int) & out;,然后通过 m 约束将其传递给 asm(强制编译器将地址存储到内存中)是有趣的。 - Peter Cordes

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接