Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1299722
  • 博文数量: 554
  • 博客积分: 10425
  • 博客等级: 上将
  • 技术积分: 7555
  • 用 户 组: 普通用户
  • 注册时间: 2006-11-09 09:49
文章分类

全部博文(554)

文章存档

2012年(1)

2011年(1)

2009年(8)

2008年(544)

分类:

2008-04-11 14:58:01


第6 章• 支持接口173
Elf32_Word sndx, Elf_Data * data, Elf * elf, unit_t flags);
void ld_input_section64(const char * name, Elf64_Shdr ** shdr,
Elf64_Word sndx, Elf_Data * data, Elf * elf, uint_t flags);
name 是输入节的名称。shdr 是指向关联节标题的指针。sndx 是输入文件内的节索
引。data 是指向关联数据缓冲区的指针。elf 是指向文件的ELF 描述符的指针。flags 保留
供将来使用。
节标题的修改是通过重新分配节标题并为新标题重新指定*shdr 来完成的。链接编辑器使
用从ld_input_section() 返回时*shdr 所指向的节标题信息来处理节。
通过重新分配数据并重新指定Elf_Data 缓冲区的d_buf 指针,可以修改数据。对数据进
行任何修改都应确保正确设置Elf_Data 缓冲区的d_size 元素。对于成为输出映像一部分
的输入节,将d_size 元素设置为零可以有效地删除输出映像中的数据。
flags 字段指向初始值为零的uint_t 数据字段。虽然在将来的更新中可通过链接编辑器或
支持库来指定标志,但是当前未指定任何标志。
ld_section()
此函数针对传播给输出文件的输入文件的每一节调用,并且在执行任何节数据处理之前
即会调用此函数。
void ld_section(const char * name, Elf32_Shdr * shdr,
Elf32_Word sndx, Elf_Data * data, Elf * elf);
void ld_section64(const char * name, Elf64_Shdr * shdr,
Elf64_Word sndx, Elf_Data * data, Elf * elf);
name 是输入节的名称。shdr 是指向关联节标题的指针。sndx 是输入文件内的节索
引。data 是指向关联数据缓冲区的指针。elf 是指向文件ELF 描述符的指针。
通过重新分配数据并重新指定Elf_Data 缓冲区的d_buf 指针,可以修改数据。对数据进
行任何修改都应确保正确设置Elf_Data 缓冲区的d_size 元素。对于成为输出映像一部分
的输入节,将d_size 元素设置为零可以有效地删除输出映像中的数据。
注– 使用链接编辑器的-s 选项删除的各节,或者由于SHT_SUNW_COMDAT 处理
或SHF_EXCLUDE 标识而废弃的各节不会向ld_section() 进行报告。请参见第227 页中的
“COMDAT 节”和表7–8。
ld_input_done()
此函数在完成输入文件处理之后但在对输出文件进行布局之前调用。
链接编辑器支持接口
174 链接程序和库指南• 2006 年10 月
void ld_input_done(uint_t flags);
flags 字段指向初始值为零的uint_t 数据字段。虽然在将来的更新中可通过链接编辑器或
支持库来指定标志,但是当前未指定任何标志。
ld_atexit()
此函数在完成链接编辑时调用。
void ld_atexit(int status);
void ld_atexit64(int status);
status 是将由链接编辑器返回的exit(2) 代码,可以是EXIT_FAILURE 或EXIT_SUCCESS,如
stdlib.h 中所定义。
支持接口示例
以下示例创建了一个支持库,其中列显了在32 位链接编辑过程中处理的任何可重定位目标
文件的节名。
$ cat support.c
#include
#include
static int indent = 0;
void
ld_start(const char * name, const Elf32_Half type,
const char * caller)
{
(void) printf("output image: %s\n", name);
}
链接编辑器支持接口
第6 章• 支持接口175
void
ld_file(const char * name, const Elf_Kind kind, int flags,
Elf * elf)
{
if (flags & LD_SUP_EXTRACTED)
indent = 4;
else
indent = 2;
(void) printf("%*sfile: %s\n", indent, "", name);
}
void
ld_section(const char * name, Elf32_Shdr * shdr, Elf32_Word sndx,
Elf_Data * data, Elf * elf)
{
Elf32_Ehdr * ehdr = elf32_getehdr(elf);
if (ehdr->e_type == ET_REL)
(void) printf("%*s section [%ld]: %s\n", indent,
"", (long)sndx, name);
}
此支持库依赖于libelf 来提供用于确定输入文件类型的ELF 访问函数
elf32_getehdr(3ELF)。此支持库通过使用以下命令生成:
$ cc -o support.so.1 -G -K pic support.c -lelf -lc
链接编辑器支持接口
176 链接程序和库指南• 2006 年10 月
以下示例说明了从可重定位目标文件和本地归档库构造普通应用程序所产生的节诊断信
息。使用-S 选项不仅可以处理缺省调试信息,还可以调用支持库。
$ LD_OPTIONS="-S./support.so.1 -Slibldstab.so.1" \
cc -o prog main.c -L. -lfoo
output image: prog
file: /opt/COMPILER/crti.o
section [1]: .shstrtab
section [2]: .text
.......
file: /opt/COMPILER/crt1.o
section [1]: .shstrtab
section [2]: .text
.......
file: /opt/COMPILER/values-xt.o
section [1]: .shstrtab
section [2]: .text
.......
file: main.o
section [1]: .shstrtab
section [2]: .text
.......
file: ./libfoo.a
file: ./libfoo.a(foo.o)
section [1]: .shstrtab
链接编辑器支持接口
第6 章• 支持接口177
section [2]: .text
.......
file: /lib/libc.so
file: /opt/COMPILER/crtn.o
section [1]: .shstrtab
section [2]: .text
.......
注– 为了简化输出,已经减少了本示例中显示的节数。另外,编译器驱动程序所包含的文件
也会有所不同。
运行时链接程序审计接口
进程可以使用rtld-audit 接口访问与其自身相关的运行时链接信息。第143 页中的“配置共
享库”中介绍对共享库的运行时配置即是使用此机制的一个示例。
rtld-audit 接口实现为提供一个或多个审计接口例程的审计库。如果将该库作为进程的一部
分装入,则运行时链接程序会在进程执行的不同阶段调用审计例程。审计库可以使用这些
接口访问以下各项:
 依赖项搜索。可以通过审计库替换搜索路径。
 与装入的目标文件相关的信息。
 装入的目标文件之间进行的符号绑定。可以通过审计库更改这些绑定。
 通过利用过程链接表各项所提供的延迟绑定机制,可以审计函数调用及其返回值。可以
