动态链接库中的全局变量

Last Updated: 2023-07-10 06:49:19 Monday

-- TOC --

动态链接库中定义的全局变量,在每个加载这个库的各个process中,都有独立的副本,多process对此全局变量的访问,相互之间无关!

进程启动时,首先动态加载它依赖的各种库,当各种库在进程的虚拟地址空间中位置确定后,动态链接器修改动态库的GOT(Global Offset Table)表。如果某个全局变量在process中存在引用,则修改为指向process空间中的变量。如果process没有对此全局变量的引用,动态链接库中的代码在访问时,就是访问其内部地址。动态链接库在编译期并不能确定它定义的全局变量会不会被进程访问,因为这个全局变量一定是定义在.got区域,除非是static变量。

进程代码访问动态链接库中的全局变量,代码中仅仅有个extern的申明。链接时,这个全局变量的地址必须确定下来,因为进程代码在运行时,没有重定位(要么绝对地址,要么PIE)。此时,链接器在创建可执行文件的时候,会在它的.bss段,为此全局变量创建一个副本。同时,在编译动态链接库的时候,默认都会把定义在内部的全局变量,当做定义在其它模块的全局变量,即动态链接库通过GOT表来实现它自己定义的全局变量的访问。

当加载动态链接库时,如果全局变量在可执行文件中有副本,那么链接器就会把GOT中的相应的地址指向该副本,这样就保证了该变量在运行时,始终只有一个实例。同时,也实现了不同进程对该变量的访问相互之间的无关性,就像这个全局变量,是定义在各个进程内部的一样。实际上,也真是定义在进程内部的。

如果这个全局变量在动态链接库中有初始值,动态链接器会将此初始值在加载时赋给进程中的副本。如果这个全局变量在进程中没有副本,即进程没有对此变量的引用,那么在动态链接库的GOT表中的地址,就会指向动态链接库内部。


对于动态链接库中的.data区域,它在每个进程中,都有一个副本,所以不用担心会被别的进程修改。因此,可以用装载时重定位的方法,来解决.data中对绝对地址的引用。那些进程没有用到的,定义在动态链接库中的全局变量,在每个进程中,也是独立的存在,它们与库中的代码一起,为进程提供接口和功能。

想象一下:动态链接库中的.data一定是独立的page,在不同的process地址空间中,具有不同的虚拟地址,OS也完全可以做到将此.data映射到不同的物理内存,实现每个进程独有的副本。


可执行文件不是PIC代码,它内部的各种地址,都需要在链接的时候确定下来。对于定义在动态链接库中的全局变量,可执行文件在链接时,发现这个符号属于动态链接库,链接时没有地址可以用,因此也就只能在自己的.bss段开辟一个空间,确定此地址,代码中所有对此变量的访问,都重定位到这个地址。


做个测试,验证一下。

test.c

访问两个外部符号,打印data的值和地址。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

extern int data;
extern void pdata_addr();

int main() {
    printf("%d\n", data);
    printf("%p\n", &data);
    pdata_addr();
    sleep(-1);
    return 0;
}

t.c

定义两个外部符号,在pdata_addr中打印data的值和地址。

#include <stdio.h>

int data = 5;

void pdata_addr(){
    printf("in t.so, data: %d\n", data);
    printf("in t.so, data: %p\n", &data);
}

编译链接运行

$ gcc -fpic -shared t.c -o t.so
$ gcc test.c t.so -o test
$ LD_LIBRARY_PATH=. ./test
5
0x404034
in t.so, data: 5
in t.so, data: 0x404034

可以看到,两次对data值和地址的打印,是相同的。

$ readelf -S test
...
[25] .bss              NOBITS           0000000000404034  00003034
       000000000000000c  0000000000000000  WA       0     0     4
...

t.so中对自己定义的全局变量的访问,默认被当作访问外部符号,可以看到t.so的.rela.dyn表中对这个自己定义的全局变量的动态重定位信息:

$ readelf -r t.so

Relocation section '.rela.dyn' at offset 0x4a0 contains 8 entries:
  Offset          Info           Type           Sym. Value    Sym. Name + Addend
...
000000003fe8  000600000006 R_X86_64_GLOB_DAT 0000000000004028 data + 0
...

t.so对data的访问,通过它的.got表(先在.got表中拿到真实地址,然后再访问此地址),但.got表中的内容,是加载时确定的,在非运行的状态下,我们只能看到t.so中的代码,在访问.got表:

0000000000001109 <pdata_addr>:
    1109:       55                      push   rbp
    110a:       48 89 e5                mov    rbp,rsp
    110d:       48 8b 05 d4 2e 00 00    mov    rax,QWORD PTR [rip+0x2ed4]        # 3fe8 <data@@Base-0x40>
    1114:       8b 00                   mov    eax,DWORD PTR [rax]
    1116:       89 c6                   mov    esi,eax
    1118:       48 8d 05 e1 0e 00 00    lea    rax,[rip+0xee1]        # 2000 <_fini+0xeb0>
    111f:       48 89 c7                mov    rdi,rax
    1122:       b8 00 00 00 00          mov    eax,0x0
    1127:       e8 04 ff ff ff          call   1030 <printf@plt>
    112c:       48 8b 05 b5 2e 00 00    mov    rax,QWORD PTR [rip+0x2eb5]        # 3fe8 <data@@Base-0x40>
    1133:       48 89 c6                mov    rsi,rax
    1136:       48 8d 05 d6 0e 00 00    lea    rax,[rip+0xed6]        # 2013 <_fini+0xec3>
    113d:       48 89 c7                mov    rdi,rax
    1140:       b8 00 00 00 00          mov    eax,0x0
    1145:       e8 e6 fe ff ff          call   1030 <printf@plt>
    114a:       90                      nop
    114b:       5d                      pop    rbp
    114c:       c3                      ret    

代码两次访问 0x3fe8 这个地址,这个地址在.got内:

$ readelf -S t.so | grep .got
  [21] .got              PROGBITS         0000000000003fd8  00002fd8
  [22] .got.plt          PROGBITS         0000000000004000  00003000

动态链接库是PIC代码,编译链接完成后,库的load地址显示为0,代码中都使用相对寻址,主要是基于RIP的相对寻址。0x3fe8这个值,是基于0地址的计算值。真正不变的,是rax,QWORD PTR [rip+0x2ed4]这条指令中的立即数。

t.so在进程地址空间中的地址,在比较高的位置:

$ ps -ef | grep test
xinlin     15413    2615  0 16:41 pts/1    00:00:00 ./test
xinlin     15912    2617  0 16:59 pts/2    00:00:00 grep --color=auto test
$ cat /proc/15413/maps
00400000-00401000 r--p 00000000 00:1e 858379                             /home/xinlin/test/test
00401000-00402000 r-xp 00001000 00:1e 858379                             /home/xinlin/test/test
00402000-00403000 r--p 00002000 00:1e 858379                             /home/xinlin/test/test
00403000-00404000 r--p 00002000 00:1e 858379                             /home/xinlin/test/test
00404000-00405000 rw-p 00003000 00:1e 858379                             /home/xinlin/test/test
01770000-01791000 rw-p 00000000 00:00 0                                  [heap]
7f3f26e00000-7f3f26e28000 r--p 00000000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3f26e28000-7f3f26f9c000 r-xp 00028000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3f26f9c000-7f3f26ff4000 r--p 0019c000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3f26ff4000-7f3f26ff8000 r--p 001f3000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3f26ff8000-7f3f26ffa000 rw-p 001f7000 00:1e 639528541                  /usr/lib64/libc.so.6
7f3f26ffa000-7f3f27002000 rw-p 00000000 00:00 0 
7f3f27035000-7f3f27038000 rw-p 00000000 00:00 0 
7f3f27049000-7f3f2704a000 r--p 00000000 00:1e 858378                     /home/xinlin/test/t.so
7f3f2704a000-7f3f2704b000 r-xp 00001000 00:1e 858378                     /home/xinlin/test/t.so
7f3f2704b000-7f3f2704c000 r--p 00002000 00:1e 858378                     /home/xinlin/test/t.so
7f3f2704c000-7f3f2704d000 r--p 00002000 00:1e 858378                     /home/xinlin/test/t.so
7f3f2704d000-7f3f2704e000 rw-p 00003000 00:1e 858378                     /home/xinlin/test/t.so
7f3f2704e000-7f3f27050000 rw-p 00000000 00:00 0 
7f3f27050000-7f3f27052000 r--p 00000000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3f27052000-7f3f27079000 r-xp 00002000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3f27079000-7f3f27084000 r--p 00029000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3f27085000-7f3f27087000 r--p 00034000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7f3f27087000-7f3f27089000 rw-p 00036000 00:1e 639528538                  /usr/lib64/ld-linux-x86-64.so.2
7ffd735fe000-7ffd7361f000 rw-p 00000000 00:00 0                          [stack]
7ffd7371b000-7ffd7371f000 r--p 00000000 00:00 0                          [vvar]
7ffd7371f000-7ffd73721000 r-xp 00000000 00:00 0                          [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0                  [vsyscall]

到此,动态链接库中的全局变量,已经基本分析清楚了!

运行时调试

重新编译,动态链接库和程序代码都带上-g

$ gcc -g -fpic -shared t.c -o t.so
$ gcc -g test.c t.so -o test
$ export LD_LIBRARY_PATH=$(pwd)

用gdb启动test,跟踪到动态链接库内部,查看库内部函数的汇编:

$ gdb -q test
Reading symbols from test...
(gdb) b main
Breakpoint 1 at 0x40114a: file test.c, line 9.
(gdb) r
Starting program: /home/xinlin/test/test 

This GDB supports auto-downloading debuginfo from the following URLs:
https://debuginfod.fedoraproject.org/ 
Enable debuginfod for this session? (y or [n]) 
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib64/libthread_db.so.1".

Breakpoint 1, main () at test.c:9
9           printf("%d\n", data);
Missing separate debuginfos, use: dnf debuginfo-install glibc-2.35-22.fc36.x86_64
(gdb) print data
$1 = 5
(gdb) print &data
$2 = (int *) 0x404034 <data>
(gdb) list
4
5       extern int data;
6       extern void pdata_addr();
7
8       int main() {
9           printf("%d\n", data);
10          printf("%p\n", &data);
11          pdata_addr();
12          sleep(-1);
13          return 0;
(gdb) b 11
Breakpoint 2 at 0x401175: file test.c, line 11.
(gdb) c
Continuing.
5
0x404034

Breakpoint 2, main () at test.c:11
11          pdata_addr();
(gdb) s
pdata_addr () at t.c:7
7           printf("in t.so, data: %d\n", data);
(gdb) list
2
3
4       int data = 5;
5
6       void pdata_addr(){
7           printf("in t.so, data: %d\n", data);
8           printf("in t.so, data: %p\n", &data);
9       }
(gdb) set disassembly-flavor intel
(gdb) disass pdata_addr
Dump of assembler code for function pdata_addr:
   0x00007ffff7fba109 <+0>:     push   rbp
   0x00007ffff7fba10a <+1>:     mov    rbp,rsp
=> 0x00007ffff7fba10d <+4>:     mov    rax,QWORD PTR [rip+0x2ed4]        # 0x7ffff7fbcfe8
   0x00007ffff7fba114 <+11>:    mov    eax,DWORD PTR [rax]
   0x00007ffff7fba116 <+13>:    mov    esi,eax
   0x00007ffff7fba118 <+15>:    lea    rax,[rip+0xee1]        # 0x7ffff7fbb000
   0x00007ffff7fba11f <+22>:    mov    rdi,rax
   0x00007ffff7fba122 <+25>:    mov    eax,0x0
   0x00007ffff7fba127 <+30>:    call   0x7ffff7fba030 <printf@plt>
   0x00007ffff7fba12c <+35>:    mov    rax,QWORD PTR [rip+0x2eb5]        # 0x7ffff7fbcfe8
   0x00007ffff7fba133 <+42>:    mov    rsi,rax
   0x00007ffff7fba136 <+45>:    lea    rax,[rip+0xed6]        # 0x7ffff7fbb013
   0x00007ffff7fba13d <+52>:    mov    rdi,rax
   0x00007ffff7fba140 <+55>:    mov    eax,0x0
   0x00007ffff7fba145 <+60>:    call   0x7ffff7fba030 <printf@plt>
   0x00007ffff7fba14a <+65>:    nop
   0x00007ffff7fba14b <+66>:    pop    rbp
   0x00007ffff7fba14c <+67>:    ret    
End of assembler dump.
(gdb) x /xw 0x7ffff7fbcfe8
0x7ffff7fbcfe8: 0x00404034
(gdb) x /w 0x00404034
0x404034 <data>:        0x00000005

此时运行时,动态链接库中的pdata_addr在访问data时,先访问0x7ffff7fbcfe8,拿到data的真实地址0x00404034,然后再访问此地址,获得data的值。

=> 0x00007ffff7fba10d <+4>:     mov    rax,QWORD PTR [rip+0x2ed4]        # 0x7ffff7fbcfe8
   0x00007ffff7fba114 <+11>:    mov    eax,DWORD PTR [rax]

动态链接库的运行速度

使用动态链接库,必然会导致进程整体的运行速度慢一丢丢,这一丢丢,就是对外部全局符号的访问,需要通过GOT和PLT。GOT和PLT就像是个中间层,用来适配动态链接时必须的动态地址解析。每一次对外部全局符号的访问,都要经过这个中间层!因此,如果仅仅是动态链接库内部使用的变量,请申明为static,防止访问经过GOT表。

本文链接:https://cs.pynote.net/sf/c/cdm/202302161/

-- EOF --

-- MORE --