「深入浅出」Linux ntohl函数使用方法详解 (linux ntohl函数)

深入浅出:Linux ntohl函数使用方法详解

网络字节序和主机字节序的转换是网络编程中不可避免的问题。ntohl函数是Linux中处理字节序转换的重要函数之一。本文将深入浅出地讲解ntohl函数的使用方法。

一、关于字节序

在计算机系统中,数据类型是以数值形式存储在内存中的。然而,在网络传输中,数据需要以二进制流的形式进行传输。由于不同的系统和处理器所使用的字节序(大端序或小端序)不同,为了确保数据在网络传输过程中能够被正确地读取,需要进行字节序的转换。

网络字节序是一种特定的字节序,用于在网络中传输数据。网络字节序采用大端字节序,在其中,高位字节存储在低地址中,低位字节存储在高地址中。

主机字节序则根据处理器的不同,有可能采用大端字节序,也有可能采用小端字节序。在大端字节序中,高位字节存储在低地址,低位字节存储在高地址;而在小端字节序中,则是恰恰相反。

例如,对于十六进制数0x12345678,在不同的字节序中所对应的内存存储方式如下:

大端字节序:0x12 0x34 0x56 0x78

小端字节序:0x78 0x56 0x34 0x12

在网络编程中,需要使用一些函数将主机字节序转换成网络字节序,或者将网络字节序转换成主机字节序,这就是ntohl函数发挥作用的时候了。

二、ntohl函数的介绍

ntohl函数的作用就是将网络字节序转换成主机字节序。ntohl函数在头文件中被定义,它的原型如下:

unsigned long int ntohl (unsigned long int x);

该函数将一个网络字节序的32位整数x转换成一个主机字节序的32位整数。函数返回一个主机字节序的32位整数。

在ntohl函数的实现中,将x强转成了unsigned char*类型,并按照网络字节序的顺序将4个字节的数据写入一个32位整数中,然后将其返回。

在ntohl函数中,大端字节序和小端字节序的数据处理方法是完全一致的,因此ntohl函数的使用与系统的字节序无关。

三、ntohl函数的使用

下面的代码片段演示了如何使用ntohl函数将网络字节序的IP地址转换成字符串:

“`

#include

#include

#include

#include

int mn() {

uint32_t ip = 0x7f000001;

uint8_t *p = (uint8_t*) &ip;

struct in_addr ip_addr;

memcpy(&ip_addr, &ip, sizeof(ip_addr));

char ip_str[INET_ADDRSTRLEN];

memset(ip_str, ‘\0’, INET_ADDRSTRLEN);

inet_ntop(AF_INET, &ip_addr, ip_str, INET_ADDRSTRLEN);

printf(“The IP address is: %s\n”, ip_str);

printf(“The IP address in host byte order is: %lu\n”, (unsigned long) ntohl(ip));

return 0;

}

“`

在这个代码片段中,我们定义了一个32位的IP地址ip,这个IP地址是在网络字节序下存储的。我们使用了ntohl函数将它转化为主机字节序,并输出了它的十进制形式。

需要注意的是,在使用ntohl函数时,需要将网络字节序的数据以某种形式存储在内存中,才能对其进行处理。例如在上述代码片段中,我们将数字变量ip强制转换为一个unsigned char指针,并使用memcpy函数将其值复制到了一个struct in_addr类型的变量中,以便于inet_ntop函数将其转化为字符串形式。

四、

相关问题拓展阅读:

大端的计算机用语

端模式(Endian)的这个词出自JonathanSwift书写的《格列佛游记》。这本书根据将鸡蛋敲开的方法不同将所有的人分为两类,从圆头开始将鸡蛋敲开的人被归为BigEndian,从尖头开始将鸡蛋敲开的人被归为LittileEndian。小人国的内战就源于吃鸡蛋时是究竟从大头(Big-Endian)敲开还是从小头(Little-Endian)敲开。在计算机业BigEndian和LittleEndian也几乎引起一场战争。在计算机业界,Endian表示数据在存储器中的存放顺序。下文举例说明在计算机中大小端模式的区别。