通过审计库修改函数参数及其返回值。请参见第287 页中的“过程链接表(特定于处理
器)”。
通过预装入专用的共享库可以实现其中的部分功能。但是,预装入的目标文件与进程目标
文件存在于同一名称空间内。这种预装入通常会限制预装入的共享库的实现或者使实现变
得更为复杂。rtld-audit 接口会为用户提供唯一的名称空间,用于在其中执行其审计库。此
名称空间可确保在进程内进行正常绑定时审计库不会侵入。
建立名称空间
运行时链接程序将动态可执行文件与其依赖项绑定时,会生成链接映射的链接列表,用于
对此进程进行说明。/usr/include/sys/link.h 中定义的链接映射结构说明了进程内的每个
运行时链接程序审计接口
178 链接程序和库指南• 2006 年10 月
目标文件。绑定应用程序的目标文件所需的符号搜索机制会遍历此链接映射的列表。此链
接映射列表用于提供进程符号解析的名称空间。
运行时链接程序也通过链接映射来进行说明。此链接映射以不同于应用程序目标文件列表
的列表中进行维护。因此,运行时链接程序驻留在其自己唯一的名称空间中,从而可防止
以任何方式将应用程序直接绑定到运行时链接程序内的服务。应用程序只能通过过滤器
libc.so.1 或libdl.so.1 调用运行时链接程序的公共服务。
rtld-audit 接口使用自己的用于维护审计库的链接映射列表。因此,在应用程序的的符号绑
定要求中,不涉及审计库。通过dlmopen(3C) 可检查应用程序链接映射列表。将
RTLD_NOLOAD 标志用于dlmopen(3C) 时,审计库可以在不装入目标文件的情况下查询此目标
文件是否存在。
/usr/include/link.h 中定义了两个标识符,用于定义应用程序和运行时链接程序的链接映
射列表:
#define LM_ID_BASE 0 /* application link-map list */
#define LM_ID_LDSO 1 /* runtime linker link-map list */
针对每个rtld-audit 支持库会指定一个唯一的新链接映射标识符。
创建审计库
审计库的生成方式与其他任何共享库的生成方式相同。但是,必须注意进程内审计库名称
空间的唯一性。
 该库必须提供所有依赖性需求。
 该库不应使用无法用于进程内多个接口实例的系统接口。
如果审计库调用printf(3C),则审计库必须定义与libc 之间的依赖性。请参见第46 页中的
“生成共享库输出文件”。由于审计库具有唯一的名称空间,因此,所审计的应用程序中
提供的libc 无法满足符号引用。如果审计库依赖于libc,则会向进程中装入两种版本的
libc.so.1。一种版本用于满足应用程序链接映射列表的绑定要求,另一种版本用于满足审
计链接映射列表的绑定要求。
要确保生成的审计库会记录所有的依赖项,请使用链接编辑器的-z defs 选项。
部分系统接口会假定其是进程内实现的唯一实例,例如信号和malloc(3C)。审计库应该避
免使用此类接口,因为这样做可能会无意中更改应用程序的行为。
注– 审计库可以使用mapmalloc(3MALLOC) 来分配内存,因为此分配方法可以与应用程序通
常使用的任何分配方案同时存在。
运行时链接程序审计接口
第6 章• 支持接口179
调用审计接口
rtld-audit 接口可通过以下两种方法之一来启用。每种方法都会指示一个所审计的目标文件
的范围。
 全局审计,通过使用环境变量LD_AUDIT 来启用。通过此方法可用的审计库附带有与进
程所使用的所有动态库相关的信息。
 局部审计,通过生成目标文件时在目标文件内记录的动态项来启用。通过此方法可用的
审计库附带有与标识用于审计的那些动态库相关的信息。
任一调用方法都包含一个字符串,其中包含通过dlopen(3C) 装入的以冒号分隔的共享库列
表。每个目标文件都装入各自的审计链接映射列表中。使用dlsym(3C) 可搜索每个目标文件
中的审计例程。在应用程序执行过程中的不同阶段会调用找到的审计例程。
通过rtld-audit 接口可以提供多个审计库。希望以此方式使用的审计库不应更改通常由运行
时链接程序返回的绑定。更改这些绑定会在后面的审计库中产生意外结果。
安全应用程序只能从受信任的目录中获取审计库。缺省情况下,用于32 位目标文件的运行
时链接程序可识别的受信任的目录仅有/lib/secure 和/usr/lib/secure。对于64 位目标文
件,受信任的目录是/lib/secure/64 和/usr/lib/secure/64。
记录局部审计程序
使用链接编辑器选项-p 或-P 生成目标文件时,可以确定局部审计要求。例如,要使用审计
库audit.so.1 审计libfoo.so.1,请在链接编辑时使用-p 选项记录要求:
$ cc -G -o libfoo.so.1 -Wl,-paudit.so.1 -K pic foo.c
$ dump -Lv libfoo.so.1 | fgrep AUDIT
[3] AUDIT audit.so.1
在运行时,如果存在此审计标识符,则会装入审计库并将与标识目标文件相关的信息传递
到此审计库。
如果单独使用此机制,则在装入审计库之前会显示搜索标识目标文件之类的信息。要提供
尽可能多的审计信息,需要将存在的要求局部审计的目标文件传播给此目标文件的用户。
例如,如果生成的应用程序依赖于libfoo.so.1,则会对此应用程序进行标识,指明其依赖
项需要审计:
$ cc -o main main.c libfoo.so.1
$ dump -Lv main | fgrep AUDIT
[5] DEPAUDIT audit.so.1
通过此机制启用的审计会导致向审计库中传递与所有应用程序显式依赖项有关的信息。使
用链接编辑器的-P 选项,还可以在创建目标文件时直接记录此依赖项审计:
运行时链接程序审计接口
180 链接程序和库指南• 2006 年10 月
$ cc -o main main.c -Wl,-Paudit.so.1
$ dump -Lv main | fgrep AUDIT
[5] DEPAUDIT audit.so.1
注– 通过将环境变量LD_NOAUDIT 设置为非空值,可以在运行时禁用审计。
审计接口函数
rtld-audit 接口提供了以下函数。这些函数按照其预期的使用顺序进行说明。
注– 为了简化讨论,对体系结构或目标文件类特定接口的引用会缩减为其通用名称。例如,
对la_symbind32() 和la_symbind64() 的引用会指定为la_symbind()。
la_version()
此函数可提供运行时链接程序与审计库之间的初次握手。必须提供此接口才能装入审计
库。
uint_t la_version(uint_t version);
运行时链接程序通过其可以支持的version 最高的rtld-audit 接口来调用此接口。审计库可
以检验此版本是否足以供其使用,并返回审计库预期使用的版本。此版本通常为
/usr/include/link.h 中定义的LAV_CURRENT。
如果审计库返回零,或者返回的版本高于运行时链接程序所支持的rtld-audit 接口的版
本,则会废弃该审计库。
la_activity()
此函数可通知审计程序正在进行链接映射活动。
void la_activity(uintptr_t * cookie, uint_t flags);
cookie 标识作为链接映射标题的目标文件。flags 表示活动类型,如/usr/include/link.h
中所定义:
 LA_ACT_ADD-正在向链接映射列表中添加目标文件。
 LA_ACT_DELETE-正在从链接映射列表中删除目标文件。
 LA_ACT_CONSISTENT-已经完成目标文件活动。
