当前位置: 首页 > news >正文

wordpress编辑器 填满seo关键词排名优化是什么

wordpress编辑器 填满,seo关键词排名优化是什么,崇礼做网站的公司,账号权重查询Linux下的ELF文件、链接、加载与库 链接是将将各种代码和数据片段收集并组合为一个单一文件的过程#xff0c;这个文件可以被加载到内存并执行。链接可以执行与编译时#xff0c;也就是在源代码被翻译成机器代码时#xff1b;也可以执行于加载时#xff0c;也就是被加载器加…Linux下的ELF文件、链接、加载与库 链接是将将各种代码和数据片段收集并组合为一个单一文件的过程这个文件可以被加载到内存并执行。链接可以执行与编译时也就是在源代码被翻译成机器代码时也可以执行于加载时也就是被加载器加载到内存执行时甚至执行于运行时也就是由应用程序来执行。 本文主要参考[原创] Linux环境下程序的链接, 装载和库[完结] 2020 南京大学 “操作系统设计与实现” (蒋炎岩)两个视频课程并有CSAPP中介绍的一些内容。 常用工具 我们首先列出一些在接下来的介绍过程中会频繁使用的分析工具如果从事操作系统相关的较底层的工作那这些工具应该再熟悉不过了。不熟悉的读者可以先看一下这里的简单的功能介绍我们会在后文中介绍一些详细的参数选项和使用场景。 另外建议大家在遇到自己不熟悉的命令时通过 man 命令来查看手册这是最权威的、第一手的资料。 工具功能strace跟踪程序执行过程中产生的系统调用及接收到的信号readelf用于查看ELF格式的文件信息file用于辨识文件类型objdump以一种可阅读的格式让你更多地了解二进制文件可能带有的附加信息ldd列出一个程序所需要得动态链接库hexdumphexdump主要用来查看“二进制”文件的十六进制编码ar创建静态库插入/删除/列出/提取 成员函数strings列出目标文件中所有可打印的字符串nm列出目标文件中符号表所定义的符号strip从目标文件中删除符号表的信息size列出目标文件中各个段的大小ELF文件详解 ELF文件的三种形式 在Linux下可执行文件/动态库文件/目标文件可重定向文件都是同一种文件格式我们把它称之为ELF文件格式。虽然它们三个都是ELF文件格式但都各有不同。以下文件的格式信息可以通过 file 命令来查看。 可重定位relocatable目标文件通常是.o文件。包含二进制代码和数据其形式可以再编译时与其他可重定位目标文件合并起来创建一个可执行目标文件。 可执行executable目标文件是完全链接的可执行文件即静态链接的可执行文件。包含二进制代码和数据其形式可以被直接复制到内存并执行。 共享shared目标文件通常是.so动态链接库文件或者动态链接生成的可执行文件。一种特殊类型的可重定位目标文件可以在加载或者运行时被动态地加载进内存并链接。注意动态库文件和动态链接生成的可执行文件都属于这一类。会在最后一节辨析时详细区分。 因为我们知道ELF的全称Executable and Linkable Format即 ”可执行、可链接格式“很显然这里的三个ELF文件形式要么是可执行的、要么是可链接的。 其实还有一种core文件也属于ELF文件在core dumped时可以得到。我们这里暂且不提。 注意在Linux中并不以后缀名作为区分文件格式的绝对标准。 节头部表和程序头表和ELF头 在我们的ELF文件中有两张重要的表节头部表Section Tables和程序头表Program Headers。可以通过readelf -l [fileName]和readelf -S [fileName]来查看。 但并不是所有以上三种ELF的形式都有这两张表 如果用于编译和链接可重定位目标文件则编译器和链接器将把elf文件看作是节头表描述的节的集合程序头表可选。如果用于加载执行可执行目标文件则加载器则将把elf文件看作是程序头表描述的段的集合一个段可能包含多个节节头部表可选。如果是共享目标文件则两者都含有。因为链接器在链接的时候需要节头部表来查看目标文件各个 section 的信息然后对各个目标文件进行链接而加载器在加载可执行程序的时候需要程序头表 ,它需要根据这个表把相应的段加载到进程自己的的虚拟内存虚拟地址空间中。 我们在后面的还会详细介绍这两张表。 此外整个ELF文件的前64个字节成为ELF头可以通过readelf -h [fileName]来查看。我们也会在后面详细介绍。 可重定位ELF文件的内容分析 #include elf.h该头文件通常在/usr/include/elf.h可以自己vim查看。 首先有一个64字节的ELF头Elf64_Ehdr其中包含了很多重要的信息可通过readelf -h [fileName]来查看这些信息中有一个很关键的信息叫做Start of section headers它指明了节头部表Section Headers Elf64_Shdr的位置。段表中储存了ELF文件中各个的偏移量以记录其位置。ELF中的各个段可以通过readelf -S [fileName]来查看。下表是up主fengzimu2003总结的ELF文件的内容 其中各个节的含义如下 名称意义.text已编译程序的机器代码.rodata只读数据.data已初始化的全局变量和静态变量.bss未初始化的全局变量和静态变量.symtab一个符号表存放在程序中定义和引用的函数和全局变量的信息.rel.text一个.text节中位置的列表当链接器把其他文件和目标文件组合时需要修改这些位置.rel.data被模块引用或定义的所有全局变量的重定位信息.debug一个调试符号表其条目是程序中定义的局部变量和类型定义需要-g才有.line原始C源程序中的行号和.text节中机器指令的映射需要-g才有.strtab一个字符串表 这样我们就把一个可重定位的ELF文件中的每一个字节都搞清楚了。 静态链接 编译、链接的需求 为了节省空间和时间不将所有的代码都写在同一个文件中是一个很基本的需求。 为此我们的C语言需要实现这样的需求允许引用其他文件C标准成为编译单元Compilation Unit里定义的符号。C语言中不禁止你随便声明符号的类型但是类型不匹配是Undefined Behavior。 假如我们有三个c文件分别是a.cb.cmain.c // a.c int foo(int a, int b){return a b; }// b.c int x 100, y 200;// main.c extern int x, y; int foo(int a, int b); int main(){printf(%d %d %d\n, x, y, foo(x, y)); }我们在main.c中声明了外部变量x,y和函数fooC语言并不禁止我们这么做并且在声明时C也不会做什么类型检查。当然在编译main.c的时候我们看不到这些外部变量和函数的定义也不知道它们在哪里。 我们编译链接这些代码Makfile如下 CFLAGS : -Osa.out: a.o b.o main.ogcc -static -Wl,--verbose a.o b.o main.oa.o: a.cgcc $(CFLAGS) -c a.cb.o: b.cgcc $(CFLAGS) -c b.cmain.o: main.cgcc $(CFLAGS) -c main.cclean:rm -f *.o a.out结果生成的可执行文件可以正常地输出我们想要的内容。 make ./a.out # 输出 # 100 200 300我们知道foo这个符号是一个函数名在代码区。但这时如果我们将main.c中的foo声明为一个整型并且直接打印出这个整型然后尝试对其加一。即我们将main.c改写为下面这样会发生什么事呢 // main.c (changed) #include stdio.h extern int x, y; // int foo(int a, int b); extern int foo; int main(){printf(%x\n, foo);foo 1;// printf(%d %d %d\n, x, y, foo(x, y)); }输出 c337048d Segmentation fault (core dumped)我们发现其实是能够打印出四个字节整型为4个字节但这四个字节是什么东西呢 C语言中的类型C语言中的其实是可以理解为没有类型的在C语言的眼中只有内存和指针也就是内存地址而所谓的C语言中的类型其实就是对这个地址的一个解读。比如有符号整型就按照补码解读接下来的4个字节地址又比如浮点型就是按照IEEE754的浮点数规定来解读接下来的4字节地址。 那我们这里将符号foo定义为了整型那编译器也会按照整型4个自己来解读它而这个地址指针指向的其实还是函数foo的地址。那这四个字节应该就是函数foo在代码段的前四个字节。我们不妨用objdump反汇编来验证我们的想法 objdump -d a.out输出节选 我们看到foo函数在代码段的前四个字节的地址确是就是我们上面打印输出的c3 37 04 8d注意字节序为小端法。 那我们接下来试图对foo进行加一操作相当于是对代码段的写操作而我们知道内存中的代码段是 可读可执行不可写 的这就对应了上面输出的Segmentation fault (core dumped)。 总结一下通过这个例子我们应当理解 编译链接的需求允许引用其他文件C标准成为编译单元Compilation Unit里定义的符号。C语言中不禁止你随便声明符号的类型但是类型不匹配是Undefined Behavior。C语言中类型的概念C语言中的其实是可以理解为没有类型的在C语言的眼中只有内存和指针也就是内存地址而所谓的C语言中的类型其实就是对这个地址的一个解读。 程序的编译 - 可重定向文件 我们先用file命令来查看main.c编译生成的main.o文件的属性 file main.o输出 main.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped 我们看到这里的main.o文件是可重定向( relocatable) 的ELF文件这里的重定向指的就是我们链接过程中对外部符号的引用。也就是说编译过的main.o文件对于其中声明的外部符号如foox,y是不知道的。 既然外部的符号是在链接时才会被main程序知道那在编译main程序生成可重定向文件时这些外部的符号是怎么处理的呢我们同样通过objdump工具来查看编译出的main.o文件未修改的原版本 objdump -d main.o输出 main在编译的时候引用的外部符号就只能 ”留空(0)“ 了。 我们看到在编译但还未链接的main.o文件中对于引用的外界符号的部分是用留空的方式用0暂时填充的。即上图中红框框出来的位置。注意图中的最后一列是笔者添加的注释指明了本行中留空的地方对应那个外部符号。 另外注意这里的%rip相对寻址的偏移量都是0一会儿我们会讲到在静态链接完成之后它们的偏移量会被填上正确的数值。 我们已经知道在编译时生成的文件中外部符号的部分使用0暂时留空的这些外部符号是待链接时再填充的。那么我们在链接时究竟需要填充哪些位置呢我们可以使用readelf工具来查看ELF文件的重定位信息 readelf -r main.o这个图中上方是readelf的结果下面是objdump的结果笔者在这里已经将前两个外部符号的偏移量的对应关系用红色箭头指了出来其他的以此类推。这种对应也可以证明我们上面的分析是正确的的。 应当讲可重定向ELF文件如main.o已经告诉了我们足够多的信息指示我们应该将相应的外部符号填充到哪个位置。 另外注意%rip寄存器指向了当前指令的末尾也就是下一条指令的开头所以上图中最后的偏移量要减4如 y - 4。 程序的静态链接 简单讲程序的静态链接是会把所需要的文件链接起来生成可执行的二进制文件将相应的外部符号填入正确的位置就像我们上面查看的那样。 1 段的合并 首先会做一个段的合并。即把相同的段比如代码段 .text识别出来并放在一起。 2 重定位 重定位表可用objdump -r [fileName] 查看。 简单讲就是当某个文件中引用了外部符号在编译时编译器是不会阻止你这样做的因为它相信你会在链接时告诉它这些外部符号是什么东西。但在编译时它也不知到这些符号具体在什么地址因此这些符号的地址会在编译时被留空为0。此时的重定位就是链接器将这些留空为0的外部符号填上正确的地址。 具体的链接过程可以通过ld --verbose来查看默认的链接脚本并在需要的时候修改链接脚本。 我们可以通过使用gcc的 -Wl,--verbose将--verbose传递给链接器ld从而直接观察到整个静态链接的过程包括 ldscript里面各个section是按照何种顺序 “粘贴”ctors / dtors (constructors / destructores) 的实现 我们用过__attribute__((contructor)) 只读数据和读写数据之间的padding. DATA_SEGMENT_ALIGN … 我们可以通过objdump来查看静态链接完成以后生成的可执行文件a.out的内容 objdump -d a.out注意这个a.out的objdump结果图要与我们之前看到的main.o的objdump输出对比着来看。 我们可以看到之前填0留空的地方都被填充上了正确的数值%rip相对寻址的偏移量以被填上了正确的数值而且objdump也能够正确地解析出我们的外部符号名最后一列的框。 静态链接库的构建与使用 假如我们要制作一个关于向量的静态链接库libvector.a它包含两个源代码addvec.c和multvec.c如下 // addvec.c int addcnt 0;void addvec(int *x, int *y, int*z, int n){int i;addcnt;for (i0; in; i) z[i] x[i] y[i]; }// multvec.v int multcnt 0;void multvec(int *x, int *y, int*z, int n){int i;multcnt;for (i0; in; i) z[i] x[i] * y[i]; }我们只需要这样来进行编译 gcc -c addvec.c multvec.c ar rcs libvector.a addvec.o multvec.o假如我们有个程序main.c要调用这个静态库libvector.a // main.c #include stdio.h #include vector.hint x[2] {1, 2}; int y[2] {3, 4}; int z[2];int main(){addvec(x, y, z, 2);printf(z [%d %d]\n, z[0], z[1]);return 0; }// vector.h void addvec(int*, int*, int*, int); void multvec(int*, int*, int*, int);只需要在这样编译链接即可 gcc -c main.c gcc -static main.o ./libvector.a静态链接过程图示 我们以使用刚才构建的静态库libvector.a的程序为例画出静态链接的过程。 可执行文件的装载 进程和装载的基本概念的介绍 程序可执行文件和进程的区别 程序是静态的概念它就是躺在磁盘里的一个文件。进程是动态的概念是动态运行起来的程序。 现代操作系统如何装载可执行文件 给进程分配独立的虚拟地址空间将可执行文件映射到进程的虚拟地址空间mmap将CPU指令寄存器设置到程序的入口地址开始执行 可执行文件在装载的过程中实际上如我们所说的那样是映射的虚拟地址空间所以可执行文件通常被叫做映像文件(或者Image文件)。 可执行ELF文件的两种视角 可执行ELF格式具有不寻常的双重特性编译器、汇编器和链接器将这个文件看作是被区段section头部表描述的一系列逻辑区段的集合而系统加载器将文件看成是由程序头部表描述的一系列段segment的集合。一个段segment通常会由多个区段section组成。例如一个“可加载只读”段可以由可执行代码区段、只读数据区段和动态链接器需要的符号区段组成。 区段section是从链接器的视角来看ELF文件对应段表 Section Headers而段segment是从执行的视角来看ELF文件也就是它会被映射到内存中对应程序头表 Program Headers。 我们用命令readelf -a [fileName] 中的Section to Segment mapping部分来看一下可执行文件中段的映射关系。 可执行文件的程序头表 我们用readelf -h [fileName]命令查看一个可执行ELF文件的ELF头时会发现与可重定位ELF文件的ELF头有一个重大不同可重定位文件ELF头中 Start of program headers 为0因为它是没有程序头表Program HeadersElf64_Phdr的而在可执行ELF文件中Start of program headers 是有值的为64也就是说在可执行ELF文件中程序头表会紧接着ELF头因为ELF头的大小即为64字节。 我们通过readelf -l [fileName]可以直接查看到程序头表。 可执行ELF文件个进程虚拟地址空间的映射关系 我们可以通过 cat /proc/[pid]/maps 来查看某个进程的虚拟地址空间。 该虚拟文件有6列分别为 含义地址虚拟内存区域的起始和终止地址权限虚拟内存的权限r读,w写,x执行,s共享,p私有偏移量虚拟内存区域在被映射文件中的偏移量设备映像文件的主设备号和次设备号节点映像文件的节点号路径映像文件的路径 vdso的全称是虚拟动态共享库virtual dynamic shared library而vsyscall的全称是虚拟系统调用virtual system call关于这部分内容有兴趣的读者可以看看https://0xax.gitbooks.io/linux-insides/content/SysCall/syscall-3.html。 总体来说在程序加载过程中磁盘上的可执行文件进程的虚拟地址空间还有机器的物理内存的映射关系如下 Linux下的装载过程 接下来我们进一步探究一下Linux是怎么识别和装载ELF文件的我们需要深入Linux内核去寻找答案 (内核实际处理过程涉及更多的过程我们这里主要关注和ELF文件处理相关的代码)。 当我们在bash下输入命令执行某一个ELF文件的时候首先bash进程调用fork()系统调用创建一个新的进程然后新的进程调用execve()系统调用执行指定的ELF文件 ,内核开始真正的装载工作。 下图是Linux内核代码中与ELF文件的装载相关的一些代码 /fs/binfmt_elf.c中 Load_elf_binary的代码走读 检查ELF文件头部信息(一致性检查)加载程序头表(可以看到一个可执行程序必须至少有一个段segment而所有段的大小之和不能超过64K(65536u))寻找和处理解释器段(动态链接部分会介绍)装入目标程序的段(elf_map)填写目标程序的入口地址填写目标程序的参数环境变量等信息(create_elf_tables)start_thread会将 eip 和 esp 改成新的地址就使得CPU在返回用户空间时就进入新的程序入口… 例子静态ELF加载器加载 a.out 执行 我们同样以刚才介绍静态链接时的a.c、b.c、main.c的例子来看一下静态链接的可执行文件的加载。 静态ELF文件的加载将磁盘上静态链接的可执行文件按照ELF program header正确地搬运到内存中执行。 操作系统在execve时完成 操作系统在内核态调用mmap 进程还未准备好时由内核直接执行 ”系统调用“映射好 a.out 的代码、数据、堆区、堆栈、vvar、vdso、vsyscall 更简单的实现直接读入进程的地址空间 加载完成之后静态链接的程序就开始从ELF entry开始执行之后就变成我们熟悉的状态机唯一的行为就是取指执行。 我们通过readelf来查看a.out文件的信息 readelf -h a.out输出 我们这里看到程序的入口地址是Entry point address: 0x400a80。我们接着用gdb来调试 上图是笔者在gdb中调试的一些内容 我们用starti来使得程序在第一条指令就停下可以看到程序确实是从0x400180开始的与我们上面查到的入口地址一致。而我们用cat /proc/[PID]/maps 来查看这个程序中内存的内容看到我们之前提到的代码、数据、堆区、堆栈、vvar、vdso、vsyscall都已经被映射进了内存中。 调试的结果符合我们对静态程序加载时操作系统的行为的预期。 动态链接 什么是动态链接以及为什么需要动态链接 实际上链接程序在链接时一般是优先链接动态库的除非我们显式地使用-static参数指定链接静态库像这样 gcc -static hello.c静态链接和动态链接的可执行文件的大小差距还是很显著的 因为静态库被链接后库就直接嵌入可执行文件中了。 这样就带来了两个弊端 首先就是系统空间被浪费了。这是显而易见的想象一下如果多个程序链接了同一个库则每一个生成的可执行文件就都会有一个库的副本必然会浪费系统空间。再者一旦发现了库中有bug或者是需要升级必须把链接该库的程序找出来然后全部需要重新编译。 libc.so中有300K 条指令2 MiB 大小每个程序如果都静态链接浪费的空间很大最好是整个系统里只有一个 libc 的副本而每个用到 libc 的程序在运行时都可以用到 libc 中的代码。 下图中的 hello-dy 和 hello-st 是同一个hello源文件hello.c分别动态 / 静态链接后生成的可执行文件的大小大家可以感受一下查了一百倍。而且这只是链接了libc标准库在大型项目中我们要链接各种各样的第三方库而静态链接会把全部在链接时就链接到同一个可执行文件那么其大小是很难接受的。 动态库的出现正是为了弥补静态库的弊端。因为动态库是在程序运行时被链接的所以磁盘上和内存中只要保留一份副本因此节约了磁盘空间。如果发现了bug或要升级也很简单只要用新的库把原来的替换掉就行了。 Linux环境下的动态链接对象都是以.so为扩展名的共享对象(Shared Object). 真的是动态链接的吗 我们常说gcc默认的链接类型就是动态链接而且我们及其中运行的大部分进程也都是动态链接的真的是这样的吗我们不妨来做个实验验证一下。 我们通过创建一个动态链接库 libhuge.so 然后创建1000个进程去调用这个库中的foo函数该函数是128M 个 nop。如果程序不是动态链接的话1000 * 128MB的内存占用足以撑爆大多数个人电脑的内存。而如果程序确实是动态链接的即内存中只有一份代码那么只会有很小的内存占用。我们是这样做的 首先我们有huge.S .global foo foo:# 128MiB of nop.fill 1024 * 1024 * 128, 1, 0x90ret这就是我们刚才说的一个动态链接库的源代码。我们一会儿会把他编译成 libhuge.so供我们的huge.c调用我们的huge.c是这样的 #include unistd.h #include stdio.h int main(){foo(); // huge code, dynamic linkedprintf(pid %d\n, getpid());while (1) sleep(1); }它会调用foo函数并在结束后打印自己的PID然后睡眠。Makefile如下 LIB : /tmp/libhuge.soall: $(LIB) a.out$(LIB): huge.Sgcc -fPIC -shared huge.S -o $a.out: huge.c $(LIB)gcc -o $ huge.c -L/tmp -lhugeclean:rm -f *.so *.out $(LIB)正如我们刚才所介绍的我们会先将huge.S编译成动态链接库libhuge.so放在/tmp下然后我们的huge.c回去动态链接这个库并完成自己的代码。这还不够我们要创建1000个进程来执行上述行为。这样才能验证我们的动态链接是不是在内存中真的只有一份代码我们用下面的脚本来完成 #!/bin/bash# for i in {1...1000} for i in seq 1 100 doLD_LIBRARY_PATH/tmp ./a.out donewait # ps | grep a.out | grep -Po ^(\d)* | xargs kill -9 用于清空生成的进程实验证明我们的操作系统能够很好地运行这1000个进程并且内存只多占用了 400MB。也就是说库中的foo函数确实是动态链接的内存中只有一份foo的副本。 这在操作系统内核不难实现所有以只读方式映射同一个文件的部分如代码部分时都指向同一个副本这个过程中会创建引用计数。 动态链接的例子 假如我们要制作一个关于向量的动态链接库libvector.so它包含两个源代码addvec.c和multvec.c如下我们只需要这样来进行编译 gcc -shared -fpic -o libvector.so addvec.c multvec.c其中-fpic选项告诉编译器生成位置无关代码PIC而-shared选项告诉编译器生成共享库。 我们现在拿一个使用到这个共享库的可执行文件来看一下其源代码main.c // main.c #includestdio.hint addvec(int*, int*, int*, int);int x[2] {1, 2}; int y[2] {3, 4}; int z[2];int main(){addvec(x, y, z, 2);printf(z [%d %d]\n, z[0], z[1]);while(1);return 0; }注意我们在最后加了一个死循环是为了让进程保持运行然后去查看进程的虚拟地址空间。 我们先编译源码注意在同目录下可以直接按以下命令编译之后我们会介绍将动态链接库放到环境目录后的编译命令。 gcc main.c ./libvector.so 然后先用file命令查看生成的可执行文件a.out的文件信息再用ldd命令查看其需要的动态库最后查看其虚拟地址空间。 file a.out输出 我们看到该可执行文件是共享对象并且是动态链接的。 ldd a.out输出 ldd命令就是用来查看该文件所依赖的动态链接库。 ./a.out cat /proc/12002/maps输出 我们看到除了像静态链接时进程地址空间中的堆、栈、vvar、vdso、vsyscall等之外还有了许多动态链接库.so。 动态链接的实现机制 程序头表 我们同样用readelf -l [fileName]来查看动态链接的可执行ELF文件的程序头表 readelf -l a.out可以看到编译完成之后地址是从 0x00000000 开始的即编译完成之后最终的装载地址是不确定的。 关键技术 之前在静态链接的过程中我们提到过重定位的过程那个时候其实属于链接时的重定位现在我们需要装载时的重定位 主要使用了以下关键技术 PIC位置无关代码GOT全局偏移表GOT配合PLT实现的延迟绑定技术 引入动态链接之后实际上在操作系统开始运行我们的应用程序之前首先会把控制权交给动态链接器它完成了动态链接的工作之后再把控制权交给应用程序。 可以看到动态链接器的路径在.interp这个段中体现并且通常它是个软链接最终链接在像ld-2.27.so这样的共享库上。 .dynamic段 我们来看一下和动态链接相关的.dynamic段和它的结构.dynamic段其实就是全局偏移表的第一项即GOT[0]。 可以通过readelf -d [fileName]来查看。 它对应的是elf.h中的Elf64_Dyn这个结构体。 动态链接器ld 对于动态链接的可执行文件内核会分析它的动态链接器地址把动态链接器映射到进程的地址空间把控制权交给动态链接器。动态链接器本身也是.so文件但是它比较特殊它是静态链接的。本身不依赖任何其他的共享对象也不能使用全局和静态变量。这是合理的试想如果动态链接器都是动态链接的话那么由谁来完成它的动态链接呢 Linux的动态链接器是glibc的一部分入口地址是sysdeps/x86_64/dl-machine.h中的_start然后调用 elf/rtld.c 的_dl_start函数最终调用 dl_main(动态链接器的主函数)。 动态链接过程图示 动态链接库的构建与使用 创建自己的动态链接库 创建号一个动态链接库如我们的libvector.so之后我们肯定不可能只在当前目录下使用它那样他就不能被叫做 ”库“了。 为了在全局使用动态链接库我们可以将我们自己的动态链接库移动到/usr/lib下 sudo mv libvector.so /usr/lib之后我们只要在需要使用到相关库时加上-l[linName]选项即可如 gcc main.c -lvector大家也注意到了上面的命令要用到管理员权限sudo。适应为/usr/lib和/lib是系统级的动态链接目录我们要创建自己的第三方库最好不要直接放在这个目录中而是创建一个自己的动态链接库目录并将这个目录添加到环境变量 LD_LIBRARY_PATH 中 mkdir /home/song/dynlib mv libvector.so /home/song/dynlib export LD_LIBRARY_PATH$LD_LIBRARY_PATH:/home/song/dynlib命名规范 动态链接库要命名为lib[libName].so 的形式。 实现动态链接及实际ELF的动态链接 想必大家看了上面一节对动态链接的介绍已经明白动态链接以及动态链接库的大体过程和用法但是对其中具体的实现细节还是比较迷惑。本节是笔者在听南大蒋炎岩老师的录播课程时做的笔记。蒋老师从分析实现一个简易的动态链接的三个等级的需求讲起逐步引出了上面笔者提到的动态链接的三个关键技术PIC、GOT并加以介绍最后通过介绍实际中ELF的动态加载过程介绍GOT、PLT配合实现的lazy symbol resolve。想要更加深入地理解动态链接的实现过程的朋友可以读一下本节。如果笔者的笔记有令人疑惑的地方也可以去看蒋老师在B站的录播课程。 讲解的总体思路如下 我们通过逐步把需求进行分解从加载的视角理解链接 需要加载一段代码fooPIC通过使用PC相对寻址 mmap代码需要伴随数据bar数据也使用PC相对寻址 mmap需要解析动态符号baz查表GOT、优化PLTlazy symbol resolve 实现动态链接与加载 我们要实现动态链接需要具体做到哪些事情呢我们希望有一个库函数其中包含一些代码所有的进程链接这一段代码这段代码在内存中只有一份拷贝。 实现动态加载1 需求1加载纯粹的代码 编译成位置无关代码Position Independent Code, PIC即可即引用代码跳转全部使用PC相对寻址。x86已经是这样了。直接把代码mmap进地址空间就行了。 # foo.S .global fool foo:movl $1, %eaxret比如上面这段代码它很简单就是返回1。 实现动态加载2 需求2动态链接库只有纯粹的代码没有数据可不行我们要能加载代码并且代码有附带的数据。 这也好办将代码和数据放在一起然后都使用PC相对寻址就好了。 对于x86不支持rip相对寻址我们可以通过 call __i686. get_pc_thunk.bx 来得到下条指令的地址。 我们有这样一段代码 # bar.S x: # 数据不能共享 MAP_PRIVATE 方式映射.int 0.global bar bar:addl $1, x(%rip)movl x(%rip), %eaxret这相当于这样一段C代码 int bar(){static int x 0;return x; }即在静态区定义一个变量x然后每次调用bar函数时都会将x加一并返回。这也是一段位置无关代码也可以直接mmap到内存中去执行。 实现动态加载3 需求3比较难的是一个文件或者一个动态链接库想要访问另外一个动态链接库导出的符号。因为我们想要知道的符号比如bar也是动态加载的也就是说符号的地址是运行加载的时候才能确定的。而我们在编译比如编译baz时的时候无法知道动态加载的符号bar的地址。即允许访问其他动态链接库导出的符号代码 / 数据。 解决方法是我们用一张表编译时编译成call *table[bar]。bar.o会先被映射到进程的地址空间中然后我们要将baz.o映射到地址空间时我们会给baz所保有的这张表中bar所对应的表项填上正确的数值即此时已知的bar的地址。即我们为每个动态加载的符号代码 / 数据创建一张变在运行时每次用到这些动态符号时才解析符号的地址。 .global ..bar ..bat: bar:.quad 0.global baz baz:movq baz(%rip), %rdicall *%rdiret重填相当于在运行时重做静态链接这样行吗不行因为这样违背了我们动态链接的初衷希望整个内存中只有一份代码的副本而每次重填会导致每次都在内存中多一份代码的副本。而上面的解决方案只有这张表是需要复制的这大幅减少了系统中冗余的内存。 总结 总结一下实现动态链接和加载就是两个关键点 PIC位置无关代码不管是代码还是数据我们全部都要通过PC相对寻址来使得它们是位置无关代码。要引用动态链接库中的符号编译时不知道时我们创建一张表在运行加载时将其填上正确的地址。 例子 假如我们是十几种有这样一个动态链接共享代码的需求 main需要调用libc中的printfprintf需要调用libfoo中的foo 我们知道动态加载的程序最先并不是从main的入口地址开始执行的。而是需要先由加载器libld进行动态加载。libld由操作系统加载按照相互依赖相反的方向加载 libld加载libfoo一切顺利libld加载libc libc对foo的调用在编译时被编译为call *libc.tab[FOO]libld调用dl_runtime_resolve解析符号填入libc.tab[FOO]因为此时libfoo已经被加载到地址空间中了foo地址是已知的 libld完成main的初始化 a.out对printf的调用在编译时被编译成call *a.out.tab[PRINTF]libld机械printf的地址填入call *a.out.tab[PRINTF]因为此时libc已经被加载到地址空间中了printf地址是已知的 所有的填表都完成之后就跳转到main的入口地址开始执行。 ELF 动态链接与加载 上面一种简化版的动态加载过程实际的ELF动态加载比这要复杂一点。 GOT (Global Offset Table) GOT GOTshared object用来存储动态符号的表格。库函数有可执行文件也有。 所以用file命令查看a.out和libc.so时都是 ”shared object“ 。也就是说我们生成的可执行文件其实和库函数是同一种文件格式。它们都需要调用其他的动态链接库中的符号。 GOT中储存的数据 GOT[0].dynamic节的地址GOT1link map用于遍历依赖的动态链接库GOT2dl_runtime_resolve 的地址即call *GOT2 可以完成符号解析GOT[i]程序所需的动态符号的地址printf, … 新需求 新需求能否降低实际没有调用到的符号的开销 程序可能会引用很多符号但执行时可能大部分符号都没用到逐个dl_runtime_resolve的话会造成不必要的开销。 lazy symbol resolution 想法加载时设置为NULL加载时来判断 / 解析 使用一小段 ”trampoline code“ 跳板代码 如果符号还未解析就解析跳转到解析后的符号执行 int print_internal(const char *fmt, ...){if (GOT[PRINRF]){GOT[PRINTF] call_dl_runtime_reslove(printf);}return GOT[PRINTF]{...}; }需要编译器把向printf动态链接库的调用翻译成call printf_internal 坏处fast path多做一次判断call load 判断 jump会损失一定的性能。 黑科技让printfGOT指向trampoline的下一条指令。 只有两条指令call printplt; jmp *a.out.GOT[PRINTF]对现代处理器非常友好因为有branch-target-bufferBTB几乎不损失性能。 Takeaways and Wrap-up 我们通过逐步把需求进行分解从加载的视角理解链接 需要加载一段代码fooPIC通过使用PC相对寻址 mmap代码需要伴随数据bar数据也使用PC相对寻址 mmap需要解析动态符号baz查表GOT、优化PLTlazy symbol resolve 入口函数和运行库 入口函数 初学者可能一直以来都认为C程序的第一条指令就是从我们的main函数开始的实际上并不是这样在main开始前和结束后系统其实帮我们做了很多准备工作和扫尾工作下面这个例子可以证明 我们有两个C代码 // entry.c #include stdio.h__attribute((constructor)) void before_main() { printf(%s\n,__FUNCTION__); }int main() {printf(%s\n,__FUNCTION__); }// atexit.c #include stdio.hvoid post(void) {printf(goodbye!\n); }int main() {atexit(post);printf(exiting from main\n); }分别编译运行这两个程序输出结果分别为 # entry.c before_main main # atexit.c exiting from main goodbye!可见在main开始前和结束后其实还有一部分程序在运行。 事实上操作系统装载程序之后首先运行的代码并不是我们编写的main函数的第一行而是某些运行库的代码它们负责初始化main函数正常执行所需要的环境并负责调用main函数并且在main返回之后记录main函数的返回值调用atexit注册的函数最后结束进程。以Linux的运行库glibc为例所谓的入口函数其实 就是指ld 默认的链接脚本所指定的程序入口_start (默认情况下) 运行库 glibc GNU C library Linux环境下的C语言运行库glibc包括 启动和退出相关的函数 C标准库函数的实现 (标准输入输出字符处理数学函数等等) … 事实上运行库是和平台相关的和操作系统联系的非常紧密我们可以把运行库理解成我们的C语言(包括c)程序和操作系统之间的抽象层使得大部分时候我们写的程序不用直接和操作系统的API和系统调用直接打交道运行库把不同的操作系统API抽象成相同的库函数方便应用程序的使用和移植。 Glibc有几个重要的辅助程序运行的库 /usr/lib64/crt1.o, /usr/lib64/crti.o, /usr/lib64/crtn.o。 其中crt1包含了基本的启动退出代码 ctri和crtn包含了关于.init段及.finit段相关处理的代码(实际上是_init()和_finit()的开始和结尾部分) Glibc是运行库它对语言的实现并不太了解真正实现C语言特性的是gcc编译器所以gcc提供了两个目标文件crtbeginT.o和crtend.o来实现C的全局构造和析构 – 实际上以上两个高亮出来的函数就是gcc提供的有兴趣的读者可以自己翻阅gcc源代码进一步深入学习。 几组概念的辨析 动态链接的可执行文件和共享库文件的区别 问题: 可执行文件和动态库之间的区别我们在第一节中提到过动态链接的可执行文件和动态库文件file命令的查看结果是类似的都是shared object一个不同之处在于可执行文件指明了解释器intepreter 可执行文件和动态库之间的区别简单来说可执行文件中有main函数动态库中没有main函数可执行文件可以被程序执行动态库需要依赖程序调用者。 在可执行文件的所有符号中main函数是一个很特别的函数对C/C程序开发人员来说main函数是整个程序的起点但是main函数却不是程序启动后真正首先执行的代码。 除了由程序员编写的源代码编译成目标文件进而链接到程序内存映射还有一部分机器指令代码是在链接过程中添加到程序内存映射中。 比如程序的启动代码放在内存映射的起始处在执行main函数之前执行以及在程序终止后完成一些任务 编译动态库时链接器没有添加这部分代码。这是可执行文件和动态库之间的区别。 静态链接 / 动态链接的可执行文件的第一条指令地址 静态链接可执行文件的第一条指令地址 我们之前提到过静态链接的可执行文件的其实地址就是本文件的_strat即readelf -h所得到的的起始地址。对于一个hello程序 // hello.c #include stdio.hint main(){printf(Hellow World.\n);return 0; }我们先用选项-static来静态链接它得到hello-st gcc -static hello.c -o hello-st我们先用file命令看一下 它是静态链接的可执行文件。 我们用readelf -h查看其入口地址并在gdb中starti查看它实际的第一条指令的地址 可以看到与我们的预期是一致的确是是从文件本身真正的入口地址entry point0x400a50开始执行第一条指令。而在动态链接的可执行文件中我们将看到不同。 动态链接的可执行文件的第一条指令地址 我们现在动态链接默认编译hello程序得到hello-dy gcc hello.c -o hello-dy还是先来file一下 我们看到hello-dy是一个动态链接的共享目标文件当然它也是可执行的共享库文件和可执行的共享目标文件的区别我们上面已经介绍过了。大家注意这里还多了一个奇怪的家伙解释器interpreter /lib64/ld-linux-x86-64.so.2。 实际上它就是动态链接文件的链接加载器。我们之前已经介绍过在动态链接的可执行文件中外部符号的地址在程序加载、运行的过程中才被确定下来。这个链接加载器 ld 就是负责完成这个工作的。当 ld 将外部符号的地址都确定好之后才将指令指针执行程序本身的_start。也就是说在动态链接的可执行文件中第一条指令应该在链接加载器 ld 中。我们接下来还是通过readelf -h和gdb来验证一下。 可以看到我们的动态链接的可执行程序的第一条指令的地址并不是本文件的entry point 0x530而是链接加载器 ld 的第一条指令_start的地址 0x7ffff7dd4090。 这就验证了我们上面的说法动态链接的可执行文件的第一条指令是链接加载器的程序入口它会完成外部符号地址的绑定然后将控制权交还给程序本身开始执行。 静态库和共享库 库有时候需要把一组代码编译成一个库这个库在很多项目中都要用到例如libc就是这样一个库我们在不同的程序中都会用到libc中的库函数例如printf。 共享库和静态库的区别在链接libc共享库时只是指定了动态链接器和该程序所需要的库文件并没有真的做链接可执行文件调用的libc库函数仍然是未定义符号要在运行时做动态链接。而在链接静态库时链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。 静态库链接后指令由相对地址变为绝对地址各段的加载地址定死了。共享库链接后指令仍是相对地址共享库各段的加载地址并没有定死可以加载到任意位置。 静态库好处静态库中存在很多部分链接器可以从静态库中只取出需要的部分来做链接 比如main.c需要stach.c其中的一个函数而stach.c中有4个函数则打包库后只会链接用到那个函数。另一个好处就是使用静态库只需写一个库文件名而不需要写一长串目标文件名。 Ref Computer Systems A Programmer’s Perspective - by Randal E. Bryant David O’Hallaron https://www.bilibili.com/video/BV1hv411s7ew https://blog.csdn.net/weixin_44966641/article/details/120616894?spm1001.2014.3001.5501 https://www.bilibili.com/video/BV1N741177F5?p15 https://www.jianshu.com/p/7c609b70acbd https://blog.csdn.net/xuehuafeiwu123/article/details/72963229
http://www.pierceye.com/news/886670/

