动态库为什么不能运行

so运行的方法
在我之前的印象中,so文件是不能直接运行的,但是ld.so改变了我的这种偏见。执行一下动态链接库文件,执行结果为
tsecer@harry :/lib/ld-linux.so.2 
Usage: ld.so [OPTION]... EXECUTABLE-FILE [ARGS-FOR-PROGRAM...]
甚至libc.so也是可以直接运行的:
tsecer@harry :/lib/libc.so.6 
GNU C Library stable release version 2.4 (20101025), by Roland McGrath et al.
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.
There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A
PARTICULAR PURPOSE.
为什么通常的so不能运行
在so文件中,它使用到的一些全局变量及外部函数的位置在so中是没有确定和初始化的,这个工作是由动态链接器来完成。直接运行so文件,当执行到这些没有定义的外部符号时,就会出现访问错误。
 
任何可执行的代码,如果要运行就要有入口,虽然大部分情况下没有指定,但是这个入口就是gcc在生成so文件时自动添加的_start函数,这个符号的定义位于crt1.o中,在调用gcc时由gcc自动添加,当然还添加了crti,crtn,crtstart crtend等。对于so文件,gcc并不会指定crt1文件,在所有的输入中也就没有了入口位置的指定,在默认情况下,链接器使用整个代码段(.text)段的开始位置作为整个so文件的入口
tsecer@harry :readelf -a /lib/libnss_files.so.2  | grep "text\|Entry" 
  Entry point address:               0x1c30
  [13] .text             PROGBITS        00001c30 001c30 005e34 00  AX  0   0 16
   02     .note.ABI-tag .note.SuSE .hash .dynsym .dynstr .gnu.version .gnu.version_d .gnu.version_r .rel.dyn .rel.plt .init .plt .text .fini .rodata .interp .eh_frame_hdr .eh_frame 
执行一个so文件,可以生成core文件,但是ip位置值为1,此时自然而然是尝试调试一下,由于已经知道入口地址,所以在此处设置断点:
(gdb) b call_gmon_start
Breakpoint 1 at 0x1c34
(gdb) r
Starting program: /lib/libnss_files.so.2 
(no debugging symbols found)
(no debugging symbols found)
Warning:
Cannot insert breakpoint 1.
Error accessing memory address 0x1c34: Input/output error.
 
(gdb) info prog
        Using the running image of child process 419.
Program stopped at 0xb7f559f1.
(gdb) shell pmap 419
419: libnss_files.so
START       SIZE     RSS   DIRTY PERM MAPPING
80000000     36K      8K      0K r-xp /lib/libnss_files-2.4.so
80009000      8K      8K      8K rw-p /lib/libnss_files-2.4.so
内核中对于so文件执行时做了特殊处理,so文件的位置不是在so文件声明的位置,而是一个特殊的0x80000000位置,看一下内核的代码
linux-2.6.21\binfmt_elf.c:static int load_elf_binary(struct linux_binprm *bprm, struct pt_regs *regs)
if (loc->elf_ex.e_type == ET_EXEC || load_addr_set) {
elf_flags |= MAP_FIXED;
} else if (loc->elf_ex.e_type == ET_DYN) {
/* Try and get dynamic programs out of the way of the
 * default mmap base, as well as whatever program they
 * might try to exec.  This is because the brk will
 * follow the loader, and is not movable.  */
load_bias = ELF_PAGESTART(ELF_ET_DYN_BASE - vaddr);
}
 
linux-2.6.21\include\asm-i386\elf.h
/* This is the location that an ET_DYN program is loaded if exec'ed.  Typical
   use of this is to invoke "./ld.so someprog" to test out a new version of
   the loader.  We need to make sure that it is out of the way of the program
   that it will "exec", and that there is sufficient room for the brk.  */
 
#define ELF_ET_DYN_BASE         (TASK_SIZE / 3 * 2)
所以断点应该设置在在这个偏移量基础上的地址
(gdb) b *0x80001c34
Breakpoint 1 at 0x80001c34
(gdb) r
Starting program: /lib/libnss_files.so.2 
(no debugging symbols found)
(no debugging symbols found)
 
