Linker Set 是什么?
Table of contents
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 就类似我们平时在代码文件开头定一个数组,里面放我们要处理的数据,用于在另一处代码中处理:
int data[] = {1, 2, 3, 4, 5};
int
main()
{
...process data
}这样我们要加新数据的话,必须去这个数组定义的地方加。而 Linker Set 可以使用一个宏,写在任意地方,来达到同样的效果。
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 的类型:
#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 中关于宏扩展的部分):
#define __MCONCAT(x, y) x ## y
__MCONCAT(__MCONCAT("a", "b"), "c")clang -E 扩展会报错:
__MCONCAT("a", "b"test.c:5:1: error: pasting formed ')"c"', an invalid preprocessing token因为结果:
__MCONCAT("a", "b")"c"是个非法语句,## 阻止了内层 __MCONCAT 的进一步展开。
__MGLOBL 宏是定义一个全局变量……用 asm extension 的方式:
__asm__ (".globl name")定义一个对 linker 全局可见的符号 name。
接下来是声明一个 Set 的宏:
#define DECLARE_SET(set, set_type) \
extern set_type __attribute__((weak)) *__start_set_##set; \
extern set_type __attribute__((weak)) *__stop_set_##set这个宏接受两个参数,第一个是 Set 名(可以说是变量名),第二个是类型。
DECLARE_SET(test, struct test_struct)展开后的代码:
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, .bss 等 Section 组成。
__attribute__(weak) 的作用是告诉 linker ,这是个 weak symbol,不需要在链接时寻找它的定义(参考 GCC C Extension 或者 StackOverflow),因为我们不会在 C 代码中定义这两个符号。
有了定义 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)使用:
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 的第一行:
static struct test_struct uniquifier ## _test_set = {a, b, c};就是定义一个结构体,没什么特殊的。大部分事情是下面 __DATA_WSET 的工作,__DATA_WSET 第一个参数为 Set 的名字,即 DECLARE_SET 的第一个参数,我们上面定义的 test,第二个参数是我们要加入 Set 的数据。
先看前两行:
__MGLOBL(__MCONCAT(__start_set_, set)); \
__MGLOBL(__MCONCAT(__stop_set_, set)); \
首先 __MCONCAT 连接 __start_set_ 和 set,set 是 __DATA_WSET 的第一个参数,比如我们这里会展开成:
__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 的知识,目前自己也不太清楚。
最后是一个指针定义:
static void const * const __set_##set##_sym_##sym \
__attribute__((section("set_" #set))) __attribute__((used)) = &(sym)
我们这里展开后:
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 的开始和结束位置:
#define SET_START(set) (&__MCONCAT(__start_set_, set))
#define SET_LIMIT(set) (&__MCONCAT(__stop_set_, set))这里就是 Linker Set 的全部内容了,合并起来:
#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');这段代码的输出为:
0x4008a8 0x4008c0
A A A
B B B
C C C可以看到,通过遍历 SET_START 和 SET_LIMIT 之间,可以访问到 Set 中所有的数据。
检查生成的可执行文件来看 Linker Set 所做的事情:
(x64 little-endian)
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):
...
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 有些不同,需要做一些修改,原理是一致的:
#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