为什么这两个应该相同的指针指向不同的数据呢?
Why do these two pointers that should be the same point to different data?
我正在用 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))
的工作原理?具有相同属性的其他结构在我的内核的其他部分按照我期望的方式工作。如果需要,我可以 post 一个 link 到完整的源代码。
编辑:完整的源代码在 this gitlab repository 的 stack-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);
都编译为:(评论我的)
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;
}
现在很清楚打包结构确实按照我认为的方式工作,但我仍然不确定是什么导致了问题。我将相同的值传递给一个应该是确定性的函数,但我在不同的调用中得到不同的结果。
我的内核使用 32 位保护模式分段。我的数据段为 0x0000.0000 - 0x000f.ffff,堆栈段为 0x0003.8000 - 0x0003.ffff,以便在堆栈溢出时触发一般保护错误,而不是让它溢出到其他内核数据和代码。
但是,当 GCC 编译 C 代码时,它假定堆栈和数据段具有相同的基址,因为这是最常见的情况。这导致了一个问题,因为当我获取局部变量的地址时,它是相对于堆栈段的(因为局部变量在堆栈上),但是当我在被调用的函数中取消引用指针时,它是相对于数据段。
我更改了分段模型,使堆栈位于数据段而不是它自己的段中,这解决了问题。
我正在用 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))
的工作原理?具有相同属性的其他结构在我的内核的其他部分按照我期望的方式工作。如果需要,我可以 post 一个 link 到完整的源代码。
编辑:完整的源代码在 this gitlab repository 的 stack-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);
都编译为:(评论我的)
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;
}
现在很清楚打包结构确实按照我认为的方式工作,但我仍然不确定是什么导致了问题。我将相同的值传递给一个应该是确定性的函数,但我在不同的调用中得到不同的结果。
我的内核使用 32 位保护模式分段。我的数据段为 0x0000.0000 - 0x000f.ffff,堆栈段为 0x0003.8000 - 0x0003.ffff,以便在堆栈溢出时触发一般保护错误,而不是让它溢出到其他内核数据和代码。
但是,当 GCC 编译 C 代码时,它假定堆栈和数据段具有相同的基址,因为这是最常见的情况。这导致了一个问题,因为当我获取局部变量的地址时,它是相对于堆栈段的(因为局部变量在堆栈上),但是当我在被调用的函数中取消引用指针时,它是相对于数据段。
我更改了分段模型,使堆栈位于数据段而不是它自己的段中,这解决了问题。