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 Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS hhh.c2: 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 y7: 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 sss12: 0000000000000000    20 FUNC    GLOBAL DEFAULT    1 add13: 0000000000000014    82 FUNC    GLOBAL DEFAULT    1 main14: 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 Name0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS weakref.c2: 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 main10: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _GLOBAL_OFFSET_TABLE_11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND puts12: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _foo

发现了吧,根本就没有foo这个符号,因此weakref就是把所以引用foo的地方,全部替换为了_foo,所以foo一定得声明为内部符号,不然外部引用foo失败会引发误解

接下来我们看看最终的可执行文件是怎么样的

$ gcc -o weakref weakref.c
$ readelf -s weakref | grep _foo5: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _foo59: 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,%rax67a:    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)))

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

  1. 《程序员的自我修养》第3章---目标文件里有什么

    第3章 目标文件里有什么 3.1 目标文件的格式: 编译器编译源代码后生成的文件叫做 "目标文件". 目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程, ...

  2. 《程序员的自我修养》解析第一章

    最开始毕业的时候,看这本书完全不懂它的意思,主要原因还是因为读书的时候没有接触过对应项目,现在给大家一章一章的分析对应的内容. 如果不是程序员那么对程序的概念大概是windows上面的后缀exe文件, ...

  3. 《程序员的自我修养》第4章---静态链接

    第4章 静态链接 4.1 空间和地址分配: a.c : extern int shared;int main() {int a = 100;swap(&a, &shared); } b ...

  4. 【读书笔记】【程序员的自我修养 -- 链接、装载与库(二)】进程虚拟地址空间、装载与动态链接、GOT、全局符号表、共享库的组织、DLL、C++与动态链接

    文章目录 前言 介绍 可执行文件的装载与进程 进程虚拟地址空间 装载方式 操作系统对可执行文件的装载 进程虚存空间分布 ELF文件的链接视图和执行视图 堆和栈 Linux 内核装载ELF & ...

  5. 《程序员的自我修养》

    <程序员的自我修养>这本书偏底层,来来回回读了有三四遍了,每一次都有新的收获,不过很快又会忘记,所以写下了这本书从17年12月份至今的全书的笔记,留作以后自己复习. 第二章:编译和链接 源 ...

  6. 程序员的自我修养阅读笔记

    编译和链接 将编译和链接合并到一起的过程称为构建(Build). 从源文件生成最终可执行目标文件共有4个步骤: 预处理(Prepressing) 编译(Compilation) 汇编(Assembly ...

  7. 《程序员的自我修养》读书总结

    http://www.jianshu.com/p/47156b4259ed 最初买<程序员的自我修养>这本书,只因为在京东买书差一些钱,不够用优惠券.买回来以后的很长一段时间,我都以为这本 ...

  8. 从实践理解《程序员的自我修养》(1)

    从实践理解<程序员的自我修养>(1) 前言 这篇文档主要从实践的角度充分理解<程序员的自我修养>一书中提到的细节.书中提到的各种机制.数据结构,我都将在实际系统中找到并理解它们 ...

  9. 程序员的自我修养--链接、装载与库笔记:总结

    <程序员的自我修养----链接.装载与库>这本书是2009年出版的,书中有些内容的介绍可能已经过时,已不再适用于现在的C/C++开发,而且书中展示的结果均是在32位机上进行的操作,这里全部 ...

  10. 【读书笔记】【程序员的自我修养 -- 链接、装载与库(三)】函数调用与栈(this指针、返回值传递临时对象构建栈、运行库与多线程、_main函数、系统调用与中断向量表、Win32、可变参数、大小端

    文章目录 前言 介绍 内存 内存布局 栈与调用惯例 堆与内存管理 运行库 入口函数和程序初始化 C/C++运行库 运行库与多线程 C++全局构造与析构 fread 实现 系统调用与API 系统调用介绍 ...

最新文章

  1. websocket(二):SSM+websocket的聊天室
  2. python hdfs初体验
  3. 18-Chain of trust bindings
  4. bgb邻居关系建立模型_学习开发知识图谱中的长期关系依赖
  5. 诺基亚7plus更新android10,诺基亚发布第五次Android 10更新 诺基亚7+可升级
  6. Python tab 补全
  7. 带动态属性的自定义标签
  8. 基于React开发一个音乐播放器
  9. [redis] Redis 配置文件置参数详解
  10. CartoonGAN照片动漫化
  11. 2018北京java面试心得体会(一年经验)
  12. 阿里代码规范检查自定义规则扩展
  13. 备忘录:XCode配置
  14. 中国GDP与百姓收入
  15. 网络工程师学习必备!路由器的工作原理,你真的懂了吗?【超详细|深度解析】
  16. 外媒分析:为何说苹果一定没造车!
  17. Oracle 权限详解(grant,revoke)
  18. ## YARN运行资源配置
  19. 失落的帝国-亚特兰蒂斯
  20. 论文研究 | 基于机器视觉的 PCB 缺陷检测算法研究现状及展望

热门文章

  1. java提供对多线程同步语言级的支持_赞同科技笔试题11
  2. 《我们这样近,我们这样远》
  3. 华为认证含金量高吗?
  4. 三行代码解决全部网页木马
  5. MySQL中的子查询,最好最全的子查询分类
  6. eventlet如何绿化pyserial最好
  7. iOS Principle:CGAffineTransform
  8. 龙梦3A4000代码获Kernel 5.5合并
  9. ARM设备上的轻量化NAS-Kiftd
  10. 开源项目-排班管理系统,考勤管理系统