51CTO首页
AI.x社区
博客
学堂
精品班
直播训练营
企业培训
鸿蒙开发者社区
WOT技术大会
AIGC创新中国行
IT证书
公众号矩阵
移动端

Linux 动态链接过程中的【重定位】底层原理

系统 Linux
我们这篇文章来一起探索一下:动态链接过程中是如何进行符号重定位的。文中使用大量的【代码+图片】的方式,来真实的感受一下实际的内存模型。

大家好,我是道哥,你技术修炼道路上的垫脚石。

在上一篇文章中,我们一起学习了Linux系统中 GCC编译器在编译可执行程序时,静态链接过程中是如何进行符号重定位的。

为了完整性,我们这篇文章来一起探索一下:动态链接过程中是如何进行符号重定位的。

老样子,文中使用大量的【代码+图片】的方式,来真实的感受一下实际的内存模型。

文中使用了大量的图片,建议您在电脑上阅读此文。

关于为什么使用动态链接,这里就不展开讨论了,无非就几点:

  1. 节省物理内存。
  2. 可以动态更新。

动态链接要解决什么问题?

静态链接得到的可执行程序,被操作系统加载之后就可以执行执行。

因为在链接的时候,链接器已经把所有目标文件中的代码、数据等Section,都组装到可执行文件中了。

并且把代码中所有使用的外部符号(变量、函数),都进行了重定位(即:把变量、函数的地址,都填写到代码段中需要重定位的地方),因此可执行程序在执行的时候,不依赖于其它的外部模块即可运行。

详细的静态链接过程,请参考上一篇文章:【图片+代码】:GCC 链接过程中的【重定位】过程分析。

也就是说:符号重定位的过程,是直接对可执行文件进行修改。

但是对于动态链接来说,在编译阶段,仅仅是在可执行文件或者动态库中记录了一些必要的信息。

真正的重定位过程,是在这个时间点来完成的:可执行程序、动态库被加载之后,调用可执行程序的入口函数之前。

只有当所有需要被重定位的符号被解决了之后,才能开始执行程序。

既然也是重定位,与静态链接过程一样:也需要把符号的目标地址填写到代码段中需要重定位的地方。

矛盾:代码段不可写

问题来了!

我们知道,在现代操作系统中,对于内存的访问是有权限控制的,一般来说:

  • 代码段:可读、可执行。
  • 数据段:可读、可写。

如果进行符号重定位,就需要对代码进行修改(填写符号的地址),但是代码段又没有可写的权限,这是一个矛盾!

解决这个矛盾的方案,就是Linux系统中动态链接器的核心工作!

解决矛盾:增加一层间接性

David Wheeler有一句名言:“计算机科学中的大多数问题,都可以通过增加一层间接性来解决。”

解决动态链接中的代码重定位问题,同样也可以通过增加一层间接性来解决。

既然代码段在被加载到内存中之后不可写,但是数据段是可写的。

在代码段中引用的外部符号,可以在数据段中增加一个跳板:让代码段先引用数据段中的内容,然后在重定位时,把外部符号的地址填写到数据段中对应的位置,不就解决这个矛盾了吗?!

如下图所示:

理解了上图的解决思路,基本上就理解了动态链接过程中重定位的核心思想。

示例代码

我们需要3个源文件来讨论动态链接中重定位的过程:main.c、a.c、b.c,其中的a.c和b.c被编译成动态库,然后main.c与这两个动态库一起动态链接成可执行程序。

它们之间的依赖关系是:

b.c

代码如下:

#include <stdio.h>
int b = 30;
void func_b(void)
{
printf("in func_b. b = %d \n", b);
}

代码说明:

定义一个全局变量和一个全局函数,被 a.c 调用。

a.c

代码如下(稍微复杂一些,主要是为了探索:不同类型的符号如何处理重定位):

#include <stdio.h>
//
static int a1 = 10;
//
int a2 = 20;
//
extern int b;
//
extern void func_b(void);
//
static void func_a2(void)
{
printf("in func_a2 \n");
}
//
void func_a3(void)
{
printf("in func_a3 \n");
}
// main
void func_a1(void)
{
printf("in func_a1 \n");
//
a1 = 11;
a2 = 21;
//
b = 31;
//
func_a2();
func_a3();
//
func_b();
}

