【问题标题】:Why do these two pointers that should be the same point to different data?为什么这两个应该相同的指针指向不同的数据?
【发布时间】:2020-05-24 21:20:07
【问题描述】:

我正在用 GNU C 为一个爱好操作系统编写一个 FAT16 驱动程序,我有一个这样定义的结构:

struct directory_entry {
  uint8_t name[11];
  uint8_t attrib;
  uint8_t name_case;
  uint8_t created_decimal;
  uint16_t created_time;
  uint16_t created_date;
  uint16_t accessed_date;
  uint16_t ignore;
  uint16_t modified_time;
  uint16_t modified_date;
  uint16_t first_cluster;
  uint32_t length;
} __attribute__ ((packed));

我的印象是name 将与整个结构在同一个地址,而attrib 之后将是 11 个字节。事实上,(void *)e.name - (void *)&e 是 0,(void *)&e.attrib - (void *)&e 是 11,其中 e 的类型是 struct directory_entry

在我的内核中,一个指向e 的空指针被传递给一个从磁盘读取其内容的函数。在此函数之后,*(uint8_t *)&e 为 80,*((uint8_t *)&e + 11 为 8,正如磁盘上所预期的那样。但是,e.name[0]e.attrib 都是 0。

这里给出了什么?我是否误解了__attribute__ ((packed)) 的工作原理?具有相同属性的其他结构按照我对内核其他部分的期望工作。如果需要,我可以发布完整来源的链接。

编辑:完整源代码位于this gitlab repositorystack-overflow 分支上。相关部分是 src/kernel/main.c 的第 34 到 52 行。当我检查*(uint8_t *)&e*((uint8_t *)&e + 11) 时,我确信数据填充正确。当我运行它时,该部分输出以下内容:

(void *)e.name - *(void *)&e
  => 0
*(uint8_t *)&e
  => 80
e.name[0]
  => 0
(void *)&e.attrib - (void *)&e
  => 11
*((uint8_t *)&e + 11)
  => 8
e.attrib
  => 0

我很困惑为什么e.name[0] 会与*(uint8_t *)&e 不同。

编辑2:我用objdump反汇编了这部分,看看编译后的代码有什么不同,但现在我更加困惑了。 u8_dec(*(uint8_t *)&e, nbuf);u8_dec(e.name[0], nbuf); 都编译为:(cmets mine)

lea   eax, [ebp - 0x30] ;loads address of e from stack into eax
movzx eax, byte [eax]   ;loads byte pointed to by eax into eax, zero-extending
movzx eax, al           ;not sure why this is here, as it's already zero-extended
sub esp, 0x8
push  0x31ce0 ;nbuf
push  eax     ;the byte we loaded
call  0x3162f ;u8_dec
add esp, 0x10

正如预期的那样,这会传入结构的第一个字节。我确定u8_dec 不会修改 e,因为它的第一个参数是按值传递的,而不是按引用传递的。 nbuf 是在文件范围内声明的数组,而 e 在函数范围内声明,所以它们不是重叠或任何东西。也许u8_dec 没有做好它的工作?这是它的来源:

void u8_dec(uint8_t n, uint8_t *b) {
  if (!n) {
    *(uint16_t *)b = '0';
    return;
  }
  bool zero = false;
  for (uint32_t m = 100; m; m /= 10) {
    uint8_t d = (n / m) % 10;
    if (zero)
      *(b++) = d + '0';
    else if (d) {
      zero = true;
      *(b++) = d + '0';
    }
  }
  *b = 0;
}

现在很清楚,打包结构确实可以按照我的想法工作,但我仍然不确定是什么导致了问题。我将相同的值传递给应该是确定性的函数,但在不同的调用中我得到不同的结果。

【问题讨论】:

  • 请出示代码。请创建一个minimal reproducible example
  • (void *)&e.attrib - (void *)&e 不是正确的 C 代码。您不能使用 void * 指针进行指针运算。
  • @AndrewHenle:您可以使用 gcc,并且该问题已标记为 gcc 并使用其他 gcc 扩展...
  • 你对__attribute__((packed))的理解是正确的,所以你的bug很可能是别的。
  • 既然您有了答案,请考虑将其写为答案。 StackOverflow 不是您通过编辑标题来标记已解决问题的论坛。你写一个答案并标记它。

标签: c gcc freestanding


【解决方案1】:

我的内核使用 32 位保护模式分段。我的数据段为 0x0000.0000 - 0x000f.ffff,堆栈段为 0x0003.8000 - 0x0003.ffff,以在堆栈溢出时触发一般保护错误,而不是让它溢出到其他内核数据和代码中.

但是,当 GCC 编译 C 代码时,它假定堆栈和数据段具有相同的基数,这是最常见的情况。这导致了一个问题,因为当我获取局部变量的地址时,它是相对于堆栈段的(因为局部变量在堆栈上),但是当我取消引用被调用函数中的指针时,它是相对于数据段。

我已经更改了我的分段模型,使堆栈位于数据段而不是它自己的段中,这已经解决了问题。

【讨论】:

  • 这很有趣。因此,当您执行struct directory_entry e; fs_read(root, 32, &e); 时,数据并未读入e(在堆栈上),而是读入其他内存区域,因为fs_read 使用了相对于ds 的指针。当您执行u8_dec(*(uint8_t *)&e, nbuf) 时,编译器会生成lea eax, [ebp - 0x30] ; movzx eax, byte [eax][eax]ds 相关,因此这有效地进行了相同的翻译并从“错误”区域获取,因此您看到了预期的值。 [...]
  • 另一方面,u8_dec(e.name[0], nbuf); 编译成movzx eax, byte [ebp - 0x30]。这是相对于 ss 的,因此它实际上确实从堆栈上的“正确”地址获取,但当然预期的数据不存在。
  • 确实,编译器假设 ss 和 ds 具有相同基数的唯一方法是对所有内容使用“远”48 位 seg:ofs 指针,如 8086“大”型号。这将是非常低效的,因为您必须为每次访问重新加载一个段寄存器,这会触发一大堆内部检查。我不知道是否存在任何可以做到这一点的 32 位 x86 编译器。当然 gcc 从来没有这样做过。
猜你喜欢
  • 2012-07-21
  • 1970-01-01
  • 2018-05-03
  • 1970-01-01
  • 2021-04-17
  • 1970-01-01
  • 1970-01-01
  • 2020-05-03
  • 1970-01-01
相关资源
最近更新 更多