为啥 fread 会弄乱我的字节顺序?

Posted

技术标签:

【中文标题】为啥 fread 会弄乱我的字节顺序?【英文标题】:Why does fread mess with my byte order?为什么 fread 会弄乱我的字节顺序? 【发布时间】:2012-01-23 07:45:36 【问题描述】:

我正在尝试使用fread() 解析一个 bmp 文件,当我开始解析时,它会颠倒我的字节顺序。

typedef struct
    short magic_number;
    int file_size;
    short reserved_bytes[2];
    int data_offset;
BMPHeader;
    ...
BMPHeader header;
    ...

十六进制数据为42 4D 36 00 03 00 00 00 00 00 36 00 00 00; 我正在通过fread(&header,14,1,fileIn); 将十六进制数据加载到结构中

我的问题是幻数应该是0x424d //'BM' fread() 它将字节翻转为0x4d42 // 'MB'

为什么 fread() 会这样做,我该如何解决它;

编辑:如果我不够具体,我需要将整块十六进制数据读取到结构中,而不仅仅是幻数。我只选择了幻数作为例子。

【问题讨论】:

...面包弄乱了你的点心?你试过啃吗? 你的标题不是fread而不是bread吗? 对不起。我仍然必须习惯 Lions Auto 正确。我修好了 【参考方案1】:

我认为这是一个字节序问题。即您将字节 424D 放入您的 short 值中。但是您的系统是小端(我可能有错误的名称),它实际上从左到右而不是从右到左读取字节(在多字节整数类型内)。

在此代码中演示:

#include <stdio.h>

int main()

    union 
        short sval;
        unsigned char bval[2];
     udata;
    udata.sval = 1;
    printf( "DEC[%5hu]  HEX[%04hx]  BYTES[%02hhx][%02hhx]\n"
          , udata.sval, udata.sval, udata.bval[0], udata.bval[1] );
    udata.sval = 0x424d;
    printf( "DEC[%5hu]  HEX[%04hx]  BYTES[%02hhx][%02hhx]\n"
          , udata.sval, udata.sval, udata.bval[0], udata.bval[1] );
    udata.sval = 0x4d42;
    printf( "DEC[%5hu]  HEX[%04hx]  BYTES[%02hhx][%02hhx]\n"
          , udata.sval, udata.sval, udata.bval[0], udata.bval[1] );
    return 0;

给出以下输出

DEC[    1]  HEX[0001]  BYTES[01][00]
DEC[16973]  HEX[424d]  BYTES[4d][42]
DEC[19778]  HEX[4d42]  BYTES[42][4d]

因此,如果您想要便携,则需要检测系统的字节序,然后在需要时进行字节洗牌。互联网上有很多交换字节的例子。

后续问题:

我问只是因为我的文件大小是 3 而不是 196662

这是由于内存对齐问题。 196662 是字节 36 00 03 00,3 是字节 03 00 00 00。大多数系统需要像int 这样的类型,而不是在多个内存words 上拆分。如此直观地,您认为您的结构在内存中布局如下:

                          Offset
short magic_number;       00 - 01
int file_size;            02 - 05
short reserved_bytes[2];  06 - 09
int data_offset;          0A - 0D

但是在 32 位系统上,这意味着 files_size 在与 magic_number 相同的 word 中有 2 个字节,在下一个 word 中有两个字节。大多数编译器不会支持这一点,所以结构在内存中的布局方式实际上是这样的:

short magic_number;       00 - 01
<<unused padding>>        02 - 03
int file_size;            04 - 07
short reserved_bytes[2];  08 - 0B
int data_offset;          0C - 0F

因此,当您读取 36 00 中的字节流时,您的填充区域将让您的 file_size 得到 03 00 00 00。现在,如果您使用fwrite 创建此数据,它应该没问题,因为填充字节会被写出。但是,如果您的输入总是采用您指定的格式,那么将整个结构与 fread 一起读取是不合适的。相反,您需要单独阅读每个元素。

【讨论】:

抱歉,过早点击保存。现在都在那里 +1 用于演示,尽管在这里明确小端假设会很好。 这是否只影响short?我问只是因为我的文件大小是 3 而不是 196662 不,它影响所有大于 1 字节的整数类型,所以 shortintlonglong long。如果您使用我的代码作为调试的基础,您可能需要删除/更改 printf 格式中的 h 字符。 h 用于短裤,hh 用于无符号字符。详情请查看man 3 printf @Sodved 我没有使用h 字符。我仍然遇到 file_size 的问题【参考方案2】:

这不是fread 的错,而是你的CPU(显然)是小端的。也就是说,您的 CPU 将 short 值中的第一个字节视为 8 位,而不是(如您所料)高 8 位。

每当您读取二进制文件格式时,您必须将文件格式的字节序显式转换为 CPU 的原生字节序。您可以使用以下函数来做到这一点:

/* CHAR_BIT == 8 assumed */
uint16_t le16_to_cpu(const uint8_t *buf)

   return ((uint16_t)buf[0]) | (((uint16_t)buf[1]) << 8);

uint16_t be16_to_cpu(const uint8_t *buf)

   return ((uint16_t)buf[1]) | (((uint16_t)buf[0]) << 8);

您将fread 放入适当大小的uint8_t 缓冲区,然后手动将所有数据字节复制到BMPHeader 结构中,并根据需要进行转换。看起来像这样:

/* note adjustments to type definition */
typedef struct BMPHeader

    uint8_t magic_number[2];
    uint32_t file_size;
    uint8_t reserved[4];
    uint32_t data_offset;
 BMPHeader;

