[c/c++] linux下变量和函数的动态链接说明

一、简要背景

1.linux平台,64位

2.使用.so文件,动态绑定

3.说明基于elf文件

elf(可执行和链接格式文件),即其有两种视图,一个是执行视图(也可以叫程序加载视图,用于将文件加载到内存中并可以执行的视图)图右。另一个是链接视图,用于给链接器在链接阶段使用,图左。如图:

 

二、例子

fun.h

#ifndef _FUN_H_
#define _FUN_H_

extern int fun_num1;

int fun_print();

#endif

fun.c

#include <stdio.h>

int fun_num1 = 5;

int fun_print() {
    printf("fun_num1 %d\n", fun_num1);
}

fun1.h

#ifndef _FUN1_H_
#define _FUN1_H_

extern int fun1_num1;

int fun1_print();

#endif

fun1.c

#include "fun1.h"

#include <stdio.h>

int fun1_num1 = 3;

int fun1_print() {
    printf("fun1_num1:%d\n", fun1_num1);
}

main.c

#include "fun.h"
#include "fun1.h"

int main() {
    fun1_print();
    fun_print();

    fun1_num1 = 1;
    fun_num1 = 2;
    
    fun1_print();
    fun_print();
    
    return 0;
}

makefile

flag=-g
all:main

main.o:main.c
	gcc -o $@ -c $^ $(flag)

main:main.o libfun.so fun1.o
	gcc -o main_s main.o fun1.o -L ./ -lfun

fun_s.o:fun.h fun.c
	gcc -fPIC -o fun_s.o -c fun.c $(flag)

libfun.so:fun_s.o
	gcc --shared -o $@ $^

fun1.o:fun1.h fun1.c
	gcc -o fun1.o -c fun1.c $(flag)

clean:
	rm -rf *.o *.so

.PHONY:clean

运行:

make
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
./main_s

运行结果:

fun1_num1:3
fun_num1 5
fun1_num1:1
fun_num1 2

 

三、函数的动态链接

静态函数链接:fun1_print

动态函数链接:fun_print

1.gdb main_s, disas main

(gdb) disas main
Dump of assembler code for function main:
   0x0000000000400716 <+0>:	push   %rbp
   0x0000000000400717 <+1>:	mov    %rsp,%rbp
   0x000000000040071a <+4>:	mov    $0x0,%eax
   0x000000000040071f <+9>:	callq  0x40075d <fun1_print>
   0x0000000000400724 <+14>:	mov    $0x0,%eax
   0x0000000000400729 <+19>:	callq  0x4005f0 <fun_print@plt>
   0x000000000040072e <+24>:	movl   $0x1,0x200908(%rip)        # 0x601040 <fun1_num1>
   0x0000000000400738 <+34>:	movl   $0x2,0x200906(%rip)        # 0x601048 <fun_num1>
   0x0000000000400742 <+44>:	mov    $0x0,%eax
   0x0000000000400747 <+49>:	callq  0x40075d <fun1_print>
   0x000000000040074c <+54>:	mov    $0x0,%eax
   0x0000000000400751 <+59>:	callq  0x4005f0 <fun_print@plt>
   0x0000000000400756 <+64>:	mov    $0x0,%eax
   0x000000000040075b <+69>:	pop    %rbp
   0x000000000040075c <+70>:	retq   
End of assembler dump.

2.查看静态函数链接fun1_print

0x000000000040071f <+9>: callq  0x40075d <fun1_print>

3.disas 0x40075d,查看该地址是否是fun1_print的函数地址

Dump of assembler code for function fun1_print:
   0x000000000040075d <+0>:	push   %rbp
   0x000000000040075e <+1>:	mov    %rsp,%rbp
   0x0000000000400761 <+4>:	mov    0x2008d9(%rip),%eax        # 0x601040 <fun1_num1>
   0x0000000000400767 <+10>:	mov    %eax,%esi
   0x0000000000400769 <+12>:	mov    $0x400804,%edi
   0x000000000040076e <+17>:	mov    $0x0,%eax
   0x0000000000400773 <+22>:	callq  0x4005e0 <printf@plt>
   0x0000000000400778 <+27>:	nop
   0x0000000000400779 <+28>:	pop    %rbp
   0x000000000040077a <+29>:	retq   
End of assembler dump.

可以看到在gdb下,0x40075d便是静态函数fun1_print的地址,因此对于静态函数链接,其在静态链接阶段便可以知道函数的绝对虚拟地址,便可以直接通过callq来进行函数调用。

4.查看动态函数fun_print

0x0000000000400729 <+19>: callq  0x4005f0 <fun_print@plt>

