目录

程序运行环境

c_dev_procedure.jpg

编译时定义宏

$gcc -DDEV hello.c

这个等同于在文件的开头定义宏,即 #define macro,但是在命令行定义更灵活。例如,在源代码中有这些语句。

#ifdef DEV
     printf("this code is for debugging\n");
#endif

如果编译时加上-DDEBUG选项,那么编译器就会把printf所在的行编译进目标代码,从而方便地跟踪该位置的某些程序状态。这样-DDEBUG就可以当作一个调试开关,编译时加上它就可以用来打印调试信息,发布时则可以通过去掉该编译选项把调试信息去掉。

ELF 文件

ELF文件格式总共有三种类型:

  • relocatable可重定位.o

  • shared libarary共享库.a .so

  • executable可执行

无论是文件头部、程序头部表、节区头部表,还是节区,都对应着C语言里头的一些结构体(elf.h 中定义)。

文件头部主要描述ELF 文件的类型,大小,运行平台,以及和程序头部表和节区头部表相关的信息。

节区头部表则用于可重定位文件,以便描述各个节区的信息,这些信息包括节区的名字、类型、大小等。

程序头部表则用于描述可执行文件或者动态链接库,以便系统加载和执行它们。

而节区主要存放各种特定类型的信息,比如程序的正文区(.text)、数据区(初始化和未初始化的数据)、调试信息、以及用于动态链接的一些节区,比如解释器(.interp)节区将指定程序动态装载链接器ld-linux.so的位置,而过程链接表(.plt)、全局偏移表(.got)、重定位表则用于辅助动态链接过程。

  • .o可以没有程序头;exe.a.so可以没有节区表。

  • .o本身不可以运行,仅仅是作为可执行文件、静态链接库、动态链接库的组件。

  • .a.so本身不可执行,只作为exe的组件。.a也是可重定位文件,在链接时编入到exe中。

  • .so本身并没有添加到exe中,只在exe中加入了该.so的名字等信息,以便在exe运行时引用库中的函数,由动态链接器去查找相关函数的内存地址,并调用它们。

ELF 主体:节区

下面是常见节区 :

.text             代码段,存放各种指令
.data             数据段,存放已经初始化了的全局变量和静态局部变量
.bss              存放未初始化的全局变量和静态局部变量
.rodata           只读数据
.comment          编译器版本信息等
.debug            调试信息
.dynamic          动态链接信息
.hash             符号hash表
.line             源代码行号与编译后指令对应表
.note             额外的编译器信息
.strtab           字符串表,用于存储ELF文件中使用到的各种字符串
.symtab           符号表
.shstrtab         段名表
.plt              动态链接的跳转表
.got              全局入口表
.init             程序初始化段,与C++全局构造有关
.fini             终结代码段,与C++全局析构有关

.o中,节区表描述的就是各种节区本身。生成exe时,各个节区会组成段Segment,同时还会生成 程序头 用于描述这些段。程序运行时,程序装载器通过程序头知道如何对这些段进行内存映象。

// SimpleSection.c
int printf( const char* format, ... );
int global_init_var = 84;
int global_init_uninit_var;
void func1( int i )
{
    printf("%d\n", i );
}
// 编译时: 将全局变量和函数放在自定义段
__attribute__((section("FOO"))) int global = 42;
__attribute__((section("BAR"))) void foo()
{
    // do nothing
}
int main( void )
{
    static int static_var = 85;
    static int static_var2;
    int a = 1;
    int b;
    func1( static_var + static_var2 + a + b );
    return a;
}
  • objdump -sd SimpleSection.o 反编译查看 elf 的各种段和节区,多而繁杂

  • objdump -x SimpleSection.o 查看 ELF 的段与节区,相对简洁

  • readelf -r myprintf.o 查看 elf 中,需要重定位的符号

  • readelf -x .rodata myprintf.o 查看只读节区.rodata

  • readelf -x .strtab myprintf.o 查看字符串表 .strtab 包含字符串、文件名、函数名、变量名

  • readelf -h 读取ELF Header 文件头

  • readelf -l 读取Program Headers Table段表,查看程序被装载时 可执行文件与进程虚拟空间映射关系

  • readelf -S 读取Section Headers Table节区表

  • objdump -d -j .text myprintf.o 查看指定节区的内容

  • size SimpleSection.o 查看代码段、数据段、BSS 段的长度

  • nm SimpleSection.o 查看 ELF 文件的符号表,有过滤数据

  • readelf -s SimpleSection.o 查看 ELF 文件的符号表,无过滤