la_objsearch()
此函数可通知审计程序将要搜索目标文件。
char * la_objsearch(const char * name, uintptr_t * cookie, uint_t flags);
name 表示所搜索的文件名或路径名。cookie 标识启动搜索的目标文件。flags 标识name 的
来源和创建方式,如/usr/include/link.h 中所定义:
运行时链接程序审计接口
第6 章• 支持接口181
 LA_SER_ORIG-初始搜索名称。通常,此名称表示记录为DT_NEEDED 项的文件名或者提
供给dlopen(3C) 的参数。
 LA_SER_LIBPATH-已经通过LD_LIBRARY_PATH 组件创建了路径名。
 LA_SER_RUNPATH-已经通过运行路径组件创建了路径名。
 LA_SER_DEFAULT-已经通过缺省搜索路径组件创建了路径名。
 LA_SER_CONFIG-路径组件源自配置文件。请参见crle(1)。
 LA_SER_SECURE-路径组件特定于安全目标文件。
返回值会指明运行时链接程序应该继续处理的搜索路径名。值为零表示应该忽略此路
径。监视搜索路径的审计库会返回name。
la_objopen()
此函数在运行时链接程序装入新目标文件时调用。
uint_t la_objopen(Link_map * lmp, Lmid_t lmid, uintptr_t * cookie);
lmp 提供说明新目标文件的链接映射结构。lmid 标识添加了目标文件的链接映射列
表。cookie 提供指向某个标识符的指针。此标识符会初始化为目标文件lmp。审计库可以
修改此标识符,以便更好地标识其他rtld-audit 接口例程的目标文件。
la_objopen() 函数会返回表示与此目标文件相关的符号绑定的值。返回值是
/usr/include/link.h 中定义的以下值的掩码:
 LA_FLG_BINDTO-审计到此目标文件的符号绑定。
 LA_FLG_BINDFROM-审计来自此目标文件的符号绑定。
通过这些值,审计程序可以选择要使用la_symbind() 监视的目标文件。返回值为零表示
绑定信息与此目标文件无关。
例如,审计程序可以监视从libfoo.so 到libbar.so 的绑定。将la_objopen() 用于
libfoo.so 会返回LA_FLG_BINDFROM。将la_objopen()用于libbar.so 会返回
LA_FLG_BINDTO。
审计程序可以监视libfoo.so 与libbar.so 之间的所有绑定。将la_objopen() 用于这两个
目标文件会返回LA_FLG_BINDFROM 和LA_FLG_BINDTO。
审计程序可以监视到libbar.so 的所有绑定。将la_objopen() 用于libbar.so 会返回
LA_FLG_BINDTO。所有la_objopen() 调用都会返回LA_FLG_BINDFROM。
la_objfilter()
此函数在过滤器装入新的filtee 时调用。请参见第121 页中的“作为过滤器的共享库”。
int la_objfilter(uintptr_t * fltrcook, const char * fltestr,
uintptr_t * fltecook, uint_t flags);
fltrcook 标识过滤器。fltestr 指向filtee 字符串。fltecook 标识filtee。flags 当前未使用。对于
过滤器和filtee,la_objfilter() 在la_objopen() 之后调用。
运行时链接程序审计接口
182 链接程序和库指南• 2006 年10 月
值为零表示应该忽略此filtee。监视过滤器使用情况的审计库会返回非零值。
la_preinit()
为应用程序装入所有目标文件之后但在将控制权转交给应用程序之前,会调用一次此函
数。
void la_preinit(uintptr_t * cookie);
cookie 标识启动进程的主目标文件,通常为动态可执行文件。
la_symbind()
在已经通过la_objopen() 标记用于绑定通知的两个目标文件之间进行绑定时,会调用此
函数。
uintptr_t la_symbind32(Elf32_Sym * sym, uint_t ndx,
uintptr_t * refcook, uintptr_t * defcook, uint_t * flags);
uintptr_t la_symbind64(Elf64_Sym * sym, uint_t ndx,
uintptr_t * refcook, uintptr_t * defcook, uint_t * flags,
const char * sym_name);
sym 是构造的符号结构,其sym->st_value 表示所绑定的符号定义的地址。请参见
/usr/include/sys/elf.h。la_symbind32() 可将sym->st_name 调整为指向实际符号名称。
la_symbind64() 可保留sym->st_name 作为绑定目标文件字符串表的索引。
ndx 表示绑定目标文件的动态符号表内的符号索引。refcook 标识引用此符号的目标文
件。此标识符与传递给la_objopen() 函数的标识符相同,此函数会返回
LA_FLG_BINDFROM。defcook 标识定义此符号的目标文件。此标识符与传递给la_objopen()
的标识符相同,此函数会返回LA_FLG_BINDTO。
flags 指向可以传送与绑定相关的信息的数据项。此数据项还可用于修改对此过程链接表
项的继续审计。该值是/usr/include/link.h 中定义的符号绑定标志的掩码。
可以为la_symbind() 提供以下标志:
 LA_SYMB_DLSYM-由于调用dlsym(3C) 而发生的符号绑定。
 LA_SYMB_ALTVALUE (LAV_VERSION2)-通过先前调用la_symbind() 为符号值返回替换
