1. 环境表

    每个程序都会从父进程那里接收一张环境表。和参数包一样,环境表也是一个字符指针数组,其中每个指针包含你一个null结束的C字符串地址。全局变量environ则包含该指针数组地址,称为环境指针。环境指针指向环境表,保存每个环境字符串的地址。每个环境字符串都是name=value的形式。可以用getenv和putenv来访问特定的环境标量。
extern char **environ;
环境指针 环境表
environ-----> 环境字符串地址-----> HOME=/home/luffy\0
环境字符串地址-----> PATH=:/bin:/usr/bin\0
环境字符串地址-----> SHELL=/bin/bash\0
环境字符串地址-----> USER=luffy\0
环境字符串地址-----> LOGNAME=luffy\0
NULL
 
2. C程序的存储空间布局
一般来说,从低地址和到高地址依次是:
(1). 正文段 这是CPU执行的机器指令部分。通常,正文段是可共享的,而且常常是只读的,以防止程序由于意外而修改其自身指令。
(2). 初始化数据段 通常称之为数据段,它包含了程序中需明确赋初值的变量。例如C程序出现在函数之外的声明:int maxcount = 99;
使此便利那个带有其初值存放在初始化数据段中。
(3). 非初始化数据段 通常称之为bss(起源与早期的汇编运算符,意思是block started by symbol由符号开始的段)段。在程序开始执行之前,内核将此段中的数据初始化为0或空指针(通常exec时),这就是为什么静态变量和全局变量会初始化为0。锁例如出现在函数之外的声明:long sum[1000];
(4). 堆 由上面的弟低地址开始向高地址延展。通常在堆中进行动态存储分配。位于BSS和堆之间。
(5). 栈 自动变量,函数调用时所需要保存的信息(返回地址,调用着寄存器环境信息)都保存在这个段中。然后被调用的函数会在栈上为自动和临时变量分配存储空间。这就实现了递归,函数实例中的变量集不会影响该函数另一个实例中的变量。栈由高地址向低地址延展。高地址临接环境参数和命令行参数的段。
(6). 环境参数和命令行参数 保存进程环境信息。
size命令报告程序的各个段长度。
$ size /bin/bash
   text   data    bss    dec    hex filename
 916355  35848  23304 975507  ee293 /bin/bash
 
3. 共享库
    Linux 系统上有两类根本不同的 Linux 可执行程序。第一类是静态链接的可执行程序。静态可执行程序包含执行所需的所有函数 — 换句话说,它们是“完整的”。因为这一原因,静态可执行程序不依赖任何外部库就可以运行。第二类是动态链接的可执行程序。
我们可以用 ldd 命令来确定某一特定可执行程序是否为静态链接的:
# ldd /sbin/sln
not a dynamic executable
“not a dynamic executable”是 ldd 说明 sln 是静态链接的一种方式。现在,让我们比较 sln 与其非静态同类 ln 的大小:
# ls -l /bin/ln /sbin/sln
-rwxr-xr-x    1 root     root        23000 Jan 14 00:36 /bin/ln
-rwxr-xr-x    1 root     root       381072 Jan 14 00:31 /sbin/sln
    如您所见,sln 的大小超过 ln 十倍。ln 比 sln 小这么多是因为它是动态可执行程序。动态可执行程序是不完整的程序,它依靠外部共享库来提供运行所需的许多函数。
要查看 ln 依赖的所有共享库的列表,可以使用 ldd 命令:
# ldd /bin/ln
libc.so.6 => /lib/libc.so.6 (0x40021000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)
   如您所见,ln 依赖外部共享库 libc.so.6 和 ld-linux.so.2。通常,动态链接的程序比其静态链接的等价程序小得多。不过,静态链接的程序可以在某些低级维护任务中发挥作用(动态共享库减小了文件大小,以及运行时内存和磁盘空间的占用,但是在动态链接dynamic loading时增加了时间开销)。例如,sln 是修改位于 /lib 中的不同库符号链接的极佳工具。但通常您会发现几乎所有Linux系统上的可执行程序都是某种动态链接的变体。
动态装入器:那么,如果动态可执行程序不包含运行所需的所有函数,Linux 的哪部分负责将这些程序和所有必需的共享库一起装入,以使它们能正确执行呢?答案是动态装入器(dynamic loader),它实际上是您在 ln 的 ldd 清单中看到的作为共享库相关性列出的 ld-linux.so.2 库。动态装入器负责装入动态链接的可执行程序运行所需的共享库。
    现在,让我们迅速查看一下动态装入器如何在系统上找到适当的共享库。
    动态装入器找到共享库要依靠两个文件 — /etc/ld.so.conf 和 /etc/ld.so.cache。如果您对 /etc/ld.so.conf 文件进行 cat 操作,您可能会看到一个与下面类似的清单:
$ cat /etc/ld.so.conf
/usr/X11R6/lib
/usr/lib/gcc-lib/i686-pc-linux-gnu/2.95.3
/usr/lib/mozilla
/usr/lib/qt-x11-2.3.1/lib
/usr/local/lib
ld.so.conf 文件包含一个所有目录(/lib 和 /usr/lib 除外,它们会自动包含在其中)的清单,动态装入器将在其中查找共享库。
但是在动态装入器能“看到”这一信息之前,必须将它转换到 ld.so.cache 文件中。可以通过运行 ldconfig 命令做到这一点:
#ldconfig
当ldconfig 操作结束时,您会有一个最新的 /etc/ld.so.cache 文件,它反映您对 /etc/ld.so.conf 所做的更改。从这一刻起,动态装入器在寻找共享库时会查看您在 /etc/ld.so.conf 中指定的所有新目录。
ldconfig 技巧
要查看 ldconfig 可以“看到”的所有共享库,请输入: # ldconfig -p | less
还有另一个方便的技巧可以用来配置共享库路径。有时候您希望告诉动态装入器在尝试任何 /etc/ld.so.conf 路径以前先尝试使用特定目录中的共享库。在您运行的较旧的应用程序不能与当前安装的库版本一起工作的情况下,这会比较方便。
要指示动态装入器首先检查某个目录,请将LD_LIBRARY_PATH变量设置成您希望搜索的目录。多个路径之间用逗号分隔;例如:
#export LD_LIBRARY_PATH="/usr/lib/old:/opt/lib"
导出 LD_LIBRARY_PATH 后,如有可能,所有从当前 shell 启动的可执行程序都将使用 /usr/lib/old 或 /opt/lib 中的库,如果仍不能满足一些共享库相关性要求,则转回到 /etc/ld.so.conf 中指定的库。
    如果编译时阻止使用共享库,则编译后文件的大小会大大增加。
例如:$cc -static hello1.c
然后查看大小$ls -l a.out
再$size a.out可以得到hello world的text正文段增加了300多KB。
 
4. 用Linux进行动态链接
    Linux中的动态链接的共享库的过程。当用户启动一个应用程序时,它们正在调用一个可执行和链接格式(Executable and Linking Format,ELF)映像。内核首先将 ELF 映像加载到用户空间虚拟内存中。然后内核会注意到一个称为 .interp 的 ELF 部分,它指明了将要被使用的动态链接器(/lib64/ld-linux-x86-64.so.2)。这与 UNIX® 中的脚本文件的解释器定义(#!/bin/sh)很相似:只是用在了不同的上下文中。
luffy@luffy-laptop:/usr/bin$ readelf -l zip
Elf file type is EXEC (Executable file)
Entry point 0x402250
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]
......
补充:readelf的选项(一部分)
-a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
-h --file-header       Display the ELF file header
-l --program-headers   Display the program headers
   --segments          An alias for --program-headers
-S --section-headers   Display the sections' header
   --sections          An alias for --section-headers
-g --section-groups    Display the section groups
-t --section-details   Display the section details
-e --headers           Equivalent to: -h -l -S
-s --syms              Display the symbol table
   --symbols           An alias for --syms
--dyn-syms             Display the dynamic symbol table
 
    由上面可以看出/lib64/ld-linux-x86-64.so.2本身就是一个 ELF 共享库,但它是静态编译的并且不依赖其他共享库。当需要动态链接时,内核会引导动态链接(ELF解释器),该链接首先会初始化自身,然后加载指定的共享对象(已加载则不必)。接着它会执行必要的再定位,包括目标共享对象所使用的共享对象。在LD_LIBRARY_PATH 环境变量定义查找可用共享对象的位置。定义完成后,控制权会被传回到初始程序以开始执行。
    再定位是通过一个称为 Global Offset Table(GOT)和 Procedure Linkage Table(PLT)的间接机制来处理的。这些表格提供了 ld-linux.so 或ld-linux-x86-64.so在再定位过程中加载的外部函数和数据的地址。这意味着无需改动需要间接机制(即,使用这些表格)的代码:只需要调整这些表格。一旦进行加载,或者只要需要给定的函数,就可以发生再定位。
    再定位完成后,动态链接器就会允许任何加载的共享对象来执行可选的初始化代码。该函数允许库来初始化内部数据并备之待用。这个代码是在上述 ELF 映像的.init 部分中定义的。在卸载库时,它还可以调用一个终止函数(定义为映像的.fini 部分)。当初始化函数被调用时,动态链接器会把控制权转让给加载的原始映像。
 