静态链接

  • 重定位 : 是将符号引用与符号定义进行链接的过程。

  • 链接 : 处理可重定位文件,把它们的各种符号引用和符号定义转换为可执行文件中的合适信息(一般是虚拟内存地址)的过程。

  • 静态链接 :使用ld.o.a里的内容全部链接进入到exe中。ld需要计算.o.a的各个节区的虚拟内存位置,并处理一些需要重定位的符号,然后设定它们的虚拟内存地址,最终产生一个可执行文件。被链接后的符号都有一个虚拟内存地址,以便程序运行时能够正确使用该节区中的数据。

经过链接后,多个节区重排后会组成一个段Segment,段用于告诉系统如何加载这个程序到内存中。

常见的段Segment

  • PHDR : 给出了程序表自身的大小和位置,不能出现一次以上。

  • INTERP : 因为程序中调用了 puts(在动态链接库中定义),使用了动态链接库,因此需要动态装载器/链接器(ld-linux.so)

  • LOAD : 包括程序的指令,.text 等节区都映射在该段,只读(R)

  • LOAD : 包括程序的数据,.data .bss 等节区都映射在该段,可读写(RW)

  • DYNAMIC : 动态链接相关的信息,比如包含有引用的动态链接库名字等信息

  • NOTE : 给出一些附加信息的位置和大小

  • GNU_STACK : 这里为空,应该是和 GNU 相关的一些信息

ld 的参数

gcc 在进行了相关配置./configure后,调用了collect2,却并没有调用ld,通过查找gcc文档中和collect2相关的部分发现collect2 在后台实际上还是去寻找ld命令的。

  • -m elf_i386 这里指定不同平台上的链接脚本,如果不是交叉编译,那么无须指定该选项。

  • -dynamic-linker /lib/ld-linux.so.2 指定动态装载器,即程序中的INTERP段中的内容

  • /usr/lib/crt1.o /usr/lib/crti.o /usr/lib/gcc/i486-slackware-linux/4.1.2/crtbegin.o,链接到test文件开头的一些内容,这里实际上就包含了.init等节区。.init节区包含一些可执行代码,在main函数之前被调用,以便进行一些初始化操作,在C++中完成构造函数功能。

  • -L/usr/lib/gcc/i486-slackware-linux/4.1.2 -L/usr/i486-slackware-linux/lib -L/usr/lib/ -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed链接libgcc库和libc库,后者定义有我们需要的puts函数。

  • /usr/lib/gcc/i486-slackware-linux/4.1.2/crtend.o /usr/lib/crtn.o 链接到test文件末尾的一些内容,这里实际上包含了.fini等节区。.fini节区包含了一些可执行代码,在程序退出时被执行,作一些清理工作,在C++中完成析构造函数功能。我们往往可以通过atexit来注册那些需要在程序退出时才执行的函数。

_start 是可执行程序真正的入口,_exit(0)是真正执行退出的代码。

动态链接

可重定位文件仅仅包含用户自定义的一些符号:全局变量、函数、动态链接库里函数。(不包括局部变量)

$ gcc -c test.c
$ nm test.o
00000000 B global   # 地址未确定
00000000 T main     # 地址未确定
          U printf

可执行文件还会加上编译器引入的一些符号

$ gcc -o test test.o
$ nm test | egrep "main$| printf|global$"
080495a0 B global        # 经过静态链接后,地址确定了
08048354 T main          # 经过静态链接后,地址确定了
          U printf@@GLIBC_2.0 # 地址未确定
# 经链接,global 和 main 的地址都已经确定了,但是 printf 却还没
# 因为它是动态链接库glibc中定义函数,需要动态链接,而不是这里的静态链接

动态链接 : 程序运行期间系统调用动态链接器ld-linux.so,对符号进行重定位,确定符号对应的内存地址的过程。动态链接过程涉及到的符号引用和符号定义分别对应可执行文件和动态链接库,在可执行文件中可能引用了某些动态链接库中定义的符号,这类符号通常是函数。

Linux下符号的动态链接默认采用Lazy Mode方式,也就是说在程序运行过程中用到该符号时才去解析它的地址。这样一种符号解析方式有一个好处:只解析那些用到的符号,而对那些不用的符号则永远不用解析,从而提高程序的执行效率。通过设置LD_BIND_NOW可修改Lazy Mode, 修改后动态链接器将在程序加载后和符号被使用之前就对这些符号的地址进行解析。

为了让动态链接器能够进行符号的重定位,必须把动态链接库的相关信息写入到可执行文件当中,这些信息是什么呢?

$ readelf -d test | grep NEEDED
0x00000001 (NEEDED)                     Shared library: [libc.so.6]

ELF 文件有一个特别的节区:.dynamic,它存放了和动态链接相关的很多信息,例如动态链接器通过它找到该文件使用的动态链接库。不过,该信息并未包含动态链接库libc.so.6的绝对路径,那动态链接器去哪里查找相应的库呢?