如果将一个32位的整数0x存放到一个整型变量(int)中,这个整型变量采用大端或者小端模式在内存中的存储由下表所示。为简单起见,本书使用OP0表示一个32位数据的更高字节MSB(Most Significant Byte),使用OP3表示一个32位数据更低字节LSB(Least Significant Byte)。 ;地址偏移 ;大端模式 ;小端模式 0x00 12(OP0) 78(OP3) 0x01 34(OP1) 56(OP2) 0x02 56(OP2) 34(OP1) 0x03 78(OP3) 12(OP0) 如果将一个16位的整数0x1234存放到一个短整型变量(short)中。这个短整型变量在内存中的存储在大小端模式由下表所示。 ;地址偏移 ;大端模式 ;小端模式 0x00 12(OP0) 34(OP1) 0x01 34(OP1) 12(OP0) 由上表所知,采用大小模式对数据进行存放的主要区别在于在存放的字节顺序,大端方式将高位存放在低地址,小端方式将高位存放在高地址。采用大端方式进行数据存放符合人类的正常思维,而采用小端方式进行数据存放利于计算机处理。到目前为止,采用大端或者小端进行数据存放,其孰优孰劣也没有定论。

有的处理器系统采用了小端方式进行数据存放,如Intel的奔腾。有的处理器系统采用了大端方式进行数据存放,如IBM半导体和Freescale的PowerPC处理器。不仅对于处理器,一些外设的设计中也存在着使用大端或者小端进行数据存放的选择。

因此在一个处理器系统中,有可能存在大端和小端模式同时存在的现象。这一现象为系统的软硬件设计带来了不小的麻烦,这要求系统设计工程师,必须深入理解大端和小端模式的差别。大端与小端模式的差别体现在一个处理器的寄存器,指令集,系统总线等各个层次中。 从软件的角度上,不同端模式的处理器进行数据传递时必须要考虑端模式的不同。如进行网络数据传递时,必须要考虑端模式的转换。有过Socket接口编程经验的程序员一定使用过以下几个函数用于大小端字节序的转换。

¨ #define ntohs(n) //16位数据类型网络字节顺序到主机字节顺序的转换

¨ #define htons(n) //16位数据类型主机字节顺序到网络字节顺序的转换

¨ #define ntohl(n) //32位数据类型网络字节顺序到主机字节顺序的转换

¨ #define htonl(n) //32位数据类型主机字节顺序到网络字节顺序的转换

其中互联网使用的网络字节顺序采用大端模式进行编址,而主机字节顺序根据处理器的不同而不同,如PowerPC处理器使用大端模式,而Pentuim处理器使用小端模式。

大端模式处理器的字节序到网络字节序不需要转换,此时ntohs(n)=n,ntohl =n;而小端模式处理器的字节序到网络字节必须要进行转换,此时ntohs(n) = __swab16(n),ntohl =__swab32(n)。__swab16与__swab32函数定义如下所示。 #define ___swab16(x)

{

__u16 __x = (x);

((__u16)(

(((__u16)(__x) & (__u16)0x00ffU) > 8)));

}

#define ___swab32(x)

{

__u32 __x = (x);

((__u32)(

(((__u32)(__x) & (__u32)0x000000ffUL) > 8) |

(((__u32)(__x) & (__u32)0xff000000UL) >> 24)));

} PowerPC处理器提供了lwbrx,lhbrx,stwbrx,sthbrx四条指令用于处理字节序的转换以优化__swab16和__swap32这类函数。此外PowerPC处理器中的rlwimi指令也可以用来实现__swab16和__swap32这类函数。在LinuxPowerPC中,定义了一系列有关字节序转换的函数,其详细定义在./include/a-powerpc/byteorder.h文件中。

程序员在对普通文件进行处理也需要考虑端模式问题。在大端模式的处理器下对文件的32,16位读写操作所得到的结果与小端模式的处理器不同。读者单纯从软件的角度理解上远远不能真正理解大小端模式的区别。事实上,真正的理解大小端模式的区别,必须要从系统的角度,从指令集,寄存器和数据总线上深入理解,大小端模式的区别。 除了4.2.1节中,软件上对不同端模式编程上的差异,处理器在硬件上也由于端模式问题在设计中有所不同。从系统的角度上看,端模式问题对软件和硬件的设计带来了不同的影响,当一个处理器系统中大小端模式同时存在时,必须要对这些不同端模式的访问进行特殊的处理。

