第4章 静态链接

静态链接

       一、空间与地址分配

       这里的“空间和地址”有两个含义:第一,在输出的可执行文件ab.o中的空间;第二,是在装载后的虚拟地址中的虚拟地址空间。

       现在的链接器的策略基本上都是:将a.o和b.o中相似段合并(如.text和.text段合并),然后再分配空间(ab文件中分配空间基本上是两个.text段加起来的大小;类似地,在虚拟地址空间中也去指定一段空间)。见P102图

实验:观察静态链接过程的“空间和地址分配”:

1. 将a.o和b.o链接成ab.o

两个源代码文件:

/*a.c*/
extern int shared;
int main(){
    int a=100;
    swap(&a, &shared);
}

 和

/*b.c*/
int shared =1 ;
void swap(int *a, int* b){
    *a^=*b^=*a^=*b;
}

$gcc -c a.c b.c ==> 使用-c参数只完成预处理、编译、汇编,(不链接),产生a.o和b.o

$ld a.o b.o -e main -o ab ==> 链接a.o和b.o,指定main()作为程序入口(默认入口是_start),指定输出ab

 

2. 观察“空间和地址分配情况”

$ readelf -S a.o  或  objdump -h a.o

$ readelf -S b.o  或  objdump -h b.o

$ readelf -S ab  或  objdump -h ab

 

       二、符号解析与重定位

        上面虽然解决了地址和空间的分配,但是对于a.o目标文件引用到b.o中“变量shared”和"函数swap"的问题还为解决。每个目标文件都可能定义一些符号,也可能引用到其他目标文件的符号,输入链接器的目标文件的符号表会组成一个“全局符号表”;在“符号解析”的过程中找到了一些在a.o中未定义、引用其他目标文件中定义的符号;而将a.o中这些未定义的符号的占位符(目标文件中某些偏移处),修改为正确的符号变量地址/函数入口地址(注:也是“虚拟地址空间中的地址”),进而产生可执行文件ab的过程,称为“指令修正/重定位”。

 

实验:符号解析和指令修正

 1. 通过查看a.o的重定位段(又称为“重定位表”),可以找到未定义的符号。这些符号必须通过ld链接从其他模块获取,否则报错:一般地,UND这种未定义的符号都是因为该目标文件中有关于它们的重定位项,所以在链接器扫描完所有的输入目标文件之后,所有这些未定义的符号都应该能够在全局符号表中找到,否则链接器就会报“符号未定义”错。

[hadoop@sam1 mydir]$ readelf -s a.o

Symbol table '.symtab' contains 10 entries:

   Num:    Value  Size Type    Bind   Vis      Ndx Name

     0: 00000000     0 NOTYPE  LOCAL  DEFAULT  UND 

     1: 00000000     0 FILE    LOCAL  DEFAULT  ABS a.c

     2: 00000000     0 SECTION LOCAL  DEFAULT    1 

     3: 00000000     0 SECTION LOCAL  DEFAULT    3 

     4: 00000000     0 SECTION LOCAL  DEFAULT    4 

     5: 00000000     0 SECTION LOCAL  DEFAULT    6 

     6: 00000000     0 SECTION LOCAL  DEFAULT    5 

     7: 00000000    39 FUNC    GLOBAL DEFAULT    1 main

     8: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND shared

     9: 00000000     0 NOTYPE  GLOBAL DEFAULT  UND swap

 

2. 反汇编a.o和ab,可以看到指令修正的痕迹。

 

[hadoop@sam1 mydir]$ objdump -r a.o   ==> 查看“重定位表”(即“重定位段”),可知文件a.o中分别偏移0x15和0x21两个位置的4个字节需要“重定位”(或称“符号修正”)。

a.o:     file format elf32-i386

RELOCATION RECORDS FOR [.text]:

OFFSET   TYPE              VALUE 

00000015 R_386_32          shared          ==> 每个需要重定位之处(目标文件中某个偏移)叫“重定位入口”

00000021 R_386_PC32        swap

 

Sam: 从上面可以看到重定位表的结构:一个需要重定位的section对应有一个重定位段;是一个数组,每一个元素记录的是某个偏移处需要被重定位的变量(函数)名称。

 

[hadoop@sam1 mydir]$ objdump -d a.o

a.o:     file format elf32-i386

Disassembly of section .text:

00000000 <main>:

   0: 55                   push   %ebp

   1: 89 e5                 mov    %esp,%ebp

   3: 83 e4 f0             and    $0xfffffff0,%esp

   6: 83 ec 20             sub    $0x20,%esp

   9: c7 44 24 1c 64 00 00 movl   $0x64,0x1c(%esp)

  10: 00 

  11: c7 44 24 04 00 00 00 movl   $0x0,0x4(%esp)

  18: 00 

  19: 8d 44 24 1c           lea    0x1c(%esp),%eax

  1d: 89 04 24             mov    %eax,(%esp)

  20: e8 fc ff ff ff       call   21 <main+0x21>

  25: c9                   leave  

  26: c3                   ret  

  