Breakpoint 1, 0x80001c34 in ?? ()
(gdb) x/100i 0x80001c34
0x80001c34:     sub    $0x4,%esp
0x80001c37:     call   0x80001c3c
0x80001c3c:     pop    %ebx
0x80001c3d:     add    $0x83b8,%ebx
0x80001c43:     mov    0xfffffffc(%ebx),%edx
0x80001c49:     test   %edx,%edx
0x80001c4b:     je     0x80001c52
0x80001c4d:     call   0x80001c04
0x80001c52:     pop    %eax
0x80001c53:     pop    %ebx
0x80001c54:     leave  
0x80001c55:     ret    
(gdb) si
0x80001c37 in ?? ()
(gdb) 
0x80001c3c in ?? ()
(gdb) 
0x80001c3d in ?? ()
(gdb) 
0x80001c43 in ?? ()
(gdb) 
0x80001c49 in ?? ()
(gdb) 
0x80001c4b in ?? ()
(gdb) 
0x80001c52 in ?? ()
(gdb) 
0x80001c53 in ?? ()
(gdb) 
0x80001c54 in ?? ()
(gdb) 
0x80001c55 in ?? ()
(gdb) 
0x00000001 in ?? ()
(gdb) 
 
Program received signal SIGSEGV, Segmentation fault.
0x00000001 in ?? ()
(gdb) info reg
eax            0xbfbc7f70       -1078165648
ecx            0xbfbc7f74       -1078165644
edx            0x0      0
ebx            0xb7f4bff4       -1208696844
esp            0xbfbc7f74       0xbfbc7f74
ebp            0x0      0x0
esi            0xbfbc7f7c       -1078165636
edi            0x80001c30       -2147476432
eip            0x1      0x1
eflags         0x210246 [ PF ZF IF RF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb) 
为什么会core在IP地址为1的位置处?
操作系统将控制权转移给用户态进程之后,最开始的EIP指针设置在entry位置,对于我们的例子,这个绝对位置就是0x80001c34。内核做的不仅仅是这么简单,而是还在堆栈上放了一些其它的信息,那就是标准main函数的参数(int argc, char * argv[], char * evnp[]),根据C函数调用的规则,第一个参数在栈顶的最开始位置。这个位置本来应该是函数的返回地址,也就是EIP位置,由于内核直接将EIP放在了这个位置,之上放入堆栈信息,所以在该函数ret时从堆栈中取到了参数个数变量,这也就是core中现场显示IP位置为1的原因。正常的可执行程序没有这个问题的原因要归结与CRT库做的各种处理,由于暂时对于我们理解问题没有帮助,所以不再详细说明。
使用ld.so来加载so文件为什么会core掉?
tsecer@harry :cat demo.c 
#include <stdio.h>
 
void greet() __attribute__((constructor));
void greet()
{
        printf("%s %s %d\n", __FILE__, __func__, __LINE__);
}
void farewell() __attribute__((destructor));
void farewell()
{
        printf("%s %s %d\n", __FILE__, __func__, __LINE__);
}
tsecer@harry :gcc -shared -o libdemo.so demo.c -g
tsecer@harry :/lib/ld-linux.so.2 ./libdemo.so 
demo.c greet 6
Segmentation fault (core dumped)
tsecer@harry :
这次主动让动态链接器来完成可能存在的外部符号,这里主要的就是printf函数,从这个运行结果可以看出来,构造函数中的内容被打印出来,但是析构函数没有打印出来。
tsecer@harry :readelf -a libdemo.so | grep "480\|Entry"
  Entry point address:               0x480
  [10] .text             PROGBITS        00000480 000480 000164 00  AX  0   0 16
     2: 00000480     0 SECTION LOCAL  DEFAULT   10 
    10: 00000480     0 SECTION LOCAL  DEFAULT   10 
    36: 00000480     0 FUNC    LOCAL  DEFAULT   10 call_gmon_start