代码说明:

1、定义了 2 个全局变量:一个静态,一个非静态;

2、定义了 3 个函数:

  • func_a2是静态函数,只能在本文件中调用。
  • func_a1和func_a3是全局函数,可以被外部调用。

3、在 main.c 中会调用func_a1。

main.c

代码如下:

#include <stdio.h>
#include <unistd.h>
#include <dlfcn.h>
//
extern int a2;
extern void func_a1();
typedef void (*pfunc)(void);

int main(void)
{
printf("in main \n");
//
void *handle = dlopen(0, RTLD_NOW);
if (NULL == handle)
{
printf("dlopen failed! \n");
return -1;
}
printf("\n------------ main ---------------\n");
// main
pfunc addr_main = dlsym(handle, "main");
if (NULL != addr_main)
printf("addr_main = 0x%x \n", (unsigned int)addr_main);
else
printf("get address of main failed! \n");
printf("\n------------ liba.so ---------------\n");
// liba.so
unsigned int *addr_a1 = dlsym(handle, "a1");
if (NULL != addr_a1)
printf("addr_a1 = 0x%x \n", *addr_a1);
else
printf("get address of a1 failed! \n");
unsigned int *addr_a2 = dlsym(handle, "a2");
if (NULL != addr_a2)
printf("addr_a2 = 0x%x \n", *addr_a2);
else
printf("get address of a2 failed! \n");
// liba.so
pfunc addr_func_a1 = dlsym(handle, "func_a1");
if (NULL != addr_func_a1)
printf("addr_func_a1 = 0x%x \n", (unsigned int)addr_func_a1);
else
printf("get address of func_a1 failed! \n");
pfunc addr_func_a2 = dlsym(handle, "func_a2");
if (NULL != addr_func_a2)
printf("addr_func_a2 = 0x%x \n", (unsigned int)addr_func_a2);
else
printf("get address of func_a2 failed! \n");
pfunc addr_func_a3 = dlsym(handle, "func_a3");
if (NULL != addr_func_a3)
printf("addr_func_a3 = 0x%x \n", (unsigned int)addr_func_a3);
else
printf("get address of func_a3 failed! \n");
printf("\n------------ libb.so ---------------\n");
// libb.so
unsigned int *addr_b = dlsym(handle, "b");
if (NULL != addr_b)
printf("addr_b = 0x%x \n", *addr_b);
else
printf("get address of b failed! \n");
// libb.so
pfunc addr_func_b = dlsym(handle, "func_b");
if (NULL != addr_func_b)
printf("addr_func_b = 0x%x \n", (unsigned int)addr_func_b);
else
printf("get address of func_b failed! \n");
dlclose(handle);
//
a2 = 100;
//
func_a1();
// 退便
while(1) sleep(5);
return 0;
}

纠正:代码中本来是想打印变量的地址的,但是不小心加上了 *,变成了打印变量值。最后检查的时候才发现,所以就懒得再去修改了。

代码说明:

  1. 利用 dlopen 函数(第一个参数传入 NULL),来打印此进程中的一些符号信息(变量和函数)。
  2. 赋值给 liba.so 中的变量 a2,然后调用 liba.so 中的 func_a1 函数。

编译成动态链接库

把以上几个源文件编译成动态库以及可执行程序:

$ gcc -m32 -fPIC --shared b.c -o libb.so
$ gcc -m32 -fPIC --shared a.c -o liba.so -lb -L./
$ gcc -m32 -fPIC main.c -o main -ldl -la -lb -L./

有几点内容说明一下:

  1. -fPIC 参数意思是:生成位置无关代码(Position Independent Code),这也是动态链接中的关键。
  2. 既然动态库是在运行时加载,那为什么在编译的时候还需要指明?

因为在编译的时候,需要知道每一个动态库中提供了哪些符号。Windows 中的动态库的显性的导出和导入标识,更能体现这个概念(__declspec(dllexport), __declspec(dllimport))。

此时,就得到了如下几个文件:

动态库的依赖关系

对于静态链接的可执行程序来说,被操作系统加载之后,可以认为直接从可执行程序的入口函数开始(也就是ELF文件头中指定的e_entry这个地址),执行其中的指令码。

但是对于动态链接的程序来说,在执行入口函数的指令之前,必须把该程序所依赖的动态库加载到内存中,然后才能开始执行。

对于我们的实例代码来说:main程序依赖于liba.so库,而liba.so库又依赖于libb.so库。

可以用ldd工具来分别看一下动态库之间的依赖关系:

可以看出:

  1. 在 liba.so 动态库中,记录了信息:依赖于 libb.so。
  2. 在 main 可执行文件中,记录了信息:依赖于 liba.so, libb.so。

也可以使用另一个工具patchelf来查看一个可执行程序或者动态库,依赖于其他哪些模块。例如:

那么,动态库的加载是由谁来完成的呢?动态链接器!

动态库的加载过程

动态链接器加载动态库

当执行main程序的时候,操作系统首先把main加载到内存,然后通过.interp段信息来查看该文件依赖哪些动态库:

上图中的字符串/lib/ld-linux.so.2,就表示main依赖动态链接库。

ld-linux.so.2也是一个动态链接库,在大部分情况下动态链接库已经被加载到内存中了(动态链接库就是为了共享),操作系统此时只需要把动态链接库所在的物理内存,映射到 main进程的虚拟地址空间中就可以了,然后再把控制权交给动态链接器。

动态链接器发现:main依赖liba.so,于是它就在虚拟地址空间中找一块能放得下liba.so的空闲空间,然后把liba.so中需要加载到内存中的代码段、数据段都加载进来。

当然,在加载liba.so时,又会发现它依赖libb.so,于是又把在虚拟地址空间中找一块能放得下libb.so的空闲空间,把libb.so中的代码段、数据段等加载到内存中,示意图如下所示:

动态链接器自身也是一个动态库,而且是一个特殊的动态库:它不依赖于其他的任何动态库,因为当它被加载的时候,没有人帮它去加载依赖的动态库,否则就形成鸡生蛋、蛋生鸡的问题了。

动态库的加载地址

一个进程在运行时的实际加载地址(或者说虚拟内存区域),可以通过指令:$ cat /proc/[进程的 pid]/maps 读取出来。

例如:我的虚拟机中执行main程序时,看到的地址信息是:

黄色部分分别是:main, liba.so, libb.so 这3个模块的加载信息。

另外,还可以看到c库(libc-2.23.so)、动态链接器(ld-2.23.so)以及动态加载库libdl-2.23.so的虚拟地址区域,布局如下:

可以看出出来:main可执行程序是位于低地址,所有的动态库都位于4G内存空间的最后1G空间中。

还有另外一个指令也很好用 $ pmap [进程的 pid],也可以打印出每个模块的内存地址:

符号重定位

全局符号表

在之前的静态链接中学习过,链接器在扫描每一个目标文件(.o文件)的时候,会把每个目标文件中的符号提取出来,构成一个全局符号表。

然后在第二遍扫描的时候,查看每个目标文件中需要重定位的符号,然后在全局符号表中查找该符号被安排在什么地址,然后把这个地址填写到引用的地方,这就是静态链接时的重定位。

但是动态链接过程中的重定位,与静态链接的处理方式差别就大很多了,因为每个符号的地址只有在运行的时候才能知道它们的地址。

例如:liba.so引用了libb.so中的变量和函数,而libb.so中的这两个符号被加载到什么位置,直到main程序准备执行的时候,才能被链接器加载到内存中的某个随机的位置。

也就是说:动态链接器知道每个动态库中的代码段、数据段被加载的内存地址,因此动态链接器也会维护一个全局符号表,其中存放着每一个动态库中导出的符号以及它们的内存地址信息。

在示例代码main.c函数中,我们通过dlopen返回的句柄来打印进程中的一些全局符号的地址信息,输出内容如下:

上文已经纠错过:本来是想打印变量的地址信息,但是 printf 语句中不小心加上了型号,变成了打印变量值。