[hadoop@sam1 mydir]$ objdump -d ab

ab:     file format elf32-i386

Disassembly of section .text:

08048094 <main>:

 8048094: 55                   push   %ebp

 8048095: 89 e5                 mov    %esp,%ebp

 8048097: 83 e4 f0             and    $0xfffffff0,%esp

 804809a: 83 ec 20             sub    $0x20,%esp

 804809d: c7 44 24 1c 64 00 00 movl   $0x64,0x1c(%esp)

 80480a4: 00 ==> 这里4个字节本类被修正为“shared的地址” (P109 “绝对近址32位寻址”方式,重定位入口修正)

 80480a5: c7 44 24 04 f8 90 04 movl   $0x80490f8,0x4(%esp)

 80480ac: 08 

 80480ad: 8d 44 24 1c           lea    0x1c(%esp),%eax

 80480b1: 89 04 24             mov    %eax,(%esp)

 80480b4: e8 03 00 00 00       call   80480bc <swap>  ==> 这里4个字节被修正为“swap的入口地址”(P109 “相对近址32位寻址”方式,重定位入口修正)

 80480b9: c9                   leave  

 80480ba: c3                   ret    

 80480bb: 90                   nop

 

080480bc <swap>:

 80480bc: 55                   push   %ebp

 80480bd: 89 e5                 mov    %esp,%ebp

 80480bf: 53                   push   %ebx

 80480c0: 8b 45 08             mov    0x8(%ebp),%eax

 80480c3: 8b 10                 mov    (%eax),%edx

 80480c5: 8b 45 0c             mov    0xc(%ebp),%eax

 80480c8: 8b 08                 mov    (%eax),%ecx

 80480ca: 8b 45 08             mov    0x8(%ebp),%eax

 80480cd: 8b 18                 mov    (%eax),%ebx

 80480cf: 8b 45 0c             mov    0xc(%ebp),%eax

 80480d2: 8b 00                 mov    (%eax),%eax

 80480d4: 31 c3                 xor    %eax,%ebx

 80480d6: 8b 45 08             mov    0x8(%ebp),%eax

 80480d9: 89 18                 mov    %ebx,(%eax)

 80480db: 8b 45 08             mov    0x8(%ebp),%eax

 80480de: 8b 00                 mov    (%eax),%eax

 80480e0: 31 c1                 xor    %eax,%ecx

 80480e2: 8b 45 0c             mov    0xc(%ebp),%eax

 80480e5: 89 08                 mov    %ecx,(%eax)

 80480e7: 8b 45 0c             mov    0xc(%ebp),%eax

 80480ea: 8b 00                 mov    (%eax),%eax

 80480ec: 31 c2                 xor    %eax,%edx

 80480ee: 8b 45 08             mov    0x8(%ebp),%eax

 80480f1: 89 10                 mov    %edx,(%eax)

 80480f3: 5b                   pop    %ebx

 80480f4: 5d                   pop    %ebp

 80480f5: c3                   ret  

 

注:对于32位x86平台的ELF文件的重定位入口修正的指令寻址方式只有以上两种。这两种方式 每个被修正的位置的长度都是4个字节。 

 

       三、COMMON块 (P112)

(1)编译为目标文件时,未初始化的局部静态变量就在.bss段中分配空间了;

(2)编译为目标文件时,未初始化的全局变量是弱符号,占用空间大小未知(因为其他编译单元中所占用的空间可能比本编译单元所占空间要大,最终分配空间取最大的一个),因此暂时标记为common;当链接器读取所有输入目标文件之后,才能最终确定在.bss段中分配的空间——因此,最终还是被要在.bss段中分配空间的。

 

实验:链接之前,未初始化的全局变量尚未放在.bss段中(而是被标记为COMMON)

 

C代码test.c

int printf(const char* format, ...);

int global_init_var=84;                 //.data
int global_uninit_var;                  //COM, 可能在别的文件中被定义

static int static_global_init_var=84;   //.data
static int static_global_uninit_var;    //.bss, 只在本文件中被用到

void func1(int i){
    printf("%d\n",i);
}