PowerPC处理器主导网络市场,可以说绝大多数的通信设备都使用PowerPC处理器进行协议处理和其他控制信息的处理,这也可能也是在网络上的绝大多数协议都采用大端编址方式的原因。因此在有关网络协议的软件设计中,使用小端方式的处理器需要在软件中处理端模式的转变。而Pentium主导个人机市场,因此多数用于个人机的外设都采用小端模式,包括一些在网络设备中使用的PCI总线,Flash等设备,这也要求硬件工程师在硬件设计中注意端模式的转换。

本书中的小端外设是指这种外设中的寄存器以小端方式进行存储,如PCI设备的配置空间,NOR FLASH中的寄存器等等。

对于有些设备,如DDR颗粒,没有以小端方式存储的寄存器,因此从逻辑上讲并不需要对端模式进行转换。在设计中,只需要将双方数据总线进行一一对应的互连,而不需要进行数据总线的转换。

如果从实际应用的角度说,采用小端模式的处理器需要在软件中处理端模式的转换,因为采用小端模式的处理器在与小端外设互连时,不需要任何转换。

而采用大端模式的处理器需要在硬件设计时处理端模式的转换。大端模式处理器需要在寄存器,指令集,数据总线及数据总线与小端外设的连接等等多个方面进行处理,以解决与小端外设连接时的端模式转换问题。

在寄存器和数据总线的位序定义上,基于大小端模式的处理器有所不同。

一个采用大端模式的32位处理器,如基于E500内核的MPC8541,将其寄存器的更高位m(mostsignificant bit)定义为0,更低位l(lease significantbit)定义为31;而小端模式的32位处理器,将其寄存器的更高位定义为31,低位地址定义为0。

与此向对应,采用大端模式的32位处理器数据总线的更高位为0,更高位为31;采用小端模式的32位处理器的数据总线的更高位为31,更低位为0。如图4.5所示。 OP0 OP1 OP2 OP3 OP0 OP1 OP2 OP0 ;图4.5大小端模式处理器的寄存器的定义 ;大端模式处理器寄存器位序定义 ;小端模式处理器寄存器位序定义 大小端模式处理器外部总线的位序也遵循着同样的规律,根据所采用的数据总线是32位,16位和8位,大小端处理器外部总线的位序有所不同。

¨ 大端模式下32位数据总线的m是第0位,MSB是数据总线的第0~7的字段;而l是第31位,LSB是第24~31字段。小端模式下32位总线的m是第31位,MSB是数据总线的第31~24位,l是第0位,LSB是7~0字段。

¨ 大端模式下16位数据总线的m是第0位,MSB是数据总线的第0~7的字段;而l是第15位,LSB是第8~15字段。小端模式下16位总线的m是第15位,MSB是数据总线的第15~7位,l是第0位,LSB是7~0字段。

¨ 大端模式下8位数据总线的m是第0位,MSB是数据总线的第0~7的字段;而l是第7位,LSB是第0~7字段。小端模式下8位总线的m是第7位,MSB是数据总线的第7~0位,l是第0位,LSB是7~0字段。

由上分析,我们可以得知对于8位,16位和32位宽度的数据总线,采用大端模式时数据总线的m和MSB的位置都不会发生变化,而采用小端模式时数据总线的l和LSB位置也不会发生变化。

为此,大端模式的处理器对8位,16位和32位的内存访问(包括外设的访问)一般都包含第0~7字段,即MSB。小端模式的处理器对8位,16位和32位的内存访问都包含第7~0位,小端方式的第7~0字段,即LSB。

由于大小端处理器的数据总线其8位,16位和32位宽度的数据总线的定义不同,因此需要分别进行讨论在系统级别上如何处理端模式转换。

在一个大端处理器系统中,需要处理大端处理器对小端外设的访问。