/* in general this is _not_ equal to sizeof(BMPHeader) */
#define BMP_WIRE_HDR_LEN (2 + 4 + 4 + 4)

/* returns 0=success, -1=error */
int read_bmp_header(BMPHeader *hdr, FILE *fp)

    uint8_t buf[BMP_WIRE_HDR_LEN];

    if (fread(buf, 1, sizeof buf, fp) != sizeof buf)
        return -1;

    hdr->magic_number[0] = buf[0];
    hdr->magic_number[1] = buf[1];

    hdr->file_size = le32_to_cpu(buf+2);

    hdr->reserved[0] = buf[6];
    hdr->reserved[1] = buf[7];
    hdr->reserved[2] = buf[8];
    hdr->reserved[3] = buf[9];

    hdr->data_offset = le32_to_cpu(buf+10);

    return 0;

您确实假设CPU的字节顺序与文件格式的相同即使您知道现在它们是相同的;无论如何,您都要编写转换,以便将来您的代码无需修改即可在具有相反字节序的 CPU 上运行。

您可以通过使用固定宽度的&lt;stdint.h&gt; 类型、使用无符号类型(除非绝对需要能够表示负数)以及在字符时使用整数来让自己的生活更轻松数组会做。我在上面的例子中做了所有这些事情。你可以看到你不需要对幻数进行字节序转换,因为你唯一需要做的就是测试magic_number[0]=='B' &amp;&amp; magic_number[1]=='M'

相反方向的转换,顺便说一句,如下所示:

void cpu_to_le16(uint8_t *buf, uint16_t val)

   buf[0] = (val & 0x00FF);
   buf[1] = (val & 0xFF00) >> 8;

void cpu_to_be16(uint8_t *buf, uint16_t val)

   buf[0] = (val & 0xFF00) >> 8;
   buf[1] = (val & 0x00FF);

32 位/64 位量的转换留作练习。

【讨论】:

如果你要使用uint32_t file_size,字节序固定在LE,所以有理由不使用uint16_t magic_number 不,因为您不要将fread 直接插入BMPHeader 对象。您将fread 转换为uint8_t buf[sizeof(BMPHeader)],然后手动复制每个字段,并在适当时进行转换;因此使用两个字符的字符串作为幻数可以避免转换。此外,我认为将“幻数”视为两个字符的字符串更为自然(在这种情况下)。 @Zack 在这种情况下你将如何复制数据? 如果你不看magic_number看它是0x424D还是0x4D42,你怎么知道你需要转换LE->BE? @Gabe 你不会问这个问题。您总是将文件定义的字节序(在本例中为 LE)转换为CPU 想要的任何字节。你不需要知道 CPU 是什么字节序来进行转换——我的_to_cpu 函数无论如何都可以工作。【参考方案3】:

将结构写入文件是高度不可移植的——根本不尝试这样做是最安全的。只有在以下情况下才能保证使用这样的结构:a) 结构既作为结构写入又作为结构读取(从不是字节序列),并且 b) 它总是在同一(类型的)机器上写入和读取。不仅存在不同 CPU 的“字节序”问题(这似乎是您遇到的问题),还有“对齐”问题。不同的硬件实现对于仅将整数放置在偶数 2 字节甚至 4 字节甚至 8 字节边界上具有不同的规则。编译器完全了解这一切,并将隐藏的填充字节插入到您的结构中,因此它始终可以正常工作。但是由于隐藏的填充字节,假设结构的字节像你想象的那样布置在内存中是不安全的。如果你很幸运,你在一台使用 big-endian 字节顺序并且完全没有对齐限制的计算机上工作,所以你可以将结构直接放在文件上并让它工作。但你可能没那么幸运——当然,需要“可移植”到不同机器的程序必须避免尝试将结构直接放在任何文件的任何部分。

【讨论】:

感谢您分享您的知识。这是有道理的,如果我选择让它更便携,我会在未来更改代码。 Blender 3d 将其整个文件格式基于读取/写入文件结构,甚至管理指针、字节序和 32/64 位转换。它不是微不足道的,但我不会说 - “根本不要这样做” @ideasman42 我完全不同意。正确地读/写结构是非常重要的,并且很容易在特定于平台的微妙方式中出错(例如不能在机器之间共享文件)。编写与平台无关的手动读/写字段是微不足道的,而且很难出错,更不用说它可以在任何地方工作,也可以在任何地方工作。正确地读/写结构体并没有那么难,但毫无益处地肯定更难。 它在 Blender 中工作了 20 多年,提供非常快的文件 IO。不同意有“没有好处”,如果您有许多不同的结构(100 个或更多,随着软件的改进而变化),必须手动读/写需要一些努力来编写和维护.结构有一些限制(指针/双精度需要 8 个字节对齐,即使在 32 位系统上也是如此),但这可以检查并确保可移植。因此,尽管您确实有一点,但在实践中它可以很好地工作。对于单个文件头 - 同意它不值得这样做。

以上是关于为啥 fread 会弄乱我的字节顺序?的主要内容,如果未能解决你的问题,请参考以下文章

为啥 PHP 会弄乱我的 CSS?

为啥画布会弄乱我的图像颜色?

为啥浏览器的后退按钮会弄乱我的 Vue 组件?

为啥 Python 多处理队列会弄乱字典?

为啥 matplotlib.PatchCollection 会弄乱补丁的颜色?

字节顺序测试:为啥以下代码有效?