最近遇到了不少结构体,在计算偏移的时候有些麻烦,故摸鱼写下此文。

基础知识

在X86或ARM上,基本数据类型并不是存放在任意内存地址上的。 每种类型除了char都有对齐要求(alignment requirement); char类型可以开始于任何地址,但2字节的short类型必须存放在偶数地址上,4字节的整型或浮点型必须放在能被4整除的位置上,而8字节的long或double型必须放在能被8整除的地址上。有符号或无符号没有差别。术语上,这叫自对齐(self-aligned),至于为什么要这么对齐,可以简单的理解为是为了提高cpu的访问速度。

在没有#pragma pack这个宏的声明下,遵循下面三个原则:

1、***结构体中元素按照定义顺序依次置于内存中,但并不是紧密排列。从结构体首地址开始依次将元素放入内存时,元素会被放置在其自身对齐大小的整数倍地址上。***(这里说的地址是元素在结构体中的偏移量,结构体首地址偏移量为0。)

2、*如果结构体大小不是所有元素中最大对齐大小的整数倍,则结构体对齐到最大元素对齐大小的整数倍,填充空间放置到结构体末尾。*

3、基本数据类型的对齐大小为其自身的大小,结构体数据类型的对齐大小为其元素中最大对齐大小元素的对齐大小。

举例:

1
2
3
4
5
6
struct test
{
char a; //1byte
short b; //2bytes
char c; //1byte
};

由于对齐的原因,该结构体大小不是1+1+2=4字节,第二个short地址应该为2字节对齐,然而该结构体大小也不是5,而是6,因为还需遵守第2个规则,结构体的总大小,为其成员中所含最大类型的整数倍,故test结构体的大小为6字节。

大致如下:

image-20221009134119169

在缺省情况下,C编译器为每一个变量或是数据单元按其自然对界条件分配空间。

但也可以声明让编译器按其他方式优化对齐,如:

· __attribute((aligned (n))),让所作用的结构成员对齐在n字节自然边界上。如果结构中有成员的长度大于n,则按照最大成员的长度来对齐。
· attribute ((packed)),取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。

计算偏移

内核中有大量的庞大结构体,按上面的方法自己去计算是非常麻烦的,在我查询资料的途中,找到了几种计算方法

方法一:

使用#include <stddef.h> 中的 offsetof函数来实现

1
2
3
4
5
6
7
8
9
offsetof(type, member-designator)

参数
type -- 这是一个 class 类型,其中,member-designator 是一个有效的成员指示器。
member-designator -- 这是一个 class 类型的成员指示器。

返回值
该宏返回类型为 size_t 的值,表示 type 中成员的偏移量。

示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stddef.h>
#include <stdio.h>

struct address {
char name[50];
char street[50];
int phone;
};

int main()
{
printf("address 结构中的 name 偏移 = %d 字节。\n",
offsetof(struct address, name));

printf("address 结构中的 street 偏移 = %d 字节。\n",
offsetof(struct address, street));

printf("address 结构中的 phone 偏移 = %d 字节。\n",
offsetof(struct address, phone));

return 0;
}

结果:

1
2
3
address 结构中的 name 偏移 = 0 字节。
address 结构中的 street 偏移 = 50 字节。
address 结构中的 phone 偏移 = 100 字节。

方法二

把源码中结构体的定义截出来,然后自己编译运行计算一下

比如说随便找个内核结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
struct old_timespec32 {
old_time32_t tv_sec; /* 0 4 */
s32 tv_nsec; /* 4 4 */

/* size: 8, cachelines: 1, members: 2 */
/* last cacheline: 8 bytes */
};

int main(){
struct old_timespec32 test;
print("offset of tv_nsec:%lx",(long)((void*)&test.tv.nsec-(void*)&test));
return 0;
}

这样的

方法三

然而,很多内核结构体编译的时候都有各种条件,很多时候没法还原,以及各种结构体之间是嵌套的,要自己编译非常麻烦

如task_struct 结构体位于/linux-5.8/include/linux/sched.h 中,但定义是带各种ifdef 条件的

image-20221009135853147

这种时候我们要调试计算结构体,一开始我是用gdb直接内存地址偏移相差来计算的,但这样非常非常麻烦。

最后找到了pahole工具,只能说非常方便(

pahole 工具位于dwarves包中,可以直接apt安装:

1
sudo apt install dwarves -y

官方使用手册:

1
https://lwn.net/Articles/335942/

image-20221009140111984

–help可以查看帮助

一般来说

我们直接无脑使用-V参数即可,它会打印所有的hole情况

1
pahole -V vmlinux > outputfile

最终会看到

image-20221009140646408

非常详细

结构体末尾处还有这样的统计:

image-20221009140433241

当然我们也可以使用-C参数:

1
pahole -C task_struct vmlinux > output

这样就会只打印task_struct结构体的

参考链接:

https://blog.csdn.net/sinat_31039061/article/details/104366376

https://lwn.net/Articles/335942/

https://cloud.tencent.com/developer/article/1703257

https://blog.csdn.net/zzxfbdfhbdfhbdb/article/details/120561973