弱符号与弱引用 -> 程序员的自我修养 第3,4章笔记
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操作系统中,有一个外部引用变量,它的值是在链接时候由链接器来确定的,用于标记代码段和数据段的结尾地址(这样该值后面的内存就是空闲内存,可以加入到空闲链表中)。
- 另外一个有意思的点在于
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 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
最常用的是weak
和alias
关键字结合起来用,在musl-c源码中大量使用了这种技术。这里的__typeof
关键字返回表达式的类型。
// src/include/features.h
#define weak_alias(old, new) \extern __typeof(old) new __attribute__((__weak__, __alias__(#old)))
弱符号与弱引用 -> 程序员的自我修养 第3,4章笔记相关推荐
- 《程序员的自我修养》第3章---目标文件里有什么
第3章 目标文件里有什么 3.1 目标文件的格式: 编译器编译源代码后生成的文件叫做 "目标文件". 目标文件从结构上讲,它是已经编译后的可执行文件格式,只是还没有经过链接的过程, ...
- 《程序员的自我修养》解析第一章
最开始毕业的时候,看这本书完全不懂它的意思,主要原因还是因为读书的时候没有接触过对应项目,现在给大家一章一章的分析对应的内容. 如果不是程序员那么对程序的概念大概是windows上面的后缀exe文件, ...
- 《程序员的自我修养》第4章---静态链接
第4章 静态链接 4.1 空间和地址分配: a.c : extern int shared;int main() {int a = 100;swap(&a, &shared); } b ...
- 【读书笔记】【程序员的自我修养 -- 链接、装载与库(二)】进程虚拟地址空间、装载与动态链接、GOT、全局符号表、共享库的组织、DLL、C++与动态链接
文章目录 前言 介绍 可执行文件的装载与进程 进程虚拟地址空间 装载方式 操作系统对可执行文件的装载 进程虚存空间分布 ELF文件的链接视图和执行视图 堆和栈 Linux 内核装载ELF & ...
- 《程序员的自我修养》
<程序员的自我修养>这本书偏底层,来来回回读了有三四遍了,每一次都有新的收获,不过很快又会忘记,所以写下了这本书从17年12月份至今的全书的笔记,留作以后自己复习. 第二章:编译和链接 源 ...
- 程序员的自我修养阅读笔记
编译和链接 将编译和链接合并到一起的过程称为构建(Build). 从源文件生成最终可执行目标文件共有4个步骤: 预处理(Prepressing) 编译(Compilation) 汇编(Assembly ...
- 《程序员的自我修养》读书总结
http://www.jianshu.com/p/47156b4259ed 最初买<程序员的自我修养>这本书,只因为在京东买书差一些钱,不够用优惠券.买回来以后的很长一段时间,我都以为这本 ...
- 从实践理解《程序员的自我修养》(1)
从实践理解<程序员的自我修养>(1) 前言 这篇文档主要从实践的角度充分理解<程序员的自我修养>一书中提到的细节.书中提到的各种机制.数据结构,我都将在实际系统中找到并理解它们 ...
- 程序员的自我修养--链接、装载与库笔记:总结
<程序员的自我修养----链接.装载与库>这本书是2009年出版的,书中有些内容的介绍可能已经过时,已不再适用于现在的C/C++开发,而且书中展示的结果均是在32位机上进行的操作,这里全部 ...
- 【读书笔记】【程序员的自我修养 -- 链接、装载与库(三)】函数调用与栈(this指针、返回值传递临时对象构建栈、运行库与多线程、_main函数、系统调用与中断向量表、Win32、可变参数、大小端
文章目录 前言 介绍 内存 内存布局 栈与调用惯例 堆与内存管理 运行库 入口函数和程序初始化 C/C++运行库 运行库与多线程 C++全局构造与析构 fread 实现 系统调用与API 系统调用介绍 ...
最新文章
- websocket(二):SSM+websocket的聊天室
- python hdfs初体验
- 18-Chain of trust bindings
- bgb邻居关系建立模型_学习开发知识图谱中的长期关系依赖
- 诺基亚7plus更新android10,诺基亚发布第五次Android 10更新 诺基亚7+可升级
- Python tab 补全
- 带动态属性的自定义标签
- 基于React开发一个音乐播放器
- [redis] Redis 配置文件置参数详解
- CartoonGAN照片动漫化
- 2018北京java面试心得体会(一年经验)
- 阿里代码规范检查自定义规则扩展
- 备忘录:XCode配置
- 中国GDP与百姓收入
- 网络工程师学习必备!路由器的工作原理,你真的懂了吗?【超详细|深度解析】
- 外媒分析:为何说苹果一定没造车!
- Oracle 权限详解(grant,revoke)
- ## YARN运行资源配置
- 失落的帝国-亚特兰蒂斯
- 论文研究 | 基于机器视觉的 PCB 缺陷检测算法研究现状及展望