Linker Set 是什么?

Jan 06, 2020 / 5 min read

Linker Set 是什么?

这个问题在 Google 上搜索也没什么详细的介绍,在 FreeBSD Architecture Handbook 中有一句介绍,虽然只有简单的一句,但已经把这个东西的作用说明白了:

///

SYSINIT relies on the ability of the linker to take static data declared at multiple locations throughout a program’s source and group it together as a single contiguous chunk of data. This linker technique is called a “linker set”.

Linker Set 就类似我们平时在代码文件开头定一个数组,里面放我们要处理的数据,用于在另一处代码中处理:

c ///
int data[] = {1, 2, 3, 4, 5};

int 
main()
{
...process data
}

这样我们要加新数据的话,必须去这个数组定义的地方加。而 Linker Set 可以使用一个宏,写在任意地方,来达到同样的效果。

c ///
INIT(set)
AddToSet(set, 1)
AddToSet(set, 2)

int 
main()
{
...process set[1,2,3]
} 

AddToSet(set, 2)

注意 AddToSet 必须是宏,因为函数在编译阶段是不会执行的,但宏会在编译阶段扩展。 Linker Set 的核心就是在 link 阶段把分布在代码各处的数据集中到一个数据块中来。

Linker Set 的实现

以下实现参照了 FreeBSD 中的 Linker Set 实现。
有关一些汇编的符号可以参考这里。 有关 GCC C Extensions 参考这里。 Clang 也可以参考 GCC C Extension:

///

…In addition to the language extensions listed here, Clang aims to support a broad range of GCC extensions. Please see the GCC manual for more information on these extensions.

首先我们需要一些基本的 Helper 宏以及一个 struct,我们将使用这个 struct 作为 Linker Set 的类型:

c ///
#define __MCONCAT1(x, y) x ## y  
#define __MCONCAT(x, y) __MCONCAT1(x, y)  

#define __MGLOBL1(sym) __asm__(".globl " #sym) 
#define __MGLOBL(sym) __GLOBL1(sym)

struct test_struct {  
    char a;  
    char b;  
    char c;  
};

__MONCAT从名字就能看出来,是将 x y 的值连接起来。这里为什么要用两层(__MCONCAT 调用了 __MCONCAT2)宏?因为 ### 会抑制宏扩展(可以参考 The C Programming Language 中关于宏扩展的部分):

c ///
#define __MCONCAT(x, y) x ## y

__MCONCAT(__MCONCAT("a", "b"), "c")

clang -E 扩展会报错:

c ///
__MCONCAT("a", "b"test.c:5:1: error: pasting formed ')"c"', an invalid preprocessing token

因为结果:

c ///
__MCONCAT("a", "b")"c"

是个非法语句,## 阻止了内层 __MCONCAT 的进一步展开。

__MGLOBL 宏是定义一个全局变量……用 asm extension 的方式:

c ///
__asm__ (".globl name")

定义一个对 linker 全局可见的符号 name

接下来是声明一个 Set 的宏:

c ///
#define DECLARE_SET(set, set_type) \  
extern set_type __attribute__((weak)) *__start_set_##set; \  
extern set_type __attribute__((weak)) *__stop_set_##set

这个宏接受两个参数,第一个是 Set 名(可以说是变量名),第二个是类型。

c ///
DECLARE_SET(test, struct test_struct)

展开后的代码:

c ///
extern struct test_struct __attribute__((weak)) *__start_set_test
extern struct test_struct __attribute__((weak)) *__stop_set_test

首先,extern 很明显,是声明,我们在这里声明两个指向 struct test_struct 类型的变量,变量名为 __start_set_test, __start_set_test,从名字可以猜出来,应该是分别对应 set_test 的开始和结尾——正是如此__start_xxx 可以获得 xxx Section 的开始地址,通过 __stop_xxx 可以获取 xxx Section 的结束地址,所谓 Section 就是程序的一个连续的内存区块,最基本的比如一个程序可能由 .text, .data, .bssSection 组成。

__attribute__(weak) 的作用是告诉 linker ,这是个 weak symbol,不需要在链接时寻找它的定义(参考 GCC C Extension 或者 StackOverflow),因为我们不会在 C 代码中定义这两个符号。

有了定义 Set 的宏,我们需要能往 Set 里面添加数据:

c ///
#define __DATA_WSET(set, sym) \  
__MGLOBL(__MCONCAT(__start_set_, set)); \  
__MGLOBL(__MCONCAT(__stop_set_, set)); \  
static void const * const __set_##set##_sym_##sym \  
__attribute__((section("set_" #set))) __attribute__((used)) = &(sym)  
  
#define TEST_SET_ADD(uniquifier, a, b, c) \  
static struct test_struct uniquifier ## _test_set = {a, b, c}; \  
__DATA_WSET(test, uniquifier ## _test_set)

使用:

c ///
TEST_SET_ADD(foobar, 1, 2, 3);

这两个宏定义看起来很复杂,我们一步步来看:

TEST_SET_ADD 是添加一个到 Set 中,__DATA_WSET 是个 Helper。
TEST_SET_ADD 接受四个参数,第一个是 uniquifier,这个数据的唯一标识符。后面三个参数为 test_struct 的 fields,因为每个 struct 的接口不同,所以针对每一种 struct 都要单独定义一个 ADD 宏。 TEST_SET_ADD 的第一行:

c ///
static struct test_struct uniquifier ## _test_set = {a, b, c};

就是定义一个结构体,没什么特殊的。大部分事情是下面 __DATA_WSET 的工作,__DATA_WSET 第一个参数为 Set 的名字,即 DECLARE_SET 的第一个参数,我们上面定义的 test,第二个参数是我们要加入 Set 的数据。

先看前两行:

c ///
__MGLOBL(__MCONCAT(__start_set_, set)); \  
__MGLOBL(__MCONCAT(__stop_set_, set)); \  

首先 __MCONCAT 连接 __start_set_setset__DATA_WSET 的第一个参数,比如我们这里会展开成:

c ///
__asm__(".globl " "__start_set_test_set"); \
__asm__(".globl " "__stop_set_test_set");

定义两个全局符号 __start_set_test_set __stop_set_test_set,可以看到,这两个名字跟我们上面 DECLARE_SET 声明的变量一致。注意每一次 ADD 都会有这两句定义,而 C 是不允许同一个变量定义多次的,所以这里实际上不能严格地称为定义,至少不是 C 的定义,可以说是一个 Tag?这部分属于 asm 的知识,目前自己也不太清楚。

最后是一个指针定义:

c ///
static void const * const __set_##set##_sym_##sym \  
__attribute__((section("set_" #set))) __attribute__((used)) = &(sym) 

我们这里展开后:

c ///
static void const * const __set_test_set_sym_foobar_test_set __attribute__((section("set_" "test_set"))) __attribute__((used)) = &(foobar_test_set);

首先,这是一个指针的定义,指向一个 void const 类型,且此指针是个常量指针,其指向不可被修改。中间冗长的变量名不用去过度解析,只是个唯一变量名。这个指针有两个 attribute 扩展:

  • section("set_" "test_set"),表明我们指定这个变量要放在 set_test_set 这个自定义 Section 中,默认 linker 会将这些静态数据放在 .data 或者 .bss 中。
  • used 留给文档解释:This attribute, attached to a variable with static storage, means that the variable must be emitted even if it appears that the variable is not referenced.

最后可以定义一个 Helper 宏获取我们 Set 的开始和结束位置:

c ///
#define SET_START(set) (&__MCONCAT(__start_set_, set))
#define SET_LIMIT(set) (&__MCONCAT(__stop_set_, set))

这里就是 Linker Set 的全部内容了,合并起来:

c ///
#include <stdio.h>

#define __MCONCAT1(x, y) x ## y                                                                                                                                       [0/63]
#define __MCONCAT(x, y) __MCONCAT1(x, y)
#define __MGLOBL1(sym) __asm__(".globl " #sym)
#define __MGLOBL(sym) __GLOBL1(sym)

#define DECLARE_SET(set, set_type)                                      \
    extern set_type __attribute__((weak)) *__start_set_##set;           \
    extern set_type __attribute__((weak)) *__stop_set_##set

#define __DATA_WSET(set, sym)                         \
    __MGLOBL(__MCONCAT(__start_set_, set));           \
    __MGLOBL(__MCONCAT(__stop_set_, set));            \
    static void const * const __set_##set##_sym_##sym \
    __attribute__((section("set_" #set))) __attribute__((used)) = &(sym);

#define TEST_SET_ADD(uniquifier, a, b, c)                           \
    static struct test_struct uniquifier ## _test_set = {a, b, c};  \
    __DATA_WSET(test, uniquifier ## _test_set)

#define SET_START(set) (&__MCONCAT(__start_set_, set))
#define SET_LIMIT(set) (&__MCONCAT(__stop_set_, set))

struct test_struct {
    char a;
    char b;
    char c;
};

DECLARE_SET(test, struct test_struct);
TEST_SET_ADD(foobar1, 'A', 'A', 'A');
TEST_SET_ADD(foobar2, 'B', 'B', 'B');

int main()
{
    struct test_struct **start, **end;
    start = SET_START(test);
    end = SET_LIMIT(test);
    printf("%p %p\n", start, end);
    for (struct test_struct **i = start; i < end; i++) {
        printf("%c %c %c\n", (*i)->a, (*i)->b, (*i)->c);
    }
}

TEST_SET_ADD(foobar3, 'C', 'C', 'C');

这段代码的输出为:

text ///
0x4008a8 0x4008c0
A A A
B B B
C C C

可以看到,通过遍历 SET_STARTSET_LIMIT 之间,可以访问到 Set 中所有的数据。 检查生成的可执行文件来看 Linker Set 所做的事情: (x64 little-endian)

shell ///
objdump -s a.out
...
Contents of section set_test:
 4008a8 810b6000 00000000 840b6000 00000000  ..`.......`.....
 4008b8 870b6000 00000000                    ..`.....
Contents of section .eh_frame_hdr:
4008c0 011b033b 2c000000 04000000 20fcffff  ...;,....... ...

section set_test 是我们在 __DATA_WSET 中指定的 Section,起开始地址位于 0x4008a8,结束在 0x4008c0,跟我们输出 SET_START(test)SET_LIMIT(test) 相符合。 section set_test 的长度为 24bytes。在代码中我们添加了三个数据到 set_test 中,且每一个数据都是一个 void const * const 指针,指针的长度为 64bits,即 8bytes,总共 24bytes。(注意这里,我们在代码中是通过给指向 struct 的指针添加__attribute__((section("set_test)),使其被加入到了 section set_test中,而不是把 struct 定义本身加入 Set 中,后者也是可以的,只不过这里没有这么做)。 这三个指针的值分别为:0x600b81, 0x600b84, 0x600b87(因为机器是 little-endian,按 octet 为单位将顺序反过来即可,这里省略了高位的 0):

text ///
...
Contents of section .data:
 600b68 a6084000 00000000 00000000 00000000  ..@.............
 600b78 b0096000 00000000 00414141 42424243  ..`......AAABBBC
 600b88 4343                                 CC
...

0x600b78 开始往后数一下可以找到三个地址分别对应的 struct 值 AAA BBB CCC

Linker Set on macOS

macOS 中的 linker 有些不同,需要做一些修改,原理是一致的:

c ///
#define __MCONCAT1(x, y) x ## y
#define __MCONCAT(x, y) __MCONCAT1(x, y)
#define __MGLOBL1(sym) __asm__(".globl " #sym)
#define __MGLOBL(sym) __MGLOBL1(sym)

#if defined(__APPLE__) && defined(__MACH__)
#define DECLARE_SET(set, type) \
  extern type __attribute__((weak)) *__MCONCAT(__start_set_, set) __asm__("section$start$__DATA$set_" #set); \
  extern type __attribute__((weak)) *__MCONCAT(__stop_set_, set) __asm__("section$end$__DATA$set_" #set)

#define __DATA_WSET(set, sym)                                           \
  __MGLOBL(__MCONCAT(__start_set_, set));                               \
  __MGLOBL(__MCONCAT(__stop_set_, set));                                \
  static void const * const                                             \
  __set_##set##_sym_##sym                                               \
  __attribute__((section("__DATA,set_" #set))) __attribute__((used)) = &(sym);

#define ADD_TEST_SET(uniquifier, a, b, c)                         \
  static struct test_struct uniquifier ## _test_set = {a, b, c }; \
  __DATA_WSET(test, uniquifier ## _test_set)

#endif