我在做2018年版麻省理工学院6.828版的时候遇到了一些奇怪的事情,这个实验室运行在带有模拟80386 CPU的qemu上:

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

首先,我定义了一个具有位文件的 struct ,因为它实际上是硬件中的一个寄存器:


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
};

我决定通过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 }

然后,在我的.c文件中,我try 访问并启动注册表:


    // 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);

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


Memory address: content
0x????????: 0x12005452 0x80005634

然而,结果以:


Memory address: content
0x????????: 0x12005452 0x00000080

这很奇怪,所以我在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)


以下是我不能理解的几点:

  1. 汇编代码试图用0xfc来代替0x5406(%eax)中的AND个字节,但实际上它似乎清除了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

  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

  1. 最后,它ORBS字节为0x5404(%eax),它应该用0x5407(%eax)表示or

(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

  1. 顺便说一句,当我try 以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

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

非常感谢您的回答!

推荐答案

该硬件定义其寄存器为32位宽.这意味着您需要一次读写它们32位.您的C代码不会做任何事情来确保发生这种情况;当您对指向 struct 的指针进行操作时,编译器假定您正在读写普通老式RAM.对于RAM,通过一次读写不到32位来更新32位值中的子字段是很好的,这就是编译器通过其字节和字操作生成的代码所做的事情.然而,这将不能在设备寄存器上正常工作.(QEMU的实现将忽略字节和字访问try ;当您try 通过gdb存根访问设备时也可以看到这一点.)

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

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

C++相关问答推荐

了解返回函数指针的函数定义

为什么海湾合作委员会在共享对象中的. init_data的虚拟内存地址之前留出一个空白

getchar读css + z还是返回css?

如何将字符串argv[]赋给C中的整型数组?

在没有动态内存分配的情况下,用C语言最快地将各种数组复制到单个较大的数组中

如何调试LD_PRELOAD库中的构造函数?

LONG_DOUBLE_T是否存在(标准C:C23)

非常大的数组的大小

SSH会话出现意外状态

S将C语言宏定义为自身的目的是什么?(在glibc标题中看到)

OpenSSL:如何将吊销列表与SSL_CTX_LOAD_VERIFY_LOCATIONS一起使用?

我在反转双向链表时遇到问题

使用ld将目标文件链接到C标准库

将数字的每一位数平方,并使用C将它们连接为一个数字(程序不能正确处理0)

在运行时判断C/C++指针是否指向只读内存(在Linux操作系统中)

在文件描述符上设置FD_CLOEXEC与将其传递给POSIX_SPOWN_FILE_ACTIONS_ADCLOSE有区别吗?

我的代码可以与一个编译器一起使用,但不能与其他编译器一起使用

";错误:寄存器的使用无效;当使用-masm=intel;在gcc中,但在AT&;T模式

指向返回 struct 成员的指针,安全吗?

当 a 是代码块时使用逗号运算符 (a, b)