5.disas 0x4005f0,查看该地址是否是fun_print的函数地址

Dump of assembler code for function fun_print@plt:
   0x00000000004005f0 <+0>:	jmpq   *0x200a2a(%rip)        # 0x601020
   0x00000000004005f6 <+6>:	pushq  $0x1
   0x00000000004005fb <+11>:	jmpq   0x4005d0
End of assembler dump.

我们可以看到这里仅仅只有3个指令,而且没有与printf相关的指令,因此这并不是实际fun_print的函数地址。

下面进行解说

1)callq 0x4005f0, callq是将下一个地址0x40072e的地址压栈,然后跳转到0x4005f0进行函数执行,当该函数执行完后,可以通过这个压栈的值,跳转会这个地址代码中往下执行。

Dump of assembler code for function main:
 ...
   0x0000000000400729 <+19>:	callq  0x4005f0 <fun_print@plt>
   0x000000000040072e <+24>:	movl   $0x1,0x200908(%rip)        # 0x601040 <fun1_num1>
 ...
End of assembler dump.

2)对于fun_print@plt,这个函数是链接器会对每一个动态链接的函数自动生成,一一对应的。也就是说如果你有一个函数如fun在共享库so中,那么链接会自动生成fun@plt函数,而plt为procedure linkage table(过程链接表,在c语言中过程应该就是函数的一个意思),而plt便是该fun函数的一个中间代理。下面说下这个函数的主要作用.

Dump of assembler code for function fun_print@plt:
   0x00000000004005f0 <+0>:	jmpq   *0x200a2a(%rip)        # 0x601020
   0x00000000004005f6 <+6>:	pushq  $0x1
   0x00000000004005fb <+11>:	jmpq   0x4005d0
End of assembler dump.

jmpq *0x200a2a(%rip)

jmpq:是根据后面的值作为地址值进行跳转

*:意思是取后面的值作为地址,该地址存取的值才是最终的使用的地址值

%rip:是当前指令的地址,这里需要注意一下,当进行jmpq *0x200a2a(%rip)这条指令进行取指令操作后,%rip便已经变更为下一跳指令的地址了,因此当前%rip为0x4005f6,所以0x200a2a(%rip)=0x200a2a + 0x4005f6 = 0x601020。

啰嗦一下,0x601020地址是.got.plt section中,got意思是全局偏移表,这个主要是用于动态链接使用的section,这里面会存储动态链接变量和函数的地址,而.got.plt则是给plt section函数使用的全局偏移表。

可通过指令:readelf -SW main_s,.got.plt的address(虚拟地址)起始地址0x601000,size:0x30,所以0x601000~0x601030,所以0x601020的地址在.got.plt中

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
...
  [12] .plt              PROGBITS        00000000004005d0 0005d0 000040 10  AX  0   0 16
  [13] .plt.got          PROGBITS        0000000000400610 000610 000008 00  AX  0   0  8
  [14] .text             PROGBITS        0000000000400620 000620 0001d2 00  AX  0   0 16
...
  [16] .rodata           PROGBITS        0000000000400800 000800 000012 00   A  0   0  4
...
  [23] .got              PROGBITS        0000000000600ff8 000ff8 000008 08  WA  0   0  8
  [24] .got.plt          PROGBITS        0000000000601000 001000 000030 08  WA  0   0  8
  [25] .data             PROGBITS        0000000000601030 001030 000014 00  WA  0   0  8
  [26] .bss              NOBITS          0000000000601048 001044 000008 00  WA  0   0  8
...

3) 查看0x601020地址存储的值,x/2x 0x601020

(gdb) x/2x 0x601020
0x601020:	0x004005f6	0x00000000

存储的地址值为0x00000000004005f6,细心的你会发现这个其实就是fun_print@plt函数中jmpq *0x200a2a(%rip)的下一条指令地址pushq $0x1:

Dump of assembler code for function fun_print@plt:
   ...
   0x00000000004005f6 <+6>:	pushq  $0x1
   0x00000000004005fb <+11>:	jmpq   0x4005d0
End of assembler dump.

到这里可以简要说下动态函数链接的过程了,在第一次调用该动态函数fun_print时,会跳转到中间代理函数fun_print@plt中,fun_print@plt会跳转到.got.plt中存储的地址值,第一次跳转的时候保存的是fun_print@plt的下一命令的地址,下一个命令的地址后续会执行到链接器的代码中,链接器会查询.so文件,查询是否有包含该fun_print函数,当查询到该fun_print函数后,会将该地址更新到.got.plt对应的位置上,那么下一次该函数调用,便会直接跳转到该函数地址,而不需要继续查询绑定,这个便是动态函数的延迟绑定

