1. Linux编译介绍
Linux的操作系统提供了一系列的接口,这些接口被称为系统调用(System Call)。在UNIX的理念中,系统调用"提供的是机制,而不是策略"。C语言的库函数通过调用系统调用来实现,库函数对上层提供了C语言库文件的接口。在应用程序层,通过调用C语言库函数和系统调用来实现功能。一般来说,应用程序大多使用C语言库函数实现其功能,较少使用系统调用。
那么最后的可执行文件到底是什么样子呢?前面已经说过,这里我们不深入分析ELF文件的格式,只是给出它的一个结构图和一些简单的说明,以方便大家理解。
ELF文件格式包括三种主要的类型:可执行文件、可重定向文件、共享库。
1.可执行文件(应用程序)
可执行文件包含了代码和数据,是可以直接运行的程序。
2.可重定向文件(*.o)
可重定向文件又称为目标文件,它包含了代码和数据(这些数据是和其他重定位文件和共享的object文件一起连接时使用的)。
.o文件参与程序的连接(创建一个程序)和程序的执行(运行一个程序),它提供了一个方便有效的方法来用并行的视角看待文件的内容,这些.o文件的活动可以反映出不同的需要。
Linux下,我们可以用gcc -c编译源文件时可将其编译成*.o格式。
3.共享文件(*.so)
也称为动态库文件,它包含了代码和数据(这些数据是在连接时候被连接器ld和运行时动态连接器使用的)。动态连接器可能称为ld.so.1,libc.so.1或者 ld-linux.so.1。我的CentOS6.0系统中该文件为:/lib/ld-2.12.so
2. 制作动态库和静态库
2.1. 示例代码
hello.c
#include <stdio.h>
void hello(void)
{
printf("Hello, library world./n");
}
hello.h
#ifndef __HELLO_H__
#define __HELLO_H__
void hello(void);
#endif
main.c
#include <stdio.h>
#include "hello.h"
int main(){
printf("call hello()");
hello();
}
2.2. 制作静态库
➜ sample✗ gcc -c hello.c -o hello.o
➜ sample✗ ar -rsv libhello.a hello.o
➜ sample✗ ls -al
-rw-rw-r-- 1 ubuntu ubuntu 6706 Jul 13 18:56 libhello.a
-rw-r--r-- 1 ubuntu ubuntu 3057 Jul 6 09:41 libhello.cpp
-rw-rw-r-- 1 ubuntu ubuntu 6280 Jul 13 18:56 libhello.o
-rw-r--r-- 1 ubuntu ubuntu 519 Jul 6 09:41 Makefile
➜ sample✗ file libhello.a
libhello.a: current ar archive
➜ sample✗ ar -t libhello.a
libhello.o
➜ sample✗ nm libhello.a
libhello.o:
0000000000000236 T _Z10CreateListPii
00000000000003d2 T _Z10GetListLenP8ListNode
000000000000047d T _Z11ListReverseP8ListNode
000000000000036d T _Z14InsertListHeadPP8ListNodei
0000000000000000 T _Z14InsertListTailPP8ListNodei
0000000000000079 T _Z17CreateHeadPointerv
00000000000000cb T _Z21CreateHeadPointerListPii
000000000000057e t _Z41__static_initialization_and_destruction_0ii
00000000000002e7 T _Z7GetElemP8ListNodeiPi
000000000000041f T _Z9ClearListPP8ListNode
00000000000004ff T _Z9PrintListP8ListNode
U _ZNSt8ios_base4InitC1Ev
U _ZNSt8ios_base4InitD1Ev
U _Znwm
0000000000000000 r _ZStL19piecewise_construct
0000000000000000 b _ZStL8__ioinit
2.3. 制作动态库
2.3.1. 第一种:没有指定soname
➜ sample✗ gcc -fPIC -shared -o libhello.so hello.c
➜ sample✗ ls -al
-rw-rw-r-- 1 ubuntu ubuntu 6706 Jul 13 18:56 libhello.a
-rw-r--r-- 1 ubuntu ubuntu 3057 Jul 6 09:41 libhello.cpp
-rw-rw-r-- 1 ubuntu ubuntu 6280 Jul 13 18:56 libhello.o
-rwxrwxr-x 1 ubuntu ubuntu 17520 Jul 13 19:02 libhello.so
2.3.2. 第一种:指定soname
gcc -fPIC -shared -o libhello.so hello.c -I./
gcc -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0 hello.o
**-shared:**该选项指定生成动态连接库(让连接器生成T类型的导出符号表,有时候也生成弱连接W类型的导出符号),不用该标志外部程序无法连接。相当于一个可执行文件。
**-fPIC:**表示编译为位置独立的代码,不用此选项的话编译后的代码是位置相关的所以动态载入时是通过代码拷贝的方式来满足不同进程的需要,而不能达到真正代码段共享的目的。
3. 动态库的版本
3.1. linux下的动态库命名规范
首先是共享库本身的文件名:共享库的命名必须如 libname.so.x.y.z最前面使用前缀”lib”,中间是库的名字和后缀”.so”,最后三个数字是版本号。x是主版本号(Major Version Number),y是次版本号(Minor Version Number),z是发布版本号(Release Version Number)。
- 主版本号(不兼容):重大升级,不同主版本的库之间的库是不兼容的。所以如果要保证向后兼容就不能删除旧的动态库的版本。
- 次版本号(向下兼容): 增量升级,增加一些新的接口但保留原有接口。高次版本号的库向后兼容低次版本号的库。
- 发布版本号(相互兼容):库的一些诸如错误修改、性能改进等,不添加新接口,也不更改接口。主版本号和次版本号相同的前提下,不同发布版本之间完全兼容。
3.1.1. 共享库本身的文件名(Real Name)
其通常包含完整的版本号,比如:libmath.so.1.1.1234 。lib是Linux库的约定前缀,math是共享库名字,so是共享库的后缀名,1.1.1234的是共享库的版本号,由主版本号+小版本号+build号组成。主版本号,代表当前动态库的版本,如果共享库的接口发生变化,那么这个版本号就要加1;后面的两个版本号(小版本号和 build号)是用来指示库的更新迭代号,表示在接口没有改变的情况下,由于需求发生变化等因素,开发的新代码。
不同类型的库可能有相同的链接名,比如C语言运行库有静态版本(libc.a)也动态版本(libc.so.x.y.z)的区别,如果在链接时使用参数”-lc”,那么连接器就会根据输出文件的情况(动态/静态)来选择合适版本的库。eg. ld使用“-static”参数时吗,”-lc”会查找libc.a;如果使用“-Bdynamic”(默认),会查找最新版本的libc.so.x.y.z。
3.1.1. 共享库的(so-name)
用来告诉应用程序,在加载共享库的时候,应该使用的文件名。其格式为lib + math + .so + (major version number)其只包含主版本号,换句话说,也就是只要共享库的接口没有变,so-name就能与real name保持一致,因为主版本号一样。所以在库的real name的小版本号和 build号发生改变时,应用程序仍然可以通过soname得知,要使用的是哪个real name。
Solaris和Linux等采用SO-NAME( Shortfor shared object name )的命名机制来记录共享库的依赖关系。每个共享库都有一个对应的“SO-NAME”(共享库文件名去掉次版本号和发布版本号)。比如一个共享库名为libtest.so.3.8.2,那么它的SO-NAME就是libtest.so.3。
建立以SO-NAME为名字的软连接的目的是,使得所有依赖某个共享库的模块,在编译、链接和运行时,都使用共享库的SO-NAME,而不需要使用详细版本号。在编译生产ELF文件时候,如果文件A依赖于文件B,那么A的链接文件中的”.dynamic”段中会有DT_NEED类型的字段,字段的值就是B的SO-NAME。这样当动态链接器进行共享库依赖文件查找时,就会依据系统中各种共享库目录中的SO-NAME软连接自动定向到最新兼容版本的共享库。
3.1.1. 共享库的链接名(Link Name)
是专门为应用程序在编译时的链接阶段而用的名字。这个名字就是lib + math +.so ,比如libmath.so。其是不带任何版本信息的。在共享库的编译过程中,编译器将生成一个共享库及real name,同时将共享库的soname写在共享库文件里的文件头里面。可以用命令readelf -d sharelibrary | grep soname查看。
在应用程序引用共享库时,链接选项里面用的是共享库的link name。通过link名字找到对应的real name动态库,并且把其中的soname提取出来,写在应用程序自己的文件头的共享库字段里面。当应用程序运行时,就会通过soname,结合动态链接程序(ld.so),在给定的路径下加载real name的共享库。
4. 动态库升级
4.1 共享库,小版本升级,即接口不变
- 低版本
- 编译
gcc -fPIC -shared -o libhello.so hello.c -I./
gcc -shared -Wl,-soname,libhello.so.1 -o libhello.so.1.0.0 hello.o
gcc -shared -o libhello.so
gcc -fPIC -shared hello.c -I./ -Wl,-soname,libhello.so.1 -o libhello.so.1.0.0 hello.o
- 查看链接
lrwxrwxrwx 1 ubuntu ubuntu 13 Jul 14 18:37 libhello.so -> libhello.so.1
lrwxrwxrwx 1 ubuntu ubuntu 17 Jul 14 18:37 libhello.so.1 -> libhello.so.1.0.0
-rwxrwxr-x 1 ubuntu ubuntu 16200 Jul 14 18:36 libhello.so.1.0.0
- 高版本
#include <stdio.h>
void hello(void)
{
printf("Hello, library world.\n");
printf("this is a new building version\n");
}
- 编译
gcc -fPIC -shared hello.c -I./ -Wl,-soname,libhello.so.1 -o libhello.so.1.0.1
- 查看链接
lrwxrwxrwx 1 ubuntu ubuntu 13 Jul 14 18:37 libhello.so -> libhello.so.1
lrwxrwxrwx 1 root root 17 Jul 14 18:39 libhello.so.1 -> libhello.so.1.0.1
-rwxrwxr-x 1 ubuntu ubuntu 16200 Jul 14 18:36 libhello.so.1.0.0
-rwxrwxr-x 1 ubuntu ubuntu 16200 Jul 14 18:39 libhello.so.1.0.1
- 运行可执行文件
➜ sample✗ ./main
call hello()Hello, library world.
this is a new building version
注意,按照约定,由于新的版本只是修改了接口,可以兼容之前的版本,所以soname并不需要改变。生成新的real name库以后,我们只需要执行ldconfig,即可自动更新soname到新real name库的软链接。不需要重新编译main执行文件
4.2 共享库,小版本升级,即接口不变
- 高版本
#include <stdio.h>
void print_hello2()
{
printf("this is a new release version");
}
void hello(void)
{
printf("Hello, library world.\n");
printf("this is a new building version\n");
}
- 编译
gcc -fPIC -shared hello.c -I./ -Wl,-soname,libhello.so.2 -o libhello.so.2.0.0
- 查看链接
rwxrwxrwx 1 ubuntu ubuntu 13 Jul 14 18:37 libhello.so -> libhello.so.1
lrwxrwxrwx 1 root root 17 Jul 14 18:39 libhello.so.1 -> libhello.so.1.0.1
-rwxrwxr-x 1 ubuntu ubuntu 16200 Jul 14 18:36 libhello.so.1.0.0
-rwxrwxr-x 1 ubuntu ubuntu 16200 Jul 14 18:39 libhello.so.1.0.1
lrwxrwxrwx 1 ubuntu ubuntu 17 Jul 14 18:55 libhello.so.2 -> libhello.so.2.0.0
-rwxrwxr-x 1 ubuntu ubuntu 16288 Jul 14 18:55 libhello.so.2.0.0
生成新的so
ln -s libhello.so.2 libhello.so
lrwxrwxrwx 1 ubuntu ubuntu 13 Jul 14 18:56 libhello.so -> libhello.so.2
lrwxrwxrwx 1 root root 17 Jul 14 18:39 libhello.so.1 -> libhello.so.1.0.1
-rwxrwxr-x 1 ubuntu ubuntu 16200 Jul 14 18:36 libhello.so.1.0.0
-rwxrwxr-x 1 ubuntu ubuntu 16200 Jul 14 18:39 libhello.so.1.0.1
lrwxrwxrwx 1 ubuntu ubuntu 17 Jul 14 18:55 libhello.so.2 -> libhello.so.2.0.0
-rwxrwxr-x 1 ubuntu ubuntu 16288 Jul 14 18:55 libhello.so.2.0.0
- 运行可执行文件
➜ sample✗ ldd main
linux-vdso.so.1 (0x00007fff4539f000)
libhello.so.1 => /home/ubuntu/sample/libhello.so.1 (0x00007f52ac901000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f52ac70f000)
/lib64/ld-linux-x86-64.so.2 (0x00007f52ac915000)
注意:尽管共享库升级,但是你的程序依旧用的是旧的共享库,并且两个之间不会相互影响
问题是如果更新的共享库只是增加一些接口,并没有修改已有的接口,也就是向前兼容。但是这时候它的主版本号却增加1. 如果你的应用程序想调用新的共享库,该怎么办? 简单,只要手工把soname 文件修改,使其指向新的版本就可以。(这时候ldconfig 文件不会帮你做这样的事,因为这时候soname 和real name 的版本号主板本号不一致,只能手动修改)。
➜ sample✗ ln -s libhello.so.2.0.0 libhello.so.1
➜ sample✗ ls -al
lrwxrwxrwx 1 ubuntu ubuntu 13 Jul 14 18:56 libhello.so -> libhello.so.2
lrwxrwxrwx 1 ubuntu ubuntu 17 Jul 14 19:05 libhello.so.1 -> libhello.so.2.0.0
lrwxrwxrwx 1 ubuntu ubuntu 17 Jul 14 18:55 libhello.so.2 -> libhello.so.2.0.0
-rwxrwxr-x 1 ubuntu ubuntu 16288 Jul 14 18:55 libhello.so.2.0.0
➜ sample✗ ldd main
linux-vdso.so.1 (0x00007fff287c8000)
libhello.so.1 => /home/ubuntu/sample/libhello.so.1 (0x00007f21c3f1b000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f21c3d29000)
/lib64/ld-linux-x86-64.so.2 (0x00007f21c3f2f000)
➜ sample✗ ./main
call hello()Hello, library world.
this is a new building version
但是有时候,主版本号增加,接口发生变化,可能向前不兼容。这时候再这样子修改,就会报错,“xx”方法找不到之类的错误。
4.3. 编译的时候没有指定共享库的soname
- 编译
➜ sample✗ gcc -fPIC -shared -o libhello.so hello.c
➜ sample✗ ls
hello.c hello.h libhello.so main.c
- 查看soname
➜ sample✗ readelf -d libhello.so
Dynamic section at offset 0x2e20 contains 24 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
这是一个trick 的地方。第一系统将会在生成库的时候,就没有soname放到库的头里面。从而应用程序连接时候,就把linkname放到应用程序依赖库里面。或者换句话说就是,soname这时候不带版本号。 有时候有人直接利用这点来升级应用程序,比如,新版本的库,直接拷贝到系统目录下,就会覆盖掉已经存在的旧的库文件,直接升级。 这个给程序员很大程度的便利性,如果一步小心,就会调到类似windows的Dll hell 陷阱里面。建议不要这样做。
5. 动态库的搜索路径
5.1. 动态库的搜索
5.1.1. 搜索顺序
- LD_LIBRARY_PATH环境变量中的路径
- /etc/ld.so.cache缓存文件
- /usr/lib和/lib
众所周知, Linux 动态库的默认搜索路径是 /lib 和 /usr/lib。动态库被创建后,一般都复制到这两个目录中。当程序执行时需要某动态库, 并且该动态库还未加载到内存中,则系统会自动到这两个默认搜索路径中去查找相应的动态库文件,然后加载该文件到内存中,这样程序就可以使用该动态库中的函 数,以及该动态库的其它资源了。在 Linux 中,动态库的搜索路径除了默认的搜索路径外,还可以通过以下三种方法来指定。
5.1.2. 指定搜索路径
方法一:
在配置文件 /etc/ld.so.conf 中指定动态库搜索路径。每次编辑完该文件后,都必须运行命令 ldconfig 使修改后的配置生效 。
方法二:
通过环境变量 LD_LIBRARY_PATH 指定动态库搜索路径。
export DYLD_LIBRARY_PATH=/Users/user/Desktop
方法三:
在编译目标代码时指定该程序的动态库搜索路径。
还可以在编译目标代码时指定程序的动态库搜索路径。 -Wl, 表示后面的参数将传给 link 程序 ld (因为 gcc 可能会自动调用ld )。这里通过 gcc 的参数 “-Wl,-rpath,” 。当指定多个动态库搜索路径时,路径之间用冒号 " : " 分隔。
6. 动态库的使用
6.1. 动态库的使用
6.1.1. 第一种
gcc main.c -lhello -L./ -o main
6.1.2. 第二种
-
重要的dlfcn.h头文件
LINUX下使用动态链接库,源程序需要包含dlfcn.h头文件,此文件定义了调用动态链接库的函数的原型。下面详细说明一下这些函数。 -
dlerror
原型为:
const char *dlerror(void);
当动态链接库操作函数执行失败时,dlerror可以返回出错信息,返回值为NULL时表示操作函数执行成功。
- dlopen
原型为:
void *dlopen (const char *filename, intflag);
dlopen用于打开指定名字(filename)的动态链接库,并返回操作句柄。
filename: 如果名字不以/开头,则非绝对路径名,将按下列先后顺序查找该文件。
- 用户环境变量中的LD_LIBRARY值;
- 动态链接缓冲文件/etc/ld.so.cache
- 目录/lib,/usr/lib
flag表示在什么时候解决未定义的符号(调用)。取值有两个: - RTLD_LAZY : 表明在动态链接库的函数代码执行时解决。
- RTLD_NOW : 表明在dlopen返回前就解决所有未定义的符号,一旦未解决,dlopen将返回错误。
dlopen调用失败时,将返回NULL值,否则返回的是操作句柄。
- dlsym : 取函数执行地址
原型为:
void *dlsym(void *handle, char*symbol);
dlsym根据动态链接库操作句柄(handle)与符号(symbol),返回符号对应的函数的执行代码地址。由此地址,可以带参数执行相应的函数。
如程序代码: void (add)(int x,int y); / 说明一下要调用的动态函数add */
add=dlsym("xxx.so","add");/* 打开xxx.so共享库,取add函数地址 */
add(89,369); /* 带两个参数89和369调用add函数 */
- dlclose : 关闭动态链接库
原型为:
int dlclose (void *handle);
dlclose用于关闭指定句柄的动态链接库,只有当此动态链接库的使用计数为0时,才会真正被系统卸载。
7. 动态库的查看工具
7.1. readelf调试工具
readelf参数较多,可直接通过readelf -h获取readelf的所有参数及用法,此处只对其中的几个常用参数以及数据详解。
- readelf -h
➜ sample✗ readelf -h libhello.so
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Shared object file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x1080
Start of program headers: 64 (bytes into file)
Start of section headers: 14376 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 11
Size of section headers: 64 (bytes)
Number of section headers: 30
Section header string table index: 29
- readelf -d
➜ sample✗ readelf -d main
➜ sample✗ readelf -d main | grep libhello
Dynamic section at offset 0x2db0 contains 28 entries:
Tag Type Name/Value
0x0000000000000001 (NEEDED) Shared library: [libhello.so.1]
0x0000000000000001 (NEEDED) Shared library: [libc.so.6]
0x000000000000000c (INIT) 0x1000
0x000000000000000d (FINI) 0x1208
0x0000000000000019 (INIT_ARRAY) 0x3da0
0x000000000000001b (INIT_ARRAYSZ) 8 (bytes)
0x000000000000001a (FINI_ARRAY) 0x3da8
0x000000000000001c (FINI_ARRAYSZ) 8 (bytes)
0x000000006ffffef5 (GNU_HASH) 0x3a0
0x0000000000000005 (STRTAB) 0x488
0x0000000000000006 (SYMTAB) 0x3c8
7.2. ldd调试工具
7.2.1. 参数
--version:打印指令版本号;
-v:详细信息模式,打印所有相关信息;
-u:打印未使用的直接依赖;
-d:执行重定位和报告任何丢失的对象;
-r:执行数据对象和函数的重定位,并且报告任何丢失的对象和函数;
--help:显示帮助信息。
➜ sample✗ ldd main
linux-vdso.so.1 (0x00007ffd163e4000)
libhello.so.1 => not found
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f1c9cfe9000)
/lib64/ld-linux-x86-64.so.2 (0x00007f1c9d1ea000)
➜ sample✗
7.3. nm调试工具
7.3.1. 参数说明
NAME
nm - list symbols from object files
SYNOPSIS
nm [-A|-o|--print-file-name] [-a|--debug-syms]
[-B|--format=bsd] [-C|--demangle[=style]]
[-D|--dynamic] [-fformat|--format=format]
[-g|--extern-only] [-h|--help]
[-l|--line-numbers] [-n|-v|--numeric-sort]
[-P|--portability] [-p|--no-sort]
[-r|--reverse-sort] [-S|--print-size]
[-s|--print-armap] [-t radix|--radix=radix]
[-u|--undefined-only] [-V|--version]
[-X 32_64] [--defined-only] [--no-demangle]
[--plugin name] [--size-sort] [--special-syms]
[--synthetic] [--target=bfdname]
[objfile...]
DESCRIPTION
GNU nm lists the symbols from object files objfile.... If no object
files are listed as arguments, nm assumes the file a.out.
For each symbol, nm shows:
# ... run man nm for detials.
7.3.2. 输出说明
Value | Descripition | Note |
---|---|---|
A | The symbol’s value is absolute, and will not be changed by further linking. | 符号绝对,链接过程不会改变 |
B/b | The symbol is in the uninitialized data section (known as BSS). | 非初始化符号 |
C | The symbol is common. | 公有符号,链接时会被同名符号覆盖 |
D/d | The symbol is in the initialized data section. | 初始化符号 |
G/g | The symbol is in an initialized data section for small objects. | 初始化符号,面向小数据访问优化 |
I | The symbol is an indirect reference to another symbol. | 其它符号的间接引用 |
N | The symbol is a debugging symbol. | 调试符号 |
P | The symbols is in a stack unwind section. | 栈区符号(清空) |
R/r | The symbol is in a read only data section. | 符号只读 |
S/s | The symbol is in an uninitialized data section for small objects. | 非初始化符号,面向小数据访问优化 |
T/t | The symbol is in the text (code) section. | 代码区符号 |
U | The symbol is undefined. | 未定义或在外部定义的符号 |
u | The symbol is a unique global symbol. | 全局唯一,GNU保留符 |
V/v | The symbol is a weak object. | 弱定义符(详见C++强弱符号定义) |
W/w | The symbol is a weak symbol that has not been specifically tagged as a weak object symbol. | emm…绕口令符号 |
7.3.3. 实例说明
➜ sample✗ cat main.c
#include <stdio.h>
#include "hello.h"
int a;
int b = 1;
int main(){
printf("call hello()");
hello();
}
➜ sample✗ gcc -fPIC -shared -o libhello.so hello.c
➜ sample✗ nm libhello.so
0000000000004030 b completed.8060
w __cxa_finalize@@GLIBC_2.2.5
0000000000001080 t deregister_tm_clones
00000000000010f0 t __do_global_dtors_aux
0000000000003e18 d __do_global_dtors_aux_fini_array_entry
0000000000004028 d __dso_handle
0000000000003e20 d _DYNAMIC
0000000000001178 t _fini
0000000000001130 t frame_dummy
0000000000003e10 d __frame_dummy_init_array_entry
0000000000002140 r __FRAME_END__
0000000000004000 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
0000000000002058 r __GNU_EH_FRAME_HDR
0000000000001155 T hello
0000000000001000 t _init
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
U printf@@GLIBC_2.2.5
0000000000001139 T print_hello2
U puts@@GLIBC_2.2.5
00000000000010b0 t register_tm_clones
0000000000004030 d __TMC_END__
➜ sample✗ gcc main.c -lhello -L./ -o main
➜ sample✗ nm main
0000000000004018 B a **非初始化符号**
0000000000004010 D b **初始化符号**
0000000000004014 B __bss_start
0000000000004014 b completed.8060
w __cxa_finalize@@GLIBC_2.2.5
0000000000004000 D __data_start
0000000000004000 W data_start
00000000000010b0 t deregister_tm_clones
0000000000001120 t __do_global_dtors_aux
0000000000003da8 d __do_global_dtors_aux_fini_array_entry
0000000000004008 D __dso_handle
0000000000003db0 d _DYNAMIC
0000000000004014 D _edata
0000000000004020 B _end
0000000000001208 T _fini
0000000000001160 t frame_dummy
0000000000003da0 d __frame_dummy_init_array_entry
000000000000215c r __FRAME_END__
0000000000003fb0 d _GLOBAL_OFFSET_TABLE_
w __gmon_start__
0000000000002014 r __GNU_EH_FRAME_HDR
U hello
0000000000001000 t _init
0000000000003da8 d __init_array_end
0000000000003da0 d __init_array_start
0000000000002000 R _IO_stdin_used
w _ITM_deregisterTMCloneTable
w _ITM_registerTMCloneTable
0000000000001200 T __libc_csu_fini
0000000000001190 T __libc_csu_init
U __libc_start_main@@GLIBC_2.2.5
0000000000001169 T main
U printf@@GLIBC_2.2.5
00000000000010e0 t register_tm_clones
0000000000001080 T _start
0000000000004018 D __TMC_END__
https://tldp.org/HOWTO/Program-Library-HOWTO/shared-libraries.html