值。
如果la_pltenter() 或la_pltexit() 函数存在,则对于过程链接表的各项,这些函数在
la_symbind() 之后调用。每次引用符号时都会调用这些函数。另请参见第187 页中的“
审计接口限制”。
la_symbind() 可以提供以下标志来更改此缺省行为。这些标志可应用于按位或运算(包
含边界值),并通过flags 参数来指示其值。
运行时链接程序审计接口
第6 章• 支持接口183
 LA_SYMB_NOPLTENTER-请勿针对此符号调用la_pltenter() 函数。
 LA_SYMB_NOPLTEXIT-请勿针对此符号调用la_pltexit() 函数。
返回值表示在此调用后将控制权传递到的地址。监视符号绑定的审计库应该返回值
sym->st_value,以便将控制权传递给绑定符号定义。审计库可以通过返回不同的值对符
号绑定进行专门重定向。
sym_name 仅适用于la_symbind64(),其中包含所处理的符号的名称。对于32 位接口,
可在sym->st_name 字段中使用此名称。
la_pltenter()
这些函数是系统特定的。调用过程链接表中位于已经标记用于绑定通知的两个目标文件
之间的一项时,会调用这些函数。
uintptr_t la_sparcv8_pltenter(Elf32_Sym * sym, uint_t ndx,
uintptr_t * refcook, uintptr_t * defcook,
La_sparcv8_regs * regs, uint_t * flags);
uintptr_t la_sparcv9_pltenter(Elf64_Sym * sym, uint_t ndx,
uintptr_t * refcook, uintptr_t * defcook,
La_sparcv9_regs * regs, uint_t * flags,
const char * sym_name);
uintptr_t la_i86_pltenter(Elf32_Sym * sym, uint_t ndx,
uintptr_t * refcook, uintptr_t * defcook,
La_i86_regs * regs, uint_t * flags);
uintptr_t la_amd64_pltenter(Elf64_Sym * sym, uint_t ndx,
uintptr_t * refcook, uintptr_t * defcook,
La_amd64_regs * regs, uint_t * flags, const char * sym_name);
sym、ndx、refcook、defcook 和sym_name 提供的信息与传递给la_symbind() 的信息相
同。
运行时链接程序审计接口
184 链接程序和库指南• 2006 年10 月
对于la_sparcv8_pltenter() 和la_sparcv9_pltenter(),regs 指向输出寄存器。对于
la_i86_pltenter(),regs 指向栈寄存器和帧寄存器。对于la_amd64_pltenter(),regs 指
向栈寄存器、帧寄存器以及用于传递整数参数的寄存器。regs 在/usr/include/link.h 中
定义。
flags 指向可以传送与绑定相关的信息的数据项。此数据项可用于修改对此过程链接表项
的继续审计。此数据项与la_symbind() 中的flags 指向的数据项相同。
la_pltenter() 可以提供以下标志来更改当前的审计行为。这些标志可应用于按位或运算
(包含边界值),并通过flags 参数来指示其值。
 LA_SYMB_NOPLTENTER-不能针对此符号再次调用la_pltenter()。
 LA_SYMB_NOPLTEXIT-不能针对此符号再次调用la_pltexit()。
返回值表示在此调用后将控制权传递到的地址。监视符号绑定的审计库应该返回值
sym->st_value,以便将控制权传递给绑定符号定义。审计库可以通过返回不同的值对符
号绑定进行专门重定向。
la_pltexit()
返回过程链接表中位于已经标记用于绑定通知的两个目标文件之间的一项时,会调用此
函数。此函数在调用方获取控制权之前调用。
uintptr_t la_pltexit(Elf32_Sym * sym, uint_t ndx, uintptr_t * refcook,
uintptr_t * defcook, uintptr_t retval);
uintptr_t la_pltexit64(Elf64_Sym * sym, uint_t ndx, uintptr_t * refcook,
uintptr_t * defcook, uintptr_t retval, const char * sym_name);
sym、ndx、refcook、defcook 和sym_name 提供的信息与传递给la_symbind() 的信息相
同。retval 是绑定函数的返回代码。监视符号绑定的审计库应该返回retval。审计库可以
专门返回不同的值。
注– la_pltexit() 接口是实验接口。请参见第187 页中的“审计接口限制”。
la_objclose()
此函数在执行目标文件的任何终止代码之后和卸载目标文件之前调用。
uint_t la_objclose(uintptr_t * cookie);
cookie 标识目标文件,并从先前的la_objopen() 中获取。当前会忽略任何返回值。
运行时链接程序审计接口
第6 章• 支持接口185
审计接口示例
以下简单示例创建了一个审计库,其中列显了动态可执行文件date(1) 装入的每个共享库依
赖项的名称。
$ cat audit.c
#include
#include
uint_t
la_version(uint_t version)
{
return (LAV_CURRENT);
}
uint_t
la_objopen(Link_map * lmp, Lmid_t lmid, uintptr_t * cookie)
{
if (lmid == LM_ID_BASE)
(void) printf("file: %s loaded\n", lmp->l_name);
return (0);
}
$ cc -o audit.so.1 -G -K pic -z defs audit.c -lmapmalloc -lc
$ LD_AUDIT=./audit.so.1 date
file: date loaded
file: /lib/libc.so.1 loaded
file: /lib/libm.so.2 loaded
运行时链接程序审计接口
186 链接程序和库指南• 2006 年10 月
file: /usr/lib/locale/en_US/en_US.so.2 loaded
Thur Aug 10 17:03:55 PST 2000
审计接口演示
/usr/demo/link_audit 下的SUNWosdem 软件包中提供了许多使用rtld-audit 接口的演示应用程
序:
sotruss
此演示跟踪指定的应用程序的动态库之间的过程调用。
whocalls
此演示跟踪指定的应用程序每次调用指定函数时所用栈。
perfcnt
此演示跟踪指定的应用程序的每个函数执行时间。
symbindrep
此演示报告为装入指定的应用程序而执行的所有符号绑定。
sotruss(1) 和whocalls(1) 包括在SUNWtoo 软件包中。perfcnt 和symbindrep 是程序示例。这
些应用程序不适用于生产环境。
审计接口限制
使用la_pltexit() 系列存在一些限制。这些限制是由于需要在调用方和被调用方之间插入
额外栈帧,以提供la_pltexit() 返回值。此要求在仅调用la_pltenter() 例程时不会产生问
题。在这种情况下,可以在将控制权转交给目标函数之前清除任何干预栈。
由于存在这些限制,因此应该将la_pltexit() 视为实验接口。在不确定的情况下,请避免
使用la_pltexit() 例程。
直接检查栈的函数
有少量函数可以直接检查栈或对其状态做出假设。这些函数的一些示例包括setjmp(3C) 系
列、vfork(2) 以及返回结构而不是指向结构的指针的任何函数。为支持la_pltexit() 而创
建的额外栈会破坏这些函数。
由于运行时链接程序无法检测此类型的函数,因此,审计库创建者会负责针对此类例程禁
用la_pltexit()。
运行时链接程序审计接口
第6 章• 支持接口187
运行时链接程序调试器接口
运行时链接程序可执行许多操作,包括将目标文件映射到内存中以及绑定符号。调试程序
通常需要在分析应用程序的过程中访问说明这些运行时链接程序操作的信息。这些调试程
序作为不同于其所分析的应用程序的进程运行。
本节介绍了用于监视和修改其他进程中的动态链接应用程序的rtld-debugger 接口。此接口
的体系结构采用libc_db(3LIB) 中所使用的模型。
使用rtld-debugger 接口时,至少涉及两个进程:
 一个或多个目标进程。目标进程必须动态链接,对于32 位进程,使用运行时链接程序