6 简要查看fun_print@plt后续指令执行到何处,通过objdump -d main_s(不是gdb了)

Disassembly of section .plt:

00000000004005d0 <printf@plt-0x10>:
  4005d0:	ff 35 32 0a 20 00    	pushq  0x200a32(%rip)        # 601008 <_GLOBAL_OFFSET_TABLE_+0x8>
  4005d6:	ff 25 34 0a 20 00    	jmpq   *0x200a34(%rip)        # 601010 <_GLOBAL_OFFSET_TABLE_+0x10>
  4005dc:	0f 1f 40 00          	nopl   0x0(%rax)

00000000004005e0 <printf@plt>:
  4005e0:	ff 25 32 0a 20 00    	jmpq   *0x200a32(%rip)        # 601018 <_GLOBAL_OFFSET_TABLE_+0x18>
  4005e6:	68 00 00 00 00       	pushq  $0x0
  4005eb:	e9 e0 ff ff ff       	jmpq   4005d0 <_init+0x28>

可以看到会跳转到jmpq 0x4005d0,即函数print@plt-0x10,这里一个公用的函数,用于触发链接器函数的调用,那么在这里可以看到其会继续跳转到0x601010,这时候我们再回到gdb进行查看,在gdb中执行下面指令

1)b *_start+0 (代码执行的开始入口)

2)r main (让代码实际加载起来,这样才会将链接器ld映射进行)

3)x/2x 0x601010 

(gdb) x/2x 0x601010
0x601010:	0xf7dee870	0x00007fff

4)disas 0x00007ffff7dee870

(gdb) disas 0x00007ffff7dee870
Dump of assembler code for function _dl_runtime_resolve_avx:
...
   0x00007ffff7dee91e <+174>:	callq  0x7ffff7de69f0 <_dl_fixup>
...
End of assembler dump.

在这里可以看到其会进入链接器_dl_runtime_resolve_avx函数中,然后会进一步调用_dl_fixup,在这里我没有进一步去查看具体实现了,不过在这里已经可以知道后续是会触发链接器的执行的,那么如何证明之前的猜测是否会将fun_print的实际地址更新到.got.plt中呢,下面继续

1.1) b *main+24 (在0x40072e处打下断点,就是在调用完fun_print@plt后的下一个指令)

1.2) c (让代码执行到0x40072e)

1.3) x/2x 0x601020 (查看fun_print@plt中跳转到.got.plt中保存的地址值,从下面可以知道应该由原来的0x4005e6更新为0x00007ffff7bd56e0)

(gdb) x/2x 0x601020
0x601020:	0xf7bd56e0	0x00007fff

1.4) disas 0x00007ffff7bd56e0 (可以知道这个便是实际的fun_print函数了)

Dump of assembler code for function fun_print:
   0x00007ffff7bd56e0 <+0>:	push   %rbp
   0x00007ffff7bd56e1 <+1>:	mov    %rsp,%rbp
   0x00007ffff7bd56e4 <+4>:	mov    0x2008ed(%rip),%rax        # 0x7ffff7dd5fd8
   0x00007ffff7bd56eb <+11>:	mov    (%rax),%eax
   0x00007ffff7bd56ed <+13>:	mov    %eax,%esi
   0x00007ffff7bd56ef <+15>:	lea    0x17(%rip),%rdi        # 0x7ffff7bd570d
   0x00007ffff7bd56f6 <+22>:	mov    $0x0,%eax
   0x00007ffff7bd56fb <+27>:	callq  0x7ffff7bd55c0 <printf@plt>
   0x00007ffff7bd5700 <+32>:	nop
   0x00007ffff7bd5701 <+33>:	pop    %rbp
   0x00007ffff7bd5702 <+34>:	retq   
End of assembler dump.

1.5) 经过上面的操作后,也印证了,第一次执行动态函数会触发链接器查询动态函数的实际地址,然后其会更新.got.plt中保存的地址值,以后该函数调用便可以直接跳转到实际的函数地址了

 

四 变量的动态链接

静态变量链接:fun1_num1

动态变量链接:fun_num1

0.预先操作

1)gdb main_s (调试main_s)

2)b *_start+0 (打断点在代码入口点)

3)r (执行代码)

1.gdb main,disas main

