问题 为什么fread会弄乱我的字节顺序?


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

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; 我正在将十六进制数据加载到struct中 fread(&header,14,1,fileIn);

我的问题是神奇数字应该在哪里 0x424d //'BM' fread()它会翻转字节 0x4d42 // 'MB'

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

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


2375
2017-12-19 03:53


起源

......咬你的咬口令面糊吗?你尝试过啃? - Mehrdad
不是那样的 fread 代替 bread 你的头衔? - buruzaemon
抱歉。我仍然需要使用狮子会自动纠正。我修好了它 - Chase Walden
字节序..... - Mitch Wheat


答案:


这不是故障 fread,但你的CPU,(显然)是小端的。也就是说,你的CPU处理a中的第一个字节 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上工作。

您可以使用固定宽度让自己的生活更轻松 <stdint.h> 类型,通过使用无符号类型,除非能够表示负数是绝对必需的,并且  在字符数组执行时使用整数。我在上面的例子中完成了所有这些事情。你可以看到你不需要麻烦地使用endian转换幻数,因为你唯一需要做的就是测试 magic_number[0]=='B' && 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位数量作为练习。


14
2017-12-19 04:09



如果你打算用 uint32_t file_size,字节顺序固定在LE,因此有理由不使用 uint16_t magic_number。 - Gabe
不,因为你 别 fread 直接进入BMPHeader对象。您 fread 成 uint8_t buf[sizeof(BMPHeader)] 然后你手动复制每个字段,适当时进行转换;因此,使用两个字符的字符串作为幻数避免转换。此外,我认为将“神奇数字”视为双字符串更为自然(在这种情况下)。 - zwol
@Zack在这种情况下如何复制数据? - Chase Walden
你怎么知道的 需要 转换LE-> BE,如果你不看 magic_number 看看是不是 0x424D 要么 0x4D42? - Gabe
@Gabe你不问这个问题。您 总是 从文件的定义的字节顺序(在这种情况下为LE)转换为 无论CPU想要什么。您不需要知道CPU进行转换的字节顺序 - 我的 _to_cpu 功能无论如何都可以。 - zwol


我认为这是一个endian问题。即您正在放置字节 42 和 4D 进入你的 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 有两个字节在同一个 word 如 magic_number 和下一个两个字节 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的结构是不合适的。相反,您需要单独阅读每个元素。


2
2017-12-19 04:07



对不起,太早点击保存。现在都在那里 - Sodved
+1用于演示,尽管将little-endian假设显式化是很好的。 - zwol
这只会影响一个 short?我问的只是因为我的文件大小是3而不是196662 - Chase Walden
不,它会影响大于1个字节的所有整数类型,所以 short, int, long,和 long long。如果您使用我的代码作为调试的基础,您可能需要删除/更改 h 中的人物 printf 格式。 h 是短裤, hh 用于unsigned char。检查 man 3 printf 详情。 - Sodved
@Sodved我没用过 h 字符。我仍然遇到file_size的问题 - Chase Walden


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


0
2017-08-31 02:42



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