/usr/lib/ld.so.1,对于64 位进程,使用/usr/lib/64/ld.so.1。
 控制进程与rtld-debugger 接口库链接,并使用该库来检查目标进程的动态方面。64 位控
制进程可以调试64 位目标和32 位目标。但是,32 位控制进程只能调试32 位目标。
当控制进程为调试器并且其目标为动态可执行文件时,最需要使用rtld-debugger 接口。
rtld-debugger 接口可启用目标进程的以下活动:
 与运行时链接程序初次会合。
 通知装入和卸载动态库。
 检索与任何装入的目标文件相关的信息。
 跳过过程链接表项。
 启用目标文件填充。
控制进程和目标进程之间的交互
要检查和处理目标进程, rtld-debugger 接口需要使用导出接口、导入接口以及代理在这些
接口之间进行通信。
控制进程与librtld_db.so.1 所提供的rtld-debugger 接口链接,并会请求从该库导出的接
口。此接口在/usr/include/rtld_db.h 中定义。与此相反,librtld_db.so.1 会请求从控制
进程导入的接口。通过此交互,rtld-debugger 接口可以执行以下操作:
 在目标进程中查找符号。
 在目标进程中读写内存。
导入接口由许多proc_service 例程组成,大多数调试器已经使用这些例程来分析进程。这
些例程将在第198 页中的“调试器导入接口”中进行介绍。
rtld-debugger 接口假定请求rtld-debugger 接口时会停止进程分析。如果未停止分析,则目标
进程的运行时链接程序内的数据结构在检查时可能处于不一致状态。
下图中显示了librtld_db.so.1、控制进程(调试器)和目标进程(动态可执行文件)之间
的信息流程。
运行时链接程序调试器接口
188 链接程序和库指南• 2006 年10 月
图6–1 rtld-debugger 信息流程
注– rtld-debugger 接口依赖于proc_service 接口/usr/include/proc_service.h,后者被视为
实验接口。rtld-debugger 接口可能必须跟踪proc_service 接口在发展中的变化。
/usr/demo/librtld_db 下的SUNWosdem 软件包中提供了使用rtld-debugger 接口的控制进程的
实现样例。此调试器rdb 提供了使用proc_service 导入接口的示例,并说明了所有
librtld_db.so.1 导出接口所需的调用顺序。以下各节介绍rtld-debugger 接口。可以查看调
试器样例,获取更多详细信息。
调试器接口代理
代理提供了可以描述内部接口结构的不透明处理方式,还提供了导出接口与导入接口之间
的通信机制。rtld-debugger 接口旨在供可以同时处理多个进程的调试器使用,这些代理用于
标识进程。
struct ps_prochandle
控制进程创建的不透明结构,用于标识在导出接口与导入接口之间传递的目标进程。
struct rd_agent
Is an opaque structure created by thertld-debugger 接口创建的不透明结构,用于标识在导出
接口与导入接口之间传递的目标进程。
运行时链接程序调试器接口
第6 章• 支持接口189
调试器导出接口
本节介绍/usr/lib/librtld_db.so.1 审计库所导出的各种接口。可以将这些接口分为不同
的功能组。
代理处理接口
rd_init()
此函数可确定rtld-debugger 的版本要求。基本version 会定义为RD_VERSION1。当前
version 始终由RD_VERSION 定义。
rd_err_e rd_init(int version);
Solaris 8 10/00 发行版中添加的版本RD_VERSION2 扩展了rd_loadobj_t 结构。请参见第191
页中的“扫描可装入目标文件”中的rl_flags、rl_bend 和rl_dynamic 字段。
Solaris 8 01/01 发行版中添加的版本RD_VERSION3 扩展了rd_plt_info_t 结构。请参见第196
页中的“跳过过程链接表”中的pi_baddr 和pi_flags 字段。
如果控制进程要求的版本高于可用的rtld-debugger 接口版本,则会返回RD_NOCAPAB。
rd_new()
此函数可创建新的导出接口代理。
rd_agent_t * rd_new(struct ps_prochandle * php);
php 是控制进程所创建的cookie,用于标识目标进程。此cookie 供控制进程提供的导入接
口用于维护上下文,并且对于rtld-debugger 接口是不透明的。
rd_reset()
此函数可基于为rd_new() 提供的相同ps_prochandle 结构重置代理内的信息。
rd_err_e rd_reset(struct rd_agent * rdap);
此函数在重新启动目标进程时调用。
rd_delete()
此函数可删除代理并释放与其关联的任何状态。
void rd_delete(struct rd_agent * rdap);
错误处理
rtld-debugger 接口(在rtld_db.h 中定义)可以返回以下错误状态:
typedef enum {
RD_ERR,
运行时链接程序调试器接口
190 链接程序和库指南• 2006 年10 月
RD_OK,
RD_NOCAPAB,
RD_DBERR,
RD_NOBASE,
RD_NODYNAM,
RD_NOMAPS
} rd_err_e;
以下接口可用于收集错误信息。
rd_errstr()
此函数可返回说明错误代码rderr 的描述性错误字符串。
char * rd_errstr(rd_err_e rderr);
rd_log()
此函数可启用(1) 或禁用(0) 日志记录。
void rd_log(const int onoff);
启用日志记录时,会使用更多详细诊断信息来调用控制进程所提供的导入接口函数
ps_plog()。
扫描可装入目标文件
可以获取运行时链接程序中维护的每个目标文件的信息。通过使用rtld_db.h 中定义的以下
结构,可实现链接映射:
typedef struct rd_loadobj {
psaddr_t rl_nameaddr;
unsigned rl_flags;
psaddr_t rl_base;
psaddr_t rl_data_base;
unsigned rl_lmident;
psaddr_t rl_refnameaddr;
psaddr_t rl_plt_base;
运行时链接程序调试器接口
第6 章• 支持接口191
unsigned rl_plt_size;
psaddr_t rl_bend;
psaddr_t rl_padstart;
psaddr_t rl_padend;
psaddt_t rl_dynamic;
} rd_loadobj_t;
请注意,在此结构中提供的所有地址(包括字符串指针)都是目标进程中的地址,而不是
控制进程本身的地址空间中的地址。
rl_nameaddr
指向包含动态库名称的字符串的指针。
rl_flags
在修订版RD_VERSION2 中,使用RD_FLG_MEM_OBJECT 标识动态装入的可重定位目标文件。
rl_base
动态库的基本地址。
rl_data_base
动态库数据段的基本地址。
rl_lmident
链接映射标识符(请参见第178 页中的“建立名称空间”)。
rl_refnameaddr
如果动态库是标准过滤器,则指向filtee 的名称。
rl_plt_base、rl_plt_size
提供这些元素是为了向下兼容,当前未使用。
rl_bend
目标文件的结束地址(text + data + bss)。在修订版RD_VERSION2 中,动态装入的可重定
位目标文件将导致此元素指向创建的目标文件(包括其节标题)的结尾。
rl_padstart
动态库之前填充的基本地址(请参阅第198 页中的“动态库填充”)。
rl_padend
动态库之后填充的基本地址(请参阅第198 页中的“动态库填充”)。
rl_dynamic
添加了RD_VERSION2 的此字段可提供目标文件动态节的基本地址,从而可允许引用
DT_CHECKSUM 之类的项(请参见表7–32)。
rd_loadobj_iter() 例程使用此目标文件数据结构来访问运行时链接程序的链接映射列表中
的信息:
运行时链接程序调试器接口
192 链接程序和库指南• 2006 年10 月
rd_loadobj_iter()
会对当前在目标进程中装入的所有动态库重复执行此函数。
typedef int rl_iter_f(const rd_loadobj_t *, void *);
rd_err_e rd_loadobj_iter(rd_agent_t * rap, rl_iter_f * cb,
void * clnt_data);
每次重复时都会调用cb 指定的导入函数。可以使用clnt_data 将数据传递给cb 调用。通过
指向可变(已分配的栈)rd_loadobj_t 结构的指针可返回有关每个目标文件的信息。
cb 例程中的返回代码通过rd_loadobj_iter() 进行检查,并具有以下含义:
 1-继续处理链接映射。
 0-停止处理链接映射并将控制权返回给控制进程。