(gdb) disas main
Dump of assembler code for function main:
   0x0000000000400716 <+0>:	push   %rbp
   0x0000000000400717 <+1>:	mov    %rsp,%rbp
   0x000000000040071a <+4>:	mov    $0x0,%eax
   0x000000000040071f <+9>:	callq  0x40075d <fun1_print>
   0x0000000000400724 <+14>:	mov    $0x0,%eax
   0x0000000000400729 <+19>:	callq  0x4005f0 <fun_print@plt>
   0x000000000040072e <+24>:	movl   $0x1,0x200908(%rip)        # 0x601040 <fun1_num1>
   0x0000000000400738 <+34>:	movl   $0x2,0x200906(%rip)        # 0x601048 <fun_num1>
   0x0000000000400742 <+44>:	mov    $0x0,%eax
   0x0000000000400747 <+49>:	callq  0x40075d <fun1_print>
   0x000000000040074c <+54>:	mov    $0x0,%eax
   0x0000000000400751 <+59>:	callq  0x4005f0 <fun_print@plt>
   0x0000000000400756 <+64>:	mov    $0x0,%eax
   0x000000000040075b <+69>:	pop    %rbp
   0x000000000040075c <+70>:	retq   
End of assembler dump.

2 查看静态变量调用

0x000000000040072e <+24>: movl   $0x1,0x200908(%rip)        # 0x601040 <fun1_num1>

3 x 0x601040

0x601040 <fun1_num1>:	0x00000003

值是3,因此在可执行文件main_s,fun1_num1是绝对虚拟地址访问就可以了

4 查看动态变量调用

0x0000000000400738 <+34>: movl   $0x2,0x200906(%rip)        # 0x601048 <fun_num1>

5 x 0x601048

0x601048 <fun_num1>:	0x00000005

值是5,因此动态变量在可行文件main_s,fun_num1也是绝对虚拟地址访问就可以了,在这里似乎有个疑问,不应该是不一样的吗?怎么会一样的。这是因为对于最终生成的文件main_s中,如果该静态链接有用到全局变量,那么该全局变量会直接保存到main_s的数据段,可能在.data(已经初始化的变量,相对于静态链接时是否有初始化),可能在.bss(未初始化的变量,相对于静态链接时是否有初始化)。而这些全局变量不管是否是在.so中定义,还是在静态目标文件中有定义,只要是被静态目标文件有使用,一律会在最终的可执行文件中有一份保存在数据段中。而其他的静态目标文件会之间重定位地址使用绝对虚拟地址进行访问,而其他的.so文件则会使用间接访问的方式来访问该全局变量。因此要查看不同点,需要查看.so文件的函数代码。

6 disas fun_print

(gdb) disas fun_print
Dump of assembler code for function fun_print:
   0x00007ffff7bd56e0 <+0>:	push   %rbp
   0x00007ffff7bd56e1 <+1>:	mov    %rsp,%rbp
   0x00007ffff7bd56e4 <+4>:	mov    0x2008ed(%rip),%rax        # 0x7ffff7dd5fd8
   0x00007ffff7bd56eb <+11>:	mov    (%rax),%eax
   0x00007ffff7bd56ed <+13>:	mov    %eax,%esi
   0x00007ffff7bd56ef <+15>:	lea    0x17(%rip),%rdi        # 0x7ffff7bd570d
   0x00007ffff7bd56f6 <+22>:	mov    $0x0,%eax
   0x00007ffff7bd56fb <+27>:	callq  0x7ffff7bd55c0 <printf@plt>
   0x00007ffff7bd5700 <+32>:	nop
   0x00007ffff7bd5701 <+33>:	pop    %rbp
   0x00007ffff7bd5702 <+34>:	retq   
End of assembler dump.

0x00007ffff7bd56e4 <+4>: mov    0x2008ed(%rip),%rax        # 0x7ffff7dd5fd8

0x00007ffff7bd56eb <+11>: mov    (%rax),%eax

可以看到这里有两个指令,第一个是将0x2008ed(%rip)的值作为地址0x7ffff7dd5fd8,该地址所存放值放到寄存器%rax中,然后再将%rax所存放的值作为地址,然后将该地址所指的值存放到寄存器eax中,这里的eax便是最终实际fun_num1值打印出来。

1) x/2x 0x7ffff7dd5fd8

(gdb) x/2x 0x7ffff7dd5fd8
0x7ffff7dd5fd8:	0x00601048	0x00000000

会发现该地址所存放的值为0x601048,正好是前面fun_num1中的地址值

(gdb) disas main
Dump of assembler code for function main:
...
   0x000000000040072e <+24>:	movl   $0x1,0x200908(%rip)        # 0x601040 <fun1_num1>
   0x0000000000400738 <+34>:	movl   $0x2,0x200906(%rip)        # 0x601048 <fun_num1>