5. 用Linux进行动态加载
    动态加载是由应用程序自身管理共享库的加载/链接(而Linux内核并不负责)。使用动态加载,应用程序能够先指定要加载的库,然后将该库作为一个可执行文件来使用(即调用其中的函数)。但是正如您在前面所了解到的,用于动态加载的共享库与标准共享库(ELF共享对象)过程一样。ld-linux或ld-linux-x86-64.so动态链接器仍然参与到这个过程中。Dynamic Loading API就是为动态加载而存在的,在头文件dlfcn.h中声明。
    首先是调用 dlopen,提供要访问的文件对象和模式。调用 dlopen 的结果是稍候要使用的对象的句柄。mode 参数通知动态链接器何时执行再定位。有两个可能的值。第一个是 RTLD_NOW,它表明动态链接器将会在调用 dlopen 时完成所有必要的再定位。第二个可选的模式是 RTLD_LAZY,它只在需要时执行再定位。这是通过在内部使用动态链接器重定向所有尚未再定位的请求来完成的。这样,动态链接器就能够在请求时知晓何时发生了新的引用,而且再定位可以正常进行。后面的调用无需重复再定位过程。
#include <dlfcn.h>
void *dlopen( const char *file, int mode );
    有了ELF对象的句柄,就可以通过调用dlsym来识别这个对象内的符号的地址了。该函数采用一个符号名称,如对象内的一个函数的名称。返回值为对象符号的解析地址:
void *dlsym( void *restrict handle, const char *restrict name );
    如果调用该 API 时发生了错误,可以使用 dlerror 函数返回一个表示此错误的人类可读的字符串。该函数没有参数,它会在发生前面的错误时返回一个字符串,在没有错误发生时返回NULL:
char *dlerror();
    最后,如果无需再调用共享对象的话,应用程序可以调用 dlclose 来通知操作系统不再需要句柄和对象引用了。它完全是按引用来计数的,所以同一个共享对象的多个用户相互间不会发生冲突(只要还有一个用户在使用它,它就会待在内存中)。任何通过已关闭的对象的 dlsym 解析的符号都将不再可用。
char *dlclose( void *handle );
 
6. dl的示例
用dl api实现shell,它允许操作员来指定库、函数和参数。
 
 
  
  1. #include <stdio.h> 
  2. #include <dlfcn.h> 
  3. #include <string.h> 
  4.  
  5. #define MAX_STRING      80 
  6.  
  7. void invoke_method( char *lib, char *method, float argument ) 
  8.   void *dl_handle; 
  9.   float (*func)(float); 
  10.   char *error; 
  11.   /* Open the shared object */ 
  12.   dl_handle = dlopen( lib, RTLD_LAZY ); 
  13.   if (!dl_handle) { 
  14.     printf( "!!! %s\n", dlerror() ); 
  15.     return
  16.   } 
  17.   /* Resolve the symbol (method) from the object */ 
  18.   func = dlsym( dl_handle, method ); 
  19.   error = dlerror(); 
  20.   if (error != NULL) { 
  21.     printf( "!!! %s\n", error ); 
  22.     return
  23.   } 
  24.   /* Call the resolved method and print the result */ 
  25.   printf("  %f\n", (*func)(argument) ); 
  26.   /* Close the object */ 
  27.   dlclose( dl_handle ); 
  28.   return
  29.  
  30. int main( int argc, char *argv[] ) 
  31.   char line[MAX_STRING+1]; 
  32.   char lib[MAX_STRING+1]; 
  33.   char method[MAX_STRING+1]; 
  34.   float argument; 
  35.   while (1) { 
  36.     printf("> "); 
  37.     line[0]=0; 
  38.     fgets( line, MAX_STRING, stdin); 
  39.     if (!strncmp(line, "bye", 3)) break
  40.     sscanf( line, "%s %s %f", lib, method, &argument); 
  41.     invoke_method( lib, method, argument ); 
  42.   } 
编译:指定-rdynamic用来通知链接器将所有符号添加到动态符号表中(目的是能够通过使用 dlopen 来实现向后跟踪)。-ldl表明一定要将dllib链接于该程序。
$ gcc -rdynamic -o dl dynamicloading.c -ldl
$ ./dl
> libm.so cosf 0.0
  1.000000
> bye
 
 
参考/转载:
APUE
Linux programmer's manual
IBM文库-Linux动态库抛析 http://www.ibm.com/developerworks/cn/linux/l-dynamic-libraries/