问题

下面是在进行 H264 数据分析时碰到的问题

H264 nal indecator 是在 00 00 00 01 后面的一字节,其字段内容为: * forbidden_zero_bit(1) * nal_ref_idc(2) * nal_unit_type(5)

比如对 0x67 / 0b01100111,各字段为: * forbidden_zero_bit = 0 * nal_ref_idc = 0b11 = 3 * nal_unit_type = 0b111 = 7

常规的提取方式如下:

uint8_t a = 0x67;
uint8_t f    = (a & 0b10000000) >> 7;
uint8_t nri  = (a & 0b01100000) >> 5;
uint8_t type = (a & 0b00011111);

f = 0, nri = 3, type = 7 结果没有问题。

但是如果采用 bit field 进行数据拆解则获取不到正确的结果:

#include <stdio.h>
#include <inttypes.h>

struct NalIndicator {
    uint8_t f : 1;
    uint8_t nri : 2;
    uint8_t type : 5;
};

int main()
{
    NalIndicator * n;

    uint8_t a = 0x67;
    n = (NalIndicator *)(&a);

    uint8_t f    = (a & 0b10000000) >> 7;
    uint8_t nri  = (a & 0b01100000) >> 5;
    uint8_t type = (a & 0b00011111);

    printf("sizeof NalIndicator %d\n", sizeof(NalIndicator));
    printf("f    : %d, %d\n", n->f, f);
    printf("nri  : %d, %d\n", n->nri, nri);
    printf("type : %d, %d\n", n->type, type);

    return 0;
}

输出 > sizeof NalIndicator 1 f : 1, 0 nri : 3, 3 type : 12, 7 请按任意键继续…

分析

经过分析,发现是字序影响到了 bit field 的顺序而导致的。

按照以上代码的定义,实际上的各字段是按照从右往左排列的,也就是说:

0x67 = 01100111 = 01100,11,1

其中 f = 1, nri = 0b11 = 3, type = 0b0110 = 12。

没想到字序除了会决定字节的顺序,还会对 bit field 的顺序造成影响。

解决方式

如果要编写跨平台的代码,需要针对不同字序的平台定义不同的 struct,并通过预处理器进行定义的切换,用户需要定义类似 BIG_ENDIAN / LITTLE_ENDIAN 的宏,比如 SDL 的 SDL_endian.h

/**
 *  \name The two types of endianness
 */
/* @{ */
#define SDL_LIL_ENDIAN  1234
#define SDL_BIG_ENDIAN  4321
/* @} */

#ifndef SDL_BYTEORDER           /* Not defined in SDL_config.h? */
#ifdef __linux__
#include <endian.h>
#define SDL_BYTEORDER  __BYTE_ORDER
#else /* __linux__ */
#if defined(__hppa__) || \
    defined(__m68k__) || defined(mc68000) || defined(_M_M68K) || \
    (defined(__MIPS__) && defined(__MISPEB__)) || \
    defined(__ppc__) || defined(__POWERPC__) || defined(_M_PPC) || \
    defined(__sparc__)
#define SDL_BYTEORDER   SDL_BIG_ENDIAN
#else
#define SDL_BYTEORDER   SDL_LIL_ENDIAN
#endif
#endif /* __linux__ */
#endif /* !SDL_BYTEORDER */

想要在编译阶段自动检测字序是不可能的,无论什么方式都需要在程序执行阶段才能对字序及进行判断

#include <stdint .h>
#define IS_BIG_ENDIAN (*(uint16_t *)"\0\xff" < 0x100)

不过我们可以针对用户通过宏定义的字序进行检查,并在运行时抛出异常:

inline int IsBigEndian()
{
    int i=1;
    return ! *((char *)&i);
}

/* ... */

#ifdef BIG_ENDIAN
assert(IsBigEndian());
#elif LITTLE_ENDIAN
assert(!IsBigEndian());
#else
#error "No endianness macro defined"
#endif

还是乖乖用位运算吧

参考资料 > http://mjfrazer.org/mjfrazer/bitfields/ > https://yumichan.net/video-processing/video-compression/introduction-to-h264-nal-unit/