C 中操作位域的汇编代码在 QEMU 中表现得很奇怪

Sta*_*fly 2 c assembly gcc gdb qemu

我在做 2018 版本 MIT 6.828 时遇到了一些奇怪的事情,该实验室在模拟 80386 CPU 的 QEMU 上运行:

我想做的是初始化 INTEL 82540EM 芯片(也称为 E1000)的接收过程。我基本上只是将一些字节写入设备的寄存器。

首先我定义了一个带有位域的结构,因为它实际上是硬件中的寄存器:


struct rx_addr_reg {
    // low 32 bit
    unsigned ral : 32;  // 0 - 31
    // high 32 bit
    unsigned rah : 16;  // 0 -15
    unsigned as  : 2;   // 16 - 17
    unsigned rs  : 13;  // 18 - 30
    unsigned av  : 1;   // 31
};

Run Code Online (Sandbox Code Playgroud)

我决定通过 C 宏使用它:


#define E1000_RA       0x05400  /* Receive Address - RW Array */
#define E1000_RAH_AV  0x80000000        /* Receive descriptor valid */


#define E1000_GET_REG(base,reg) \
{   ((void*)(base) + (reg))    }
#define E1000_SET_RECEIVE_ADDR_REG(addr,as,rs,av) (struct rx_addr_reg)\
{   (addr >> 16) & 0xffffffff, (addr) & 0xffff, \
    (as) & 0x3, (rs) & 0x1fff, (av) & 0x1 }

Run Code Online (Sandbox Code Playgroud)

然后在我的.c文件中,我尝试访问并启动寄存器:


    // Receive Initialization
    // Program the Receive Address Registers (RAL/RAH) with the desired Ethernet addresses
    struct rx_addr_reg* rar = (struct rx_addr_reg*) E1000_GET_REG(e1000_va, E1000_RA);
    *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1);

Run Code Online (Sandbox Code Playgroud)

我期望在内存中看到的rar是这样的:


struct rx_addr_reg {
    // low 32 bit
    unsigned ral : 32;  // 0 - 31
    // high 32 bit
    unsigned rah : 16;  // 0 -15
    unsigned as  : 2;   // 16 - 17
    unsigned rs  : 13;  // 18 - 30
    unsigned av  : 1;   // 31
};

Run Code Online (Sandbox Code Playgroud)

然而,结果却是这样结束的:


#define E1000_RA       0x05400  /* Receive Address - RW Array */
#define E1000_RAH_AV  0x80000000        /* Receive descriptor valid */


#define E1000_GET_REG(base,reg) \
{   ((void*)(base) + (reg))    }
#define E1000_SET_RECEIVE_ADDR_REG(addr,as,rs,av) (struct rx_addr_reg)\
{   (addr >> 16) & 0xffffffff, (addr) & 0xffff, \
    (as) & 0x3, (rs) & 0x1fff, (av) & 0x1 }

Run Code Online (Sandbox Code Playgroud)

这很奇怪,所以我检查了GDB中的程序:


+ target remote localhost:26000
The target architecture is assumed to be i8086
[f000:fff0]    0xffff0: ljmp   $0xf000,$0xe05b
0x0000fff0 in ?? ()
+ symbol-file obj/kern/kernel
(gdb) br e1000.c:64
Breakpoint 1 at 0xf0107470: file kern/e1000.c, line 64.
(gdb) si
[f000:e05b]    0xfe05b: cmpl   $0x0,%cs:0x6ac8
0x0000e05b in ?? ()
(gdb) c
Continuing.
The target architecture is assumed to be i386
=> 0xf0107470 <pci_e1000_attach+264>:   movl   $0x60200a,0x410(%eax)

Breakpoint 1, pci_e1000_attach (pcif=0xf012af10) at kern/e1000.c:64
64          *(uint32_t*)((char*)e1000_va + E1000_TIPG) |= 10 | 8 << 10 | 6 << 20;
(gdb) si
=> 0xf010747a <pci_e1000_attach+274>:   movl   $0x12005452,0x5400(%eax)
82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb)
=> 0xf0107484 <pci_e1000_attach+284>:   movw   $0x5634,0x5404(%eax)
0xf0107484      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb)
=> 0xf010748d <pci_e1000_attach+293>:   andb   $0xfc,0x5406(%eax)
0xf010748d      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00005634
(gdb) si
=> 0xf0107494 <pci_e1000_attach+300>:   andw   $0x8003,0x5406(%eax)
0xf0107494      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000034
(gdb) si
=> 0xf010749d <pci_e1000_attach+309>:   orb    $0x80,0x5407(%eax)
0xf010749d      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000000
(gdb) si
=> 0xf01074a4 <pci_e1000_attach+316>:   movl   $0x1,0xc(%esp)
86          cprintf("[RAH:RAL] [av]: [%x:%x] [%x]\n", rar->rah, rar->ral, rar->av);
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000080
(gdb)


