作者:高玉涵
时间:2022.2.11 14:30 (福虎年十一)
博客:blog.youkuaiyun.com/cg_i
平台:HP-UX、LINUX
序言
接触 UNIX 程序移值到 Linux 项目,已 11 个多月了,经过大家的努力,初步实现了目标,但接下来的日子,才是完成总体目标的关键,这期间确实有很多感触。作为一个一直都很热爱“底层”技术的开发人员,我即庆幸能接触这个项目,也感慨于有如此多的未知、茫然,无处下手,不过这些状况随着项目的发展,已逐步得到改善。
项目移植中存在平台依赖的结构。以往只在书中看到的东西,现在确亲身经历,真正印证了“纸上得来终觉浅,唯有实践出真知”这句话。在这里,当基于 RISC 结构的 UNIX 平台移植到基于 X86 结构的平台上时,应用程序代码对字节序的依赖,例如,为了计算或数据操作而使用了字节交换的应用程序。移植使用了字节交换逻辑的代码到 Intel 机器(Little Endian,小端体系结构)上时,需要按照小端(Little Endian)模式修改代码。在没有正确修改字节交换逻辑的情况下,调试问题就变得非常麻烦,因为很难找到数据在哪里出了问题,这主要是因为问题的根源通常是发生问题的代码之前很远的地方。
我移植的项目,是核心业务系统,处理起来较为棘手,一度给移植过程带来很大麻烦,经过对代码移植前后的区别制定了标准,并做相应修改。
我在项目移植过程中,每遇到一个问题,基本上都会记录下来,想着给需要的人提供参考,虽然您不一定能遇到,但是移植思想和方法都是相通的,只是具体的内容不同而已。毕竟我是乐于分享的。
为初学者解释什么是平台依赖结构
我曾有一段时间痴迷于学习网络编程,我并不是特别聪明或理解力特别强的人,所以花费大量时间学习属于程序员必修课的操作系统和算法。对我而言,学习计算机理论是不小的负担。为了能够通俗易懂的说明,同时符合我的水平,我将摘出网络编程中使用的部分技术来举例,它们都很小巧,理解起来不会成为你的负担。至于为何要用网络技术来举例,是因为网络互通就是解决平台依赖最经典的成功案例。
网络字节与地址变换
不同 CPU 中,4 字节整数型值 1 在内在空间的保存方式是不同的。4 字节整数型值 1 可用 2 进制表示如下。
00000000 00000000 00000000 00000001
有些 CPU 以这种顺序保存到内在,另外一些 CPU 则以倒序保存。
00000001 00000000 00000000 00000000
若不考虑这些收发数据则会发生问题,因为保存顺序的不同意味着对接收数据的解析顺序也不同。
字节序(Order)与网络字节序
CPU 向内在保存数据的方式有 2 种,这意味着 CPU 解析数据的方式也分为 2 种。
- 大端序(Big Endian):高位字节存放到低位地址。
- 小端序(Little Endian):高位字节存放到高位地址。
仅凭描述很难解释清楚,下面通过示例进行说明。假设在 0x20 号开始的地址中保存 4 字节 int 类型数 0x12345678 。大端序 CPU 保存方式如图 1-1 所示。
整数 0x12345678 中,0x12 是高位字节,0x78 是低位字节。因此,大端序中先保存最高位字节 0x12(最高位字节 0x12 存放到低位地址)。小端序保存方式如图 1-2 所示。
先保存的是最低字节 0x78 。从以上分析可以看出,每种 CPU 的数据保存方式均不同。因此,代表 CPU 数据保存方式的主机字节序(Host Byte Order)在不同 CPU 中也各不相同。目前主流的 Intel 系列 CPU 以小端方式保存数据。接下来分析 2 台字节序不同的计算机之间数据传递过程中可能出现的问题,如图 1-3 所示。
0x12 和 0x34 构成的大端序系统值与 0x34 和 0x12 构成的小端序系统值相同。换言之,只有改变数据保存顺序才能被识别为同一值。图 1-3 中,大端序系统传输数据 0x1234 时未考虑字节序问题,而直接以 0x12、0x34 的顺序发送。结果接收端以小端序保存数据,因此小端序接收的数据变成 0x3412,而非 0x1234 。正因如此,在通过网络传输数据时约定统一方式,这种约定称为网络字节序(Network Byte Order),非常简单——统一为大端序。
即,先把数据数组转化成大端序格式再进行网络传输。因此,所有计算机接收数据时应识别该数据是网络字节序格式,小端序系统传输时应转化为大端序排列方式。
字节转换(Endian Conversions)
接下来介绍帮助转换字节序的函数。
- unsigned short htons(unsigned short);
- unsigned short ntohs(unsigned short);
- unsigned long htonl(unsigned long);
- unsigned long ntohl(unsigned long);
通过函数名应该能掌握其功能,只需了解以下细节。
- htons 中的 h 代表主机(host)字节序。
- ntohs 中的 n 代表网络(network)字节序。
另外,s 指的是 short,l 指的是 long(Linux 中 long 类型占用 4 个字节,这很关键)。因此,htons 是 h、to、n、s 的组合,也可以解释为“把 short 型数据从主机字节序转化为网络字节序“。
再举个例子,ntohs 可以解释为“把 short 型数据从主机字节序转化为网络字节序“。
通常,以 s 作为后缀的函数中,s 代表 2 个字节 short,因此用于端口号转换;以 l 作为后缀的函数中,l 代表 4 个字节,因此用于 IP 地址转换。另外,有些读者可能有如下凝问:
“我的系统是大端序的,就不需要转换字节序了吧?“
这么说也不能算错。但我认为,有必要编写与大端序无关的统一代码。这样,即使在大端序系统中,最好也经过主机字节序转换为网络字节序的过程。当然,此时主机字节序与网络字节序相同,不会有任何变化。
#include <stdio.h>
#include <arpa/inet.h>
int main(int argc, char *argv[])
{
unsigned short host_port = 0x1234;
unsigned short net_port;
unsigned long host_addr = 0x12345678;
unsigned long net_addr;
net_port = htons(host_port);
net_addr = htonl(host_addr);
printf("Host ordered port: %#x \n", host_port);
printf("Network ordered port: %#x \n", net_port);
printf("Host ordered address: %#lx \n", host_addr);
printf("Network ordered address: %#lx \n", net_addr);
return 0;
}
运行结果:endian_conv.c
gcc endian_conv.c -o conv
./conv
Host ordered port: 0x1234
Network ordered port: 0x3412
Host ordered address: 0x12345678
Network ordered address: 0x78563412
这就是在小端序 CPU 中运行的结果。如果在大端序 CPU 中运行,则变量不会改变大部分朋友都会得到类式的运行结果,因为 Intel 和 AMD 系列的 CPU 都采用小端序标准。