This post is only available in Chinese at this moment.
这是我在实验室学习过程中撰写的读书笔记的一部分。本文以 Linux 中比较有代表性的宏为例,向你介绍 Linux 中的 GNU C。
__attribute__((packed))
假设有如下代码:
struct foo {
char a;
int x[z];
} __attribute__((packed));
__attribute__
是 gcc 的一个保留字,用于作属性描述。如果不使用这个保留字,由于 gcc 优先尊重标准 C,则会创建一个名为packed
的变量。packed
属性表示关闭编译器的地址对齐功能。CPU 读取内存通常按块 (chuck) 进行,例如在 x86 架构的计算机上,一个 chuck 大小为 4 bytes(即 32 bits)。为了加速 CPU 对属性的访问,编译器会在属性间加入无意义的内容。具体来说,以 x86 为例:
struct foo {
char a;
char b;
/* 2-byte padding */
int c;
};
单个结构体占用的内存为 8 bytes,而不是 1 + 1 + 4 = 6 bytes(可以使用 gcc 编译,打印 sizeof(foo)
试一试)。如果没有 padding,则读取 foo.c
需要分两次(先读取第一个 chuck,取后两个字节,再读取第二个 chuck,取前两个字节,拼接在一起形成 foo.c
);有了 padding 之后,读取 foo.c
可以一次性完成。
- 在 Linux 内核中,内核空间非常宝贵,因此有时需要牺牲效率,使用
__attribute__((packed))
关闭这一特性。 - 另一个相关的属性是
__attribute__((aligned(n)))
,其中n
为整数,表示属性应该按照多少个字节对齐。不难看出aligned(1)
等价于packed
。 - 编译器还支持伪指令
#pragma pack(n)
来描述这一特性。可以使用#pragma ()
来取消这一设置。例如:
#pragma pack(1) // 取消 padding
struct foo {
char a;
char b;
/* no padding */
int c;
};
#pragma pack() // 恢复默认设置,即 4 字节对齐
container_of
在 Linux 5.14.12 中,container_of
的定义为:
// from: /include/linux/kernel.h
/**
* container_of - cast a member of a structure out to the containing structure
* @ptr: the pointer to the member.
* @type: the type of the container struct this is embedded in.
* @member: the name of the member within the struct.
*/
#define container_of(ptr, type, member) ({ \
void *__mptr = (void *)(ptr); \
BUILD_BUG_ON_MSG(!__same_type(*(ptr), ((type *)0)->member) && \
!__same_type(*(ptr), void), \
"pointer type mismatch in container_of()"); \
((type *)(__mptr - offsetof(type, member))); })
这个宏用于获取 ptr
这个指针所指向的对象对应属性所属的对象,它被大量地用于 Linux 源代码中,例如:
// from: /fs/ext4/ext4.h
static inline struct ext4_inode_info *EXT4_I(struct inode *inode)
{
return container_of(inode, struct ext4_inode_info, vfs_inode);
}
这个例子中,*inode
所指向的对象实际上是 struct ext4_inode_info
的一个成员,在结构体内,该成员的名称为 vfs_inode
,即:
struct ext4_inode_info {
// ...
struct inode vfs_inode;
// ...
};
上面对 container_of
的调用将返回一个指向类型为 ext4_inode_info
的对象的指针 new_ptr
,它满足 new_ptr->vfs_inode == *inode
。
这个宏在内核的数据结构中有非常重要的作用,例如内核中定义的链表结构就反复使用了这个宏。
为了理解其工作原理,去除中间的调试代码,并将宏转换为易读的伪代码(原来的宏定义没有 return
,因为使用了 GNU C 的一个拓展,允许块中最后一条表达式作为整个块的值参与运算),则其定义为:
container_of(ptr, type, member) {
void *__mptr = (void *)(ptr);
return ((type *)(__mptr - offsetof(type, member)));
}
函数体第一行首先将 ptr
转换为任意类型指针,便于后续指针的算数运算;第二行运用到了另一个宏 offsetof
(当编译器支持时,即等价于 __compiler_offsetof
;其定义在 /include/linux/stddef.h
),用于计算结构体某个变量相对于起始地址的偏移量。例如:
struct foo {
int a;
int b;
};
offsetof(struct foo, a); // 0
offsetof(struct foo, b); // 4
关于 offsetof
的定义:
// from: /include/linux/stddef.h
#ifdef __compiler_offsetof
#define offsetof(TYPE, MEMBER) __compiler_offsetof(TYPE, MEMBER)
#else
#define offsetof(TYPE, MEMBER) ((size_t)&((TYPE *)0)->MEMBER)
#endif
// from: /include/linux/compiler_types.h
#define __compiler_offsetof(a, b) __builtin_offsetof(a, b)
__builtin_offsetof
实际上目前版本的 GCC 和 Clang 都支持。如果编译器不支持 __builtin_offsetof
,则 offsetof
的实现 fallback 到程序员手动计算。在大部分编译器上,编译器内置实现和手动计算并无差异,都能实现编译时计算,但后者实际上并非确定的行为,即在某些编译器上,这个值也可能是运行时计算的(见 Reference),故 Linux 源代码提供了两种实现,没有直接 default 到手动计算。
__randomize_layout
攻击者可能会利用结构体属性的内存排布发起攻击,例如:
struct foo {
int val;
int *important_fn(void *args);
} global_var;
// 攻击者:(仅示意)
extern int *malicious_fn(void *args);
memcpy((void *)global_var + 4, malicious_fn);
// 此时 global_var 中的 important_fn 指针已被覆盖,
// 指向攻击者的 malicious_fn;
// 系统的后续代码:
global_var->important_fn(args); // 导致恶意函数被调用
// 遭到攻击!
为了避免这种攻击,Linux 使用了 gcc randstruct
插件来允许编译器将结构体内属性的内存排布随机打乱,打乱结果由 seed 唯一确定。此插件的三个主要功能:
- 任何标记有
__randomize_layout
(实际上就是__attribute__((randomize_layout))
,这里又是 Linux 的一个宏定义)的结构体都将随机布局。 - 在打开了
randstruct
的编译过程,对于所有成员属性均为函数指针的结构体,随机布局会 自动 打开。 - 可以使用
__no_randomize_layout
显式关闭随机布局(Reference 给出了需要关闭此功能的案例)。
有些读者可能会想到可以在上面的代码中使用
offsetof
来计算偏移量,从而破解随机布局机制,然而offsetof
是编译时的机制,而攻击者攻击的是编译后的二进制,无法调用offsetof
。上面的攻击者代码只是为了演示攻击大概如何发生。(另:打开随机布局后,offsetof
的输出也会是符合随机结果的,因此不会破坏container_of
等宏的正确性。)
然而这个方案对于公开分发的 Linux 内核帮助甚小,因为这些内核必须公开自己使用的 seed 来允许第三方内核模块在内核上运行。实际上能够从这一方案中受益的是那些非公开的 Linux 内核,例如 Google 和 Facebook 运行于服务器上的内核。(见 Reference)
Sparse
除了上面说到的这些属性以外,还有一些 GCC 会直接忽略的属性,它们被 Linus 开发的 Sparse (见 Reference)静态分析工具利用。例如 __user
的定义:
// /include/linux/compiler_types.h
#define __user __attribute__((noderef, address_space(__user)))
也就是说,__user 有两层含义:noderef
表示代码中不应该直接解引用使用 __user
修饰的指针,以及修饰的指针指向用户空间内存。由于内核能够任意地访问内存,使用 __user 限定符来提醒开发者不要去访问不受信任的内存,乃至后续使用代码检查来避免有关漏洞进入主线,是非常有益处的。开发者使用 Sparse 对代码进行检查时,如果发生了违反这两种情况的代码,会收到报告。可以使用 make C=2
来对代码进行检查。