可以看到:在全局符号表中,没有找到liba.so中的变量a1和函数func_a2这两个符号,因为它俩都是static类型的,在编译成动态库的时候,没有导出到符号表中。

既然提到了符号表,就来看看这 3 个ELF文件中的动态符号表信息:

1、动态链接库中保护两个符号表:.dynsym(动态符号表: 表示模块中符号的导出、导入关系) 和 .symtab(符号表: 表示模块中的所有符号)。

  • .symtab 中包含了 .dynsym。

2、由于图片太大,这里只贴出 .dynsym 动态符号表。

绿色矩形框前面的Ndx列是数字,表示该符号位于当前文件的哪一个段中(即:段索引)。

红色矩形框前面的Ndx列是UND,表示这个符号没有找到,是一个外部符号(需要重定位)。

全局偏移表GOT

在我们的示例代码中,liba.so是比较特殊的,它既被main可执行程序所依赖,又依赖于libb.so。

而且,在liba.so中,定义了静态、动态的全局变量和函数,可以很好的概况很多种情况,因此这部分内容就主要来分析liba.so这个动态库。

前文说过:代码重定位需要修改代码段中的符号引用,而代码段被加载到内存中又没有可写的权限,动态链接解决这个矛盾的方案是:增加一层间接性。

例如:liba.so的代码中引用了libb.so中的变量b,在liba.so的代码段,并不是在引用的地方直接指向libb.so数据段中变量b的地址,而是指向了liba.so自己的数据段中的某个位置,在重定位阶段,链接器再把libb.so中变量b的地址填写到这个位置。

因为liba.so自己的代码段和数据段位置是相对固定的,这样的话,liba.so的代码段被加载到内存之后,就再也不用修改了。

而数据段中这个间接跳转的位置,就称作:全局偏移表(GOT: Global Offset Table)。

划重点:

liba.so的代码段中引用了libb.so中的符号b,既然b的地址需要在重定位时才能确定,那么就在数据段中开辟一块空间(称作:GOT表),重定位时把b的地址填写到GOT表中。

而liba.so的代码段中,把GOT表的地址填写到引用b的地方,因为GOT表在编译阶段是可以确定的,使用的是相对地址。

这样,就可以在不修改liba.so代码段的前提下,动态的对符号b进行了重定位!

其实,在一个动态库中存在 2 个GOT表,分别用于重定位变量符号(section名称:.got)和函数符号( section 名称:.got.plt)。

也就是说:所有变量类型的符号重定位信息都位于.got中,所有函数类型的符号重定位信息都位于.got.plt中。

并且,在一个动态库文件中,有两个特殊的段(.rel.dyn和.rel.plt)来告诉链接器:.got和.got.plt这两个表中,有哪些符号需要进行重定位,这个问题下面会深入讨论。

liba.so动态库文件的布局

为了更深刻的理解.got和.got.plt这两个表,有必要来拆解一下liba.so动态库文件的内部结构。

通过readelf -S liba.so指令来看一下这个ELF文件中都有哪些section:

可以看到:一共有28个section,其中的21、22就是两个GOT表。

另外,从装载的角度来看,装载器并不是把这些sections分开来处理,而是根据不同的读写属性,把多个section看做一个segment。

再次通过指令 readelf -l liba.so ,来查看一下segment信息:

也就是说:

这28个section中(关注绿色线条):

  1. section 0 ~ 16 都是可读、可执行权限,被当做一个 segment。
  2. section 17 ~ 24 都是可读、可写的权限,被动作另一个 segment。

再来重点看一下.got和.got.plt这两个section(关注黄色矩形框):

可见:.got和.got.plt与数据段一样,都是可读、可写的,所以被当做同一个 segment被加载到内存中。

通过以上这2张图(红色矩形框),可以得到liba.so动态库文件的内部结构如下:

liba.so动态库的虚拟地址

来继续观察liba.so文件segment信息中的AirtAddr列,它表示的是被加载到虚拟内存中的地址,重新贴图如下:

因为编译动态库时,使用了代码位置无关参数(-fPIC),这里的虚拟地址从0x0000_0000开始。