1.1.2.1 大端处理器对32位小端外设进行访问大端处理器采用32位总线与小端外设进行访问时,大端处理器的32位数据总线的第0~7位用来处理OP0,第8~15位用来处理OP1,第16~23位用来处理OP2,第24~31位用来处理OP3。而32位的小端设备使用数据总线的第31~24位用来处理OP0,第23~16位用来处理OP1,第15~8位用来处理OP2,第7~0位用来处理OP3。

大端处理器,如MPC8541,使用stw,sth,stb和lwz,lhz,lbz指令对32位的外部设备进行访问。在这些指令结束后,存放在外部设备的数据将被读入MPC8541的通用寄存器中。为保证软件的一致性,当访问结束后,存放在通用寄存器的字节序,即OP0,OP1,OP2和OP3必须要和存放在小端外设的字节序一致。此时在使用大端处理器的数据总线连接小端外设时必须要进行一定的处理,按照某种拓扑结构连接以保证软件的一致性。大端处理器数据总线与小端外设进行连接的拓扑结构如图4.6所示。 OP0 OP1 OP2 OP 0 ;大端处理器的32位数据总线 ;小端设备的32位总线接口 ;图4.6 大端处理器与小端外设的32位连接 OP0 OP1 OP2 OP3 如图4.6所示,采用大端处理器访问小端设备时,将各自的OP0~OP3字段直接相连。在大端处理器的32位数据总线的更高位为0,更低位为31;而小端设备的更高位为31,更低位为0。因此硬件工程师在进行信号连接时需要将采用大端处理器的0~31位分别与小端设备的31~0位一一对应,进行互连。

1.1.2.2 大端处理器对8,16位小端外设进行访问大端处理器使用8位,16位数据总线对8位,16位的小端外设进行连接。对于32位处理器,用来连接外设的总线一般是32位。因此体系结构工程师在进行大端处理器总线设计时有两种选择,是采用32位总线的高端部分(第0~15字段)还是低端部分(第16~31字段)连接小端设备。PowerPC处理器使用32位总线的高端部分,即数据总线的第0~15位连接16位的小端设备,使用0~7位连接8位的小端设备。

PowerPC处理器采用16位总线与16位的小端外设进行访问时,PowerPC处理器的16位数据总线的第0~7位用来处理OP0,第8~15位用来处理OP1。而16位的小端设备使用数据总线的第15~8位用来处理OP0,第7~0位用来处理OP1。

PowerPC处理器采用8位总线与8位的小端外设进行访问时,PowerPC处理器的8位数据总线的第0~7字段用来处理OP0。而8位的小端设备使用数据总线的第7~0位用来处理OP1。大端处理器与小端外设的连接关系如图4.7所示。 OP0 OP1 OP0 OP ;大端处理器的8/16位数据总线 ;小端设备的8/16位总线接口 ;图4.7 大端处理器与小端外设的8/16位连接 OP0 OP 与32位总线接口类似,PowerPC处理器可以使用stw,sth,stb和lwz,lhz,lbz指令对32位的外部设备进行访问,并将数据存放在相应的通用寄存器中。当访问结束后,存放在通用寄存器的字节序,即OP0,OP1必须要和存放在小端外设的字节序一致。

PowerPC处理器对8位的小端外设进行访问时,一个总线周期只能访问8位数据,如果处理器使用stw或者lwz指令访问8位的小端设备内的32位数据时,在数据总线上将OP0,OP1,OP2和OP3依次传递到PowerPC的通用寄存器中。

PowerPC处理器对16位的小端外设进行访问时,一个总线周期只能访问16位数据,如果处理器使用stw或者lwz指令访问16位的小端设备内的32位数据时,在数据总线上将OP0~1和OP2~3依次传递到PowerPC的通用寄存器中。

