Avatar image Harlan V. Wei (aka. Wei Chen)

Linux 中的 GNU C:代码解读

• Photo credits: Lukas @ Unsplash

Hero image for Linux 中的 GNU C:代码解读

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));
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 可以一次性完成。

#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

__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 来对代码进行检查。