rd_loadobj_iter() 运行成功时会返回RD_OK。返回RD_NOMAPS 表示运行时链接程序尚未
装入初始链接映射。
事件通知
控制进程可以跟踪运行时链接程序范围内发生的特定事件。这些事件包括:
RD_PREINIT
运行时链接程序已经装入并重定位所有动态库,并且即将开始调用每个装入的目标文件
的.init 节。
RD_POSTINIT
运行时链接程序已经完成调用所有的.init 节,并且即将会将控制权转交给主可执行文
件。
RD_DLACTIVITY
已经调用运行时链接程序来装入或卸载动态库。
可以使用sys/link.h 和rtld_db.h 中定义的以下接口来监视这些事件:
typedef enum {
RD_NONE = 0,
RD_PREINIT,
RD_POSTINIT,
RD_DLACTIVITY
} rd_event_e;
运行时链接程序调试器接口
第6 章• 支持接口193
/*
* Ways that the event notification can take place:
*/
typedef enum {
RD_NOTIFY_BPT,
RD_NOTIFY_AUTOBPT,
RD_NOTIFY_SYSCALL
} rd_notify_e;
/*
* Information on ways that the event notification can take place:
*/
typedef struct rd_notify {
rd_notify_e type;
union {
psaddr_t bptaddr;
long syscallno;
} u;
} rd_notify_t;
以下函数可跟踪事件:
rd_event_enable()
此函数可启用(1) 或禁用(0) 事件监视。
rd_err_e rd_event_enable(struct rd_agent * rdap, int onoff);
运行时链接程序调试器接口
194 链接程序和库指南• 2006 年10 月
注– 目前,由于性能原因,运行时链接程序会忽略事件禁用。控制进程应假定可以访问指
定的断点,因为最后调用了此例程。
rd_event_addr()
此函数可指定如何通知控制程序指定的事件。
rd_err_e rd_event_addr(rd_agent_t * rdap, rd_event_e event,
rd_notify_t * notify);
根据事件类型,通过调用notify->u.syscallno 标识的运行正常的低成本系统调用或者在
notify->u.bptaddr 指定的地址执行断点可实现控制进程通知。控制进程负责跟踪系统调
用或定位实际断点。
事件发生后,可以通过rtld_db.h 中定义的此接口获取其他信息:
typedef enum {
RD_NOSTATE = 0,
RD_CONSISTENT,
RD_ADD,
RD_DELETE
} rd_state_e;
typedef struct rd_event_msg {
rd_event_e type;
union {
rd_state_e state;
} u;
} rd_event_msg_t;
rd_state_e 值包括:
RD_NOSTATE
没有其他可用的状态信息。
RD_CONSISTANT
链接映射处于稳定状态,可以对其进行检查。
运行时链接程序调试器接口
第6 章• 支持接口195
RD_ADD
正在装入动态库,链接映射未处于稳定状态。应该在达到RD_CONSISTANT 状态之后再检查
这些链接映射。
RD_DELETE
正在删除动态库,链接映射未处于稳定状态。应该在达到RD_CONSISTANT 状态之后再检查
这些链接映射。
rd_event_getmsg() 函数用于获取此事件状态信息。
rd_event_getmsg()
此函数可提供有关事件的其他信息。
rd_err_e rd_event_getmsg(struct rd_agent * rdap, rd_event_msg_t * msg);
下表显示了各种不同事件类型的可能状态。
RD_PREINIT RD_POSTINIT RD_DLACTIVITY
RD_NOSTATE RD_NOSTATE RD_CONSISTANT
RD_ADD
RD_DELETE
跳过过程链接表
通过使用rtld-debugger 接口,控制进程可以跳过过程链接表项。第一次要求控制进程(如
调试器)步入(step into) 函数时,通过过程链接表处理可将控制权传递给运行时链接程序以
搜索函数定义。
通过使用以下接口,控制进程可以跳过运行时链接程序的过程链接表处理。控制进程可以
基于ELF 文件中提供的外部信息来确定何时遇到过程链接表项。
目标进程步入过程链接表项之后,便会调用rd_plt_resolution() 接口:
rd_plt_resolution()
此函数可返回当前过程链接表项的解析状态以及有关如何跳过此状态的信息。
rd_err_e rd_plt_resolution(rd_agent_t * rdap, paddr_t pc,
lwpid_t lwpid, paddr_t plt_base, rd_plt_info_t * rpi);
pc 表示过程链接表项的第一条指令。lwpid 提供lwp 标识符,plt_base 提供过程链接表的
基本地址。这三个变量提供的信息足以供多个体系结构用于处理过程链接表。
rpi 提供有关以下数据结构(在rtld_db.h 中定义)中定义的过程链接表项的详细信息:
typedef enum {
运行时链接程序调试器接口
196 链接程序和库指南• 2006 年10 月
RD_RESOLVE_NONE,
RD_RESOLVE_STEP,
RD_RESOLVE_TARGET,
RD_RESOLVE_TARGET_STEP
} rd_skip_e;
typedef struct rd_plt_info {
rd_skip_e pi_skip_method;
long pi_nstep;
psaddr_t pi_target;
psaddr_t pi_baddr;
unsigned int pi_flags;
} rd_plt_info_t;
#define RD_FLG_PI_PLTBOUND 0x0001
rd_plt_info_t 结构的元素包括:
pi_skip_method
标识遍历过程链接表项的方法。此方法可设置为rd_skip_e 值之一。
pi_nstep
标识返回RD_RESOLVE_STEP 或RD_RESOLVE_TARGET_STEP 时跳过的指令数。
pi_target
指定返回RD_RESOLVE_TARGET_STEP 或RD_RESOLVE_TARGET 时设置断点的地址。
pi_baddr
添加了RD_VERSION3 的过程链接表的目标地址。设置pi_flags 字段的
RD_FLG_PI_PLTBOUND 标志之后,此元素可标识已解析(绑定)的目标地址。
pi_flags
添加了RD_VERSION3 的标志字段。标志RD_FLG_PI_PLTBOUND 可将过程链接项标识为已解
析(绑定)到其目标地址,此地址可用于pi_baddr 字段。
rd_plt_info_t 返回值表明了以下可能的情况:
运行时链接程序调试器接口
第6 章• 支持接口197
 必须由运行时链接程序解析通过此过程链接表进行的首次调用。在这种情况下,