当liba.so的代码段、数据段被加载到内存中时,动态链接器找到一块空闲空间,这个空间的开始地址,就相当于一个基地址。

liba.so中的代码段和数据段中所有的虚拟地址信息,只要加上这个基地址,就得到了实际虚拟地址。

我们还是把上图中的输出信息,画出详细的内存模型图,如下所示:

GOT表的内部结构

现在,我们已经知道了liba.so库的文件布局,也知道了它的虚拟地址,此时就可以来进一步的看一下.got和.got.plt这两个表的内部结构了。

从刚才的图片中看出:

  1. .got 表的长度是 0x1c,说明有 7 个表项(每个表项占 4 个字节)。
  2. .got.plt 表的长度是 0x18,说明有 6 个表项。

上文已经说过,这两个表是用来重定位所有的变量和函数等符号的。

那么:liba.so通过什么方式来告诉动态链接器:需要对.got和.got.plt这两个表中的表项进行地址重定位呢?

在静态链接的时候,目标文件是通过两个重定位表.rel.text和.rel.data这两个段信息来告诉链接器的。

对于动态链接来说,也是通过两个重定位表来传递需要重定位的符号信息的,只不过名字有些不同:.rel.dyn和.rel.plt。

通过指令 readelf -r liba.so来查看重定位信息:

从黄色和绿色的矩形框中可以看出:

  1. liba.so 引用了外部符号 b,类型是 R_386_GLOB_DAT,这个符号的重定位描述信息在 .rel.dyn 段中。
  2. liba.so 引用了外部符号 func_b, 类型是 R_386_JUMP_SLOT,这个符号的重定位描述信息在 .rel.plt 段中。

从左侧红色的矩形框可以看出:每一个需要重定位的表项所对应的虚拟地址,画成内存模型图就是下面这样:

暂时只专注表项中的红色部分:.got表中的b, .got.plt表中的func_b,这两个符号都是libb.so中导出的。

也就是说:

liba.so的代码中在操作变量b的时候,就到.got表中的0x0000_1fe8这个地址处来获取变量b的真正地址;

liba.so的代码中在调用func_b函数的时候,就到.got.plt表中的0x0000_200c这个地址处来获取函数的真正地址;

反汇编liba.so代码

下面就来反汇编一下liba.so,看一下指令码中是如何对这两个表项进行寻址的。

执行反汇编指令:$ objdump -d liba.so,这里只贴出func_a1函数的反汇编代码:

第一个绿色矩形框(call 490 <__x86.get_pc_thunk.bx>)的功能是:把下一条指令(add)的地址存储到%ebx中,也就是:

%ebx = 0x622

然后执行: add $0x19de,%ebx,让%ebx加上0x19de,结果就是:%ebx = 0x2000。

0x2000正是.got.plt表的开始地址!

看一下第2个绿色矩形框:

mov -0x18(%ebx),%eax: 先用%ebx减去0x18的结果,存储到%eax中,结果是:%eax = 0x1fe8,这个地址正是变量b在.got表中的虚拟地址。

movl $0x1f,(%eax):在把0x1f(十进制就是31),存储到0x1fe8表项中存储的地址所对应的内存单元中(libb.so的数据段中的某个位置)。

因此,当链接器进行重定位之后,0x1fe8表项中存储的就是变量b的真正地址,而上面这两步操作,就把数值31赋值给变量b了。

第3个绿色矩形框,是调用函数func_b,稍微复杂一些,跳转到符号 func_b@plt的地方,看一下反汇编代码:

jmp指令调用了%ebx + 0xc处的那个函数指针,从上面的.got.plt布局图中可以看出,重定位之后这个表项中存储的正是func_b函数的地址(libb.so中代码段的某个位置),所以就正确的跳转到该函数中了。

责任编辑:姜华 来源: IOT物联网小镇
相关推荐
GCC 链接过程中定位过程分析
所谓的安排虚拟地址,就是指定这块内容被加载到虚拟内存的什么地方。当可执行文件被执行的时候,加载器就把每一块内容复制到虚拟内存相应的地址处。

2022-03-07 07:57:04