动态链接库的入口位于
glibc-2.6\sysdeps\i386\dl-machine.h
_start:\n\
# Note that _dl_start gets the parameter in %eax.\n\
movl %esp, %eax\n\
call _dl_start\n\
_dl_start_user:\n\
……
xorl %ebp, %ebp\n\
# Call the function to run the initializers.\n\
call _dl_init_internal@PLT\n\这里执行so及所有依赖内的初始化函数,其中就包括了例子中的greet例子。
# Pass our finalizer function to the user in %edx, as per ELF ABI.\n\
leal _dl_fini@GOTOFF(%ebx), %edx\n\ 析构函数通过edx指针传递给entry函数,
# Restore %esp _start expects.\n\
movl (%esp), %esp\n\
# Jump to the user's entry point.\n\
jmp *%edi\n\这个位置就是elf文件中描述的entry位置,so文件中的所有外部引用已经可用。执行jmp后和执行执行so的效果相同,也就是core在EIP为1的位置处。
.previous\n\
为什么ld-so可以安全运行?
首先这个工具本身就是用来做动态连接的,本身就是需要在自己被加载位置不确定的情况下消除位置相关代码,这就是通常所有的“自举”(bootstrap)。从技术上来说,ld-so在启动的时候,只使用堆栈变量、静态变量及局部函数,这些组件在访问时都是位置无关的,如果需要申请成片的空间,它使用alloca函数来完成。
在ld.so被默认启动时它如何知道自己的动态加载位置及待定位文件位置:
这个就是通过不起眼的AUXV数组:
(gdb) shell hexdump -C /proc/self/auxv 
00000000  20 00 00 00 00 e4 ff bf  21 00 00 00 00 e0 ff bf  | .......!.......|
00000010  10 00 00 00 75 8b c9 1f  06 00 00 00 00 10 00 00  |....u...........|
00000020  11 00 00 00 64 00 00 00  03 00 00 00 34 80 04 08  |....d.......4...|
00000030  04 00 00 00 20 00 00 00  05 00 00 00 08 00 00 00  |.... ...........|
内核对于该数组的填充为:
linux-2.6.21\fs\binfmt_elf.c:create_elf_tables
NEW_AUX_ENT(AT_BASE, interp_load_addr);
NEW_AUX_ENT(AT_PHDR, load_addr + exec->e_phoff);
NEW_AUX_ENT(AT_PHENT, sizeof(struct elf_phdr));
NEW_AUX_ENT(AT_PHNUM, exec->e_phnum);
NEW_AUX_ENT(AT_ENTRY, exec->e_entry);
这里最为关键的就是这这些,interp_load_addr表示了被加载的位置。这个数组通过<属性,数值>的数组形式放在envp的后面,所以只通过堆栈就可以得到这个位置,这种方法和elf文件的动态节中各个动态TAG的表示方法非常类似,优点就是扩展性非常好,确定是效率可能不高。当ld.so确定了自己的动态位置之后,就可以进而自己静态位置和动态位置差值获得自己的ProgramHeader的位置,之后是动态节,待定位信息等,从而完成对自己的动态链接。再通过AT_PHDR来获得待动态链接文件信息并最终完成动态链接信息,之后控制权传递给AT_ENTRY。
libc为什么可以运行?
libc中它的入口使用链接器选项指定了入口,并且特殊处理和该入口中使用的函数,将其中的函数通过链接时version-script脚本将它所有调用的函数均设置为local(也就是static函数),从而入口函数调用时均没有经过PLT,而是直接通过和调用者(caller)和被调用者之间的偏移量来确定。
如何让so可以运行并显示出正确结果:
显然,需要在生成的so中加入interp节,从而在so运行时执行先把控制全给动态链接库。
ld.so 的man手册中对于该选项的说明,其中说明了只有在生成可执行文件时该字段有意义,反过来就是说生成so时添加了也没有效果
       --dynamic-linker file
           Set the name of the dynamic linker.  This is  only  meaningful  when  generating
           dynamically linked ELF executables.  The default dynamic linker is normally cor-
           rect; don't use this unless you know what you are doing.