PowerPC处理器使用sth或者lhz指令访问16位的小端设备时,16位的小端设备将数据的第15~0位,传递到PowerPC处理器的总线的第0~15位,然后再将数据最终传递给相应的通用寄存器。这里有许多读者会感到困惑,因为为了保证软件的一致性,PowerPC处理器使用lhz指令访问16位的小端设备的16位寄存器时,需要将结果保存在通用寄存器的第16~31位,而不是0~15位。究竟PowerPC处理器是如何将系统总线中0~15位的数据搬移到寄存器的第16~31位中的呢?为此我们需要对lhz指令进行分析。 lhz rD,d(rA)

if rA = 0 then b ← 0

else b ← (rA)

EA ← b + EXTS(d)

rD ← (24)0 || MEM(EA,1) 由lhz指令的以上描述得知lhz指令将来自数据总线上的OP0与OP1直接存入寄存器的第16~31位,而将第0~15位直接清零。

PowerPC处理器使用stb或者lbz指令访问8位的小端设备时,8位的小端设备将数据的第7~0位,传递到PowerPC处理器的总线的第0~7位,然后再将数据最终传递给相应的通用寄存器,lbz指令的描述如下所示。 lbz rD,d(rA)

if rA = 0 then b ← 0

else b ← (rA)

EA ← b + EXTS(d)

rD ← (24)0 || MEM(EA,1) 由lhz指令的以上描述得知lhz指令将来自数据总线上的OP0直接存入寄存器的第24~31位,而将第0~23位清零。

文章二:

大端(big-endian)和小端(little-endian)

:36 ;补:x86机是小端(修改分区表时要注意),单片机一般为大端 今天碰一个关于字节顺序的问题,虽然看起来很简单,但一直都没怎么完全明白这个东西,索性就找了下资料,把它弄清楚. 因为现行的计算机都是以八位一个字节为存储单位,那么一个16位的整数,也就是C语言中的short,在内存中可能有两种存储顺序big-endian和 litte-endian.考虑一个short整数0x3132(0x32是低位,0x31是高位),把它赋值给一个short变量,那么它在内存中的存 ;储可能有如下两种情况:

大端字节(Big-endian):

—>>>>>>>>;内存地址增大方向

short变量地址

0x1000 0x1001

_____________________________| || 0x31 | 0x32|_______________ | ________________

高位字节在低位字节的前面,也就是高位在内存地址低的一端.可以这样记住(大端->;高位->;在前->;正常的逻辑顺序)

小端字节(little-endian):

—>>>>>>>>;内存地址增大方向

short变量地址

0x1000 0x1001

_____________________________| || 0x32 | 0x31|________________ | ________________低位字节在高位字节的前面,也就是低位在内存地址低的一端.可以这样记住(小端->;低位->;在前->;与正常逻辑顺序相反)

可以做个实验

在windows上下如下程序

#include

#include

void main(void)

{ short test; FILE* fp; test = 0x3132; //(31ASⅡC码的’1’,32ASⅡC码的’2’)

if ((fp = fopen (c:\\test.txt,wb)) == NULL)

assert(0); fwrite(&test,sizeof(short),1,fp); fclose(fp);

}

然后在C盘下打开test.txt文件,可以看见内容是21,而test等于0x3132,可以明显的看出来x86的字节顺序是低位在前.如果我们把这段 ;同样的代码放到(big-endian)的机器上执行,那么打出来的文件就是12.这在本机中使用是没有问题的.但当你把这个文件从一个big- endian机器复制到一个little-endian机器上时就出现问题了.

如上述例子,我们在big-endian的机器上创建了这个test文件,把其复制到little-endian的机器上再用fread读到一个 short里面,我们得到的就不再是0x3132而是0x3231了,这样读到的数据就是错误的,所以在两个字节顺序不一样的机器上传输数据时需要特别小 ;心字节顺序,理解了字节顺序在可以帮助我们写出移植行更高的代码.

正因为有字节顺序的差别,所以在网络传输的时候定义了所有字节顺序相关的数据都使用big-endian,BSD的代码中定义了四个宏来处理:

#define ntohs(n) //网络字节顺序到主机字节顺序 n代表net,h代表host,s代表short

#define htons(n) //主机字节顺序到网络字节顺序 n代表net,h代表host,s代表short

#define ntohl(n) //网络字节顺序到主机字节顺序 n代表net,h代表host,l代表 long

