首发于 一个程序员的编程随记

动态链接详解

导读

下文出现的 objects 均指代动态链接库和可执行文件。

本篇文章详细地介绍了 objects 重定位的过程:



环境

一个小例子

了解 ELF 文件

工具概述

Dump 二进制

Dump 汇编代码

Dump 元信息

解析特定 sections

ELF 文件概述



File Header 和 Program Header 在 ELF 文件的开头,Section Header 在 ELF 文件的结尾。

接下来我们会用 readelf 直接查看元数据,也会用 od 以二进制方式看看每一个 Header 。

File Header

File Header 各个字段的含义可以参考 维基百科 。

The ELF header is 52 or 64 bytes long for 32-bit and 64-bit binaries respectively.

| 位移(八进制) | 内容 | 解释 | | :------------: | :---------------------: | :----------------------------------------------------------: | | 0000000 | 7f 45 4c 46 | ELF (magic number). | | | 02 | 1 is 32-bit format, 2 is 64-bit format. | | | 01 | 1 is big endianness, 2 is litte endianness. | | | 01 | Set to 1 for the original and current version of ELF. | | | 00 | ABI version, it is often set to 0. | | | 00 | Further specifies the ABI version. | | | 00 00 00 00 00 00 00 | Padding, should be filled with zeros. | | 0000020 | 03 00 | Identifies object file type, 0x3 is ET_DYN. | | | 3e 00 | Specifies instruction set architecture, 0x3e is amd64. | | | 01 00 00 00 | Set to 1 for the original version of ELF. | | | 50 10 00 00 00 00 00 00 | Entry point address. | | 0000040 | 40 00 00 00 00 00 00 00 | The start of the program header table. 0x40 = 64. | | | 58 39 00 00 00 00 00 00 | The start of the section header table. | | 0000060 | 00 00 00 00 | Interpretation depends on the target architecture. | | | 40 00 | Size of file header. | | | 38 00 | Size of a program header table entry. | | | 0b 00 | Number of entries in the program header table. | | | 40 00 | Size of a section header table entry. | | | 1e 00 | Number of entries in the section header table. | | | 1d 00 | Index of the section header table entry that contains the section names. | | 0000100 | | |

File Header 帮助链接器:

  1. 确认是否可以装载文件,包括系统是 32 位还是 64 位、大小端、ABI 版本等;
  2. 决定如何装载文件,包括 Program Header 和 Section Header 的位置及大小、如何寻找 section 名称、entry point address 等。

Program Header

| 位移(八进制) | 内容 | 解释 | | :------------: | :--------------: | :-----------------------: | | 0000170 | 00000003 | PT_INTERP | | | 00000004 | Segment flags. | | | 00000000000002a8 | Segment file offset. | | 0000210 | 00000000000002a8 | Segment virtual address. | | | 00000000000002a8 | Segment physical address. | | 0000230 | 0000000000000033 | Segment size in file. | | | 0000000000000033 | Segment size in memory. | | 0000250 | 0000000000000001 | Segment alignment. |

比较让人迷惑的字段是 Segment physical address ,根据 What is a section and why do we need it 和 写一个工具来了解ELF文件(二) 两篇文章,Segment physical address 在现代操作系统中已经没有用处了,GCC 一般将其置为 Segment virtual address 。

根据 Program Header 的指导,从 0x2a8 开始连续读 0x33 个字节,就是 interpreter 在文件系统中的路径。

Program Header 最重要的作用是指导链接器如何装载 ELF 文件,要注意:由于对齐或者前面的某个 Segment 在文件中的大小和在内存中的大小不一致,Segment 在文件中的起始地址未必等于在内存中的起始地址,比如:

LOAD Segment 在文件中的起始地址是 0x2dc8 ,在内存中的起始地址是 0x3dc8 ,两者并不相等。

Section Header

