...
 
Commits (2)
    https://gitcode.net/weixin_42470629/computer-science/-/commit/726abf58e14dffc34fe72dbf1dad1c9e3020e049 使用版本脚本控制符号的可见性 2023-09-12T16:00:48+08:00 杜宇鹏 duyupeng36@foxmail.com https://gitcode.net/weixin_42470629/computer-science/-/commit/5cb8c3faf32bfc2f29795d5cbef9e73462231376 符号版本化 2023-09-12T16:27:21+08:00 杜宇鹏 duyupeng36@foxmail.com
......@@ -70,4 +70,4 @@
:page 736},
:content {:text "[:span]", :image 1694437268292},
:properties {:color "purple"}}],
:extra {:page 753}}
:extra {:page 757}}
//
// Created by dyp on 9/12/23.
//
#include <dlfcn.h>
#include <stdio.h>
int main() {
void * handle = dlopen("./libvis.so.1", RTLD_LAZY);
void (*fp)() = dlsym(handle, "vis_f1");
if(fp == NULL) {
fprintf(stderr, "dlsym:%s\n", dlerror());
return -1;
}
fp();
return 0;
}
VER_1 {
global:
vis_f1;
vis_f2;
local:
*;
};
\ No newline at end of file
#include <stdio.h>
void vis_comm() {
printf("at vis_comm.c");
}
#include <stdio.h>
void vis_comm();
void vis_f1() {
printf("at vis_f1.c call vis_comm()\n");
vis_comm();
}
#include <stdio.h>
void vis_comm();
void vis_f2() {
printf("at vis_f2.c call vis_comm()");
vis_comm();
}
#include <stdio.h>
void xyz(void) {
printf("v1 xyz\n");
}
#include <stdio.h>
__asm__(".symver xyz_old,xyz@VER_1");
__asm__(".symver xyz_new,xyz@@VER_2");
void xyz_old(void) {
printf("v1 xyz\n");
}
void xyz_new(void) {
printf("v2 xyz\n");
}
void pqr(void) {
printf("v2 pqr\n");
}
#include <stdlib.h>
int main(int argc, char *argv[]) {
void xyz(void);
xyz();
exit(EXIT_SUCCESS);
}
\ No newline at end of file
VER_1 {
global:
xyz;
local:
*;
};
\ No newline at end of file
VER_1 {
global:
xyz;
local:
*;
};
VER_2 {
global:
pqr;
} VER_1;
- 首先介绍两种 [[$green]]==**检测错误的方法**==:调用 `assert` 宏以及测试 `errno` 变量。然后介绍如何检测并处理称为 [[$green]]==**信号**== 的条件,一些信号用于表示错误。处理信号的函数在 `<signal.h>` 头中声明。最后,探讨 `setjmp/longjmp` 机制,它们经常用于响应错误。`setjmp``longjmp` 都属于 `<setjmp.h>`
- 错误的检测和处理并不是C语言的强项。C语言对运行时错误以多种形式表示,而没有提供一种统一的方式。而且,在C程序中,需要由程序员编写检测错误的代码。因此,很容易忽略一些可能发生的错误。一旦发生某个被忽略的错误,程序经常可以继续运行,虽然这样也不是很好
- # `<assert.h>`:诊断
collapsed:: true
- `assert` 定义在 `<assert.h>` 中。它使程序可以监控自己的行为,并尽早发现可能会发生的错误
- ```c
void assert(scalar expression);
......@@ -41,7 +40,6 @@
- [[#green]]==`NDEBUG` 宏的值不重要,只要定义了 `NDEBUG` 宏即可==。一旦之后程序又有错误发生,就可以去掉 `NDEBUG` 宏的定义来重新启用 `assert`
- 函数 `assert` 是在程序运行期间做诊断工作,[[#blue]]==从C11开始引入的静态断言 `_Static_assert` 可以把检查和诊断工作放在程序编译期间进行==
- # `<errno.h>`:错误
collapsed:: true
- 标准库中的一些函数 [[#red]]==通过向 `<errno.h>` 中声明的 `int` 类型 `errno` 变量存储一个错误码(正整数)来表示有错误发生==
- `errno` 可能实际上是个宏。如果确实是宏,C标准要求它表示左值,以便像变量一样使用
- 大部分使用 `errno` 变量的函数集中在 `<math.h>`,但也有一些在标准库的其他部分中
......@@ -98,7 +96,6 @@
puts(strerror(EDOM));
```
- # `<signal.h>`:信号处理
collapsed:: true
- `<signal.h>` 提供了 [[$green]]==处理 **异常情况(称为信号**)的工具==。[[$red]]==**信号** 有两种类型:**运行时错误****发生在程序以外的事件**==
- 许多操作系统都允许用户中断或终止正在运行的程序,C语言把这些事件视为信号
- [[#red]]==当有错误或外部事件发生时,我们称产生了一个信号==
......
......@@ -182,15 +182,162 @@
- ((65000492-81ff-4609-bbeb-8ea00c75b906))
- 当动态加载一个共享库时,`dlopen()` 接收的 `RTLD_GLOBAL` 标记可以用来指定这个库中定义的符号应该用于后续加载的库中的绑定操作,`--export–dynamic`链接器选项可以用来使主程序的全局符号对动态加载的库可用
- # 链接器版本脚本
- [[$green]]==**版本脚本** 是一个包含 **链接器** `ld` 执行的 **指令的文本文件**==。要使用版本脚本必须要指定`--version–script` 链接器选项
- [[$green]]==**版本脚本** 是一个包含 **链接器** ld 执行的 **指令的文本文件**==。要使用版本脚本必须要指定`--version–script` 链接器选项
- ```shell
gcc -Wl,--version-script,myscriptfile.map ...
```
- [[#green]]==**版本脚本** 的后缀通常(但不统一)是 **.map**==
- ## 使用版本脚本控制符号的可见性
id:: 65000492-81ff-4609-bbeb-8ea00c75b906
collapsed:: true
- [[#red]]==版本脚本的一个用途是 **控制那些可能会在无意中变成全局可见的符号(即对与该库进行链接的应用程序可见)的可见性**==
- 举一个简单的例子,假设需要从三个源文件 `vis_comm.c``vis_f1.c` 以及 `vis_f2.c` 中构建一个共享库,这三个源文件分别定义了函数 `vis_comm()``vis_f1()`以及 `vis_f2()``vis_comm()`函数由 `vis_f1()``vis_f2()` 调用,但不想被与该库进行链接的应用程序直接使用。再假设使用常规的方式来构建共享库
-
- 举一个简单的例子,假设需要从三个源文件 `vis_comm.c``vis_f1.c` 以及 `vis_f2.c` 中构建一个共享库,这三个源文件分别定义了函数 `vis_comm()``vis_f1()`以及 `vis_f2()``vis_comm()`函数由 `vis_f1()``vis_f2()` 调用,但不想被与该库进行链接的应用程序直接使用
- [[$green]]==**使用常规的方式来构建共享库**==
- ```shell
[dyp@dyp version_script]$ gcc -g -c -fPIC -Wall vis_comm.c vis_f1.c vis_f2.c
[dyp@dyp version_script]$ gcc -g -shared -Wl,-soname,libvis.so.1 -o libvis.so.1.0.0 vis_comm.o vis_f1.o vis_f2.o
[dyp@dyp version_script]$ readelf --syms --use-dynamic libvis.so.1.0.0 | grep vis_
6: 0000000000001159 37 FUNC GLOBAL DEFAULT 12 vis_f2
7: 0000000000001119 27 FUNC GLOBAL DEFAULT 12 vis_comm
8: 0000000000001134 37 FUNC GLOBAL DEFAULT 12 vis_f1
```
- 这个共享库导出了三个符号:`vis_comm()``vis_f1()` 以及 `vis_f2()`
- 但这里 [[#blue]]==需要确保这个库只导出 `vis_f1()``vis_f2()` 符号==。这种效果可以通过下面的版本脚本来实现。首先创建 `vis.map` 文件,然后键入如下内容
- ```shell
VER_1 {
global:
vis_f1;
vis_f2;
local:
*;
};
```
- 标识符 `VER_1` 是一种版本标签。每个 [[$green]]==**版本节点以花括号({})组织起来**== 并且在括号前面设置一个 [[$green]]==**唯一的版本标签**==
- 在版本节点中,[[$red]]==**关键词 global**== 标记出了以分号分隔的 [[$red]]==**对库之外的程序可见的符号列表**== 的起始位置,[[$red]]==**关键词 local**== 标记出了以分号分隔的 [[$red]]==**对库之外的程序隐藏的符号列表**== 的起始位置
- 在本例中,local 规范中的星号表示除了在 global 段中显式声明的符号之外的所有符号都对外隐藏。如果不这样声明,那么 `vis_comm()` 仍然是可见的,因为在默认情况下 C 全局符号对共享库之外的程序是可见的
- 接着可以像下面这样使用版本脚本来构建共享库
- ```shell
[dyp@dyp version_script]$ gcc -g -c -fPIC -Wall vis_comm.c vis_f1.c vis_f2.c
[dyp@dyp version_script]$ gcc -g -shared -Wl,-soname libviso.1 -Wl,--version-script,vis.map -o libvis.so.1.0.0 vis_comm.o vis_f1.o vis_f2.o
[dyp@dyp version_script]$ readelf --syms --use-dynamic libvis.so.1.0.0 | grep vis_
6: 0000000000001149 37 FUNC GLOBAL DEFAULT 13 vis_f2@@VER_1
8: 0000000000001124 37 FUNC GLOBAL DEFAULT 13 vis_f1@@VER_1
```
- 可以看出 `vis_comm()` 不再对外可见了
- ## 符号版本化
collapsed:: true
- 符号版本化允许一个共享库提供同一个函数的多个版本。每个程序会使用它与共享库进行(静态)链接时函数的当前版本。这种处理方式的结果是可以对共享库进行不兼容的改动而无需提升库的主要版本号。从极端的角度来讲,符号版本化可以取代传统的共享库主要和次要版本化模型。glibc 从 2.1 开始使用了这种符号版本化技术,因此 glibc 2.0 以及之前的所有版本都是通过单个主要库版本(`libc.so.6`)来支持的
- 下面通过一个简单的例子来展示符号版本化的用途。首先使用一个版本脚本来创建共享库的第一个版本
- 在一个 `sv_lib_v1.c` 文件键入
- ```c
#include <stdio.h>
void xyz(void) {
printf("v1 xyz\n");
}
```
-`sv_v1.map` 中键入
- ```c
VER_1 {
global: xyz;
local: *;
};
```
- 为了使例子尽量简单点,这里没有使用显式的库 soname 和库主要版本号
- ```shell
[dyp@dyp symbolic_version]$ gcc -g -c -fPIC -Wall sv_lib_v1.c
[dyp@dyp symbolic_version]$ gcc -g -shared -o libsv.so sv_lib_v1.o -Wl,--version-script,sv_v1.map
```
- 在这个阶段,版本脚本 `sv_v1.map` 只用来控制共享库的符号的可见性,即只导出 `xyz()`,同时隐藏其他所有符号(在这个简短的例子中没有其他符号了)。接着创建一个程序 `p1` 来使用这个库
id:: 65001349-5ac5-439f-93f2-64ea09bcb0e7
- ```c
#include <stdlib.h>
int main(int argc, char *argv[]) {
void xyz(void);
xyz();
exit(EXIT_SUCCESS);
}
```
- ```shell
[dyp@dyp symbolic_version]$ gcc -g -o p1 sv_prog.c libsv.so
[dyp@dyp symbolic_version]$ ./p1
./p1: error while loading shared libraries: libsv.so: cannot open shared object file: No such file or directory
[dyp@dyp symbolic_version]$ LD_LIBRARY_PATH=. ./p1
v1 xyz
```
- 现在假设需要修改库中 `xyz()` 的定义,但同时仍然需要确保程序 `pl` 继续使用老版本的函数。为完成这个任务,必须要在库中定义两个版本的 `xyz()`。我们在 `sv_lib_v2.c` 中键入
- ```c
#include <stdio.h>
__asm__(".symver xyz_old,xyz@VER_1");
__asm__(".symver xyz_new,xyz@@VER_2");
void xyz_old(void) {
printf("v1 xyz\n");
}
void xyz_new(void) {
printf("v2 xyz\n");
}
void pqr(void) {
printf("v2 pqr\n");
}
```
- 这里两个版本的 `xyz()` 是通过函数 `xyz_old()``xyz_new()` 来实现的
- `xyz_old()` 函数对应于原来的 `xyz()` 定义,`p1` 程序应该继续使用这个函数
- `xyz_new()` 函数提供了与库的新版本进行链接的程序所使用的` xyz()` 的定义
- 修改过的版本脚本(稍后给出)中的两个 `.symver` 汇编器指令将这两个函数绑定到了两个不同的版本标签上,下面将使用这个脚本来创建共享库的新版本。第一个指令指示与版本标签 `VER_1` 进行链接的应用程序(即程序 `p1`)所使用的 `xyz()` 的实现是 `xyz_old()`,与版本标签 `VER_2` 进行链接的应用程序所使用的 `xyz()` 的实现是 `xyz_new()`
- 第二个`.symver` 指令使用`@@`(不是`@`)来指示当应用程序与这个共享库进行静态链接时应该使用的 `xyz()` 的默认定义。一个符号的 `.symver` 指令中应该只有一个指令使用 `@@` 标记
- 下面是与修改过之后的库对应的版本脚本
- ```
VER_1 {
global:
xyz;
local:
*;
};
VER_2 {
global:
pqr;
} VER_1; # 标记依赖 VER_1
```
- 这个版本脚本提供了一个新版本标签 `VER_2`,它依赖于标签` VER_1`。这种依赖关系是通过 `} VER_1;` 进行标记的
- 现在按照以往方式构建库的新版本
- ```shell
[dyp@dyp symbolic_version]$ gcc -g -c -fPIC -Wall sv_lib_v2.c
[dyp@dyp symbolic_version]$ gcc -g -shared -o libsv.so sv_lib_v2.o -Wl,--version-script,sv_v2.map
```
- 现在创建一个新程序` p2`,它使用了 `xyz()` 的新定义,同时程序 `p1` 使用了旧版的 `xyz()`
- ```shell
[dyp@dyp symbolic_version]$ gcc -g -o p2 sv_prog.c libsv.so
[dyp@dyp symbolic_version]$ LD_LIBRARY_PATH=. ./p2
v2 xyz
[dyp@dyp symbolic_version]$ LD_LIBRARY_PATH=. ./p1
v1 xyz
```
- [[$green]]==可执行文件的版本标签依赖是在静态链接时进行记录的==。使用 `objdump –t` 可以打印出每个可执行文件的符号表,从而能够显示出两个程序中不同的版本标签依赖
- ```shell
[dyp@dyp symbolic_version]$ objdump -t p1 | grep xyz
0000000000000000 F *UND* 0000000000000000 xyz@VER_1
[dyp@dyp symbolic_version]$ objdump -t p2 | grep xyz
0000000000000000 F *UND* 0000000000000000 xyz@VER_2
```
- ## 初始化和终止函数
- [[#green]]==可以定义一个或多个在 **共享库被加载和卸载时自动执行** 的函数,这样在使用共享库时就能够完成一些初始化和终止工作了==。不管库是自动被加载还是使用 `dlopen` 接口显式加载的,初始化函数和终止函数都会被执行
- 初始化和终止函数是使用 `gcc``constructor``destructor` 特性来定义的
- 在库被加载时需要执行的所有函数都应该定义成下面的形式
- ```c
```
-
-
-
-
\ No newline at end of file