Top

类Unix系统链接库符号表及可见性控制详解


本文属于原创,转载注明出处,欢迎关注微信小程序小白AI博客或者网站 https://xiaobaiai.net

关注我的公众号,获取最新学习分享:

1 相关术语

术语 释义
Interface 接口,它是功能和该功能的使用者之间的“现有实体”层。接口本身不执行任何操作,它只是由“消费者”调用后面的功能。
API 应用程序编程接口,函数充当“现有实体”,“消费者”为另外一个程序
CLI 命令行界面,“消费者”是使用者,是用户,实体是“命令”
GUI 图形界面,“消费者”也是使用者,是用户,实体是窗口、按钮等
COFF COFF是由Unix System V Release 3首先提出并且使用的格式规范,后来微软公司基于COFF格式,制定了PE格式标准;System V Release 4在COFF的基础上引入了ELF格式
ELF ELF是定义ELF文件格式的可执行和链接格式,是UNIX类操作系统中普遍采用的目标文件格式,Windows平台下是PE文件格式;IOS使用的是 mach-o文件格式;因此Windows和类Linux平台可执行文件是无法兼容的,因为目标文件格式不一样;
ABI 应用程序二进制接口,可以类比API,ABI定义了编译的应用程序将用于访问外部库的结构和方法(就像API一样),仅在较低级别上,如操作系统级别,CPU级别。编译器和链接器也会遵守ABI规则,是一个程序调用标准,可以让不同编译器编译或交叉编译器编译出的程序可以跑在同一系统或另一个系统上。常见的ABI规定有,x86-64 CPU上的SYSTEM-V/MSNATIVE/VECTORCALL,ARM-32 CPU上的AAPCS,ARM-64 CPU上的AAPCS64,大部分CPU都支持多于一种的ABI。 微软C编译器支持的CDECL(/Gd)/FASTCALL(/Gr)/STDCALL(/Gz);GNU C编译器支持的CDECL/FASTCALL/STDCALL,采用都支持的、广泛的调用约定可以更好开发跨平台程序;
Calling Conventions 调用约定,指定如何将C或C ++中的函数调用转换为汇编语言。指定如何将参数传递给函数,如何将返回值传递回函数,如何调用函数以及函数如何管理堆栈及其堆栈框架,如果不存在这些标准约定,使用不同的编译器创建的程序几乎不可能相互通信和交互。默认情况下,C语言使用CDECL调用约定。
EABI EABI代表嵌入式ABI,它是某些目标(例如PPC)的应用程序二进制接口的定义,指定用于嵌入式操作系统中,如ARM系列。

每个操作系统都有自己的系统调用和内存管理实现。编译器必须在编译时解析所有函数名称或系统调用。加载程序时必须正确解释可执行格式(PE,ELF,COFF等),并根据该OS内存管理器(堆栈,堆等)的设计将其加载到RAM中。编译后的C程序与所有这些特定细节相关联,这就是为什么它不能直接在其他OS上运行的原因,你需要在目标OS上再次重新编译它。

2 外部链接与内部链接

内部连接: 如果一个名称对编译单元(.cpp)来说是局部的,在链接的时候其他的编译单元无法链接到它且不会与其它编译单元(.cpp)中的同样的名称相冲突。例如static函数,inline函数等(注 : 用static修饰的函数,本就限定在本源码文件中,不能被本源码文件以外的代码文件调用。而普通的函数或变量,默认是extern的,也就是说,可以被其它代码文件调用该函数。)

外部连接: 如果一个名称对编译单元(.cpp)来说不是局部的,而在链接的时候其他的编译单元可以访问它,也就是说它可以和别的编译单元交互。例如函数就是外部链接,全局变量也是。

C/C++中常见的内部链接有:

  • 所有被static修饰的全局变量或函数
  • 类的定义(非成员函数),如class Student {string name; …};
  • 枚举类型
  • 内联函数
  • 联合体类型
  • 名字空间中 const 常量
  • 所有声明(声明不会将任何符号引入目标文件)

具有外部链接的有:

  • 非内联的成员函数
  • 非内联、非static修饰的自由函数
  • 非static修饰的全局变量
  • extern const联合修饰时,extern将压制const这个内部链接属性,具有外部链接属性

3 Linux动态库符号表

在计算机科学中,符号表是一种用于语言翻译器(例如编译器和解释器)中的数据结构。 在符号表中,程序源代码中的每个标识符都和它的声明或使用信息绑定在一起,比如其数据类型、作用域以及内存地址。因此,有了符号表,我们能够调用函数,和调试相关函数或变量。

通过命令readelf我们可以读取动态库的符号表,读取后可以获取systabdynsym两个符号表详细信息。

dynsym与symtab符号表:

dynsymsymtab的子集,仅包含全局符号。而symtab包含更多的信息。理解dynsymsymtab两个表之前,我们需要知道allocablenon-allocable ELF sections。ELF文件包含进程在运行时所需的某些sections(例如代码和数据),这些sections被标记为allocable的,有一些其他的sections需要被链接器,调试器或者其他的工具来使用,但是又不被运行时程序所引用,这些信息被标记为non-allocable的。当操作系统加载动态库时,只有allocable部分才会映射到内存,non-allocable只会存在在文件中。使用strip命令可以移除掉symtab符号表。说白了就是我们常用的调试版本符号表信息包含在symtab中,即使你编译了一个debug版本的库或程序,也可以后面通过strip去掉debug信息,从而降低整个库或者可执行程序的大小。