Linux 工具 内存
点对点PPP协议链接过程
这里我们深度剖析了点对点PPP协议的相关内容。以及在PPP拨号过程中的链路建立和链接等内容都做了细致的讲解。

2010-09-28 09:27:27

Linux从头学:16张结构图,彻底理解【代码定位底层原理
这篇文章,我们继续以8086这个简单的处理器为原型,把程序的加载过程描述一下。其中的重点部分就是代码重定位,我们用画图的方式,尽我所能,把程序加载、地址重定位的计算过程描述清楚。

2021-08-09 06:57:42

代码 重定位 Linux
探讨关于无线路由器在连接过程中问题
目前无线路由器的应用很广泛,相信随着通信行业的发展,无线路由器技术也会更加的完善稳定,给用户带来良好的网络环境。

2009-11-26 13:50:16

无线路由器
Linux 下静态链接动态链接原理及应用
我们知道一个.c文件经过编译、链接最终可以形成一个可执行文件。

2023-03-05 16:36:14

Linux 链接 目标文件
Linux提权过程中各种姿势
提权过程中的信息收集相当于是一个手动枚举的过程,涉及对系统配置和服务信息进行详细的查看和收集,在此过程中,往往需要记录尽可能多的信息,其中有些看似不重要的信息即使现在用不到,在后期往往能提供非常有价值的灵感和思路。

2024-01-15 00:25:59

深入SpringCloud Gateway底层详解路由配置定位原理
本节主要了解系统中的谓词与配置的路由信息是如何进行初始化关联生成路由对象的。每个谓词工厂中的Config对象又是如何被解析配置的。

2023-07-20 10:04:37

底层 路由 配置
详解注墨过程中误区
本文主要为大家介绍几点喷墨打印机使用中可能存在的误区,希望大家可以通过这些讨论,消除注墨行为中的不良习惯和错误认识,更好地提高打印效果,降低使用成本。

2011-05-03 10:31:59

喷墨打印机 注墨 误区
MySQL存储过程中Hibernate JDBC
本文将介绍MySQL存储过程中的HibernateJDBC,存储过程是在数据库中预编译好的SQL语句,只需一次编译即可,大大提高了sql语句执行的速度。

2009-07-23 14:10:38

Hibernate J
阿里云“飞天”过程中坎坷
阿里云目标是打造成为互联网数据分享的第一平台,但自成立以来,起初阿里云并未在计算市场中有所成就,就在人们开始质疑,开始忘却之时,阿里云却带着新开发的产品体系强势回归。然而,以“飞天”为核心,自主研发的开放云计算服务平台的阿里云,在飞天过程中的坎坷经历却不得不说!笔者认为,每位成功者的背后,定会有他辛酸的努力过程!

2013-08-15 12:26:40

阿里云 飞天
设置SNMP过程中MIB写入
下面我们解析了PIX中的SNMP的设置。其中包括很多MIB的写入。希望下面的步骤能让大家对这个建立过程有所掌握。

2010-07-01 14:05:43

SNMP MIB
oracle存储过程中select语句
oracle数据库是一种功能性很强大的数据库系统,能够处理大量的数据,至今仍在数据库市场中占有一定份额,下文中将主要为大家介绍oracle存储过程中的select语句。

2011-04-11 17:28:50

oracle 存储 select语句
深入DB2代理工作原理与连接过程
本文详细描述了DB2UniversalDatabase(DB2UDB)代理的工作原理以及连接集中器的特性,并对DB2连接上常见的问题及代理的优化作了详细的分析。

2009-05-19 09:10:26

代理 工作代理 DB2
Javasynchronized底层实现原理
通过synchronized进行加锁,就是通过对象头的MarkWord关联起来的,里面记录着锁状态和占有锁的线程地址指针。

2022-12-26 09:27:48

Java 底层 monitor
DB2存储过程中动态游标的正确使用方案
以下的文章主要是介绍DB2存储过程中对动态游标的正确使用,以及对其在实际操作中所要用到的实际操作代码的详细描述。

2010-08-05 14:24:37

DB2存储过程
对MySQL 存储过程中乱码破解
此文章主要向大家描述的是MySQL存储过程中的乱码的实际解决方法,乱码给MySQL存储过程中带来很多的不便,以下的文章就是对乱码的破解。