动态连接器通过 LD_LIBRARY_PATH 参数去查找路径中的.so库,也可以通过/etc/ld.so.conf文件来查找,一行对应一个路径名。

为了提高查找和加载动态链接库的效率,系统启动后会通过ldconfig工具创建一个库的缓存/etc/ld.so.cache。如果用户通过/etc/ld.so.conf加入了新的库搜索路径或者是把新库加到某个原有的库目录下,最好是执行一下ldconfig以便刷新缓存。

readelf -d 可以打印出exe直接依赖的库,而通过ldd exe则可以打印出所有依赖或者间接依赖的库。

动态链接器

Linuxelf 文件的动态链接器是ld-linux.so,即/lib/ld-linux.so.2

  • LD_PRELOAD环境变量用于预装载一些库。/etc/ld.so.preload用于指定需要预装载的库。

  • LD_DEBUG 环境变量可以用来进行动态链接的相关调试。

程序执行

一个程序被exec()执行后,在它的实际指令运行之前,父进程做哪些工作呢?

  • 将可执行文件的内存段添加到进程映像中;在ELF文件的文件头中就指定了该文件的入口地址,程序的代码和数据部分会相继 map 到对应的内存中。

  • 把共享目标内存段添加到进程映像中;.dynamic 节区指定了可执行文件依赖的库名,ld-linuxLD_LIBRARY_PATH中找到相关的库文件,或者直接从 /etc/ld.so.cache 库缓冲中加载相关库到内存中。

  • 如果设置了LD_BIND_NOW环境变量,则为可执行文件和它的共享目标(动态链接库)执行重定位操作;否则将会采用 lazy mode 方式。

  • 关闭用来读入可执行文件的文件描述符,如果动态链接程序收到过这样的文件描述符的话;这个主要是释放文件描述符。

  • 将控制转交给程序,使得程序好像从exec() 直接得到控制,动态链接器把程序控制权交还给程序。

即如何进行符号的重定位

动态链接涉及到三个数据结构,它们分别是 ELF 文件的过程链接表、全局偏移表和重定位表,这三个表都是 ELF 文件的节区。

过程略,太复杂了,以后有机会再了解。

命令启动过程追本溯源

fork     execve         execve         fork           execve
init --> init --> /sbin/getty --> /bin/login --> /bin/login --> /bin/bash
  • init 程序可能在某些linux发行版中已经被systemd替换掉了。

谁启动了 init 程序

  • CPU加电,执行默认起始地址,默认起始地址里就是BIOS

  • BIOS 根据用户的设置(从 U 盘启动、从光盘启动、从哪个硬盘启动),执行代码

  • 如果是从硬盘启动,则执行MBR(主引导扇区)处的代码,一般是GrubLilo引导程序

  • Grub启动后,还可以选择执行哪一个内核的代码,比如Windows系统或Ubuntu系统,双系统就这么玩的

  • 内核启动后,依次查找/sbin/init/etc/init/bin/init/bin/sh命令执行,如果一个都找不到,那就panichang了,也就是挂起了。

  • init 启动后,它就启动/sbin/getty

  • getty 再启动 /bin/login

  • 用户通过login程序启动后,login根据用户的/etc/passwd设置,加载对应的shellbashzshfish等)程序

同名命令执行优先级

alias 别名 --> function 函数 --> builtin 内置 --> program 程序命令

特殊字符 | > < & 是如何解析的

我们自己的程序内部是不处理 |><&字符的,是shell程序对它们进行了解析。

  • 对于< > >> << <> 等重定向操作,shell程序通过C语言的dup fcntl函数实现,复制文件描述符,让多个文件描述符共享同一个文件表项。

  • 对于 | 管道,它实际上就是通过C里面的无名管道系统调用pipe来实现的。

  • & 让程序在后台运行。要实现它,涉及到了很多东西:终端会话(session)、终端信号、前台进程、后台进程等。当一个命令被加上&执行后,Shell必须让它具有后台进程的特征,让它无法响应键盘的输入,无法响应终端的信号(意味忽略这些信号),然后打印新的命令提示符,并且让命令行接口可以继续执行其他命令,这些就是Shell&的执行动作。

后台进程:无法接收用户发送给终端的信号(如 SIGHUP ,SIGQUIT ,SIGINT),无法响应键盘输入(被前台进程占用着),不过可以通过 fg 切换到前台而享受作为前台进程具有的特权。

/bin/bash 如何执行一个程序?

shellfork出一个子进程,然后执行目标程序替换掉这个子进程的内容。shell作为父进程,它把自己环境变量复制给了子进程,并且一直等待到子进程的结束。