4 可执行文件和动态库相关命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 查看动态库libhello.so符号表
$ readelf –s libhello.so
# 查看是否是debug编译库
$ readelf -sa hello | grep debug
[22] .debug_aranges PROGBITS 0000000000000000 00001052
[23] .debug_info PROGBITS 0000000000000000 00001082
[24] .debug_abbrev PROGBITS 0000000000000000 0000113c
[25] .debug_line PROGBITS 0000000000000000 000011b2
[26] .debug_str PROGBITS 0000000000000000 000011f8
$ readelf --debug-dump a.out | more
# 查看文件类型,对于动态库,可以知道该库是否是带有调试符号表,有没有strip掉 .symtab符号表
$ file libhello.so
libhello.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=4441fbe382cff0cba4d78e337f3c5608aaf0faba, with debug_info, not stripped
# strings命令也可以查看二进制文件中文本信息,不过展示结果比较乱
$ strings libhello.so
# nm可以查看符号表,默认不加参数查看的是symtab符号表信息
$ nm libhello.so
# 查看dynsym符号表信息
$ nm -D libhello.so

5 C/C++符号可见性控制

  1. 目标文件中的符号表用来输出函数/变量符号信息,供链接时给其他模块引用!这种符号表中主要包含函数/变量的名称和地址对应关系,其中的地址一般是位置无关码。因此如枚举、枚举类这种内部链接的类型跟符号表没有直接联系。
  2. gcc制作动态链接库时默认会将所有的函数及变量都导出到符号表,这里的函数及变量指的是没有使用static修饰的,使用static修饰的函数及变量不会导出。
  3. 一个程序或动态库动态库去链接多个动态库,该动态库只会链接已有函数或变量被引用的动态库
  4. 一个程序或动态库去引用其他动态库的一个变量或函数,如果其他动态库均定义了这个变量或函数,那么编译器会根据编译时链接的顺序来决定最终链接哪个动态库(链接顺序以放于前面为高优先级)

可见性控制core.hpp宏定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//core.hpp
#ifndef _CORE_
#define _CORE_

#if defined _WIN32 || defined __CYGWIN__
#ifdef BUILDING_DLL
#ifdef __GNUC__
#define DLL_PUBLIC __attribute__ ((dllexport))
#else
#define DLL_PUBLIC __declspec(dllexport) // Note: actually gcc seems to also supports this syntax.
#endif
#else
#ifdef __GNUC__
#define DLL_PUBLIC __attribute__ ((dllimport))
#else
#define DLL_PUBLIC __declspec(dllimport) // Note: actually gcc seems to also supports this syntax.
#endif
#endif
#define DLL_LOCAL
#else
#if __GNUC__ >= 4
#define DLL_PUBLIC __attribute__ ((visibility ("default")))
#define DLL_LOCAL __attribute__ ((visibility ("hidden")))
#else
#define DLL_PUBLIC
#define DLL_LOCAL
#endif
#endif

可见性控制示例1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// hell.hpp
#ifndef _HELLO_
#define _HELLO_

#include "core.hpp"

#ifdef __cplusplus
extern "C" {
#endif


namespace Test {

class DLL_PUBLIC Hello {
public:
DLL_LOCAL Hello();
~Hello();
DLL_LOCAL void Init();
void Process(const int i);
private:
void PreProcess();
};

}

#ifdef __cplusplus
}
#endif

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// hello.cpp
#include "hello.hpp"

#ifdef __cplusplus
extern "C" {
#endif


namespace Test {

Hello::Hello() {}
Hello::~Hello() {}

void Hello::Init() {

}

void Hello::Process(const int i) {

}

void Hello::PreProcess() {

}

}

#ifdef __cplusplus
}
#endif

编译:

1
2
$ g++ -g -fpic -shared -o libhello.so hello.cpp
$ nm -aD libhello.so | grep "Test"

可见性控制示例2:

1
2
3
4
5
6
7
8
9
// hello.h
#ifndef __HELLO_H
#define __HELLO_H

int hello_init();
int hello_handle();
void hello_exit();

#endif
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// hello.c
#include "hello.h"
#include "core.hpp"

DLL_LOCAL int call_count = 0;

DLL_LOCAL int hello_init()
{
call_count = 0;
return 0;
}

DLL_LOCAL int hello_call_count_add()
{
return ++call_count;
}

int hello_handle()
{
return hello_call_count_add();
}

void hello_exit()
{
call_count = 0;
}

编译:

1
$ gcc -shared -fPIC hello.c -o libhello.so

6 其他方面扩展

LSB标准:

LSB 是 Linux 标准化领域中事实上的标准,更多历史可以参见 https://www.ibm.com/developerworks/cn/linux/l-lsb-intr/#ibm-pcon

编译参数:

  • -fPIC-fpic

-fPIC-fpic都是在编译时加入的选项,用于生成位置无关的代码(Position-Independent-Code)。这两个选项都是可以使代码在加载到内存时使用相对地址,所有对固定地址的访问都通过全局偏移表(GOT)来实现。-fPIC-fpic最大的区别在于是否对GOT的大小有限制。-fPIC对GOT表大小无限制,所以如果在不确定的情况下,使用-fPIC是更好的选择。

  • -fpie/-fPIE

-fPIE-fpie是等价的。这个选项与-fPIC/-fpic大致相同,不同点在于:-fPIC用于生成动态库,-fPIE用与生成可执行文件。再说得直白一点:-fPIE用来生成位置无关的可执行代码。

7 参考

推荐文章



授权:知识共享署名-相同方式共享 4.0 国际许可协议
网站信息: 小白AI.易名
文章标题:类Unix系统链接库符号表及可见性控制详解
永久链接:https://xiaobaiai.net/2019/20190927103015.html
关注公众号:小白AI
关注微信小程序:小白AI博客
微信打赏 支付宝打赏

 发表评论

文明评论,请勿灌水。