面试官:你能用Go写段代码判断当前系统的存储方式吗?

2022-07-08 16:50:24 浏览数 (1)

前言

哈喽,大家好,我是asong。今天想与大家聊一聊计算机硬件中的两种储存数据的方式:大端字节序(big endian)、小端字节序(little endian)。老实说,我第一次知道这个概念还是在学习单片机的时候,不过当时学完就忘了,真正长记性是在面试的时候,面试官问我:你能用C语言写段代码判断机器的字节序吗?你一定好奇为什么要用C语言写,傻瓜,这是我大学的时候面试嵌入式岗位呀。扯远啦,其实当时的我是懵逼的,早就忘了什么大端、小端了,所以遗憾的错过嵌入式行业,进入了互联网行业(手动狗头)。

本文的所有代码已经上传github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/endian_demo;

为什么有大小端之分

我一直都不理解,为什么要有大小端区分,尤其是小端,总是会忘记,因为他不符合人类的思维习惯,但存在即为合理,存在就有他存在的价值。这里有一个比较合理的解释:计算机中电路优先处理低位字节,效率比较高,因为计算机都是从低位开始的,所以计算机内部处理都是小端字节序。但是我们平常读写数值的方法,习惯用大端字节序,所以除了计算机的内部,其他场景大都是大端字节序,比如:网络传输和文件储存时都是用的大端字节序。

所以大小端问题很可能与硬件或者软件的创造者们有关,实际在计算机工业应用上,不同的操作系统和不同的芯片类型都有所不同。不同的系统设计不同,所以我们也没必要深究为什么要有这个区分,只需要知道他们的原理就好了。

什么是大端、小端

大端模式:高位字节排放在内存的低地址端,低位字节排放在内存的高地址端;

小端模式:低位字节排放在内存的低地址端,高位字节排放在内存的高地址端;

这么说也有点模糊,还是配个图来看更清晰:

我们来看一看数值0x1A2B3C4D在大端与小端的表现形式,这里我们假设地址是从0x4000开始:

上图所示:大端和小端的字节序最小单位是1字节(8bit),大端字节序就和我们平时的写法顺序一样,从低地址到高地址写入0x1A2B3C4D,而小端字节序就是我们平时的写法反过来,因为字节序最小单位为1字节,所以从低地址到高地址写入0x4D3C2B1A

因为大端、小端很容易混记,所以分享一个我自己记忆的小技巧:

大端:高低高低,也就是高位字节排放在内存低地址端,高地址端存在低位字节;

小端:高高低低;也就是高位字节排放在内存的高地址端,低位字节排放在内存的低地址端;

如何使用Go区分大小端

计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读取字节,先读取第一个字节,再读取第二个字节,所以说我就可以根据这个特性来读判断大小端。

在使用Go语言实现之前,还是想再用C语言实现一遍,因为这是我一生的痛,毕竟在面试的时候没写出来。

可以利用C语言中union各字段共享内存的特性,union型数据所占的空间等于其最大的成员所占的空间,对 union 型的成员的存取都是相对于该联合体基地址的偏移量为 0 处开始,也就是联合体的访问不论对哪个变量的存取都是从 union 的首地址位置开始联合是一个在同一个存储空间里存储不同类型数据的数据类型。这些存储区的地址都是一样的,联合里不同存储区的内存是重叠的,修改了任何一个其他的会受影响。所以我们可写出代码如下:

代码语言:javascript复制
#include "stdio.h"


// big_endian: 1 
// little_endian: 2
int IsLittleEndian() {
    union {
        short value;
        char array[2];
    } u;
    u.value = 0x0102;
    if (u.array[0] == 1 && u.array[1] == 2){
        return 1;
    }else if (u.array[0] == 2 && u.array[1] == 1){
        return 2;
    }
    return -1;
}

int main() {
    
    int res;
    res = IsLittleEndian();
    printf("result is %dn",res);
    if (res == 1) {
        printf("it is big endian");
    }
    if (res == 2){
        printf("it is little endian");
    }
    return 0;
}

// 运行结果(不同系统运行结果会有不同)
result is 2
it is little endian% 

现在我们来思考一下,怎么用Go语言验证大小端,Go中是没有union这个关键字,那就要另辟蹊径,换一个方法来实现啦,我们可以通过将int32类型(4字节)强制转换成byte类型(单字节),判断起始存储位置内容来实现,因为Go不支持强制类型转换,我们可以借助unsafe包达到我们的要求,写出代码如下:

代码语言:javascript复制
package main

import (
 "fmt"
 "unsafe"
)

func IsLittleEndian()  bool{
 var value int32 = 1 // 占4byte 转换成16进制 0x00 00 00 01 
  // 大端(16进制):00 00 00 01
  // 小端(16进制):01 00 00 00
 pointer := unsafe.Pointer(&value)
 pb := (*byte)(pointer)
 if *pb != 1{
  return false
 }
 return true
}

func main()  {
 fmt.Println(IsLittleEndian())
}
// 运行结果:ture

大小端字节序转化

这里大家可能会有疑惑,为什么要有大小端转化,这是因为在涉及到网络传输、文件存储时,因为不同系统的大小端字节序不同,这是就需要大小端转化,才能保证读取到的数据是正确的。我在大学时做armdsp通信的时候,就遇到个大小端转换的问题,因为arm是小端,dsp是大端,所以在不了解这个知识点的时候,通信的数据就是乱的,导致我调试了好久。

大小端的转换其实还算比较简单,通过位操作就可以实现,这里我们用uint32类型作为例子:

代码语言:javascript复制
func SwapEndianUin32(val uint32)  uint32{
 return (val & 0xff000000) >> 24 | (val & 0x00ff0000) >> 8 |
  (val & 0x0000ff00) << 8 | (val & 0x000000ff) <<24
}

是的,你没看错,就是这么简单,这里也很简单,就不细讲了。

其实go官方库encoding/binary中已经提供了大小端使用的库,我们要想进行大小端转换,完全可以使用官方库,没必要自己造轮子。我们看一下这个库怎么使用:

代码语言:javascript复制
// use encoding/binary
// bigEndian littleEndian
func BigEndianAndLittleEndianByLibrary()  {
 var value uint32 = 10
 by := make([]byte,4)
 binary.BigEndian.PutUint32(by,value)
 fmt.Println("转换成大端后 ",by)
 fmt.Println("使用大端字节序输出结果:",binary.BigEndian.Uint32(by))
 little := binary.LittleEndian.Uint32(by)
 fmt.Println("大端字节序使用小端输出结果:",little)
}
// 结果:
转换成大端后  [0 0 0 10]
使用大端字节序输出结果: 10
大端字节序使用小端输出结果: 167772160

grpc中对大端的应用

大家对gRPC一定很熟悉,最近在看gRPC源码时,看到gRPC封装message时,在封装header时,特意指定了使用大端字节序,源码如下:

代码语言:javascript复制
// msgHeader returns a 5-byte header for the message being transmitted and the
// payload, which is compData if non-nil or data otherwise.
func msgHeader(data, compData []byte) (hdr []byte, payload []byte) {
 hdr = make([]byte, headerLen)
 if compData != nil {
  hdr[0] = byte(compressionMade)
  data = compData
 } else {
  hdr[0] = byte(compressionNone)
 }

 // Write length of payload into buf
 binary.BigEndian.PutUint32(hdr[payloadLen:], uint32(len(data)))
 return hdr, data
}

结尾

在本文的最后我们再来做一下总结:

  • 大端小端是不同的字节顺序存储方式,统称为字节序
  • 大端:是指数据的高字节位 保存在 内存的低地址中,而数据的低字节位 保存在 内存的高地址中。这样的存储模式有点儿类似于把数据当作字符串顺序处理:地址由小向大增加,而数据从高位往低位放。和我们”从左到右“阅读习惯一致。
  • 小端:是指数据的高字节位 保存在 内存的高地址中,而数据的低字节位 保存在 内存的低地址中。这种存储模式将地址的高低和数据位权有效地结合起来,高地址部分权值高,低地址部分权值低,和我们的逻辑方法一致
  • 区分:计算机处理字节序的时候,不知道什么是高位字节,什么是低位字节。它只知道按顺序读区字节,先读取第一个字节,再读取第二个字节,所以说我就可以根据这个特性来读判断大小端。
  • 转换:通过位操作就可以实现,具体可以使用标准库encoding/binary

本文的所有代码已经上传github:https://github.com/asong2020/Golang_Dream/tree/master/code_demo/endian_demo;

0 人点赞