...
End of assembler dump.

 因此在.so文件中访问全局变量,会通过一个位置来存储该全局变量的地址,而该位置其实就是.got section,因此也可以推测出.got.plt是用于动态函数链接,而.got是用于动态变量链接的。那么动态函数链接是延迟绑定的(即第一次函数调用才进行地址查找),那动态变量链接也是这样的吗?经过我的实践,发现动态变量的链接是在程序加载完执行_start后代码入口时便已经将动态变量的地址初始化好了,因此动态变量链接并不会进行延迟绑定。另一个可以证明我的言论是

1.1) ps -ef | grep main_s (查看该进程的id)

1.2) cat /proc/{pid}/maps (查看该进行的地址映射)

00400000-00401000 r-xp 00000000 fd:01 1455595                            /tmp/test/main_s
00600000-00601000 r--p 00000000 fd:01 1455595                            /tmp/test/main_s
00601000-00602000 rw-p 00001000 fd:01 1455595                            /tmp/test/main_s
7ffff780b000-7ffff79cb000 r-xp 00000000 fd:01 402248                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff79cb000-7ffff7bcb000 ---p 001c0000 fd:01 402248                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7bcb000-7ffff7bcf000 r--p 001c0000 fd:01 402248                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7bcf000-7ffff7bd1000 rw-p 001c4000 fd:01 402248                     /lib/x86_64-linux-gnu/libc-2.23.so
7ffff7bd1000-7ffff7bd5000 rw-p 00000000 00:00 0 
7ffff7bd5000-7ffff7bd6000 r-xp 00000000 fd:01 1455592                    /tmp/test/libfun.so
7ffff7bd6000-7ffff7dd5000 ---p 00001000 fd:01 1455592                    /tmp/test/libfun.so
7ffff7dd5000-7ffff7dd6000 r--p 00000000 fd:01 1455592                    /tmp/test/libfun.so
7ffff7dd6000-7ffff7dd7000 rw-p 00001000 fd:01 1455592                    /tmp/test/libfun.so
7ffff7dd7000-7ffff7dfd000 r-xp 00000000 fd:01 402246                     /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7fea000-7ffff7fed000 rw-p 00000000 00:00 0 
7ffff7ff6000-7ffff7ff7000 rw-p 00000000 00:00 0 
7ffff7ff7000-7ffff7ffa000 r--p 00000000 00:00 0                          [vvar]
7ffff7ffa000-7ffff7ffc000 r-xp 00000000 00:00 0                          [vdso]
7ffff7ffc000-7ffff7ffd000 r--p 00025000 fd:01 402246                     /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffd000-7ffff7ffe000 rw-p 00026000 fd:01 402246                     /lib/x86_64-linux-gnu/ld-2.23.so
7ffff7ffe000-7ffff7fff000 rw-p 00000000 00:00 0 
7ffffffde000-7ffffffff000 rw-p 00000000 00:00 0                          [stack]
ffffffffff600000-ffffffffff601000 r-xp 00000000 00:00 0                  [vsyscall]

可以看到7ffff7dd5000-7ffff7dd6000 r--p 00000000 fd:01 1455592                    /tmp/test/libfun.so 是只读的

而libfun.so的开始地址是0x7ffff7bd5000, 那么可以计算出来7ffff7dd5000-7ffff7dd6000对应的虚拟地址是(0x20000-0x21000)

1.3) readelf -SW libfun.so

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
...
  [21] .got              PROGBITS        0000000000200fd0 000fd0 000030 08  WA  0   0  8
  [22] .got.plt          PROGBITS        0000000000201000 001000 000020 08  WA  0   0  8
...

可以看到.got是在0x2000-0x21000之间的,因此.got是只读的,因此在程序运行的过程中,其是不允许修改的,因此其不能进行延迟绑定。

而同时可以看到.got.plt是在下一个段的rw-p中,因此.got.plt是可以读写的,因此可以在程序运行期间进行数据修改的,因此其可以完成延迟绑定的需要条件

 

五、自问自答

1.既然so文件是共享文件,物理内存中只有一份拷贝,而其他是该so文件是通过mmap地址映射(虚拟地址映射到物理实现的),那么对于其中一个已经执行起来的程序,其在动态链接的过程中,必然会修改mmap所映射的so文件的.got和.got.plt对应存储值来实现动态链接,那么这样不就会修改mmap地址映射对应的so文件的物理地址了吗?这样后面再执行的程序不就会读到被修改的地址值而受到影响了吗?

答:因为在使用mmap时,基本都是使用map_private属性的,即是用write on copy,写时拷贝,因此映射的段发生写的操作时,会拷贝一份到新的物理地址,这样便不会影响到实际的so文件的物理地址值了。因此也就可以理解为每个进程其.got,.got.plt都是进程独有了,或者说其数据段都是进程独占的。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值