修志龙_ZenonXiu · 2022年08月09日 · 上海市

必须了解的64位编程知识(2)

指针运算
64位编程中,可能会有很多指针运算编程需要注意的地方,比如下面处理,

short * test(short *ptr)
{
 unsigned int n = (unsigned int) ptr+ 2; 
 ptr= (short *) n;
 return ptr;
}

在很多时候,因为没有考虑到之后的代码会在64位系统上运行,在指针运算时为了方便,可能会把指针强制类型转换成int类型。这在32位上是没问题的,因为指针在32位系统上和int类型大小都为4 byte,但是在64 位系统上,int类型是4 byte,而指针类型是8 byte,如果做这样的强制类型转换,那么指针的高32 位会被截掉,从而产生错位。
上面的代码经编译,

ADD    w0, w0, #2   //32位的运算,因为写了W0,会将X0的高32bit 清零,也就是将指针的高32bit清零
SXTW   x0, w0       //转换回64 bit,但高32位已经被清零

这是一个比较常见的问题,Google在将Android kernel升级支持64位时,很多的patch就是处理这样的问题,
https://android-review.google...
捕获2.PNG

https://android-review.google...
捕获3.PNG

这样的例子在Android里有很多,所以你如果也这样写过代码,no shame.

解决这个移植的问题的方式,上面patch其实已经给出:
如果需要对指针进行类型转换,使用intptr_t或 uintptr_t类型,如果计算指针的差值,可以使用ptrdiff_t类型。这样写出来的代码可移植到32位hu和64位系统。

指针运算的考虑
下面一个例子,

int diff_a = -3;
unsigned int diff_b =2 ;
int * array = (int *)0x80004 ;
int *p ;

p= array+(diff_a+diff_b);

请问p的是多少?请大家先花几秒时间想想。
如果你说是0x80003, 请重修大学C语言课程。
如果你说是0x80000, 在32位系统上是对的。
但是在64位系统上,你还得一步一步按照数据转换规则来,
64bit programming-Page-5.jpg

移位操作
如果你需要对一个64bit的值的某些位设1,你也许会用这样的代码实现

long long val;
long long shift(long long a, int bits)
{
 a=a | (1<<bits);
 return a;
}

val=shift(val, 36);

以上代码是对64bit的val的36bit设1的操作。看出什么问题来了吗?
实际上上面的代码并不正确。还记得前面说的如何对待数值常量吗?

  • 整型常量会被当成是可以表示这个值的最小类型,比如‘8’ 会是 int型

因此以上代码的1会被当初是int类型,对int类型移36位会出现什么样的结果呢?
实际上这个代码会生成如下指令,

MOV    w2, #1
LSL     w1, w2, w1  //w1=bits=36
SXTW   x1, w1   //将移位值32bit的w1有符号扩展成64bit
ORR    x0, x0, x1

根据LSL指令的描述,
http://shell-storm.org/armv8-... 

LSL <Wd>, <Wn>, <Wm>
is equivalent to
LSLV <Wd>, <Wn>, <Wm>

LSLV
Logical Shift Left Variable shifts a register value left by a variable number of bits, shifting in zeros, and writes the result to the destination register. The remainder obtained by dividing the second source register by the data size defines the number of bits by which the first source register is left-shifted.

bits(datasize) result; 
bits(datasize) operand2 = X[m]; 
result = ShiftReg(n, shift_type, UInt(operand2) MOD datasize); 
X[d] = result;

因此结果是
MOV w2, #1
LSL w1, w2, w1
w1=0;

这个问题的解决方式就是指定‘1’这个常量为long long类型

long long shift(long long a, int bits)
{
 a=a | (1LL<<bits);
 return a;
}

这样生成的代码为,

MOV x2,#1
LSL x1,x2,x1
ORR x0,x0,x1

把1变成64位类型,因此LSL会移位36位,达到了目的。
一个实际driver开发的例子,
1LL.JPG

位域操作

Struct Test{
unsigned short va:15;
unsigned short vb:13;
};
Struct Test vc;
unsigned long long vx;
int main()
{
 vc.va= 0x2000;  //bit13 is set
 vx = vc.va<< 18; 
 return 0;
}

这段代码之后vx的值为多少呢?
我想很多人都会给出vx=0x8000_0000 //bit31 is set这个答案。这个答案32为位系统上是对的,但在64位系统上是错误的。
需要注意vx = vc.va<< 18 转换时,先转换成有符号的long long,再转换成无符号的long long 类型。最后的结果是0xFFFF_FFFF_8000_0000
64bit programming-bitfield.jpg

解决方式是

vx = (unsigned long long)vc.va<< 18;

注意特殊数字
如果长期在32位系统上编程,可能会形成一些思维惯性。对一些数值做一些假设。比如认为指针的大小就是4 byte, 0xFFFFFFFF就是-1等。这些会影响到代码的移植性。

#define ERROR_CODE 0xFFFFFFFF 

int bar()
{
 …
 return -1;
}

int foo()
{
 if(ERROR_CODE==bar())
   …
}

优化memory占有大小
引进64位带来很多performance的好处,但是也带来了一个缺点:
可能增加内存的占用,其中一个原因就是指针类型由4byte变成了8byte,因此地址访问和指针占有的内存更多。
有些测试数据
图片.png

当然这是所有64位构架系统的普遍现象。

那怎么可以帮助减少一些 footprint呢?

对于struct的对齐来说,如果没有packed等关键字修饰,struct的元素需要对齐到其自然边界,struct本身对齐到最大的元素大小。
因此以下数据结构的 memory layout为,

Struct
{
int a;
int * p;
int b;
}

64bit programming-footprint.jpg
可以考虑做一些优化,调整元素在struct里面的位置,

Struct
{
int a;
int b;
int * p;
}

64bit programming-Page-10.jpg
这个优化对程序中使用很多的数据结构有帮助,可以减少程序需要的内存。

结语
文章的内容其实并不仅仅针对arm系统,64位C、C++的编程需要注意:

  • 特别注意数据类型的转换
  • 指针操作
  • 不同数据类型之间的运算

Enjoy 64bit system!

必须了解的64位编程知识(1)

推荐阅读
关注数
8708
文章数
65
mindshare_zenon
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息