| 位移(八进制) | 内容 | 解释 | | :------------: | :--------------: | :----------------------------------------: | | 0036230 | 00000094 | Section name (string table index). | | | 00000001 | Section type, SHT_PROGBITS. | | | 0000000000000006 | Section flags, SHF_ALLOC \| SHF_EXECINSTR. | | 0036250 | 0000000000001040 | Section virtual address. | | | 0000000000001040 | Section file offset. | | 0036270 | 0000000000000008 | Section size in bytes. | | | 00000000 | Link to another section. | | | 00000000 | Additional section information. | | 0036310 | 0000000000000008 | Section alignment. | | | 0000000000000008 | Entry size if section holds table. |

根据 man elf 的描述,sh_link / sh_info 的含义都取决于 section 。

.got .plt .got.plt .plt.got

同文件重定位

R_X86_64_IRELATIVE

0x4028 是 global_var 的地址,0x4030 是 p_global_var 的地址。

重定位公式:*reloc_addr = l_addr + reloc->r_addendr_addend 是 0x4028 ,也就是 global_var 的地址。

R_X86_64_JUMP_SLOT

R_X86_64_JUMP_SLOT 标记的是 .got.plt 表项,.got.plt 表项也需要一次同文件重定位。

重定位公式:*reloc_addr += l_addr

Others

从 Executable and Linkable Format 101 Part 3: Relocations 中摘抄得到 x86_64 的重定位类型:

| Name | Value | Field | Calculation | | :----------------- | :---- | :---- | :------------------------------------------ | | R_X86_64_NONE | 0 | None | None | | R_X86_64_64 | 1 | qword | S + A | | R_X86_64_PC32 | 2 | dword | S + A – P | | R_X86_64_GOT32 | 3 | dword | G + A | | R_X86_64_PLT32 | 4 | dword | L + A – P | | R_X86_64_COPY | 5 | None | Value is copied directly from shared object | | R_X86_64_GLOB_DAT | 6 | qword | S | | R_X86_64_JUMP_SLOT | 7 | qword | S | | R_X86_64_RELATIVE | 8 | qword | B + A | | R_X86_64_GOTPCREL | 9 | dword | G + GOT + A – P | | R_X86_64_32 | 10 | dword | S + A | | R_X86_64_32S | 11 | dword | S + A | | R_X86_64_16 | 12 | word | S + A | | R_X86_64_PC16 | 13 | word | S + A – P | | R_X86_64_8 | 14 | word8 | S + A | | R_X86_64_PC8 | 15 | word8 | S + A – P | | R_X86_64_PC64 | 24 | qword | S + A – P | | R_X86_64_GOTOFF64 | 25 | qword | S + A – GOT | | R_X86_64_GOTPC32 | 26 | dword | GOT + A – P | | R_X86_64_SIZE32 | 32 | dword | Z + A | | R_X86_64_SIZE64 | 33 | qword | Z + A |

如何阅读代码?

_dl_relocate_object 大量使用 #ifdef 语句和宏定义,导致很难阅读。因而需要用 bear 生成的 compile_commands.json 来还原编译命令,并加上 -E -P 选项,得到预处理之后的文件。

