表示具有联合和位字段的寄存器的问题
problem representing a register with unions and bit fields
我正在用 C++ 编写一个 NES 模拟器,我遇到了一个使用位字段表示寄存器的问题,这导致了一个非常严重的错误。我将内部地址寄存器表示为:
union
{
struct
{
uint16_t coarseX : 5; // bit field type is uint16_t, same as reg type
uint16_t coarseY : 5;
uint16_t baseNametableAddressX : 1;
uint16_t baseNametableAddressY : 1;
uint16_t fineY : 3;
uint16_t unused : 1;
} bits;
uint16_t reg;
} addressT, addressV; // temporary VRAM adddress register and VRAM address register
这样我就可以访问单个位域和整个寄存器。
最初我把寄存器写成:
union
{
struct
{
uint8_t coarseX : 5; // bit field type is uint8_t, reg type is uint16_t
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
uint8_t fineY : 3;
uint8_t unused : 1;
} bits;
uint16_t reg;
} addressT, addressV; // temporary VRAM adddress register and VRAM address register
当位域的类型(例如 coarseX)与寄存器 (reg) 的类型不同时,该错误是由位域行为引起的。
在这种情况下,当我增加一个字段(即 coarseX++)时,reg 成员被更新 "incorrectly",这意味着 reg 中的位模式没有反映由位字段表示的模式(或由位字段表示为我将它们布置在结构中)。
我知道编译器可以在 "allocation units" 中打包位字段,甚至可以插入填充,但为什么当我更改位字段的类型时行为会发生变化?
谁能解释一下为什么?
您用于位字段的类型是用于内部存储的类型。实际布局完全由实现定义。我想你的编译器将位域打包到存储单元中(“坏”示例中的 uint8_t),但不允许它们跨越存储单元边界。喜欢:
uint8_t coarseX : 5;
// 3 bits remain (out of 8), not enough for coarseY. So these become padding,
// and next storage unit starts here
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
// 1 bit remain. Again, too little.
uint8_t fineY : 3;
uint8_t unused : 1;
在“好”示例中,16 位足以容纳所有位域,因此编译器可以按照您需要的方式打包它们。有关详细信息,请参阅 https://en.cppreference.com/w/cpp/language/bit_field。
还要记住,访问不活跃的联合成员是 C++ 中的 UB。因此,最好使用单个 uint16_t 字段和访问器(这不会阻止类型为 POD/trivial/standard-layout)。
你自己说的:
I know that the compiler can pack bit fields inside "allocation units", and may even insert padding, ...
这正是正在发生的事情。
uint8_t
有 8 位。结构中的前两个字段 coarseX
和 coarseY
,每个字段有 5 位,不能连续放入内存中的单个字节中。编译器将 coarseX
存储在第一个字节中,然后必须将 coarseY
推入内存中的第二个字节,在内存中的 coarseX
和 coarseY
之间留下 3 个未使用的位您在寄存器中的值。
接下来的 3 个字段,coarseY
、baseNametableAddressX
和 baseNametableAddressY
,总共 7 位,因此它们适合第二个字节。
但是那个字节不能保存 fineY
和 unused
字段,所以它们被推到内存中的第 3 个字节,在 baseNametableAddressY
和 baseNametableAddressY
之间的内存中留下 1 个未使用的位fineY
抵消了您在寄存器中的值。并且寄存器无法访问第 3 个字节!
因此,实际上,您的 struct
最终表现得就像您已这样声明它:
union
{
struct
{
// byte 1
uint8_t coarseX : 5;
uint8_t padding1 : 3;
// byte 2
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
uint8_t padding2 : 1;
// byte 3!
uint8_t fineY : 3;
uint8_t unused : 1;
uint8_t padding3 : 4;
} bits;
struct {
uint16_t reg; // <-- 2 bytes!
uint8_t padding4; // <-- !
}
} addressT, addressV; // temporary
使用 uint16_t
而不是 uint8_t
,你不会 运行 解决这个问题,因为有足够的位分配给寄存器来保存所有的您正在定义的位。
我正在用 C++ 编写一个 NES 模拟器,我遇到了一个使用位字段表示寄存器的问题,这导致了一个非常严重的错误。我将内部地址寄存器表示为:
union
{
struct
{
uint16_t coarseX : 5; // bit field type is uint16_t, same as reg type
uint16_t coarseY : 5;
uint16_t baseNametableAddressX : 1;
uint16_t baseNametableAddressY : 1;
uint16_t fineY : 3;
uint16_t unused : 1;
} bits;
uint16_t reg;
} addressT, addressV; // temporary VRAM adddress register and VRAM address register
这样我就可以访问单个位域和整个寄存器。
最初我把寄存器写成:
union
{
struct
{
uint8_t coarseX : 5; // bit field type is uint8_t, reg type is uint16_t
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
uint8_t fineY : 3;
uint8_t unused : 1;
} bits;
uint16_t reg;
} addressT, addressV; // temporary VRAM adddress register and VRAM address register
当位域的类型(例如 coarseX)与寄存器 (reg) 的类型不同时,该错误是由位域行为引起的。 在这种情况下,当我增加一个字段(即 coarseX++)时,reg 成员被更新 "incorrectly",这意味着 reg 中的位模式没有反映由位字段表示的模式(或由位字段表示为我将它们布置在结构中)。 我知道编译器可以在 "allocation units" 中打包位字段,甚至可以插入填充,但为什么当我更改位字段的类型时行为会发生变化?
谁能解释一下为什么?
您用于位字段的类型是用于内部存储的类型。实际布局完全由实现定义。我想你的编译器将位域打包到存储单元中(“坏”示例中的 uint8_t),但不允许它们跨越存储单元边界。喜欢:
uint8_t coarseX : 5;
// 3 bits remain (out of 8), not enough for coarseY. So these become padding,
// and next storage unit starts here
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
// 1 bit remain. Again, too little.
uint8_t fineY : 3;
uint8_t unused : 1;
在“好”示例中,16 位足以容纳所有位域,因此编译器可以按照您需要的方式打包它们。有关详细信息,请参阅 https://en.cppreference.com/w/cpp/language/bit_field。
还要记住,访问不活跃的联合成员是 C++ 中的 UB。因此,最好使用单个 uint16_t 字段和访问器(这不会阻止类型为 POD/trivial/standard-layout)。
你自己说的:
I know that the compiler can pack bit fields inside "allocation units", and may even insert padding, ...
这正是正在发生的事情。
uint8_t
有 8 位。结构中的前两个字段 coarseX
和 coarseY
,每个字段有 5 位,不能连续放入内存中的单个字节中。编译器将 coarseX
存储在第一个字节中,然后必须将 coarseY
推入内存中的第二个字节,在内存中的 coarseX
和 coarseY
之间留下 3 个未使用的位您在寄存器中的值。
接下来的 3 个字段,coarseY
、baseNametableAddressX
和 baseNametableAddressY
,总共 7 位,因此它们适合第二个字节。
但是那个字节不能保存 fineY
和 unused
字段,所以它们被推到内存中的第 3 个字节,在 baseNametableAddressY
和 baseNametableAddressY
之间的内存中留下 1 个未使用的位fineY
抵消了您在寄存器中的值。并且寄存器无法访问第 3 个字节!
因此,实际上,您的 struct
最终表现得就像您已这样声明它:
union
{
struct
{
// byte 1
uint8_t coarseX : 5;
uint8_t padding1 : 3;
// byte 2
uint8_t coarseY : 5;
uint8_t baseNametableAddressX : 1;
uint8_t baseNametableAddressY : 1;
uint8_t padding2 : 1;
// byte 3!
uint8_t fineY : 3;
uint8_t unused : 1;
uint8_t padding3 : 4;
} bits;
struct {
uint16_t reg; // <-- 2 bytes!
uint8_t padding4; // <-- !
}
} addressT, addressV; // temporary
使用 uint16_t
而不是 uint8_t
,你不会 运行 解决这个问题,因为有足够的位分配给寄存器来保存所有的您正在定义的位。