1. 在程序中声明并使用节名
先看下面一段有意思的程序
#include <stdio.h>
__attribute__((section("abcd"))) int sss = 3;
static int y = 1;
extern int abcd;
int add(int a, int b){
return a + b;
}
int main(){
printf("%d\n", add(sss, y));
printf("abcd %p, sss %p\n", &abcd, &sss);
return 0;
}
这里我们利用GNU的C拓展,显式定义了一个abcd section
。然后把符号sss
放入该section中。编译运行如下
4
abcd 0x55acd9aaf014, sss 0x55acd9aaf014
从该段程序中有两点启发:
- 为什么可以这样写?这是因为编译生成的符号表中有相应的符号
Symbol table '.symtab' contains 16 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS hhh.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 4 OBJECT LOCAL DEFAULT 3 y
7: 0000000000000000 0 SECTION LOCAL DEFAULT 6
8: 0000000000000000 0 SECTION LOCAL DEFAULT 8
9: 0000000000000000 0 SECTION LOCAL DEFAULT 9
10: 0000000000000000 0 SECTION LOCAL DEFAULT 7
11: 0000000000000000 4 OBJECT GLOBAL DEFAULT 5 sss
12: 0000000000000000 20 FUNC GLOBAL DEFAULT 1 add
13: 0000000000000014 82 FUNC GLOBAL DEFAULT 1 main
14: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
15: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND printf
即符号表中TYPE=SECTION
中的项,(当然这没办法引用.data之类的保留段,因为C中不能声明这样名字的变量)。关于这样的trick的作用,一个可以想到的是xv6操作系统中,有一个外部引用变量,它的值是在链接时候由链接器来确定的,用于标记代码段和数据段的结尾地址(这样该值后面的内存就是空闲内存,可以加入到空闲链表中)。
- 另外一个有意思的点在于
abcd
和sss
的地址是相同的,可以思考一下原因。在符号表中,abcd
的符号和sss
的符号项几乎相同,他们的st_value
都指向段abcd
偏移量为0处。当我们说确定一个符号的属性时,通常需要有三点,该符号对应的虚拟地址起始处,该符号的大小,对相应字节的解释方式。在汇编阶段结束后,后两者就已经确定在机器指令中了(例如ld指令就隐含了解释该符号为8字节整数等)。链接器本身只需要关注st_value
这一项,重定位符号的地址就可。 - 上面这样的理解便可以解释一个C语言数组和指针的区别。C语言的数组符号
int a[5]
,在符号表中的大小是20个字节,假设该数组的起始地址为X,那么a[1]
的含义是取出[X+4,X+8)
地址的4字节并解释为整数。这和int a*
在符号表中的大小是8字节,将8字节的内容解释为另一个地址的语义是完全不同的。 - 我之前犯过的一个错误是这样,在文件m中定义数组
int a[5]
,在文件b中试图这样引用这个数组extern int *a
。链接成功通过,但运行时引发了段错误。有了上面的理解,这样做的结果便是可以预期的了。链接器在重定位的时候,把符号int *a
定位到符号int a[5]
上了。即把int a[5]
的前8个字节解释为了int *a
指针对应的地址。
2. 弱符号与弱引用
alias -> 必须在同一个翻译单元定义。tutorial中的实例一目了然。
#include <stdio.h>
int oldname = 5;
extern int newname __attribute__((alias("oldname")));
int main()
{
printf("Value of new name is :%d\n", newname);
return 0;
}
值得注意的点是声明newname
时需要用extern
关键字,实现上其实就把符号表中的表项复制一份就好了,这也解释了为什么要求alias对应的符号一定要在本单元定义,因为链接器没办法帮你做符号表表项copy的工作,因此alias对应的符号一定得是一个强符号而不是外部引用。
weak -> 声明为弱符号(与未初始化的全局变量的区别仅在于weak声明加初始化是在.data段中,后者在.common段中,BIND属性均为STB_WEAK)
看下面一段程序。
// weafvar.c
#include <stdio.h>
__attribute__ ((weak)) int y = 2;
int main(){
printf("y : %d\n", y);
return 0;
}
// weafvar2.c
int y = 3;
运行结果如下。
$gcc -o weafvar weafvar.c weafvar2.c
$./weafvar
3
如果是把一个函数声明为weak也是类似的(只是现在弱符号的f
位于代码段),看下面的代码。
// weafvar.c
#include <stdio.h>
__attribute__ ((weak)) void f(){
printf("default f\n");
};
int main(){
f();
return 0;
}
// weafvar2.c
#include <stdio.h>
void f(){
printf("user define f\n");
};
如果链接时加入weafvar2.c
,那么强符号的f
就会覆盖弱符号的f
。
关于weakref, 看下面的示例
// weakref.c
#include <sys/types.h>
#include <stdio.h>
extern void _foo();
__attribute__ ((weakref ("_foo"))) static void foo(void);
int main(int argc, char **argv)
{
printf("calling foo.\n");
if(foo){
foo();
}else{
printf("no foo\n");
}
}
// weakref2.c
#include <stdio.h>
void _foo(void)
{
printf("user defined foo.\n");
}
如果我们在链接的时候不链接weakref2.c
,那么会输出no foo
, 如果链接weakref2.c
,会输出user defined foo.
。有意思的地方是,弱引用允许引用一个外部的符号,但要求函数本身的定义必须是static的(这正好和alias相反,alias要求别名是一个内部的符号,但是本身不需要是static的)
从实现上来思考就很好理解了。查看weakref.o
的符号表。
$ readelf -s weakref.o
Symbol table '.symtab' contains 13 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS weakref.c
2: 0000000000000000 0 SECTION LOCAL DEFAULT 1
3: 0000000000000000 0 SECTION LOCAL DEFAULT 3
4: 0000000000000000 0 SECTION LOCAL DEFAULT 4
5: 0000000000000000 0 SECTION LOCAL DEFAULT 5
6: 0000000000000000 0 SECTION LOCAL DEFAULT 7
7: 0000000000000000 0 SECTION LOCAL DEFAULT 8
8: 0000000000000000 0 SECTION LOCAL DEFAULT 6
9: 0000000000000000 65 FUNC GLOBAL DEFAULT 1 main
10: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND _GLOBAL_OFFSET_TABLE_
11: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND puts
12: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _foo
发现了吧,根本就没有foo
这个符号,因此weakref就是把所以引用foo
的地方,全部替换为了_foo
,所以foo
一定得声明为内部符号,不然外部引用foo
失败会引发误解。
接下来我们看看最终的可执行文件是怎么样的
$ gcc -o weakref weakref.c
$ readelf -s weakref | grep _foo
5: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _foo
59: 0000000000000000 0 NOTYPE WEAK DEFAULT UND _foo
_foo出现了两次是因为还有一个.dynsym
符号表,目前不清楚这个的作用。不管怎样,可执行文件中的_foo
依然是一个未定义符号,但是从反汇编的代码中可以看到,与_foo相关的汇编语句依然被重定位了
66b: e8 e0 fe ff ff callq 550 <puts@plt>
670: 48 8b 05 71 09 20 00 mov 0x200971(%rip),%rax # 200fe8 <_foo>
677: 48 85 c0 test %rax,%rax
67a: 74 07 je 683 <main+0x2e>
67c: e8 df fe ff ff callq 560 <_foo@plt>
681: eb 0c jmp 68f <main+0x3a>
683: 48 8d 3d a7 00 00 00 lea 0xa7(%rip),%rdi # 731 <_IO_stdin_used+0x11>
68a: e8 c1 fe ff ff callq 550 <puts@plt>
mov 0x200971(%rip),%rax
是把_foo
符号的地址放到了%rax
中,不知道这个地址是怎么确定的,可能与后面的动态链接有一些关系,目前暂时留作疑惑吧。
但是最终的效果是如果_foo
存在,那么可以直接使用,如果_foo
不存在,那么可以通过if
语句判否。
update1
关于这里_foo
的重定位问题,看完动态链接再回来看就很好理解了。ld在链接时,没有找到_foo的定义,为_foo生成一个got表项,外加一个对该表项的重定位条目,并且把为_foo生成了一个在动态符号表中的表项(因为是弱符号,因此在链接时没有找到_foo的定义也没有关系)。因此上面对_foo的链接时的重定位实际上是把对_foo的引用转接到.got表项中。
如果在dynamic linker进行重定位时找到了_foo符号(比如我通过LD_PRELOAD
预先加载一个有_foo符号的动态库),那么_foo能够正常调用,否则_foo的got表项值为0。
update2
最常用的是weak
和alias
关键字结合起来用,在musl-c源码中大量使用了这种技术。这里的__typeof
关键字返回表达式的类型。
// src/include/features.h
#define weak_alias(old, new) \
extern __typeof(old) new __attribute__((__weak__, __alias__(#old)))