# pwd
/root/glibc/glibc-2.28/elf
# gcc -c -std=gnu11 -fgnu89-inline -O1 -Wall -Werror -Wundef -Wwrite-strings -fmerge-all-constants -fno-stack-protector -frounding-math -ggdb -w -Wstrict-prototypes -Wold-style-definition -fno-math-errno -fPIC -fno-stack-protector -DSTACK_PROTECTOR_LEVEL=0 -mno-mmx -ftls-model=initial-exec -I../include -I/root/glibc/build/elf -I/root/glibc/build -I../sysdeps/unix/sysv/linux/x86_64/64 -I../sysdeps/unix/sysv/linux/x86_64 -I../sysdeps/unix/sysv/linux/x86/include -I../sysdeps/unix/sysv/linux/x86 -I../sysdeps/x86/nptl -I../sysdeps/unix/sysv/linux/wordsize-64 -I../sysdeps/x86_64/nptl -I../sysdeps/unix/sysv/linux/include -I../sysdeps/unix/sysv/linux -I../sysdeps/nptl -I../sysdeps/pthread -I../sysdeps/gnu -I../sysdeps/unix/inet -I../sysdeps/unix/sysv -I../sysdeps/unix/x86_64 -I../sysdeps/unix -I../sysdeps/posix -I../sysdeps/x86_64/64 -I../sysdeps/x86_64/fpu/multiarch -I../sysdeps/x86_64/fpu -I../sysdeps/x86/fpu/include -I../sysdeps/x86/fpu -I../sysdeps/x86_64/multiarch -I../sysdeps/x86_64 -I../sysdeps/x86 -I../sysdeps/ieee754/float128 -I../sysdeps/ieee754/ldbl-96/include -I../sysdeps/ieee754/ldbl-96 -I../sysdeps/ieee754/dbl-64/wordsize-64 -I../sysdeps/ieee754/dbl-64 -I../sysdeps/ieee754/flt-32 -I../sysdeps/wordsize-64 -I../sysdeps/ieee754 -I../sysdeps/generic -I.. -I../libio -I. -D_LIBC_REENTRANT -include /root/glibc/build/libc-modules.h -DMODULE_NAME=rtld -include ../include/libc-symbols.h -DPIC -DSHARED -DTOP_NAMESPACE=glibc -E -P dl-reloc.c > dl-reloc.i

跨文件重定位

开始重定位:.plt .got.plt

.plt 和 .got.plt 配合完成 lazy binding ,图片摘抄自 LIEF: 05 - Infecting the plt/got :



With lazy binding, the first time that the function is called the got entry redirects to the plt instruction.



The Second time, got entry holds the address in the shared library.

以 main 调用 foo 为例:

rip + 0x2fe2 是重定位实现 lazy binding 留下的一个占位符,这个占位符的初始值指向 plt 的第二条指令(1036),第一次调用发生后,它会被链接器修改成 foo 函数的地址,从而完成重定位。

0x1020 是所有 plt 程序共用的部分,rip + 0x2fe2 指向 link_map ,rip + 0x2fe4 指向 _dl_runtime_resolve_xsave ,这两个地址都由链接器负责填写。

剩下两个未解之谜:

  1. link_map 是怎么构造出来的?
  2. _dl_runtime_resolve_xsave 做了些什么?

_dl_runtime_resolve_xsave: before _dl_fixup

_dl_runtime_resolve_xsave 定义于 /root/glibc/glibc-2.28/sysdeps/x86_64/dl-trampoline.h ,不过文件内大量使用 #if 语句,并不适合直接阅读。

_dl_runtime_resolve_xsave 在调用 _dl_fixup 之前的主要工作是:保存寄存器。

_dl_fixup

_dl_runtime_resolve_xsave 的核心是位于 elf/dl-runtime.c 的 _dl_fixup_dl_fixup 的执行过程如下:

  1. link_map 访问 .dynamic ,分别取出 .rela.plt / .dynsym / .dynstr 的地址;
  2. .rela.plt + 参数 reloc_arg ,求出当前函数的重定位表项 Elf64_Rela 的指针,记作 reloc ;
  3. ELFW(R_SYM) (reloc->r_info) 作为 .dynsym 的下标,求出当前函数的符号表项 Elf64_Sym 的指针,记作 sym
  4. .dynstr + sym->st_name 得出符号名字 ;
  5. 在动态链接库中查找这个函数的地址,并且把地址赋值给 *(reloc->r_offset) ,即 .got.plt 表项 。

访问 .dynamic 表项