相关文章:

  • 网站开发招标技术规范书网站建设动态静态
  • 阿里巴巴网站开发工具北京的网站制作公司
  • 石家庄网站营销互联网运营培训课程
  • 单位网站服务的建设及维护oa软件定制开发
  • 银川公司网站建设zepto网站开发
  • 看谁做的好舞蹈视频网站电话营销
  • 开封建网站到哪里学平面设计
  • 电子商务与网站建设实践论文化工行业网站建设
  • 如何搭建一个网站平台卖16斤肉赚200元
  • 手机主页网站推荐江宁城乡建设局网站
  • 甜品网站设计论文张家界seo
  • 单位网站及政务新媒体建设管理wordpress刷留言
  • 用花瓣网站上的图片做游戏行吗西安霸屏推广
  • 单片机和做网站医疗网站建设渠道
  • 上海做家纺的公司网站新闻发布稿
  • 广告网站大全扁平化网站psd
  • wordpress开启多站点那个网站可以做宣传
  • 专门建站的公司本网站正在建设升级中
  • 花市小说网站那里进建网站的公司哪里有
  • 自建网站服务器备案免费做易拉宝网站
  • 经典手机网站家政公司网站模板
  • 外贸做的亚马逊网站是哪个好产品怎样推广有效
  • 网站后台数据库备份怎么做怎样在阿里做网站
  • 后台网站手机版视频怎么做安阳百姓网
  • 设计公司网站公司详情域名解析网站登录
  • 优秀学校网站设计可以免费下源码的网站
  • 榆林高端网站建设xampp使用教程 wordpress
  • 站长工具搜一搜找做网站的
  • 建一个域名网站要多少钱南京哪里有做网站的
  • 网站群的建设网站建设自助建站制作