Run Code Online (Sandbox Code Playgroud)

以下是我不明白的地方:

  1. 汇编代码尝试对中的字节进行AND0x5406(%eax)运算0xfc,但实际上 中的字节似乎很清楚0x5405

(gdb)
=> 0xf010748d <pci_e1000_attach+293>:   andb   $0xfc,0x5406(%eax)
0xf010748d      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00005634
(gdb) si
=> 0xf0107494 <pci_e1000_attach+300>:   andw   $0x8003,0x5406(%eax)
0xf0107494      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000034

Run Code Online (Sandbox Code Playgroud)
  1. 然后ANDW出了问题,似乎清楚了字节0x5404(%eax)
(gdb) si
=> 0xf0107494 <pci_e1000_attach+300>:   andw   $0x8003,0x5406(%eax)
0xf0107494      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000034
(gdb) si
=> 0xf010749d <pci_e1000_attach+309>:   orb    $0x80,0x5407(%eax)
0xf010749d      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000000

Run Code Online (Sandbox Code Playgroud)
  1. 最后它ORB s 处的字节0x5404(%eax),它应该or0x5407(%eax)

(gdb) si
=> 0xf010749d <pci_e1000_attach+309>:   orb    $0x80,0x5407(%eax)
0xf010749d      82          *rar = E1000_SET_RECEIVE_ADDR_REG(0x120054525634, 0x0, 0x0, 0x1); //0x525400123456 0x120054525634
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000000
(gdb) si
=> 0xf01074a4 <pci_e1000_attach+316>:   movl   $0x1,0xc(%esp)
86          cprintf("[RAH:RAL] [av]: [%x:%x] [%x]\n", rar->rah, rar->ral, rar->av);
(gdb) x/2xw $eax + 0x5400
0xef809400:     0x12005452      0x00000080

Run Code Online (Sandbox Code Playgroud)
  1. 顺便说一句,当我尝试打印 处的字节时0x5400(%eax),为什么 gdb 拒绝执行此操作而只显示 4 字节对齐字节处的内容?

(gdb) x/xw $eax+0x5404
0xef809404:     0x00000034
(gdb) x/xw $eax+0x5406
0xef809406:     0x00000034
(gdb) x/xb $eax+0x5406
0xef809406:     0x34
(gdb) x/xb $eax+0x5404
0xef809404:     0x34

Run Code Online (Sandbox Code Playgroud)

我认为它可以解决问题但我不确定的一点是:我定义的结构体是8字节长,并且系统运行在32位下。因此,如果设备不允许写入位字段,而只允许写入整个 4 个字节,则问题可能是合理的。

非常感谢您的回答!

Pet*_*ell 7

该硬件定义其寄存器为 32 位宽。这意味着您需要一次读取和写入 32 位。您的 C 代码不会执行任何操作来确保发生这种情况;当您操作指向结构的指针时,编译器假定您正在读取和写入普通的旧 RAM。对于 RAM,可以通过一次读取和写入少于 32 位来更新 32 位值中的子字段,这就是编译器生成的代码通过其字节和字操作所做的事情。但是,这在设备寄存器上无法正常工作。(QEMU 的实现将忽略字节和字访问尝试;当您尝试通过 gdbstub 访问设备时,您也可以看到这一点。)

因此,您不能只定义一个具有与规范中的寄存器一致的位字段的结构,并期望写入单个位字段能够正常工作。如果要更新寄存器中的单个字段,您应该读取整个 32 位寄存器,更新值的相关部分,然后再次写回整个 32 位值。(通常您希望一次更新所有字段,在这种情况下,您可以只写入完整的新值,而不必先进行读取。)

您还想确保编译器不认为这只是 RAM,因此它可以愉快地重新排序、合并或删除更新。就我个人而言,我喜欢 Linux 内核定义用于执行访问的函数的方法,这些函数最终归结为 asm 加载和存储,以便始终 100% 清楚生成的代码将做什么;还有其他方法。