typedef struct
{
  Elf64_Sxword  d_tag;                  /* Dynamic entry type */
  union
    {
      Elf64_Xword d_val;                /* Integer value */
      Elf64_Addr d_ptr;                 /* Address value */
    } d_un;
} Elf64_Dyn;
struct link_map
  {
    /* These first few members are part of the protocol with the debugger.
       This is the same format used in SVR4.  */

    ElfW(Addr) l_addr;          /* Difference between the address in the ELF
                                   file and the addresses in memory.  */
    char *l_name;               /* Absolute file name object was found in.  */
    ElfW(Dyn) *l_ld;            /* Dynamic section of the shared object.  */
    struct link_map *l_next, *l_prev; /* Chain of loaded objects.  */
    // ...
    /* Indexed pointers to dynamic section. */
    ElfW(Dyn) *l_info[DT_NUM + DT_THISPROCNUM + DT_VERSIONTAGNUM
                      + DT_EXTRANUM + DT_VALNUM + DT_ADDRNUM];
    // ...
  };
// D_PTR 的两个定义用于:完成重定位之后和完成重定位之前,区别在于有没有加上 difference 。
/* All references to the value of l_info[DT_PLTGOT],
  l_info[DT_STRTAB], l_info[DT_SYMTAB], l_info[DT_RELA],
  l_info[DT_REL], l_info[DT_JMPREL], and l_info[VERSYMIDX (DT_VERSYM)]
  have to be accessed via the D_PTR macro.  The macro is needed since for
  most architectures the entry is already relocated - but for some not
  and we need to relocate at access time.  */
#ifdef DL_RO_DYN_SECTION
# define D_PTR(map, i) ((map)->i->d_un.d_ptr + (map)->l_addr)
#else
# define D_PTR(map, i) (map)->i->d_un.d_ptr
#endif
# readelf --dynamic main | head -n 8 | tail -n 6
  Tag        Type                         Name/Value
 0x0000000000000001 (NEEDED)             Shared library: [libfoo.so]
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]
 0x000000000000001d (RUNPATH)            Library runpath: [/root/test]
 0x000000000000000c (INIT)               0x1000
 0x000000000000000d (FINI)               0x11b4

对比 .dynamic 与 section headers

初次接触 ELF 文件会被 .dynamic 和 section headers 的区别坑到,我们不禁想问:既然有 section headers 指明每个 sections 的起始地址,为什么还需要 .dynamic ?

Section header 说 .symtab 的起始地址是 0x3050 ,.dynamic 表说 .symtab 的起始地址是 0x348 ,两者为什么会不一致?

事实上,0x348 是 .dynsym 的起始地址,即 .dynamic 表和 section headers 都使用了 symtab 这个名字,但两者完全不是一个意思。

同样的情况也发生在 strtab 上:

.dynamic 表和 section headers 都使用了 strtab 这个名字,但两者完全不是一个意思。

访问 .rela.plt 表项

对照 Elf64_Rela 的定义,r_info 的值是 0x4 << 32 + 0x7 。

访问 .dynsym 表项

所以 ELFW(R_SYM) (reloc->r_info) == 4

对照 Elf64_Sym 的定义,st_name 的值是 0x50 。

访问 .dynstr 表项

函数 foo 在 mangle 后的名字是 _Z3fooV

查找符号

_dl_lookup_symbol_x 遍历 l->l_scope ,对于每一个 scope 调用 do_lookup_x 函数寻找符号。

  1. l_scope 和已加载的动态链接库的关系是什么?是怎么被构造出来的?
  2. 如何在某个动态链接库内查找符号?

l_scope

l_scope 是什么?

摘抄自 ld.so Scopes :

The scope describes which libraries should be searched for symbol lookups occuring within the scope owner. (By the way, given that lookup scope may differ by caller, implementing dlsym() is not that trivial.) It is further divided into scope elements (struct r_scope_elem) – a single scope element basically describes a single search list of libraries, and the scope (link_map.l_scope is the scope used for symbol lookup) is list of such scope elements.
To reiterate, a symbol lookup scope is a list of lists! Then, when looking up a symbol, the linker walks the lists in the order they are listed in the scope. But what really are the scope elements? There are two usual kinds:

The global scope is shared between all link_maps (in the current namespace), while the local scope is owned by a particular library.



构造 l_scope

这里只探讨用得最多的 global scope :

  1. 加载文件时,代表 objects 的 link_map 会通过 _dl_add_to_namespace_list 函数添加到一个全局链表;
  2. dl_new_object 会将 global scope 赋值给 l_scope[0]

_dl_setup_hash

.gnu.hash 需要有多个导出符号才能较方便地分析,因此我们将使用 test_gnu_hash.cpp 作为待分析的文件:

编译器会准备好查找符号需要的数据,主要是布隆过滤器和哈希表。

struct link_map
  {
    // ...
    /* Symbol hash table.  */
    // The number of hash buckets.
    Elf_Symndx l_nbuckets;
    // bitmask_words is the number of __ELF_NATIVE_CLASS sized words
    // in the Bloom filter portion of the hash table section. This value
    // must be non-zero, and must be a power of 2.
    // l_gnu_bitmask_idxbits = bitmask_nwords - 1
    Elf32_Word l_gnu_bitmask_idxbits;
    // A shift count used by the Bloom filter.
    // HashValue_2 = HashValue_1 >> l_gnu_shift.
    Elf32_Word l_gnu_shift;
    const ElfW(Addr) *l_gnu_bitmask;
    union
    {
      const Elf32_Word *l_gnu_buckets;
      const Elf_Symndx *l_chain;
    };
    union
    {
      const Elf32_Word *l_gnu_chain_zero;
      const Elf_Symndx *l_buckets;
    };
    // ...
  };
void
_dl_setup_hash (struct link_map *map)
{
  if (__glibc_likely (map->l_info[ADDRIDX (DT_GNU_HASH)] != NULL))
    {
      Elf32_Word *hash32
        = (void *) D_PTR (map, l_info[ADDRIDX (DT_GNU_HASH)]);
      map->l_nbuckets = *hash32++;
      Elf32_Word symbias = *hash32++;
      Elf32_Word bitmask_nwords = *hash32++;
      /* Must be a power of two.  */
      assert ((bitmask_nwords & (bitmask_nwords - 1)) == 0);
      map->l_gnu_bitmask_idxbits = bitmask_nwords - 1;
      map->l_gnu_shift = *hash32++;

      map->l_gnu_bitmask = (ElfW(Addr) *) hash32;
      hash32 += __ELF_NATIVE_CLASS / 32 * bitmask_nwords;

      map->l_gnu_buckets = hash32;
      hash32 += map->l_nbuckets;
      map->l_gnu_chain_zero = hash32 - symbias;
      return;
    }
  // ...
}
# readelf --dynamic libtest_gnu_hash.so | grep -E "Tag|GNU_HASH"
  Tag        Type                         Name/Value
 0x000000006ffffef5 (GNU_HASH)           0x260
# readelf --section-headers libtest_gnu_hash.so | grep -E "Nr|260" -A1 | grep -v "\-\-"
  [Nr] Name              Type             Address           Offset
       Size              EntSize          Flags  Link  Info  Align
  [ 2] .gnu.hash         GNU_HASH         0000000000000260  00000260
       0000000000000038  0000000000000000   A       3     0     8
# export gnu_hash_start_addr=0x260
# export gnu_hash_size=0x38
# od --skip-bytes=$gnu_hash_start_addr --read-bytes=$gnu_hash_size --format=xI libtest_gnu_hash.so
0001140 00000003 00000005 00000001 00000006
0001160 04200400 18012908 00000005 00000008
0001200 00000000 b8f7d29a b95a257a b9d35b69
0001220 6a5ebc3c 6a6128eb

l_nbuckets / symbias / bitmask_nwords / l_gnu_shift

