проблема представления регистра с объединениями и битовыми полями - PullRequest
1 голос
/ 14 марта 2020

Я пишу эмулятор NES на C ++ и столкнулся с проблемой использования битовых полей для представления регистра, которая вызвала очень неприятную ошибку. Я представляю регистр внутреннего адреса как:

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 был обновлен «неправильно», что означает, что битовый шаблон внутри reg не отражает шаблон, представленный битовыми полями (или битовыми полями, как я лежал из них внутри структуры). Я знаю, что компилятор может упаковать битовые поля внутри «единиц выделения» и даже может вставить заполнение, но почему меняется поведение, когда я меняю тип битового поля?

Может кто-нибудь объяснить, почему?

Ответы [ 2 ]

4 голосов
/ 14 марта 2020

Вы сказали это сами:

Я знаю, что компилятор может упаковать битовые поля внутри "единиц выделения", и даже может вставить заполнение , ...

Именно так и происходит.

uint8_t содержит 8 бит. Первые два поля в вашей структуре, coarseX и coarseY, имеющие по 5 битов, не могут помещаться последовательно в пределах одного байта в памяти. Компилятор сохраняет coarseX в 1-м байте, а затем должен записать sh coarseY во 2-й байт в памяти, оставив в памяти 3 неиспользуемых бита между coarseX и coarseY, которые смещают ваши значения в регистре .

Следующие 3 поля, coarseY, baseNametableAddressX и baseNametableAddressY, всего 7 битов, поэтому они вписываются в этот 2-й байт.

Но этот байт не может содержать Поля fineY и unused, поэтому они помещаются в 3-й байт в памяти, оставляя в памяти 1 неиспользуемый бит между baseNametableAddressY и 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, вы не столкнитесь с этой проблемой, добавив доплату, поскольку для регистра достаточно битов, выделенных для хранения всех битов, которые вы определяете.

0 голосов
/ 14 марта 2020

Тип, который вы используете для битовых полей, используется для внутреннего хранения. И фактическое расположение полностью определяется реализацией. Я предполагаю, что ваш компилятор упаковывает битовые поля в единицы хранения (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 для получения дополнительной информации.

Также помните, что доступ к неактивному члену объединения является UB в C ++. Поэтому, вероятно, лучше использовать одно поле uint16_t и методы доступа (которые не мешают типу POD / trivial / standard-layout).

...