int main(void){
    static int static_var=85;           //.data
    static int static_var2;             //.bss, 只在本文件中被用到

    int a=1;                    //stack
    int b;                      //stack

    func1(static_var + static_var2 + a + b);
    return a;
}
$ gcc -c test.c -o test.o
$ readelf -s test.o
Symbol table '.symtab' contains 18 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND
     1: 0000000000000000     0 FILE    LOCAL  DEFAULT  ABS test.c
     2: 0000000000000000     0 SECTION LOCAL  DEFAULT    1
     3: 0000000000000000     0 SECTION LOCAL  DEFAULT    3
     4: 0000000000000000     0 SECTION LOCAL  DEFAULT    4
     5: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    3 static_global_init_var
     6: 0000000000000000     0 SECTION LOCAL  DEFAULT    5
     7: 0000000000000008     4 OBJECT  LOCAL  DEFAULT    3 static_var.0
     8: 0000000000000000     4 OBJECT  LOCAL  DEFAULT    4 static_var2.1
     9: 0000000000000004     4 OBJECT  LOCAL  DEFAULT    4 static_global_uninit_var
    10: 0000000000000000     0 SECTION LOCAL  DEFAULT    6
    11: 0000000000000000     0 SECTION LOCAL  DEFAULT    8
    12: 0000000000000000     0 SECTION LOCAL  DEFAULT    9
    13: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 global_init_var
    14: 0000000000000000    31 FUNC    GLOBAL DEFAULT    1 func1
    15: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND printf
    16: 000000000000001f    45 FUNC    GLOBAL DEFAULT    1 main
    17: 0000000000000004     4 OBJECT  GLOBAL DEFAULT  COM global_uninit_var

  

       四、C++相关问题 (略,P113~)

  

       五、静态库链接

       一种语言的开发环境往往附带自己的“语言库”(Language Library),这些库是对操作系统API的封装。如C语言标准库中的printf()函数,最终会调用Linux下的write系统调用/或Windows下的WriteConsole系统API. 

       一个静态(链接)库可以简单看作是一组目标文件经过压缩打包形成的一个文件。

       Linux中最常用的C语言静态库libc是/usr/lib/libc.a (属于glibc项目一部分,貌似要安装才行), C语言集成开发环境VC2008附带的一些C运行库则放在$VC2008/lib/下。

 

       六、链接过程控制

       $ld -verbose  ==> 查看ld默认链接脚本

实验:使用自己的ld链接脚本——将三个Section合并到一个名为"tinytext"的Section,并抛弃.comment段

 

C程序TinyHelloWorld.c

char* str = "Hello world\n";

void print()
{	//使用write的系统调用, write的调用号为4,原型为int write(int filedesc, char* buffer, int size)
	//这里的系统调用,先将参数写入寄存器,之后传入write调用
	asm(
	"movl $13, %%edx \n\t"		//str的长度
	"movl %0, %%ecx \n\t"           //缓冲区,这里的%0,指的是“r”后面的字符串地址,也就是传入到这段汇编的参数。
	"movl $0, %%ebx \n\t"		//打印到标准输出, 也就是0
	"movl $4, %%eax	\n\t"		//将系统调用号传入eax。 
	"int $0x80		\n\t"	//执行中断, 调用write函数
	::"r" (str):"edx","ecx", "ebx"	//传入的参数列表, 被重命名的寄存器列表。
	);
}

void exit()
{
	asm(
		"movl $42,%ebx \n\t "			
		"movl $1, %eax \n\t"
		"int $0x80		\n\t"
	);
}
//这里是程序的入口
void nomain()
{
	print();	
	exit();
}

 

ld链接脚本TinyHelloWorld.lds  

ENTRY(nomain)
SECTIONS
{
. = 0x00008000 + SIZEOF_HEADERS;
tinytext : { *(.text) *(.data) *(.rodata) }
/DISCARD/ : { *(.comment) }
}

 

[hadoop@sam1 test]$ gcc -c -fno-builtin TinyHelloWorld.c

-c: 预处理、编译、汇编到.o文件,不链接

-fno-builtin:关闭GCC内置函数优化

[hadoop@sam1 test]$ ld -static -T TinyHelloWorld.lds -o TinyHelloWorld TinyHelloWorld.o

-static:使用静态链接,而非动态链接

[hadoop@sam1 test]$ ls -l

-rwxrwxr-x 1 hadoop hadoop    604 Jan 21 23:38 TinyHelloWorld

-rw-rw-r-- 1 hadoop hadoop    870 Jan 21 23:37 TinyHelloWorld.c

-rw-rw-r-- 1 hadoop hadoop    132 Jan 21 23:37 TinyHelloWorld.lds

-rw-rw-r-- 1 hadoop hadoop   1008 Jan 21 23:38 TinyHelloWorld.o

[hadoop@sam1 test]$ readelf -S TinyHelloWorld
There are 5 section headers, starting at offset 0xec:
Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] tinytext          PROGBITS        00008074 000074 000051 00 WAX  0   0  4
  [ 2] .shstrtab         STRTAB          00000000 0000c5 000024 00      0   0  1
  [ 3] .symtab           SYMTAB          00000000 0001b4 000080 10      4   4  4
  [ 4] .strtab           STRTAB          00000000 000234 000028 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

        七、BFD库

       BFD库(Binary File Descriptor library)是一个GNU项目,他的目标是通过一种统一的接口处理不同格式的目标文件,需要安装binutils-dev。

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值