2010-05-31 16:57:09

Oracle存储过程中应注意事项
以下的文章主要是对java调用Oracle存储过程的描述,你如果是Oracle的疯狂一族的话,以下的文章,你一定不要错过。

2010-04-15 16:54:31

Oracle存储过程
SQL Server存储过程中参数运用
SQLServer存储过程支持输入、输出参数和支持返回值参数,下文就为您举例说明这几种参数运用方法,希望对您能有所帮助。

2010-11-12 09:18:13

SQL Server存
MySQL存储过程中基本函数描述
文章主要讲述的是MySQL存储过程中的基本函数,以及对字符串类与MySQL数据库存储过程基本函数中的数学类的描述。

2010-05-27 17:45:13

MySQL存储过程
谈谈变更过程中运维意识
运维,或许是一个在IT技术岗中很尴尬的职位。其一,许多应届生都未曾接触过,对工作的职能界定非常模糊;其二,很多其他技术岗的往届生会觉得,『卧槽,这么low逼,只会重启推配置做发布』;其三,正在从事运维岗的往届生会觉得自己在公司的KPI很难体现。

2019-08-13 15:01:04

变更 运维 项目经理

代做工资流水公司杭州办理银行对公流水德阳贷款流水公司潍坊做工作收入证明株洲工资流水费用邢台个人流水办理长春流水账单公司中山房贷工资流水 多少钱商丘办理银行流水账邯郸对公账户流水公司桂林查询签证银行流水厦门制作背调工资流水成都工资代付流水公司天津公司银行流水模板上海查询银行流水修改南昌银行流水PS查询新乡代开消费贷流水宿迁转账银行流水公司汕头打印车贷流水曲靖开房贷收入证明常德代开房贷收入证明宁德工资流水app截图价格福州贷款工资流水 办理淮安工作收入证明开具成都个人流水开具江门工资代付流水台州工资代付流水办理天津车贷工资流水 制作中山办签证工资流水洛阳工资代付流水开具中山打银行流水香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声卫健委通报少年有偿捐血浆16次猝死汪小菲曝离婚始末何赛飞追着代拍打雅江山火三名扑火人员牺牲系谣言男子被猫抓伤后确诊“猫抓病”周杰伦一审败诉网易中国拥有亿元资产的家庭达13.3万户315晚会后胖东来又人满为患了高校汽车撞人致3死16伤 司机系学生张家界的山上“长”满了韩国人?张立群任西安交通大学校长手机成瘾是影响睡眠质量重要因素网友洛杉矶偶遇贾玲“重生之我在北大当嫡校长”单亲妈妈陷入热恋 14岁儿子报警倪萍分享减重40斤方法杨倩无缘巴黎奥运考生莫言也上北大硕士复试名单了许家印被限制高消费奥巴马现身唐宁街 黑色着装引猜测专访95后高颜值猪保姆男孩8年未见母亲被告知被遗忘七年后宇文玥被薅头发捞上岸郑州一火锅店爆改成麻辣烫店西双版纳热带植物园回应蜉蝣大爆发沉迷短剧的人就像掉进了杀猪盘当地回应沈阳致3死车祸车主疑毒驾开除党籍5年后 原水城县长再被查凯特王妃现身!外出购物视频曝光初中生遭15人围殴自卫刺伤3人判无罪事业单位女子向同事水杯投不明物质男子被流浪猫绊倒 投喂者赔24万外国人感慨凌晨的中国很安全路边卖淀粉肠阿姨主动出示声明书胖东来员工每周单休无小长假王树国卸任西安交大校长 师生送别小米汽车超级工厂正式揭幕黑马情侣提车了妈妈回应孩子在校撞护栏坠楼校方回应护栏损坏小学生课间坠楼房客欠租失踪 房东直发愁专家建议不必谈骨泥色变老人退休金被冒领16年 金额超20万西藏招商引资投资者子女可当地高考特朗普无法缴纳4.54亿美元罚金浙江一高校内汽车冲撞行人 多人受伤

代做工资流水公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化