rd_plt_info_t 包含以下内容:
{RD_RESOLVE_TARGET_STEP, M, , 0, 0}
控制进程会在BREAK 处设置断点,从而使目标进程继续运行。到达断点时,即会完成过
程链接表项处理。然后,控制进程可以将M 条指令转到目标函数。请注意,由于这是通
过过程链接表项进行的首次调用,因此尚未设置绑定地址(pi_baddr)。
 通过此过程链接表第N 次进行调用时,rd_plt_info_t 会包含以下内容:
{RD_RESOLVE_STEP, M, 0, , RD_FLG_PI_PLTBOUND}
过程链接表项已经过解析,并且控制进程可以将M 条指令转到目标函数。过程链接表项
绑定到的地址为,并且已在标志字段中设置了RD_FLG_PI_PLTBOUND 位。
动态库填充
运行时链接程序的缺省行为取决于要装入动态库的操作系统(可以在其中最有效地引用这
些目标文件)。如果能够对装入目标进程内存的目标文件执行填充,有些控制进程会从中
受益。控制进程可以使用此接口请求此填充。
rd_objpad_enable()
此函数可启用或禁用对目标进程的任何随后装入的目标文件的填充。可以在装入目标文
件的两端进行填充。
rd_err_e rd_objpad_enable(struct rd_agent * rdap, size_t padsize);
padsize 指定将任何目标文件装入内存前后要保留的填充大小(以字节为单位)。使用具
有PROT_NONE 权限和MAP_NORESERVE 标志的mmap(2) 可将填充保留为内存映射。实际上,
运行时链接程序可保留与任何装入目标文件相邻的目标进程虚拟地址空间区域。控制进
程随后可以利用这些空间区域。
如果padsize 为0,则对于后续目标文件将禁用目标文件填充。
注– 通过使用proc(1) 工具并引用rd_loadobj_t 中提供的链接映射信息,可报告使用mmap(2)
从具有MAP_NORESERVE 标志的/dev/zero 中获取的预留空间。
调试器导入接口
控制进程必须提供给librtld_db.so.1 的导入接口在/usr/include/proc_service.h 中定
义。可以在rdb 演示调试器中找到这些proc_service 函数的实现样例。rtld-debugger 接口仅
使用一部分可用的proc_service 接口。将来版本的rtld-debugger 接口可能会利用其他
proc_service 接口,而不会创建不兼容的更改。
当前rtld-debugger 接口会使用以下接口:
运行时链接程序调试器接口
198 链接程序和库指南• 2006 年10 月
ps_pauxv()
此函数可返回指向auxv 向量副本的指针。
ps_err_e ps_pauxv(const struct ps_prochandle * ph, auxv_t ** aux);
由于auxv 向量信息会复制到已分配的结构,因此只要ps_prochandle 有效,便会保留指
针。
ps_pread()
此函数可从目标进程中读取数据。
ps_err_e ps_pread(const struct ps_prochandle * ph, paddr_t addr,
char * buf, int size);
将size 字节从目标进程中的地址addr 复制到buf。
ps_pwrite()
此函数可将数据写入目标进程。
ps_err_e ps_pwrite(const struct ps_prochandle * ph, paddr_t addr,
char * buf, int size);
将size 字节从buf 复制到目标进程的地址addr。
ps_plog()
此函数通过rtld-debugger 接口中的其他诊断信息调用。
void ps_plog(const char * fmt, ...);
控制进程会确定在何处或者是否记录此诊断信息。ps_plog() 的参数采用printf(3C) 格
式。
ps_pglobal_lookup()
此函数可在目标进程中搜索符号。
ps_err_e ps_pglobal_lookup(const struct ps_prochandle * ph,
const char * obj, const char * name, ulong_t * sym_addr);
在目标进程ph 的名为obj 的目标文件内搜索名为name 的符号。如果找到此符号,则将符
号地址存储在sym_addr 中。
ps_pglobal_sym()
此函数可在目标进程中搜索符号。
ps_err_e ps_pglobal_sym(const struct ps_prochandle * ph,
const char * obj, const char * name, ps_sym_t * sym_desc);
运行时链接程序调试器接口
第6 章• 支持接口199
在目标进程ph 的名为obj 的目标文件内搜索名为name 的符号。如果找到此符号,则将符
号描述符存储在sym_desc 中。
如果在创建任何链接映射之前, rtld-debugger 接口需要在应用程序或运行时链接程序内查
找符号,则可以使用obj 的以下保留值:
#define PS_OBJ_EXEC ((const char *)0x0) /* application id */
#define PS_OBJ_LDSO ((const char *)0x1) /* runtime linker id */
控制进程可以使用以下伪代码将procfs 文件系统用于这些目标文件:
ioctl(.., PIOCNAUXV, ...) - obtain AUX vectors
ldsoaddr = auxv[AT_BASE];
ldsofd = ioctl(..., PIOCOPENM, &ldsoaddr);
/* process elf information found in ldsofd ... */
execfd = ioctl(.., PIOCOPENM, 0);
/* process elf information found in execfd ... */
找到文件描述符之后,控制程序即可检查ELF 文件来查找其符号信息。
运行时链接程序调试器接口
200 链接程序和库指南• 2006 年10 月
目标文件格式
本章介绍由汇编程序和链接编辑器生成的目标文件的可执行链接格式(excutable and linking
format, ELF)。其中存在三种重要类型的目标文件。
 可重定位目标文件包含代码部分和数据部分。此文件适合与其他可重定位目标文件链
