弱符号与弱引用 -> 程序员的自我修养 第3,4章笔记

本文详细探讨了C语言中利用GNU拓展定义节名并放置符号的技巧,以及弱符号和弱引用的概念。通过示例程序解释了如何通过节名控制变量的位置,以及弱符号在链接时如何被处理。弱引用允许引用可能不存在的外部符号,如果存在则使用,不存在则可以避免错误。这些知识对于理解编译器和链接器的工作原理以及操作系统中的内存管理至关重要。
摘要由CSDN通过智能技术生成

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操作系统中,有一个外部引用变量,它的值是在链接时候由链接器来确定的,用于标记代码段和数据段的结尾地址(这样该值后面的内存就是空闲内存,可以加入到空闲链表中)

  • 另外一个有意思的点在于abcdsss的地址是相同的,可以思考一下原因。在符号表中,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

最常用的是weakalias关键字结合起来用,在musl-c源码中大量使用了这种技术。这里的__typeof关键字返回表达式的类型。

// src/include/features.h
#define weak_alias(old, new) \
	extern __typeof(old) new __attribute__((__weak__, __alias__(#old)))
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值