字节序学习:大端与小端

中年男人的浪漫 软路由+NAS,充电头。在给软路由部署alist(一个支持多存储的文件列表程序)的时候发现mips 是大端,需要专门下载mipsel小端变体,之前也了解到RISCV里也存在大小端的问题,我查看ELF程序往往是little endian。今天面试正好被问到了,我就来探究下这个问题。
虽然有自己的服务器,但是限制于服务器带宽和存储有限,软路由是局域网内好手,舒服的一批,我的软路由100多天没重启过,非常舒服。

大小端是什么chatgpt的例子给得很清晰

大小端 英文资料

一、使用Linux指令查看cpu的大小端

1、lscpu 命令

使用 lscpu 我们能看到本台机器CPU,Byte Order行会看到

我的路由器

2、错误的命令 cat /proc/cpuinfo

chatgpt告诉我可能fflags可能会看到这些信息

可是我的flags是如下这些,有些我是知道的,比如fpu就是浮点支持,pse就是页面大小扩,avx2就是向量指令集

其他的flags代码什么呢,有幸搜索到这样一篇文章,并给我提供了很好的思路,我们明明可以直接看linux kernel的代码的,比如 x86的 arch/x86/include/asm/cpufeatures.h ,mips arch/mips/include/asm/cpu-features.h 和x86都没有关于order的flags,翻阅了几个文件也没找到

3、readelf 查看一个可执行文件

readelf 可以查看ELF文件,在header中的Data会有大端还是小端的标记,直接readelf 一定存在的ls看看

二、通过C程序查看

1、使用 stdlib.h 中 的 宏

#include <stdio.h>
#include <stdlib.h>

int main()
{
    // 这两个宏是gcc或者clang支持的
    if(__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
        puts("little endian");
    else if(__BYTE_ORDER__== __ORDER_BIG_ENDIAN__)
        puts("big endian\n");
    else
        puts("unknown");
    return 0;
}

LLVM源码也用的这个

#if defined(BYTE_ORDER) && defined(BIG_ENDIAN) && BYTE_ORDER == BIG_ENDIAN
constexpr bool IsBigEndianHost = true;
#else
constexpr bool IsBigEndianHost = false;
#endif

2、使用 union 联合体

union 联合体内元素共用一块内存,我们可以设置一个char和int变量。大端和小端都指的是按字节的值,char 无论是大端和小端都一样,int的比如1一定不会相等
我本机的现象,Endian.half 的地址为 0x7fff5c09dbc4,half对应的是[0x7fff5c09dbc4, 0x7fff5c09dbc5) 这段内存,word 使用的是 [0x7fff5c09dbc4, 0x7fff5c09dbc8) 这段内存,我们修改 word 为1,如果是小端,也就是修改的小地址 [0x7fff5c09dbc4, 0x7fff5c09dbc5),所以相等就是小端。

#include <stdio.h>
void CheckEndian()
{
    union check
    {
        int  word;
        char half;
    } Endian;

    Endian.word=1;

    printf("%p\n", &Endian.half);
    printf("%p\n", &Endian.half+1);
    printf("%p\n", &Endian.word);
    printf("%p\n", &Endian.word+1);

    if(Endian.half == 1)
        puts("little endian");
    else
        puts("big endian\n");
}
int main()
{
    CheckEndian();
    return 0;
}

3、 在编译时检查字节序 错误

使用 constexpr 关键字特性, 这个编译后就知道结果了。代码来源,这个是错误的,不借助宏or运行不可能完成

#include <stdio.h>
#include <stdint.h>

/**
 * hl_endianness
 *
 * This enumeration can be placed into templated objects in order to generate
 * compile-time code based on a program's target endianness.
 *
 * The values placed in this enum are used just in case the need arises in
 * order to manually compare them against the number order in the
 * endianValues[] array.
 */
enum hl_endianness : uint32_t {
    HL_LITTLE_ENDIAN   = 0x03020100,
    HL_BIG_ENDIAN      = 0x00010203,
    HL_PDP_ENDIAN      = 0x01000302,
    HL_UNKNOWN_ENDIAN  = 0xFFFFFFFF
};

/**
 * A constant array used to determine a program's target endianness. The
 * values
 *  in this array can be compared against the values placed in the
 * hl_endianness enumeration.
 */
static constexpr uint8_t endianValues[4] = {0, 1, 2, 3};

/**
 * A simple function that can be used to help determine a program's endianness
 * at compile-time.
 */
constexpr hl_endianness getEndianOrder() {
    return
        (0x00 == endianValues[0])           // If Little Endian Byte Order,
            ? HL_LITTLE_ENDIAN              // return 0 for little endian.
            : (0x03 == endianValues[0])     // Else if Big Endian Byte Order,
                ? HL_BIG_ENDIAN             // return 1 for big endian.
                : (0x02 == endianValues[0]) // Else if PDP Endian Byte Order,
                    ? HL_PDP_ENDIAN         // return 2 for pdp endian.
                    : HL_UNKNOWN_ENDIAN;    // Else return -1 for wtf endian.
}

constexpr hl_endianness endianness = getEndianOrder();
/*
 * Test program
 */
int main() {

    switch (endianness) {
        case HL_LITTLE_ENDIAN: {
            puts("little endian");
            break;
        }
        case HL_BIG_ENDIAN: {
            puts("big endian");
            break;
        }
        case HL_PDP_ENDIAN: {
            puts("pdp endian");
            break;
        }
        case HL_UNKNOWN_ENDIAN:
        default: {
            puts("unknown");
            break;
        }
    }
}

我们可以使用-S 选项查看编译后的结果

只剩 little endian 的字符串了,编译器帮我们处理了这些东西

三、为什么要设计大端字节序

当然我们看到的大多数设备都是小端,他的操作更高效,比如计算、比较、向量化操作。

1、历史原因

大端字节序在早期计算机系统中较为常见,例如 Motorola 68000 系列处理器就使用了大端字节序。一些系统仍然沿用这种传统,以保持与过去的兼容性。

2、对人类友好

大端字节序将最高有效字节放在最前面,与我们书写习惯一致。如果是大端就可以看到00010203,小端看到的是03020100,还需要倒置

#include <stdio.h>

union str2int
{
    int  word;
    char half[4];
} Endian;

int main()
{
    Endian.half[0] = '\0';
    Endian.half[1] = '\1';
    Endian.half[2] = '\2';
    Endian.half[3] = '\3';
    printf("%08x\n", Endian.word);
    return 0;
}

3、网络协议

大多数网络协议都采用大端字节序,这主要是因为大端字节序可以避免字节序转换带来的性能损失。如果在传输过程中需要进行字节序转换,会增加CPU的工作负荷,降低系统性能。此外,像TCP/IP协议,网络数据包都是先传输头部信息,然后再传输数据。采用大端字节序可以使头部信息的数据大小和起始位置固定,方便处理。

4、硬件架构

某些硬件架构使用大端字节序,如果在这些架构上进行开发,使用大端字节序可以更好地匹配硬件的存储方式,提高效率。

5、数据校验

chatgpt如是说,在某些情况下,使用大端字节序可以使数据校验更加简单,因为校验和的计算可能依赖于数据的排列顺序。我认为就是3网络协议带来的

posted @ 2023-08-16 23:07  暴力都不会的蒟蒻  阅读(281)  评论(2编辑  收藏  举报