通过搜素可以知道,可以在代码中指定interp节,__attribute__((section(".interp"))) = "/lib64/ld-linux-x86-64.so.2";
ld源代码中对于该属性的说明elf.c:binutils-2.14\bfd\map_sections_to_segments (abfd)
  /* If we have a .interp section, then create a PT_PHDR segment for
     the program headers and a PT_INTERP segment for the .interp
     section.  */
  s = bfd_get_section_by_name (abfd, ".interp");
 
一个可运行的例子:
secer@harry :cat myentry.c 
extern const char szIntpreter[] __attribute__((section(".interp"))) = "/lib/ld-linux.so.2";
 
#include <stdio.h>
 
int version()
{
        printf("version %s\n", "0.11");
        _exit(0);
        return 0;
}
tsecer@harry :gcc -shared -o libmyentry.so myentry.c -Wl,-e,version 
myentry.c:1: warning: 'szIntpreter' initialized and declared 'extern'
myentry.c: In function 'version':
myentry.c:8: warning: incompatible implicit declaration of built-in function '_exit'
tsecer@harry :./libmyentry.so 
version 0.11
tsecer@harry :
在可执行文件的依赖so加载时如何设置so内断点?
这些就是通常的延迟断点,但是在gdb一个可执行文件时,此时动态链接库ld.so还没有加载,而延迟库需要依赖在ld.so中的_dl_debug_state,在ld.so没有加载在内存中时如何设置断点。
内核态不仅在子进程fork之后通知父进程,而且在exec之后还会通知父进程,通知位置为load_elf_binary()
if (unlikely(current->ptrace & PT_PTRACED)) {
if (current->ptrace & PT_TRACE_EXEC)
ptrace_notify ((PTRACE_EVENT_EXEC << 8) | SIGTRAP);
else
send_sig(SIGTRAP, current, 0);
}
此时动态链接库和可执行文件均已加载入内存。此时的问题就是找到_dl_debug_state的位置,从gdb的代码及glibc的注释可以知道,gdb直接搜索了可执行文件的动态连接节,从中读到动态链接库位置,然后从中直接找到_dl_debug_state函数位置,glibc中的注释
/* This function exists solely to have a breakpoint set on it by the
   debugger.  The debugger is supposed to find this function's address by
   examining the r_brk member of struct r_debug, but GDB 4.15 in fact looks
   for this particular symbol name in the PT_INTERP file.  */
void
_dl_debug_state (void)
{
}
core文件如何找到DT_DEBUG位置
这个位置在core文件的动态节中被动态链接器初始化为自己的struct r_debug _r_debug;变量位置,但是可执行文件中的该值为零。gdb执行的方法依然是通过之前说的AUXV数组找到动态entry位置,结合可执行文件的entry位置计算出加载和期望位置偏移,进而计算出DT_DEBUG位置,从中读出动态so列表运行时位置。
windows如何设置版本信息
这些文件包含了windows系统DLL文件及可执行文件的"属性-->>详细信息"中描述的"产品名称","文件版本"等信息。通过dumpbin可以看到包含了版本信息的windows可执行文件中包含了rscr节,其中包含了通过rc生成的res文件,而这些信息可以通过resource文件中的VERSIONINOF描述。文档说明

VERSIONINFO resource

 

Defines a version-information resource. The resource contains such information about the file as its version number, its intended operating system, and its original filename. The resource is intended to be used with theVersion Information functions.

There are two ways to format a VERSIONINFO statement:

 
 
versionID VERSIONINFO fixed-info  { block-statement . . . }

- or -

 
 
versionID VERSIONINFO 
fixed-info
BEGIN
block-statement
. . .
END
  
 

posted on 2019-03-07 09:41  tsecer  阅读(410)  评论(0编辑  收藏  举报

导航