Linux 中的 GNU C:代码解读

October 23, 2021

image courtesy of Lukas

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 唯一确定。此插件的三个主要功能:

  1. 任何标记有 __randomize_layout (实际上就是 __attribute__((randomize_layout)) ,这里又是 Linux 的一个宏定义)的结构体都将随机布局。
  2. 在打开了 randstruct 的编译过程,对于所有成员属性均为函数指针的结构体,随机布局会 自动 打开。
  3. 可以使用 __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 来对代码进行检查。