#define htonl(n) //主机字节顺序到网络字节顺序 n代表net,h代表host,l代表 long

举例说明下这其中一个宏的实现:#define sw16(x) \ ((short)(\ (((short)(x) & (short)0x00ffU) > 8)))

这里实现的是一个交换两个字节顺序.其他几个宏类似.

我们改写一下上面的程序

#include

#include

#define sw16(x) \

((short)(\ (((short)(x) & (short)0x00ffU) > 8)))

// 因为x86下面是低位在前,需要交换一下变成网络字节顺序

#define htons(x) sw16(x)

void main(void)

{ short test; FILE* fp;

test = htons(0x3132); //(31ASⅡC码的’1’,32ASⅡC码的’2’) if ((fp = fopen (c:\\test.txt,wb)) == NULL)

assert(0); fwrite(&test,sizeof(short),1,fp); fclose(fp);

}

如果在高字节在前的机器上,由于与网络字节顺序一致,所以我们什么都不干就可以了,只需要把#define htons(x) sw16(x)宏替换为 #define htons(x) (x).

一开始我在理解这个问题时,总在想为什么其他数据不用交换字节顺序?比如说我们write一块buffer到文件,最后终于想明白了,因为都是unsigned char类型一个字节一个字节的写进去,这个顺序是固定的,不存在字节顺序的问题,够笨啊.. 文章三:

big-endian和little-endian这两个术语来自Jonathan Swift在十八世纪的嘲讽作品Gulliver’s Travels。Blefuscu帝国的国民被根据吃鸡蛋的方式划分为两个部分:一部分在吃鸡蛋的时候从鸡蛋的大端(bigend)开始,而另一部分则从鸡蛋的小端(little end)开始。

x86的CPU使用的是LE(Windows中称为“主机字节序”),而SocksAddr中使用的则是BE(就是“网络字节序”),所以在使用网络编程时需要使用htns,htnl,nths,nthl来倒字节序。

其实对汇编熟了就清楚了,惨,我的汇编很惨的LE little-endian 更符合人的思维的字节序 地址低位存储值的低位 地址高位存储值的高位 怎么讲是更符合人的思维的字节序,是因为从人的之一观感来说 低位值小,就应该放在内存地址小的地方,也即内存地址低位 反之,高位值就应该放在内存地址大的地方,也即内存地址高位 BE big-endian 最直观的字节序 地址低位存储值的高位 地址高位存储值的低位 为什么说直观,不要考虑对应关系 只需要把内存地址从左到右按照由低到高的顺序写出 把值按照通常的高位到低位的顺序写出 两者对照,一个字节一个字节的填充进去 例子:在内存中双字0x(DWORD)的存储方式 内存地址LEBEMSDN中关于LE和BE的解释Byte Ordering Byte ordering Meaning big-endian The most significant byte is on the left end of a word. little-endian The most significant byte is on the right end of a word. 这里这个最重要的字节可以解释成值的更高位,如果换成是钱的话就是最值钱的那一位 比如我有1234元人民币,最值钱的是1000元,最不值钱的是4元,那么这个1就是最重要的字节

Big endian machine: It thinks the first byte it reads is the biggest.Little endian machine: It thinks the first byte it reads is the littlest.举个例子,从内存地址0x0000开始有以下数据0x0000 0x120x0001 0x340x0002 0xab0x0003 0xcd如果我们去读取一个地址为0x0000的四个字节变量,若字节序为big-endian,则读出结果为0x1234abcd;若字节序位little-endian,则读出结果为0xcdab3412.如果我们将0x1234abcd写入到以0x0000开始的内存中,则结果为 big-endian little-endian0x0000 0x12 0xcd0x0001 0x23 0xab0x0002 0xab 0x340x0003 0xcd 0x12x86系列CPU都是little-endian的字节序。

关于linux ntohl函数的介绍到此就结束了,不知道你从中找到你需要的信息了吗 ?如果你还想了解更多这方面的信息,记得收藏关注本站。


数据运维技术 » 「深入浅出」Linux ntohl函数使用方法详解 (linux ntohl函数)