接,从而创建动态可执行文件、共享库文件或其他可重定位目标文件。
 动态可执行文件包含可随时执行的程序。此文件指定了exec(2) 创建程序的进程映像的
方式。此文件通常在运行时绑定到共享库文件以创建进程映像。
 共享库文件包含适用于进行其他链接的代码和数据。链接编辑器可将此文件与其他可重
定位目标文件和共享库文件一起处理,以创建其他目标文件。运行时链接程序会将此文
件与动态可执行文件和其他共享库文件合并,以创建进程映像。
本章的第一节,第201 页中的“文件格式”,重点介绍目标文件的格式以及格式如何与创
建程序相关。第二节,第263 页中的“动态链接”,重点介绍格式如何与装入程序相关。
程序可以使用ELF 访问库libelf 提供的函数来处理目标文件。有关libelf 内容的说明,请
参阅elf(3ELF)。/usr/demo/ELF 目录下的SUNWosdem 软件包中提供了使用libelf 的源代码
样例。
文件格式
目标文件既可用于程序链接,也可用于程序执行。为了方便和提高效率,目标文件格式提
供了文件内容的平行视图,以便反映这些活动的不同需要。下图显示了目标文件的结构。
7第7 章
201
图7–1目标文件格式
ELF 头位于目标文件的起始位置,其中包含用于说明文件结构的指南。
注– 仅有ELF 头在文件中具有固定位置。由于ELF 格式具有灵活性,因此不要求头表、节或
段具有指定的顺序。但是,此图是Solaris 中使用的典型布局。
节表示ELF 文件中可以处理的最小不可分割单位。段是节的集合。段表示可由exec(2) 或运
行时链接程序映射到内存映像的最小独立单位。
节包含链接视图的批量目标文件信息。此数据包括指令、数据、符号表和重定位信息。本
章的第一部分提供了各节的说明。本章的第二部分讨论了各段以及文件的程序执行视图。
程序头表(如果存在)指示系统如何创建进程映像。用于生成进程映像、可执行文件和共
享库的文件必须具有程序头表。可重定位目标文件无需程序头表。
节头表包含说明文件各节的信息。每节在表中有一个与之对应的项。每一项都指定了节名
和节大小之类的信息。链接编辑过程中使用的文件必须具有节头表。
数据表示形式
目标文件格式支持8 位字节、32 位体系结构和64 位体系结构的各种处理器。不过,数据表
示形式最好可扩展为更大或更小的体系结构。表7–1 和表7–2 列出了32 位数据类型和64 位
数据类型。
文件格式
202 链接程序和库指南• 2006 年10 月
目标文件表示格式与计算机无关的一些控制数据。此格式可提供目标文件的通用标识和解
释。目标文件中的其余数据使用目标处理器的编码,无论在什么计算机上创建该文件都是
如此。
表7–1 ELF 32 位数据类型
名称大小对齐用途
Elf32_Addr 4 4 无符号程序地址
Elf32_Half 2 2 无符号中整数
Elf32_Off 4 4 无符号文件偏移
Elf32_Sword 4 4 带符号整数
Elf32_Word 4 4 无符号整数
unsigned char 1 1 无符号小整数
表7–2 ELF 64 位数据类型
名称大小对齐目的
Elf64_Addr 8 8 无符号程序地址
Elf64_Half 2 2 无符号中整数
Elf64_Off 8 8 无符号文件偏移
Elf64_Sword 4 4 带符号整数
Elf64_Word 4 4 无符号整数
Elf64_Xword 8 8 无符号长整数
Elf64_Sxword 8 8 带符号长整数
unsigned char 1 1 无符号小整数
目标文件格式定义的所有数据结构都遵循相关类别的自然大小和对齐规则。数据结构可以
包含显式填充,以确保4 字节目标文件的4 字节对齐,从而强制结构大小为4 的倍数,依此
类推。数据在文件的开头也会适当对齐。例如,包含Elf32_Addr 成员的结构在文件中与4
字节边界对齐。同样,包含Elf64_Addr 成员的结构与8 字节边界对齐。
注– 为便于移植,ELF 不使用位字段。
 
 
 
以上文章转自于 : http://developers.sun.com.cn/
阅读(413) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~