【Linux&操作系统】9. 动静态库

49 阅读40分钟

9 动静态库

9.1 为什么需要库文件&什么是库文件

  • 假设你是一个大型项目,一个偏向接口编写的维护者,我需要向开发者提供接口,比方说opencv,DirectX12,它们本身并不是一个大众可用的项目,而是专门编给其他程序员的一套接口,程序员使用这一套接口就可以编写游戏引擎,游戏等等内容

  • 我们如果想提供一套接口给别人,我们会做什么?

情景形式缺点
情景1提供原始的.c.h文件大型项目接口众多,文件也相对较多,甚至高达上千个文件,并且直接给源代码给别人,是非常不安全的事情
情景2提供由.c编译形成的.o文件和.h文件,.o文件是二进制机器码,使得源码不可见但依旧不能解决文件众多的问题,链接时我们需要写的命令太长了,我们需要更加方便的方式
情景3提供一个库文件和一堆.h打包成的压缩包,库文件由全部.o文件链接而成,并且编译器可以直接使用库文件而不用解包自此解决了以上两个痛点
  • 库文件是由.o文件链接形成的,并且编译器可以直接使用库而不用先解包库

  • 所以本质上,库文件就是一个.o的集合,或者说是一堆接口的集合,因此,库文件一般不包含main()函数,如果一个库包含了main(),这个库就不能被使用,因为main()已经存在了,用户此时不能使用main()

  • 而库文件也分为"静态库"和"动态库",分别使用后缀.a.so(即achieve文件和shared object文件)具体的使用我们后面还会再谈到

9.2 如何制作一个静态库文件

  • 我们来看看库文件的制作方式(这里使用.c文件制作库)
$ ls
mystdio.c  mystdio.h  mystring.c  mystring.h
$ ar -rc libmyc.a *.c
$ ls
libmyc.a  mystdio.c  mystdio.h  mystring.c  mystring.h
$ ll
total 20
-rw-rw-r-- 1 oldking oldking 3658 Mar 13 15:52 libmyc.a
-rw-rw-r-- 1 oldking oldking 3361 Mar 12 20:53 mystdio.c
-rw-rw-r-- 1 oldking oldking  548 Mar 12 20:53 mystdio.h
-rw-rw-r-- 1 oldking oldking  167 Mar 12 22:24 mystring.c
-rw-rw-r-- 1 oldking oldking   47 Mar 12 22:24 mystring.h
  • 注意:使用.c制作库文件可能会导致有些语法错误或者一些兼容性错误无法被检查出来,所以尽量不要这样使用

  • .o文件一样可以制作库,并且使用.o文件也是更加规范的方式

$ gcc -c *.c
$ ls
mystdio.c  mystdio.h  mystdio.o  mystring.c  mystring.h  mystring.o
$ ar -rc libmyc.a *.o
$ ls
libmyc.a  mystdio.c  mystdio.h  mystdio.o  mystring.c  mystring.h  mystring.o
  • 即命令ar就是库文件的制作命令,或者说是gru归档工具,代表archive,即"归档"

  • 选项-rc代表replacecreate,replace的意思是如果源文件有变动,就对库里的变动部分做替换,而create的意思是创建文件,即如果库里没有该文件,此文件就会被添加进库

$ ar [options] [dst file] [src file]
  • 关于库文件的命名规范:
    1. 库文件的命名一定是固定的,是有一定规范的,不能乱更改
    2. 库文件的命名由 "前缀":lib "文件名":filename "后缀":.a/.so 组成
    3. 只有中间的文件名我们可以随意更改,前缀和后缀都是不能更改的,否则在使用库文件的时候可能会导致库文件无法被识别

9.3 如何使用一个静态库文件

//用户需要调用我们写的库接口
#include<stdio.h>
#include"mystdio.h"
#include<string.h>

int main()
{
    MYFILE* pfile = myFopen("log.txt", "w");
    //printf("%p", pfile);

    const char* str = "oldking is H\n";
    
    int count = 100;
    while(count--)
    {
        //int sum = myFwrite(str, strlen(str), 1, pfile); 
        myFwrite(str, strlen(str), 1, pfile); 
    }
    myFflush(pfile);
    //printf("%d", sum);
   
    //printf("%d\n", myFclose(pfile));
    //printf("%d\n", pfile->fileno);
    //printf("")
    //printf("%d", (int)myFwrite(str, strlen(str), 1, pfile)); 

    return 0;
}
$ ls
C_file  libc  log.txt  Makefile  usercode.c
$ tree libc/
libc/
├── include
│   ├── mystdio.h
│   └── mystring.h
└── lib
    └── libmyc.a

2 directories, 3 files
$ gcc usercode.c -I ./libc/include/ -L ./libc/lib/ -l myc -o test
$ ll
total 36
drwxrwxr-x 2 oldking oldking  4096 Mar 13 16:05 C_file
drwxrwxr-x 4 oldking oldking  4096 Mar 13 12:48 libc
-rw-rw-r-- 1 oldking oldking  1300 Mar 13 16:30 log.txt
-rw-rw-r-- 1 oldking oldking    69 Mar 13 12:21 Makefile
-rwxrwxr-x 1 oldking oldking 13152 Mar 13 16:33 test
-rw-rw-r-- 1 oldking oldking   575 Mar 13 16:28 usercode.c
$ ./test
$ ls
C_file  libc  log.txt  Makefile  test  usercode.c
$ cat log.txt 
oldking is H
oldking is H
oldking is H
oldking is H
# ...
  • 使用库文件相比正常编译链接,增加了几个选项
gcc usercode.c -I [head file path] -L [lib file path] -l [lib name] -o test

# -I: 表示头文件路径,后面跟头文件路径
# -L: 表示库文件路径,后面跟库文件路径
# -l:表示库文件名,后面跟库文件名
  • 如果你的头文件和usercode.c在同一目录,那么就可以不用加关于头文件目录的选项和路径
9.3.1 库文件安装(拷贝)
  • 当然,如果每次编译都要加库的路径和头文件路径啥的,未免有点太麻烦了

  • 值得一提的是,我们从很早就学习了C语言,我们也知道我们一定使用了C语言的库文件,所以意味着,系统中一定有某个目录存放这些库文件,并且这个目录会作为ar默认寻找库文件的目录,以达到每次都不需要加目录的效果

  • 头文件也是如此

  • 事实上,安装库文件的目录是比较多的,目录设计上为它们分了类

  • 该表来自Deepseek:

目录用途
/lib存放系统启动和基本命令依赖的核心动态库(如glibc)
/lib64在 64 位系统中存放 64 位核心库(某些发行版如 CentOS/RHEL 使用此目录)
/usr/lib用户安装的非核心动态库和静态库(如第三方应用程序的依赖库)
/usr/lib6464 位系统的非核心库(某些发行版使用)
/usr/local/lib用户手动编译安装的库(通常通过 make install 默认安装到此目录)
  • 头文件也是如此
  • 该表依然来自Deepseek:
目录用途
/usr/include系统或软件包管理器安装的公共头文件(如标准库,第三方库的头文件)
/usr/local/include用户手动编译安装的软件的头文件(如通过make install默认安装到此目录)
/usr/include/<子目录>某些库的头文件会按名称归类到子目录(例如/usr/include/openssl)
  • 我们可以手动拷贝库进去试试
$ tree libc
libc
├── include
│   ├── mystdio.h
│   └── mystring.h
└── lib
    └── libmyc.a