从 GNU Hash ELF Sections 摘抄了一段关于 .gnu.hash section 的描述:

The number of __ELF_NATIVE_CLASS sized words in the Bloom filter portion of the hash table section. This value must be non-zero, and must be a power of 2 as explained below.
Note that a value of 0 could be interpreted to mean that no Bloom filter is present in the hash section. However, the GNU linkers do not do this — the GNU hash section always includes at least 1 mask word.

对照前面的代码:l_nbuckets 是 3 ,symbias 是 5 ,bitmask_nwords 是 1 ,l_gnu_shift 是 6 。

symbias 表明第一个可以通过 .gnu.hash section 访问的符号(即可以提供给其它库访问的符号),在 libtest_gnu_hash.so 中这个符号是 _Z4hahav

l_gnu_bitmask

l_gnu_bitmask 是 0x260 + 4 * 4 = 0x270 ,l_gnu_bitmask 指向 libtest_gnu_hash.so 的布隆过滤器;从这个角度看的话,动态链接库的布隆过滤器是由编译器计算的,不是由链接器计算的。

布隆过滤器的原理可以参考文章 详解布隆过滤器的原理,使用场景和注意事项,简而言之,将数据使用多个不同的哈希函数生成多个哈希值,并将对应比特位置为 1 ,就能判断某个数据肯定不存在。

参考 GNU Hash ELF Sections ,构建布隆过滤器的伪代码如下:

构建布隆过滤器的 C++ 代码如下:

l_gnu_buckets / l_gnu_chain_zero

l_gnu_buckets 指向的一个数组,数组的 l_nbuckets 个元素分别是 5、8 和 0 ;5 代表 0 号桶的第一个元素是 l_gnu_chain_zero[5] ,8 代表 1 号桶的第一个元素是 l_gnu_chain_zero[8] ,0 代表 2 号桶是一个空桶;这是一种用一维数组实现二维数组的手段。



.gnu.hash 实现的是一张二维表,X 轴是哈希表的桶号,Y 轴是符号在 .dynsym 表中的下标,如下图所示:



然而,.gnu.hash 为了节省空间,做了两件非常 tricky 的事情:

  1. 将 5 个符号排序(可以发现 5 个符号在 .dynsym 表的顺序并非字母序),使得哈希后处在同一个哈希桶的多个符号彼此相邻,从而将二维表化简成一维表;
  2. 将不可导出符号(比如 __cxa_finalize@GLIBC_2.2.5 )排在可导出符号(比如 _Z3foov)的前面,从而节省掉存储不可导出符号的哈希值的空间;第一个可导出符号在 .dynsym 表的下标记为 symbias

l_gnu_chain_zero 减去 symbias ,方便后续计算符号在 .dynsym 表的下标。

do_lookup_x

哈希算法

布隆过滤

查找哈希表

  1. 根据 l_gnu_buckets 找到哈希桶的第一个元素;
  2. 顺序搜索哈希桶内的元素,直到找到相应的哈希值或者到达结尾。

回填 .got.plt 表项

对比 Elf64_Rela 的定义,r_offset 的值是 0x4018 ,而 0x4018 恰好是 .got.plt 表为函数 foo 预留的占位符。

_dl_runtime_resolve_xsave: after _dl_fixup

_dl_runtime_resolve_xsave 在调用 _dl_fixup 之后的主要工作是:调用函数。

Others

构造 link_map

可执行文件的 link_mapdl_main 函数构造。

gdb watch

watch 能帮助我们找到修改某个值的代码。

l_addr

l_addr = 进程中 segment 的虚存地址 - ELF 文件中 segment 的虚存地址 = 0x555555554000

通过 info proc mappings 能取得 l_addr

参考资料

ELF (except .plt and .got.plt and etc.):

.plt and .got.plt and etc.:

Dynamic linking:

Debug:

Others:

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

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