大端和小端差异的位运算

Bitwise operation on big endian and little endian differences

我正在为 IP 地址 space 排序 "children" 个前缀。例如8.8.8.0/24就是IP地址space中8.8.8.0/23的child。我很困惑为什么以下两个操作在我的 x86 little endian 系统上提供不同的结果

一些背景信息: /24 表示 32 位 IPv4 地址的前 24 位是 "defined"。这意味着 8.8.8.0/24 包含 8.8.8.0 - 8.8.8.255。同样,对于未定义的每一位,地址数量 space 都会加倍。 8.8.8.0/23 只定义了前 23 位,因此实际地址 space 从 8.8.8.0 到 8.8.9.255,或者是 /24 大小的两倍。

现在我遇到的困惑是以下移位

inet_addr("8.8.8.0") << (32 - 23) produces 269488128
inet_addr("8.8.9.0") << (32 - 23) produces 303042560

inet_addr 产生大端数。但是,将其转换为小端时 -

htonl(inet_addr("8.8.8.0")) >> 9 produces 263172
htonl(inet_addr("8.8.9.0")) >> 9 produces 263172

这是预期的结果。删除最后 9 位意味着 8.8.9.0 在理论上等于 8.8.8.0。

我在这里错过了什么?它不应该同样适用于大端吗?

编辑:不是重复的,因为我确实了解字节顺序如何影响数字存储方式的差异,但我显然遗漏了这些按位运算符的某些内容。这个问题更多地与按位有关,而不是字节序——字节序只是为了培养一个例子

x86 是小端。 little endian中二进制的数字1是

|10000000|00000000|00000000|00000000

如果将其向左移动 9 位,它将变为...

|00000000|01000000|00000000|00000000

在小端机器中 0xDEADBEEF 打印为一系列从低地址到高地址的字节实际上会打印 EFBEADDE,参见

https://www.codeproject.com/Articles/4804/Basic-concepts-on-Endianness

https://www.gnu-pascal.de/gpc/Endianness.html.

大多数人在用二进制思考时认为数字 1 表示如下(包括我),有些人 think 这是大端,但不是...

|00000000|00000000|00000000|00000001

在下面的代码中,我在小端打印了 0xDEADBEEF,因为我的机器是 x86 并且我使用 htonl 函数将其转换为网络字节顺序。注意网络字节顺序定义为 Big Endian。

因此,当我打印出 1 的大端值时,即 htonl(1)1 的大端表示是

|00000000|00000000|00000000|10000000

试试这个代码

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <arpa/inet.h>

void print_deadbeef(void *p, size_t bytes) {
  size_t i = 0;
  for (i = 0; i < bytes; ++i) {
    printf("%02X", ((unsigned char*)p)[i]);
  }
  printf("\n");
}

void print_bin(uint64_t num, size_t bytes) {
  int i = 0;
  for(i = bytes * 8; i > 0; i--) {
    (i % 8 == 0) ? printf("|") : 1;
    (num & 1)    ? printf("1") : printf("0");
    num >>= 1;
  }
  printf("\n");
}

int main(void) {
  in_addr_t left    = inet_addr("8.8.8.0");
  in_addr_t right   = inet_addr("8.8.9.0");
  in_addr_t left_h    = htonl(left);
  in_addr_t right_h   = htonl(right);
  in_addr_t left_s  = left  << 9;
  in_addr_t right_s = right >> 9;
  assert(left  != right);

  printf("left != right\n");
  print_bin(left, 4);
  print_bin(right, 4);
  printf("Big Endian if on x86\n");
  print_bin(left_s, 4);
  print_bin(right_s, 4);
  printf("Little Endian if on x86\n");
  print_bin(left_h, 4);
  print_bin(right_h, 4);

  printf("\n\nSome notes\n\n");

  printf("0xDEADBEEF printed on a little endian machine\n");
  uint32_t deadbeef = 0xDEADBEEF;
  print_deadbeef(&deadbeef, 4);

  uint32_t deadbeefBig = htonl(deadbeef);
  printf("\n0xDEADBEEF printed in network byte order (big endian)\n");
  print_deadbeef(&deadbeefBig, 4);

  printf("\n1 printed on a little endian machine\n");
  print_bin(1, 4);
  printf("\nhtonl(1) ie network byte order (big endian) on a little endian machine\n");
  print_bin(htonl(1), 4);

  return 0;
}

这是输出

left != right
|00010000|00010000|00010000|00000000
|00010000|00010000|10010000|00000000
Big Endian if on x86
|00000000|00001000|00001000|00001000
|00100001|00100000|00000000|00000000
Little Endian if on x86
|00000000|00010000|00010000|00010000
|00000000|10010000|00010000|00010000


Some notes

0xDEADBEEF printed on a little endian machine
EFBEADDE

0xDEADBEEF printed in network byte order (big endian)
DEADBEEF

1 printed on a little endian machine
|10000000|00000000|00000000|00000000

htonl(1) ie network byte order on a little endian machine
|00000000|00000000|00000000|10000000

Big Endian 和 Little Endian 的问题并不是机器真正知道的。

C 中的类型不包含此类信息,因为它是硬件问题,与类型无关。

机器假定所有多字节数字都根据其本地字节序排序(在 x86 上,这通常是小字节序)。

因此,总是使用本地字节序假设执行位移位。

您无法在 Little Endian 机器上正确地对 Big Endian 数字应用位移。

即使在小端机上将大端数打印到屏幕上,也无法得到有趣的结果。

这就是为什么@Harry 的回答很酷,它打印了每一位,绕过了这个问题。

维基百科 article about Endianness 有更多详细信息。

需要注意的是,字节顺序实际上是指机器在内存中存储字节的方式。

例如,如果数字是字符串,字节序指的是这样的问题:哪个"letter"(字节)先到?

有些机器会存储 "Hello",有些机器会存储 "olleH"(仅对于数字,在实际字符串中,字节总是正确排序)。

请注意,尽管字节的顺序是颠倒的,但每个字节的所有位都以相同的方式排序,因此每个字节都保留其值。

当发生位移时,它总是根据机器的字节排序系统发生,因为这就是 CPU 和内存存储的设计方式。