2 directories, 3 files
$ sudo cp ./libc/lib/libmyc.a /usr/local/lib
[sudo] password for oldking: 
$ ls
C_file  libc  log.txt  Makefile  test  usercode.c
$ ls /usr/local/lib
libmyc.a
$ rm test
$ ls /usr
bin  etc  games  include  lib  lib64  libexec  local  sbin  share  src  tmp
$ sudo cp libc/include/*.h /usr/include/
$ gcc usercode.c -l myc -o test
$ ls
C_file  libc  log.txt  Makefile  test  usercode.c
$ rm log.txt 
$ ./test
$ ls
C_file  libc  log.txt  Makefile  test  usercode.c
$ cat log.txt 
oldking is H
oldking is H
oldking is H
# ...
  • 下一小节我们会尝试写一个Makefile用于打包库文件,届时,对于用户的使用方面会更加方便一些

9.4 库文件打包

  • 我们可以简单写一个Makefile用于快捷打包库文件
AFILE=libmyc.a
CC=gcc
AR=ar
FLAGO=-o
FLAGC=-c
ARFLAG=-rc

RM=rm -rf

$(AFILE):mystdio.o mystring.o
	$(AR) $(ARFLAG) $@ $^
mystdio.o:mystdio.c
	$(CC) $(FLAGC) mystdio.c
mystring.o:mystring.c
	$(CC) $(FLAGC) mystring.c

.PHONY:output
output:
	mkdir -p lib/mylib
	mkdir -p lib/include
	cp *.h ./lib/include
	cp $(AFILE) ./lib/mylib

.PHONY:clean
clean:
	$(RM) mystdio.o mystring.o $(SO) $(AFILE) ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
Makefile  mystdio.c  mystdio.h  mystring.c  mystring.h
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ make 
gcc -c mystdio.c
gcc -c mystring.c
ar -rc libmyc.a mystdio.o mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
libmyc.a  Makefile  mystdio.c  mystdio.h  mystdio.o  mystring.c  mystring.h  mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ make output
mkdir -p lib/mylib
mkdir -p lib/include
cp *.h ./lib/include
cp libmyc.a ./lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
lib  libmyc.a  Makefile  mystdio.c  mystdio.h  mystdio.o  mystring.c  mystring.h  mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ tree lib
lib
├── include
│   ├── mystdio.h
│   └── mystring.h
└── mylib
    └── libmyc.a

2 directories, 3 files
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ make clean
rm -rf mystdio.o mystring.o  libmyc.a ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z C_file]$ ls
Makefile  mystdio.c  mystdio.h  mystring.c  mystring.h

9.5 动态库文件的制作,打包,使用

9.5.1 动态库文件的制作和打包
  • 动态库文件的制作不需要使用其他工具就可以,gcc就自带这个功能,同时因为涉及到"地址无关码"(或称"位置无关码")的问题,所以我们编译形成.o文件时,需要加一个选项,"地址无关码"我们以后会再提的,现在先记住
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ gcc -fPIC -c mystdio.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ gcc -fPIC -c mystring.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
Makefile  mystdio.c  mystdio.h  mystdio.o  mystring.c  mystring.h  mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ gcc mystring.o mystdio.o -o libmyc.so -shared
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
libmyc.so  Makefile  mystdio.c  mystdio.h  mystdio.o  mystring.c  mystring.h  mystring.o
  • 当然,我们依旧可以搞一个Makefile出来以节约时间
libmyc.so:mystring.o mystdio.o
	gcc mystring.o mystdio.o -o libmyc.so -shared
mystring.o:mystring.c
	gcc -c -fPIC mystring.c
mystdio.o:mystdio.c
	gcc -c -fPIC mystdio.c


.PHONY:output
output:
	mkdir -p lib/include
	mkdir -p lib/mylib
	cp ./*.h ./lib/include 
	cp ./*.so ./lib/mylib


.PHONY:clean
clean:
	rm -rf *.o *.so ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
Makefile  mystdio.c  mystdio.h  mystring.c  mystring.h
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ make
gcc -c -fPIC mystring.c
gcc -c -fPIC mystdio.c
gcc mystring.o mystdio.o -o libmyc.so -shared
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ make output
mkdir -p lib/include
mkdir -p lib/mylib
cp ./*.h ./lib/include 
cp ./*.so ./lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
lib  libmyc.so  Makefile  mystdio.c  mystdio.h  mystdio.o  mystring.c  mystring.h  mystring.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ tree lib
lib
├── include
│   ├── mystdio.h
│   └── mystring.h
└── mylib
    └── libmyc.so

2 directories, 3 files
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ make clean
rm -rf *.o *.so ./lib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z shared_object]$ ls
Makefile  mystdio.c  mystdio.h  mystring.c  mystring.h
9.5.2 动态库文件的使用
  • 动态库和.c的编译指令和静态库一样,就不多赘述
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib  lib  libc  log.txt  Makefile  shared_object  usercode.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ tree lib
lib
├── include
│   ├── mystdio.h
│   └── mystring.h
└── mylib
    └── libmyc.so

2 directories, 3 files
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ gcc usercode.c -o test -I ./lib/include/ -L ./lib/mylib/ -l myc
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib  lib  libc  log.txt  Makefile  shared_object  test  usercode.c
  • 但此时我们会发现一个问题,这个编译的可执行程序怎么样都运行不起来
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
./test: error while loading shared libraries: libmyc.so: cannot open shared object file: No such file or directory
  • ldd检查后发现,这个可执行程序找不到对应的库文件,这是为什么呢????
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
	linux-vdso.so.1 =>  (0x00007ffcf63e4000)
	libmyc.so => not found
	libc.so.6 => /lib64/libc.so.6 (0x00007f2af26a1000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f2af2a6f000)
  • 记得吗,静态库本质上是把库的内容和源文件编译成.o的内容打包在了一起,等到程序运行起来后一并加载到内存

  • 动态库的内容其实和静态库没有本质区别,都是打包多个.o成一个文件,但动态库和进程的内存机制不是这样,动态库是等程序运行之后一并将库分开加载到内存,以保证动态库允许被多个进程使用,而事实上,每个进程都认为这个动态库是自己独占的,其实就是让多个进程的虚拟地址映射到相同的物理地址,所以即便动态库在物理地址中只有一个,每个进程也能认为这个库文件是自己独占的

  • 所以其实本质上,动态库和静态库的区别并不是内容的差别,相反,他俩内容上没什么差别,但使用方式,或者说对于系统而言它的使用方式,有很大差别,在物理内存中,动态库仅存在一份,而静态库将可能会存在多个,而对于进程而言,他俩却也没什么差别,因为进程只能使用虚拟地址空间以保证进程独立性

  • 所以,一旦我们需要加载某个动态库进内存,首先我们得先找到它,或者说,系统得找到它,我们就需要一个固定的目录,用于系统默认加载动态库的目录

  • 根本上,我们需要一个专门的目录保存动态库而不需要保存静态库的原因是:找到一个可执行程序的静态库太简单了(事实上系统不需要找),因为其静态库在编译过程中就已经拷贝在可执行程序中了,已经成为了可执行程序的一部分了,但因为可执行程序不包含动态库的内容,所以我们就需要在打开文件时临时找动态库(在内存还没有加载进当前动态库的时候)

  • 或者说,操作系统压根就不知道静态库是什么,也不在乎静态库是啥,在操作系统看来,这里就只有一个可执行程序,静态库是什么??我(操作系统)不知道!!!不在乎!!!

  • 所以!对于动态库而言!一定会有安装这个步骤!或者说如果你要使用动态库!一定要安装!!!

  • 于是我们把库拷贝到/usr/lib64中,然后再ldd查看一下可执行程序的库连接情况

[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls /usr/lib64 | grep "myc"
libmyc.so
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
	linux-vdso.so.1 =>  (0x00007ffe003fc000)
	libmyc.so => /lib64/libmyc.so (0x00007faa056d1000)
	libc.so.6 => /lib64/libc.so.6 (0x00007faa05303000)
	/lib64/ld-linux-x86-64.so.2 (0x00007faa058d4000)
  • 此时该程序就可以正常使用了
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib  lib  libc  log.txt  Makefile  shared_object  test  usercode.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ cat log.txt 
oldking is H
oldking is H
oldking is H
oldking is H
#...
  • 当然,除了安装动态库文件到系统目录中,其实我们还有一些其他的方式

  • 使用动态库的几种方式:

    1. 安装动态库文件到系统目录中
    2. 在系统目录中创建动态库软链接
    3. 修改环境变量LD_LIBRARY_PATH
    4. 修改配置文件/etc/ld.so.conf以及ldconfig重新加载配置文件
  • 通过软链接

[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ sudo ln -s /home/oldking/code/lib_file_test/lib/mylib/libmyc.so /usr/lib64
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
	linux-vdso.so.1 =>  (0x00007ffe18654000)
	libmyc.so => /lib64/libmyc.so (0x00007f7eb3e30000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f7eb3a62000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f7eb4033000)
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ll /usr/lib64 | grep "myc"
lrwxrwxrwx   1 root root      52 Mar 20 13:05 libmyc.so -> /home/oldking/code/lib_file_test/lib/mylib/libmyc.so
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ls
achive_lib  lib  libc  log.txt  Makefile  shared_object  test  usercode.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ cat log.txt 
oldking is H
oldking is H
oldking is H
#...
  • 通过环境变量
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ echo $LD_LIBRARY_PATH
:/home/oldking/.VimForCpp/vim/bundle/YCM.so/el7.x86_64
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/oldking/code/lib_file_test/lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ldd test
	linux-vdso.so.1 =>  (0x00007ffdce388000)
	libmyc.so => /home/oldking/code/lib_file_test/lib/mylib/libmyc.so (0x00007f908ae05000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f908aa37000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f908b008000)
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ echo $LD_LIBRARY_PATH
:/home/oldking/.VimForCpp/vim/bundle/YCM.so/el7.x86_64:/home/oldking/code/lib_file_test/lib/mylib
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ ./test
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ cat log.txt 
oldking is H
oldking is H
oldking is H
#...
  • 至于通过配置文件,这个其实比较复杂,也不太常用,就不多提了,感兴趣的话可以去问问ai

  • 当然,既然gcc可以编译形成库文件,那么其他的编译器是不是也可以形成库文件?

  • 答案是当然可以,我们Windows端的vs2022也可以用于形成库文件,几乎所有常见编译器都可以用于形成动静态库

9.6 库原理

9.6.1 库的本质与ELF
  • 我们知道,库的本质是一堆.o的集合,而.o文件其实就是二进制文件,跟可执行程序没有本质区别,所以库也是二进制程序
  • 既然,库,.o文件,可执行程序都是二进制程序,此时我们就可以解释一个问题了,不过我们还得稍等一会,我们得先提一下文件格式的问题
  • 我们知道,不同的文件格式,其用途,解析文件的方式等等都不一样,意味着这些文件都不仅仅存储着数据,还有需要有配合解释该文件的程序的某些字段,我们举个例子
  • 比方说一个.jpeg格式的文件,假设你想打开这个文件,应用程序怎么知道你想解析的是一个.jpeg格式的文件,以什么算法解析这个.jpeg文件??所以必然会有多个结构,或者说字段,用于表示该文件的类型,表示解析该文件的算法类型,同时还需要有一个结构用于描述该文件中,哪些部分是用于描述文件的,哪些部分是文件真正的数据部分
  • ELF就是.o文件,库文件以及可执行程序的文件类型
名称职能
ELF头(ELF Header)标示文件类型与架构,其他表的位置信息
程序头表(Program Header Table)其内容帮助系统加载该程序的段的权限,地址,对齐方式,以及如何加载进内存
节头表(Section Header Table)帮助划分节的区间
节(Section)包含多种不同类型的节,每个节中包含同种类型的数据,未来用于合并成段
  • 上头这个表我们看个大概就行,反正等会也会具体聊到

  • 这里我们大概了解一下"节"这个概念,"节"就是一个单位,表示一部分数据,意义上和我们之前所说的"块","段"没有区别的,我们后面会谈到的"段"也是同理的(就是我们之前在了解进程地址空间的时候的那个"段",也是我们在学习C/C++的时候可能经常会谈到的"数据段"等等空间的单位)

9.6.2 库文件,.o文件与ELF
  • 我们知道,多个.o文件可以归档合并形成库文件,所以它具体是怎样做到的??

  • 其实也不难,因为.o文件和库文件的类型是一样的!所以归档的本质,其实就是将多个.o文件的相同类型的数据(或者称数据节)合并成一个数据节

image-20.png

9.6.3 段的概念以及打开程序粗谈
  • 我们知道,所有ELF类型的文件包含很多个,每个节储存着相同类型的数据,并且,一个程序的代码和数据全部存储在这些节之中,所以一个ELF中,除了节的其他的部分,全部都不是一个程序自己的内容,前面我们也解释过了,这些剩余的诸如"程序头表","节头表"之类的内容,最终目的都是用于描述程序自己的内容的空间,也就是代码和数据的空间,也就是这些节的空间

  • 我们知道,打开一个程序的过程,本质上就是拷贝一个可执行程序的代码和数据到内存,问题就是操作系统怎么知道一个可执行程序哪些部分是哪些数据??

  • 所以我们在ELF类型文件中,拥有了一堆用于描述节的数据,操作系统只需要读这些描述节的数据,就可以找到一个可执行程序的代码和数据了,跟我们之前谈的什么Super Block,GDT,在意义上都是差不多的内容,都是一段数据,用于描述另一段数据的空间

  • 我们知道,一个程序被打开之后会变成进程,用于管理进程的结构叫做task_struct,用于管理一个进程,我们也知道task_struct中存在一个叫做mm_struct的结构,其中也有用于描述各种段的数据,所以操作系统怎么知道这个mm_struct中用于描述和管理各种段的字段应该填什么?

  • 答案就是从ELF文件的各种表里来,所以,本质上,ELF的各种表,其实是用来帮助系统初始化mm_struct的!或者说,是用来帮助系统初始化task_struct的!

  • 所以,我们称,通过节合并并加载进内存的空间,称作为段

9.6.4 readelf的使用与库中的节
  • 这里了解一下查看一个可执行程序的Section所用的工具readelf的使用方式

  • 查看一个可执行程序的Section Header Table

[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ readelf -S test
There are 30 section headers, starting at offset 0x19a8:

Section Headers:
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 0]                   NULL             0000000000000000  00000000
       0000000000000000  0000000000000000           0     0     0
  [ 1] .interp           PROGBITS         0000000000400238  00000238
       000000000000001c  0000000000000000   A       0     0     1
  [ 2] .note.ABI-tag     NOTE             0000000000400254  00000254
       0000000000000020  0000000000000000   A       0     0     4
  [ 3] .note.gnu.build-i NOTE             0000000000400274  00000274
       0000000000000024  0000000000000000   A       0     0     4
  [ 4] .gnu.hash         GNU_HASH         0000000000400298  00000298
       0000000000000038  0000000000000000   A       5     0     8
  [ 5] .dynsym           DYNSYM           00000000004002d0  000002d0
       0000000000000120  0000000000000018   A       6     1     8
  [ 6] .dynstr           STRTAB           00000000004003f0  000003f0
       0000000000000087  0000000000000000   A       0     0     1
  [ 7] .gnu.version      VERSYM           0000000000400478  00000478
       0000000000000018  0000000000000002   A       5     0     2
  [ 8] .gnu.version_r    VERNEED          0000000000400490  00000490
       0000000000000020  0000000000000000   A       6     1     8
  [ 9] .rela.dyn         RELA             00000000004004b0  000004b0
       0000000000000018  0000000000000018   A       5     0     8
  [10] .rela.plt         RELA             00000000004004c8  000004c8
       0000000000000090  0000000000000018  AI       5    23     8
  [11] .init             PROGBITS         0000000000400558  00000558
       000000000000001a  0000000000000000  AX       0     0     4
  [12] .plt              PROGBITS         0000000000400580  00000580
       0000000000000070  0000000000000010  AX       0     0     16
  [13] .text             PROGBITS         00000000004005f0  000005f0
       00000000000001d2  0000000000000000  AX       0     0     16
  [14] .fini             PROGBITS         00000000004007c4  000007c4
       0000000000000009  0000000000000000  AX       0     0     4
  [15] .rodata           PROGBITS         00000000004007d0  000007d0
       0000000000000028  0000000000000000   A       0     0     8
  [16] .eh_frame_hdr     PROGBITS         00000000004007f8  000007f8
       0000000000000034  0000000000000000   A       0     0     4
  [17] .eh_frame         PROGBITS         0000000000400830  00000830
       00000000000000f4  0000000000000000   A       0     0     8
  [18] .init_array       INIT_ARRAY       0000000000600e00  00000e00
       0000000000000008  0000000000000008  WA       0     0     8
  [19] .fini_array       FINI_ARRAY       0000000000600e08  00000e08
       0000000000000008  0000000000000008  WA       0     0     8
  [20] .jcr              PROGBITS         0000000000600e10  00000e10
       0000000000000008  0000000000000000  WA       0     0     8
  [21] .dynamic          DYNAMIC          0000000000600e18  00000e18
       00000000000001e0  0000000000000010  WA       6     0     8
  [22] .got              PROGBITS         0000000000600ff8  00000ff8
       0000000000000008  0000000000000008  WA       0     0     8
  [23] .got.plt          PROGBITS         0000000000601000  00001000
       0000000000000048  0000000000000008  WA       0     0     8
  [24] .data             PROGBITS         0000000000601048  00001048
       0000000000000004  0000000000000000  WA       0     0     1
  [25] .bss              NOBITS           000000000060104c  0000104c
       0000000000000004  0000000000000000  WA       0     0     1
  [26] .comment          PROGBITS         0000000000000000  0000104c
       000000000000002d  0000000000000001  MS       0     0     1
  [27] .symtab           SYMTAB           0000000000000000  00001080
       0000000000000630  0000000000000018          28    46     8
  [28] .strtab           STRTAB           0000000000000000  000016b0
       00000000000001e9  0000000000000000           0     0     1
  [29] .shstrtab         STRTAB           0000000000000000  00001899
       0000000000000108  0000000000000000           0     0     1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)
  • 好,我们来详细解释一下该怎么看这张表(我们只抓重要的内容)
  1. 表头字段
字段意义
Name表示节的名称
Type表示节的类型
Address加载进内存之后,该节在虚拟地址空间中的起始位置
Offset该节在文件中的偏移量
Size该节的大小
Flags节的权限,包括读写,只读,只写,执行等等
  1. Name相关
字段意义
.dynsym动态符号表,存放全局符号信息,负责符号管理
.text代码段,存放可执行的指令
.rodata只读数据,例如字符串常量
.data属于数据段,用于保存已初始化的数据
.bss属于数据段,但本身不保存实际数据,以简化形式保存未初始化的数据,例如int 50,就是一共有50个未初始化的int变量,所以我们也可以称它"Better Save Space"
.symtab符号表,包含所有符号,调试会使用
  • 本质上,查看Section Header Table就是查看一个ELF类型文件的所有"节"究竟保存了多少内容,一共有多少"节",每个"节"大概都是干啥的

  • 不难发现,每一个节前面都有一个编号,所以更更本质上,查看这个Section Header Table,本质上不就是查看一个数组吗?!

  • 使用readelf查看一个ELF类型文件的Program Header Table

[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ readelf -l test

Elf file type is EXEC (Executable file)
Entry point 0x4005f0
There are 9 program headers, starting at offset 64

Program Headers:
  Type           Offset             VirtAddr           PhysAddr
                 FileSiz            MemSiz              Flags  Align
  PHDR           0x0000000000000040 0x0000000000400040 0x0000000000400040
                 0x00000000000001f8 0x00000000000001f8  R E    8
  INTERP         0x0000000000000238 0x0000000000400238 0x0000000000400238
                 0x000000000000001c 0x000000000000001c  R      1
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
  LOAD           0x0000000000000000 0x0000000000400000 0x0000000000400000
                 0x0000000000000924 0x0000000000000924  R E    200000
  LOAD           0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
                 0x000000000000024c 0x0000000000000250  RW     200000
  DYNAMIC        0x0000000000000e18 0x0000000000600e18 0x0000000000600e18
                 0x00000000000001e0 0x00000000000001e0  RW     8
  NOTE           0x0000000000000254 0x0000000000400254 0x0000000000400254
                 0x0000000000000044 0x0000000000000044  R      4
  GNU_EH_FRAME   0x00000000000007f8 0x00000000004007f8 0x00000000004007f8
                 0x0000000000000034 0x0000000000000034  R      4
  GNU_STACK      0x0000000000000000 0x0000000000000000 0x0000000000000000
                 0x0000000000000000 0x0000000000000000  RW     10
  GNU_RELRO      0x0000000000000e00 0x0000000000600e00 0x0000000000600e00
                 0x0000000000000200 0x0000000000000200  R      1

 Section to Segment mapping:
  Segment Sections...
   00     
   01     .interp 
   02     .interp .note.ABI-tag .note.gnu.build-id .gnu.hash .dynsym .dynstr .gnu.version .gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini .rodata .eh_frame_hdr .eh_frame 
   03     .init_array .fini_array .jcr .dynamic .got .got.plt .data .bss 
   04     .dynamic 
   05     .note.ABI-tag .note.gnu.build-id 
   06     .eh_frame_hdr 
   07     
   08     .init_array .fini_array .jcr .dynamic .got 
  • 不难发现,这里存在一个节与段的关系表,Section to Segment mapping

  • 所以,段,即Segment,本质其实是多个相同类型,权限的节构成的,对应的,每个段的职能则写在Section to Segment mapping的上面,也就是Program Headers

  • 相同的,这里只抓重点聊这些字段

Segment编号类型职能
00PHDRELF的头部,存储所有的程序头,即这些表
02代码LOAD其实就是代码段,存储相关代码的部分
03数据LOAD其实就是数据段,存储相关数据的部分
04DYNAMIC动态链接表,存储动态链接信息
  • 使用readelf查看一个ELF类型文件的ELF Header
[oldking@iZwz9b2bj2gor4d8h3rlx0Z lib_file_test]$ readelf -h test
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:                              EXEC (Executable file)
  Machine:                           Advanced Micro Devices X86-64
  Version:                           0x1
  Entry point address:               0x4005f0
  Start of program headers:          64 (bytes into file)
  Start of section headers:          6568 (bytes into file)
  Flags:                             0x0
  Size of this header:               64 (bytes)
  Size of program headers:           56 (bytes)
  Number of program headers:         9
  Size of section headers:           64 (bytes)
  Number of section headers:         30
  Section header string table index: 29
  • 相关字段的解释:
字段意义
Magic用于标记该文件的类型是ELF格式的文件
Data存储格式,little endian表示小端存储
Type描述ELF下细分的各种类型,包括可重定向目标文件,库文件,可执行文件,EXEC表示可执行文件
Machine机器架构
VersionELF版本号,一般是1
Entry point address程序的入口地址,一般是_start函数的入口地址
Start of program headersProgram Header Table在文件的偏移量
Start of section headersSection Header Table在文件的偏移量
Size of this headerELF Header自己的大小
Size of program headers单个Program Header的大小
Number of program headersProgram Header Table的条目个数
Size of section headers单个Section Header的大小
Number of section headersSection Header Table的条目个数
  • 事实上,一个ELF类型的文件一般是这么存储的

image-21.png

9.6.5 可执行文件的加载
  • 本小节会比较绕,比较恶心,比较难,建议多看一看
9.6.5.1 段
  • 我们知道,一个可执行文件在被加载的时候会将其中非节的内容用于初始化内存的task_struct,mm_struct,vm_area_struct等结构

  • 同时,多个相同属性或权限的节会被整合成一个段被拷贝进内存

9.6.5.2 反汇编工具的使用
  • 我们使用objdump作为反汇编工具,一般会带上选项-d,表示以简略模式反汇编

  • 这里我们简单写一个测试代码和Makefile,本小节"可执行文件的加载"所有示例的代码都是基于这三个文件进行的

//main.c
#include<stdio.h>

void run();
int main()
{
    printf("hello\n");
    run();

    return 0;
}
//run.c
#include<stdio.h>

void run()
{
    printf("run\n");
}

# Makefile 
exec:main.o run.o
	gcc main.o run.o -o exec
main.o:main.c
	gcc main.c -c -g
run.o:run.c
	gcc run.c -c -g
# 这里要使用-g选项,可以方便反汇编查看函数名称,否则函数名称显示的可能是一串地址

.PHONY:clean
clean:
	rm -rf *.o exec

  • 我们make后可以查一下反汇编
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ ls
main.c  Makefile  run.c
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ make
gcc main.c -c
gcc run.c -c
gcc main.o run.o -o exec
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ ls
exec  main.c  main.o  Makefile  run.c  run.o
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d main.o > main.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d run.o > run.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d exec > exec.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ ls
exec  exec.s  main.c  main.o  main.s  Makefile  run.c  run.o  run.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat main.s

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <main+0xe>
   e:	b8 00 00 00 00       	mov    $0x0,%eax
  13:	e8 00 00 00 00       	callq  18 <main+0x18>
  18:	b8 00 00 00 00       	mov    $0x0,%eax
  1d:	5d                   	pop    %rbp
  1e:	c3                   	retq   
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat run.s

run.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <run>:
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <run+0xe>
   e:	5d                   	pop    %rbp
   f:	c3                   	retq   
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat exec.s

exec:     file format elf64-x86-64


Disassembly of section .init:

00000000004003e0 <_init>:
  4003e0:	48 83 ec 08          	sub    $0x8,%rsp
  4003e4:	48 8b 05 0d 0c 20 00 	mov    0x200c0d(%rip),%rax        # 600ff8 <__gmon_start__>
  4003eb:	48 85 c0             	test   %rax,%rax
  4003ee:	74 05                	je     4003f5 <_init+0x15>
  4003f0:	e8 3b 00 00 00       	callq  400430 <__gmon_start__@plt>
  4003f5:	48 83 c4 08          	add    $0x8,%rsp
  4003f9:	c3                   	retq   

Disassembly of section .plt:

0000000000400400 <.plt>:
  400400:	ff 35 02 0c 20 00    	pushq  0x200c02(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  400406:	ff 25 04 0c 20 00    	jmpq   *0x200c04(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  40040c:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000400410 <puts@plt>:
  400410:	ff 25 02 0c 20 00    	jmpq   *0x200c02(%rip)        # 601018 <puts@GLIBC_2.2.5>
  400416:	68 00 00 00 00       	pushq  $0x0
  40041b:	e9 e0 ff ff ff       	jmpq   400400 <.plt>

0000000000400420 <__libc_start_main@plt>:
  400420:	ff 25 fa 0b 20 00    	jmpq   *0x200bfa(%rip)        # 601020 <__libc_start_main@GLIBC_2.2.5>
  400426:	68 01 00 00 00       	pushq  $0x1
  40042b:	e9 d0 ff ff ff       	jmpq   400400 <.plt>

0000000000400430 <__gmon_start__@plt>:
  400430:	ff 25 f2 0b 20 00    	jmpq   *0x200bf2(%rip)        # 601028 <__gmon_start__>
  400436:	68 02 00 00 00       	pushq  $0x2
  40043b:	e9 c0 ff ff ff       	jmpq   400400 <.plt>

Disassembly of section .text:

0000000000400440 <_start>:
  400440:	31 ed                	xor    %ebp,%ebp
  400442:	49 89 d1             	mov    %rdx,%r9
  400445:	5e                   	pop    %rsi
  400446:	48 89 e2             	mov    %rsp,%rdx
  400449:	48 83 e4 f0          	and    $0xfffffffffffffff0,%rsp
  40044d:	50                   	push   %rax
  40044e:	54                   	push   %rsp
  40044f:	49 c7 c0 d0 05 40 00 	mov    $0x4005d0,%r8
  400456:	48 c7 c1 60 05 40 00 	mov    $0x400560,%rcx
  40045d:	48 c7 c7 2d 05 40 00 	mov    $0x40052d,%rdi
  400464:	e8 b7 ff ff ff       	callq  400420 <__libc_start_main@plt>
  400469:	f4                   	hlt    
  40046a:	66 0f 1f 44 00 00    	nopw   0x0(%rax,%rax,1)

0000000000400470 <deregister_tm_clones>:
  400470:	b8 3f 10 60 00       	mov    $0x60103f,%eax
  400475:	55                   	push   %rbp
  400476:	48 2d 38 10 60 00    	sub    $0x601038,%rax
  40047c:	48 83 f8 0e          	cmp    $0xe,%rax
  400480:	48 89 e5             	mov    %rsp,%rbp
  400483:	77 02                	ja     400487 <deregister_tm_clones+0x17>
  400485:	5d                   	pop    %rbp
  400486:	c3                   	retq   
  400487:	b8 00 00 00 00       	mov    $0x0,%eax
  40048c:	48 85 c0             	test   %rax,%rax
  40048f:	74 f4                	je     400485 <deregister_tm_clones+0x15>
  400491:	5d                   	pop    %rbp
  400492:	bf 38 10 60 00       	mov    $0x601038,%edi
  400497:	ff e0                	jmpq   *%rax
  400499:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

00000000004004a0 <register_tm_clones>:
  4004a0:	b8 38 10 60 00       	mov    $0x601038,%eax
  4004a5:	55                   	push   %rbp
  4004a6:	48 2d 38 10 60 00    	sub    $0x601038,%rax
  4004ac:	48 c1 f8 03          	sar    $0x3,%rax
  4004b0:	48 89 e5             	mov    %rsp,%rbp
  4004b3:	48 89 c2             	mov    %rax,%rdx
  4004b6:	48 c1 ea 3f          	shr    $0x3f,%rdx
  4004ba:	48 01 d0             	add    %rdx,%rax
  4004bd:	48 d1 f8             	sar    %rax
  4004c0:	75 02                	jne    4004c4 <register_tm_clones+0x24>
  4004c2:	5d                   	pop    %rbp
  4004c3:	c3                   	retq   
  4004c4:	ba 00 00 00 00       	mov    $0x0,%edx
  4004c9:	48 85 d2             	test   %rdx,%rdx
  4004cc:	74 f4                	je     4004c2 <register_tm_clones+0x22>
  4004ce:	5d                   	pop    %rbp
  4004cf:	48 89 c6             	mov    %rax,%rsi
  4004d2:	bf 38 10 60 00       	mov    $0x601038,%edi
  4004d7:	ff e2                	jmpq   *%rdx
  4004d9:	0f 1f 80 00 00 00 00 	nopl   0x0(%rax)

00000000004004e0 <__do_global_dtors_aux>:
  4004e0:	80 3d 4d 0b 20 00 00 	cmpb   $0x0,0x200b4d(%rip)        # 601034 <_edata>
  4004e7:	75 11                	jne    4004fa <__do_global_dtors_aux+0x1a>
  4004e9:	55                   	push   %rbp
  4004ea:	48 89 e5             	mov    %rsp,%rbp
  4004ed:	e8 7e ff ff ff       	callq  400470 <deregister_tm_clones>
  4004f2:	5d                   	pop    %rbp
  4004f3:	c6 05 3a 0b 20 00 01 	movb   $0x1,0x200b3a(%rip)        # 601034 <_edata>
  4004fa:	f3 c3                	repz retq 
  4004fc:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000400500 <frame_dummy>:
  400500:	48 83 3d 18 09 20 00 	cmpq   $0x0,0x200918(%rip)        # 600e20 <__JCR_END__>
  400507:	00 
  400508:	74 1e                	je     400528 <frame_dummy+0x28>
  40050a:	b8 00 00 00 00       	mov    $0x0,%eax
  40050f:	48 85 c0             	test   %rax,%rax
  400512:	74 14                	je     400528 <frame_dummy+0x28>
  400514:	55                   	push   %rbp
  400515:	bf 20 0e 60 00       	mov    $0x600e20,%edi
  40051a:	48 89 e5             	mov    %rsp,%rbp
  40051d:	ff d0                	callq  *%rax
  40051f:	5d                   	pop    %rbp
  400520:	e9 7b ff ff ff       	jmpq   4004a0 <register_tm_clones>
  400525:	0f 1f 00             	nopl   (%rax)
  400528:	e9 73 ff ff ff       	jmpq   4004a0 <register_tm_clones>

000000000040052d <main>:
  40052d:	55                   	push   %rbp
  40052e:	48 89 e5             	mov    %rsp,%rbp
  400531:	bf f0 05 40 00       	mov    $0x4005f0,%edi
  400536:	e8 d5 fe ff ff       	callq  400410 <puts@plt>
  40053b:	b8 00 00 00 00       	mov    $0x0,%eax
  400540:	e8 07 00 00 00       	callq  40054c <run>
  400545:	b8 00 00 00 00       	mov    $0x0,%eax
  40054a:	5d                   	pop    %rbp
  40054b:	c3                   	retq   

000000000040054c <run>:
  40054c:	55                   	push   %rbp
  40054d:	48 89 e5             	mov    %rsp,%rbp
  400550:	bf f6 05 40 00       	mov    $0x4005f6,%edi
  400555:	e8 b6 fe ff ff       	callq  400410 <puts@plt>
  40055a:	5d                   	pop    %rbp
  40055b:	c3                   	retq   
  40055c:	0f 1f 40 00          	nopl   0x0(%rax)

0000000000400560 <__libc_csu_init>:
  400560:	41 57                	push   %r15
  400562:	41 89 ff             	mov    %edi,%r15d
  400565:	41 56                	push   %r14
  400567:	49 89 f6             	mov    %rsi,%r14
  40056a:	41 55                	push   %r13
  40056c:	49 89 d5             	mov    %rdx,%r13
  40056f:	41 54                	push   %r12
  400571:	4c 8d 25 98 08 20 00 	lea    0x200898(%rip),%r12        # 600e10 <__frame_dummy_init_array_entry>
  400578:	55                   	push   %rbp
  400579:	48 8d 2d 98 08 20 00 	lea    0x200898(%rip),%rbp        # 600e18 <__init_array_end>
  400580:	53                   	push   %rbx
  400581:	4c 29 e5             	sub    %r12,%rbp
  400584:	31 db                	xor    %ebx,%ebx
  400586:	48 c1 fd 03          	sar    $0x3,%rbp
  40058a:	48 83 ec 08          	sub    $0x8,%rsp
  40058e:	e8 4d fe ff ff       	callq  4003e0 <_init>
  400593:	48 85 ed             	test   %rbp,%rbp
  400596:	74 1e                	je     4005b6 <__libc_csu_init+0x56>
  400598:	0f 1f 84 00 00 00 00 	nopl   0x0(%rax,%rax,1)
  40059f:	00 
  4005a0:	4c 89 ea             	mov    %r13,%rdx
  4005a3:	4c 89 f6             	mov    %r14,%rsi
  4005a6:	44 89 ff             	mov    %r15d,%edi
  4005a9:	41 ff 14 dc          	callq  *(%r12,%rbx,8)
  4005ad:	48 83 c3 01          	add    $0x1,%rbx
  4005b1:	48 39 eb             	cmp    %rbp,%rbx
  4005b4:	75 ea                	jne    4005a0 <__libc_csu_init+0x40>
  4005b6:	48 83 c4 08          	add    $0x8,%rsp
  4005ba:	5b                   	pop    %rbx
  4005bb:	5d                   	pop    %rbp
  4005bc:	41 5c                	pop    %r12
  4005be:	41 5d                	pop    %r13
  4005c0:	41 5e                	pop    %r14
  4005c2:	41 5f                	pop    %r15
  4005c4:	c3                   	retq   
  4005c5:	90                   	nop
  4005c6:	66 2e 0f 1f 84 00 00 	nopw   %cs:0x0(%rax,%rax,1)
  4005cd:	00 00 00 

00000000004005d0 <__libc_csu_fini>:
  4005d0:	f3 c3                	repz retq 

Disassembly of section .fini:

00000000004005d4 <_fini>:
  4005d4:	48 83 ec 08          	sub    $0x8,%rsp
  4005d8:	48 83 c4 08          	add    $0x8,%rsp
  4005dc:	c3                   	retq   
9.6.5.3 ELF格式文件的链接
  • 上小节我们反汇编了一下main.o,run.o,exec三个文件,我们可以观察一下这三个文件关于run()函数的汇编代码

  • PS:因为上小节我们使用的objdump命令生成的汇编文本不包含函数名称,不方便观察,这里我们再带一个选项-S显示一下当前行代码

[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d -S run.o > run.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat run.s

run.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <run>:
#include<stdio.h>

void run()
{
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
    printf("run\n");
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <run+0xe>
}
   e:	5d                   	pop    %rbp
   f:	c3                   	retq   
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ objdump -d -S main.o > main.s
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ cat main.s

main.o:     file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <main>:
#include<stdio.h>

void run();

int main()
{
   0:	55                   	push   %rbp
   1:	48 89 e5             	mov    %rsp,%rbp
    printf("hello\n");
   4:	bf 00 00 00 00       	mov    $0x0,%edi
   9:	e8 00 00 00 00       	callq  e <main+0xe>
    run();
   e:	b8 00 00 00 00       	mov    $0x0,%eax
  13:	e8 00 00 00 00       	callq  18 <main+0x18>

    return 0;
  18:	b8 00 00 00 00       	mov    $0x0,%eax
}
  1d:	5d                   	pop    %rbp
  1e:	c3                   	retq   
  • 我们再截取一下exec.s的关于mainrun两个函数的相关部分
000000000040052d <main>:
  40052d:	55                   	push   %rbp
  40052e:	48 89 e5             	mov    %rsp,%rbp
  400531:	bf f0 05 40 00       	mov    $0x4005f0,%edi
  400536:	e8 d5 fe ff ff       	callq  400410 <puts@plt>
  40053b:	b8 00 00 00 00       	mov    $0x0,%eax
  400540:	e8 07 00 00 00       	callq  40054c <run>
  400545:	b8 00 00 00 00       	mov    $0x0,%eax
  40054a:	5d                   	pop    %rbp
  40054b:	c3                   	retq   

000000000040054c <run>:
  40054c:	55                   	push   %rbp
  40054d:	48 89 e5             	mov    %rsp,%rbp
  400550:	bf f6 05 40 00       	mov    $0x4005f6,%edi
  400555:	e8 b6 fe ff ff       	callq  400410 <puts@plt>
  40055a:	5d                   	pop    %rbp
  40055b:	c3                   	retq   
  40055c:	0f 1f 40 00          	nopl   0x0(%rax)
  • 我们来解释一下这部分汇编代码

    1. 左边部分的一串16进制数字中,前两位是机器码指令,即汇编代码callq,mov转成机器码的样子
    2. 剩下的八位内容是地址
  • 所以我们也不难发现,实际上main.c中调用的run()方法,在编译成.o之后,callq跳转命令后面根本就没有正确的跳转地址而是全0

  • 而找到地址这个过程实际上是链接的过程需要做的

  • 链接器会将两个文件进行对比,main.o中不存在run()的地址,就会跑到其他.o文件找,如果还是找不到,就往库文件找(这里指的是静态库,动态库的机制不太一样,我们留到后面再说)

  • 意味着实际上编译器是暂定我们这个函数存在,除非等到链接时找不到这个函数的地址,才会报错

  • 所以我们能看到在exec.s中,run()函数和printf()函数的地址是已经被填好的了

9.6.5.4 逻辑地址与线性地址与虚拟地址和地址映射
  • 我们知道,一个ELF类型的文件,一定会包含很多节,在加载到内存之后相同属性和权限的多个节会被合并成段
  • 问:一行代码在ELF类型文件中,有没有存储地址??
  • 答案是肯定的,他一定存储着地址,我们称这个地址为"逻辑地址"
  1. 有两种逻辑地址的表示机制,我们先聊旧方法
  • 我们知道,一个ELF文件,其实比较容易获取的地址,是所有节的起始地址,而一行代码的地址的表示方式,就可以用"当前代码所在的节的起始地址+该行代码在该节的偏移量"表示,例如.text节的起始地址是0x1000,而函数a()所在该节的偏移量为0x50
  • 所以我们就可以以0x1000 + 0x50来定位函数a()
  1. 新方法
  • 而在当今计算机中,旧方法已经很少见了,取而代之的是新方式(主要是因为引入了页表机制为了适配页表机制而采用的,同时旧方法也会因为多一步计算过程而造成一定性能损耗)

  • 在新方式中,一行代码的地址直接以0号地址作为起点+其偏移量,所以一行代码的地址直接用偏移量表示就行了,就不需要多一步计算过程了

  • 同时,使用新方式生成的地址又称作"线性地址",这个新方法又称为"平坦模式编址"

  • 而之所以一个存储在硬盘的文件需要逻辑地址,其实是为了方便页表的加载/初始化!!!

  • 我们知道,一个可执行文件一旦被打开,就需要加载页表,构建虚拟地址和物理地址的映射关系,这个映射关系是打开文件时现场算出来的吗????

  • 当然不是!你不觉得等到现场算太慢了吗?用户不会因为打开软件慢而急吗?!!!

  • 所以你以为页表的虚拟地址是怎么来的?一个进程的虚拟地址不就是储存在硬盘中的可执行文件的逻辑地址吗?!操作系统只需要无脑拷贝逻辑地址到页表的虚拟地址栏不就行了吗?!!!

  • 所以,逻辑地址,线性地址,虚拟地址的本质是同一个东西!只不过逻辑地址是站在硬盘的可执行文件的视角的,线性地址是逻辑地址的一个分支,是站在"平坦模式编址"的视角的,而虚拟地址是站在进程的视角的

9.6.5.6 可执行文件的加载
  • 在真正了解可执行文件的加载之前,我们首先需要了解一些CPU中相关的寄存器:

    1. EIP:这个寄存器会存储进程应该执行的下一条代码的虚拟地址(至于为什么是虚拟地址,我们还会再提到的)
    2. CR3:这个寄存器会存储当前进程对应的页表所在的物理地址,注意:是存储页表自己的地址而不是页表中映射的物理地址
  • 同时CPU还有一个结构叫做MMU,在一个进程被加载的过程中,他会负责将物理地址映射给该进程的虚拟地址

  • 所以,一个可执行文件被加载,会经历哪些过程?

  1. 找到该文件
    1. 通过"文件名+绝对目录"的形式,找到该文件在当前目录的inode number
    2. 通过inode number计算找到该文件存储的块组,并找到该块组inode Bitmap的位置,找到inode Table中存储的该文件的属性
    3. 通过该文件的属性,找到该文件在data Block的位置
  2. 加载器
    1. 通过文件中的ELF Header,获得该文件各区域的内存分布,同时获得该进程运行的起始虚拟地址
    2. 通过Program Header Table获得该文件相关段属性,空间分配,并以此初始化mm_struct,vm_area_struct
    3. 处理页表问题,初始化页表,通过MMU构建虚拟地址和物理地址映射关系,将页表的物理地址传递给CR3,拷贝并合并相同属性和权限的节为若干段到映射的物理内存中
    4. 将进程运行的起始虚拟地址传递给EIP
    5. 处理库问题(这个我们后面再细讲,这里我们暂时当库文件不存在)
    6. 将控制权力交给CPU,CPU开始运行程序
  3. CPU
    1. 通过EIP找到页表中映射的物理内存,通过该物理内存对应地址存储的机器码执行指令
    2. EIP存储的内容改成下一条命令对应的虚拟内存,即下一个虚拟内存(非函数跳转的情况,如果是函数跳转,则会修改EIP为该函数的虚拟地址)
  • 所以,CPU怎么知道程序的入口在哪里?答案是通过EIP Header中的一个字段Entry point address来的

  • 所以,一个可执行程序在还没有运行的时候,就已经知道该怎样加载自己到内存了,有非常多的内容存在的意义,全部是为了配合加载器加载程序的初始化相关结构而存在的

  • 所以,实际上对于CPU来说,他也只能看到一个进程的虚拟地址,所有事情都是围绕虚拟地址来看的!等真的要执行的时候,才会去找物理地址,同样是为了安全!并且CPU不用考虑物理地址实际在什么位置!拿来直接调用对应机器码就行!

  • 同时,我们再次体会到了虚拟内存这个东西的好处,我们不需要真的运行程序也能知道程序的起点地址,也知道程序的空间分配,同时在程序真正加载进内存的时候,也不会和其他进程造成地址冲突

image-22.png

9.6.6 动态库实现共享的形式
  • 在谈静态库之前,我们先来聊聊静态库

  • 所以,静态库和.o有啥区别呢?实际上没啥区别!反正就那么些个Section,该有的Table,Header啥的都有,哪能有啥区别?多个.o组合起来,本质上不还是.o吗?

  • 所以因为本质上是.o,所以静态库的所有代码和数据全部会和.o文件一样在链接的时候存进可执行文件中!但如果多个进程包含一样的静态库,内存中存在很多份重复的静态库代码,不就造成了内存的浪费吗,于是就有了动态库,以防止内存的浪费

  • 但动态库不一样,多个进程可以共享动态库的代码,所以即便多个进程使用了动态库的代码,内存中也只需要存在一份动态库代码即可,这点我们很久之前就提过了

  • 下面我们通过一张图了解一下动态库的共享机制

image-23.png

  • 所以,为什么能做到共享的操作呢?
  • 实际上在每个进程自己的角度而言,他们都认为共享库是自己独享
  • 这是因为虚拟地址和物理地址的映射关系不同导致的,虽然虚拟地址不一样,但物理地址是否可以一样呢?物理地址一样不久实现共享库了嘛?
9.6.7 动态库链接的机制
  • 动态库的链接机制比较复杂,这里我们分为三个阶段来看
    1. 编译时
    2. 加载时
    3. 运行时
9.6.7.1 编译时
  1. 首先编译器gcc会把多个.o或者静态库文件合并成一个初始的可执行文件
  2. 然后链接器ld会负责检查合并后的文件中缺少的函数地址,并进行填充
  3. 如果函数地址仍然没有找到,就将该函数所需跳转的地址暂时置空(或是留下符号引用)
  4. 填写节.dynamic中的一张叫DT_NEEDED的表,该表中标注了该可执行程序对于动态库的调用情况
  5. 生成可执行文件
9.6.7.2 加载时
  1. 加载器会解析该可执行文件的Program Header Table,并找到该程序的.Dynamic节,并找到DT_NEEDED表结构,通过该表结构,加载器会知道,该可执行文件需要哪些动态库
  2. 加载器,通过环境变量,配置文件,在对应目录中,查找有没有该可执行文件所需要的动态库
  3. 如果动态库没有被加载,将动态库加载进内存
  4. 将动态库的物理地址和该进程的虚拟地址进行映射,以共享动态库
  5. 修正编译时没有找到跳转地址的函数的跳转地址(其实是通过一个叫PLT的接口来找函数,同时初始化一张叫做.got的表)
  6. 运行ELFDT_INIT段(如果有初始化代码的话,比方说CPP全局构造函数)
9.6.7.3 运行时
  1. 可执行文件中有一个节叫做.got,.got是一个表,其中记载了该程序使用的动态库接口在当前进程的虚拟地址,程序在调用函数时,实际上会先从.got中找到该结构的虚拟地址
  2. 如果是第一次调用该函数,则PLT接口会负责跳转到解析该函数的虚拟地址的方法,通过该方法解析虚拟地址(准确来说是动态库在当前进程的起始虚拟地址+该函数在动态库中的偏移量),然后存入.got中,后续再调用该函数的时候无需使用PLT接口跳转到解析虚拟地址的方法的功能,而仅需在.got中查找
9.6.7.4 PLT/GOT
  • PLT的本质是一个函数,是一个接口,所有的调用动态库接口的过程,实际上都需要调用一次PLT接口

  • GOT本质上是一个结构,本质上存储的是动态库函数的虚拟地址(也是当前进程中的虚拟地址的一部分)

  • 所以,宏观来看,PLT就是一个工具,用于跳转到解析函数的虚拟地址的方法或者直接通过GOT获取已经解析好的函数地址,GOT是一个缓存,用于存储解析出来的函数的虚拟地址,以减少后续因为解析而造成的CPU开销

  • 但以我们的了解,加载时不就已经找到动态库函数的地址了吗,为什么后续还要用PLTGOT再找一边呢?

  • 原因是为了实现一个叫按需加载的功能,要知道,一个项目的函数量可能是非常多的,你不能指望一个程序被打开时能找到所有函数的地址还能使程序加载速度比较快

  • 所以使用了延迟加载的设计,以实现按需加载的功能

9.6.7.5 查看一个可执行文件的GOT
[oldking@iZwz9b2bj2gor4d8h3rlx0Z elf_file]$ readelf -r exec

Relocation section '.rela.dyn' at offset 0x380 contains 1 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000600ff8  000300000006 R_X86_64_GLOB_DAT 0000000000000000 __gmon_start__ + 0

Relocation section '.rela.plt' at offset 0x398 contains 3 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
000000601018  000100000007 R_X86_64_JUMP_SLO 0000000000000000 puts@GLIBC_2.2.5 + 0
000000601020  000200000007 R_X86_64_JUMP_SLO 0000000000000000 __libc_start_main@GLIBC_2.2.5 + 0
000000601028  000300000007 R_X86_64_JUMP_SLO 0000000000000000 __gmon_start__ + 0
  • 这条命令本质是列出所有的动态链接的函数
9.6.7.6 动态库的加载机制中细节问题的解释
  • 注意:不要因为动态库和可执行文件是两个文件就将它俩完全区别开了,他俩实际上是高度契合的!在可执行文件的加载过程中,如果动态库在此之前从来没有被加载过,动态库文件会跟随一起加载,本质上和加载可执行文件中其他各种段的原理是一样的

  • 同时,动态库实际上没有自己的虚拟地址的,或者说它的虚拟地址并不固定,动态库也没有自己的页表,动态库的页表是和众多使用了该库的进程一起共享的,或者说动态库就像是一个变量被多个指针指向,是被进程共享的

  • 同时,GOT存储的关于函数的虚拟地址,其实是进程自己的页表的虚拟地址

  • 所以你会发现,动态库的设计中的每一项东西,都是为了让进程认为这个库是自己独占的!

9.6.7.7 关于PIC地址无关码
  • 所以,以上通过缓存GOT,以动态库起始地址(动态库基地址)+函数在动态库的偏移量找到一个动态库函数的方式,称作PIC地址无关代码,即PIC = "动态库在当前进程的起始地址" + "函数在动态库的偏移量"

  • 所以,我们可以通过PIC地址无关码找到一个函数在当前进程的虚拟地址,并通过页表来获取对应的物理地址,即可实现一个函数的调用!

  • 所以,通过PIC地址无关码与页表机制,我们能快速获得并调用当前进程中任意一个动态库文件的任意一个函数,因为PIC地址无关码能找到这个函数在哪个库的什么位置,包括虚拟地址也包括物理地址!!!

9.6.7.8 关于-static
  • 如果编译程序时附加了选项-static,那么与动态库相关的结构(包括共享内存,GOT表,动态链接函数符号表等)就不会存在
  • 此时会强制使用静态库进行链接,动态库默认就不会链接进来

  • 如有问题或者有想分享的见解欢迎留言讨论