EMACS & 程序 编程点滴...

天下难事必作于易,天下大事必作于细

Lastupdated: 2011-01-02

BINARY&HACKS

TOPobjdump

显示文件符号,头信息
1
$ objdump -p hello
利用 checkstack.pl 显示函数堆栈使用量
1
% objdump -d foo.o | perl checkstack.pl
显示汇编
1
% objdump -Sl xxxx

TOPhexdump

显示二进制信息

1
2
3
4
$ hexdump -C hello | head -n 3
00000000  4d 5a 90 00 03 00 00 00  04 00 00 00 ff ff 00 00  |MZ..............|
00000010  b8 00 00 00 00 00 00 00  40 00 00 00 00 00 00 00  |........@.......|
00000020  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|

TOPod

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# 二进制表示,并且加上 0x前缀
$od -tx1 -An file > temp | xargs sed 's/ / 0x/g' temp
$od -tx1 -An file | sed -e 's/ / 0x/g'

# 二进制文件直接dump为c的数组
#!/bin/sh
# $O objname < in > out
objname=${1:-objname}
od -tx1 -v -An | sed -e '1i\
const unsigned char '$objname'[] = {
s/\([0-9a-f][0-9a-f]\) */0x\1,/g
$s/,$//
$a\
};
'


# 指定位置的dump
$ od --skip-bytes 0x4a0 --read-bytes 0xb0 -tx1z /bin/ls.exe
0002240 08 31 db e8 38 79 01 00 8b 40 08 89 44 24 04 8b  >.1..8y...@..D$..<
0002260 45 f0 89 04 24 e8 36 79 01 00 3b 5d ec 73 1a 90  >E...$.6y..;].s..<
0002300 8b 04 9f 43 c7 04 24 bf a7 41 00 89 44 24 04 e8  >...C..$..A..D$..<
0002320 3c 79 01 00 3b 5d ec 72 e7 c7 04 24 0a 00 00 00  ><y..;].r...$....<
0002340 e8 eb 78 01 00 8b 5d f4 8b 75 f8 8b 7d fc 89 ec  >..x...]..u..}...<
0002360 5d c3 80 4a 28 02 eb 82 90 8d b4 26 00 00 00 00  >]..J(......&....<
0002400 55 89 e5 83 ec 18 8b 45 08 8b 4d 0c 8b 50 04 8b  >U......E..M..P..<
0002420 00 89 4c 24 08 31 c9 89 4c 24 0c 89 04 24 89 54  >..L$.1..L$...$.T<
0002440 24 04 e8 19 6e 01 00 c9 c3 8d b4 26 00 00 00 00  >$...n......&....<
0002460 55 89 e5 83 ec 10 89 7d fc 8b 45 08 8b 7d 0c 89  >U......}..E..}..<
0002500 75 f8 89 5d f4 8b 58 04 c6 45 f3 00 8b 08 8b 57  >u..]..X..E.....W<
0002520

TOPnm

确认对象文件中的符号信息(函数,参数)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ nm ./test.o
00000000 b .bss
00000000 d .data
00000000 r .rdata
00000000 t .text
00000026 T __Z7bigrandv
00000060 T __Z7randintii
00000000 T __Z8functionv
         U ___main
         U __alloca
0000008e T _main
         U _printf
         U _rand
         U _srand
         U _time

查看函数符号

1
2
3
4
5
6
7
8
9
10
11
12
# 一般查看name mangling后的符号名
$ nm test.exe | grep print
00404094 I __imp__printf
004011e0 T _printf

# 查看c++ demangle后的名称(使用c++filt)
$ nm test.exe | grep print | c++filt
00404094 I test::printf_p(int,int)

# 查看c++ demangle后的名称
$ nm -C ./test.exe | grep print
00404094 I test::printf_p(int,int)

TOPreadelf

显示Program Headers

1
$ readelf -l hello

显示Section Headers

1
2
3
$ readelf -S hello

$ readelf -x1 hello   ;; 查看section 1的内容

显示符号表 .symtab

1
$ readelf -s hello

TOPldd

检查共享库的依存关系

1
2
3
4
5
6
7
# ldd /sbin/ifconfig
  libc.so.6 => /lib/libc.so.6 (0x4002a000)
  /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

# LD_TRACE_LOADED_OBJECTS=1 /sbin/ifconfig
  libc.so.6 => /lib/libc.so.6 (0x4002a000)
  /lib/ld-linux.so.2 => /lib/ld-linux.so.2 (0x40000000)

TOP符号类型一览

类型 说明
T Text领域
B 没有初始化的变量,BSS区域
D 初始化了的变量,数据段
R 只读领域

TOP去除执行文件中的符号表

只要不是为了debug, 可执行程序中的符号表是没用的,这时

1
$ strip ./test

可以去除符号表,以减小文件尺寸。

TOP调用约定

调用约定 堆栈清除 参数传递
__cdecl1 调用者 从右到左,通过堆栈传递
__stdcall 函数体 从右到左,通过堆栈传递
__fastcall 函数体 从右到左,优先使用寄存器(ECX,EDX),然后使用堆栈
thiscall 函数体 this指针默认通过ECX传递,其它参数从右到左入栈

TOPLoad&Link

查看程序中内存分配,包括共享库的加载地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
$ cat /proc/`pidof bash`/maps
00400000-00479000 rwxs 00401000 28F2:C821 6095153898657536441   /usr/bin/bash.exe
7C920000-7C9B6000 r-xs 7C932C48 28F2:C821 14978073220580836964   /home/Administrator/WINDOWS/system32/ntdll.dll
7C800000-7C91E000 r-xs 7C80B64E 28F2:C821 14000924548591500322   /home/Administrator/WINDOWS/system32/kernel32.dll
61000000-61200000 r--s 61054740 28F2:C821 9096108076149678235   /usr/bin/cygwin1.dll
77DA0000-77E49000 r-xs 77DA710B 28F2:C821 15562079428941540812   /home/Administrator/WINDOWS/system32/ADVAPI32.DLL
77E50000-77EE3000 r-xs 77E5628F 28F2:C821 11036897718844999923   /home/Administrator/WINDOWS/system32/RPCRT4.dll
77FC0000-77FD1000 r-xs 77FC2146 28F2:C821 17563709759038961587   /home/Administrator/WINDOWS/system32/Secur32.dll
6F5C0000-6F5CD000 r-xs 6F5C6058 28F2:C821 6234312161960892206   /usr/bin/cygintl-8.dll
674C0000-675BB000 rw-s 674D7330 28F2:C821 14347854796009985608   /usr/bin/cygiconv-2.dll
703C0000-703EC000 r-xs 703DAFA0 28F2:C821 15537894237328838482   /usr/bin/cygreadline6.dll
6DB80000-6DBB4000 r--s 6DBA0140 28F2:C821 297749119202364961   /usr/bin/cygncurses-9.dll
77D10000-77DA0000 r-xs 77D1B217 28F2:C821 16489187867355692592   /home/Administrator/WINDOWS/system32/USER32.dll
77EF0000-77F39000 r--s 77EF6587 28F2:C821 7868668478446318513   /home/Administrator/WINDOWS/system32/GDI32.dll
76300000-7631D000 rw-p 763012C0 28F2:C821 8266851918880024942   /home/Administrator/WINDOWS/system32/IMM32.DLL
62C20000-62C29000 rw-p 62C22EAD 28F2:C821 2483156568461121901   /home/Administrator/WINDOWS/system32/LPK.DLL
73FA0000-7400B000 r-xs 73FBE439 28F2:C821 5012392988622980855   /home/Administrator/WINDOWS/system32/USP10.dll
71A20000-71A37000 r-xs 71A21273 28F2:C821 12986312149635835406   /home/Administrator/WINDOWS/system32/ws2_32.dll
77BE0000-77C38000 r--s 77BEF2A1 28F2:C821 7858320041035033883   /home/Administrator/WINDOWS/system32/msvcrt.dll
71A10000-71A18000 r-xp 71A11638 28F2:C821 3045534239586333949   /home/Administrator/WINDOWS/system32/WS2HELP.dll
719C0000-719FE000 r-xs 719C14CD 28F2:C821 9490309259784622651   /home/Administrator/WINDOWS/System32/mswsock.dll
76EF0000-76F17000 r-xs 76EFAC82 28F2:C821 15109649782133528855   /home/Administrator/WINDOWS/system32/DNSAPI.dll
76F80000-76F88000 rw-p 76F8115D 28F2:C821 5085935385763659808   /home/Administrator/WINDOWS/System32/winrnr.dll
76F30000-76F5C000 r-xp 76F31130 28F2:C821 11010330288918674307   /home/Administrator/WINDOWS/system32/WLDAP32.dll
64000000-64025000 rw-p 640052C5 28F2:C821 2111095992844428262   /home/Administrator/Program Files/Bonjour/mdnsNSP.dll
76D30000-76D48000 r--s 76D3530A 28F2:C821 4715821055927669147   /home/Administrator/WINDOWS/system32/Iphlpapi.dll
76D70000-76D92000 r-xs 76D71C09 28F2:C821 13198018702692010344   /home/Administrator/WINDOWS/system32/Apphelp.dll

TOP动态连接库的优点

程序编制一般需经编辑、编译、连接、加载和运行几个步骤。由于一些公用代码需要反复使用,就把它们预先编译成目标文件并保存在"库"中。当它与用户程序的目标文件连接时,连接器得从库中选取用户程序需要的代码,然后复制到生成的可执行文件中。这种库称为静态库,其特点是可执行文件中包含了库代码的一份完整拷贝。显然,当静态库被多个程序使用时,磁盘上、内存中都是多份冗余拷贝。

而使用动态连接库就克服了这个缺陷。当它与用户程序的目标文件连接时,连接器只是作上标记,说明程序需要该动态连接库,而不真的把库代码复制到可执行文件中;仅当可执行文件运行时,加载器根据这个标记,检查该库是否已经被其它可执行文件加载进内存。如果已存在于内存中,不用再从磁盘上加载,只要共享内存中已有的代码即可。这样磁盘、内存中始终只有一份代码,较静态库为优。

TOPLinux动态连接库的重要特点:浮动代码

在Windows中,连接生成动态连接库时要指定一个首地址。应用程序运行时,加载器将尽可能把动态连接库装入到该地址;如果地址已被占用,该动态连接库只能被加载到其它地址空间内,这时就要对库中的代码和数据进行修补,或叫做重定位。如此一来,库的多个实例在内存中经过重定位后,彼此将不尽相同,自然不再能共享了。为了避免这个缺陷,Windows自带的库都指定了互不重叠的地址,尽管如此,其它软件厂商的产品仍然不可避免的使用重叠地址,由此部分丧失了使用动态连接库的好处。

在Linux中,为了达到更好的共享性能,使用了与Windows不一样的策略:浮动代码(Position Independent Code,简称PIC)。具体说,使用的转移指令都是相对于当前程序计数器(IP)的偏移量;代码中引用变量、函数的地址都是相对于某个基地址的偏移量。总之,从不引用一个绝对地址。这样,动态连接库无论被加载到什么地址空间,不用修补代码就可以正常工作。既然只有一份代码,就容易实现共享了。

值得指出,此处所指的共享,是指为了节省存储器,多个进程使用动态连接库代码段、只读数据段在内存中的唯一映像;另一种常用的共享定义,是指多个进程对同一段(可能是动态分配的)存储区进行读写,实现进程间通信(IPC)。后一种共享定义与本文无 关。

TOPLinux动态连接库的实现机制:重定位

TOP重定位概述
浮动代码通过重定位操作得以实现。而重定位可以按多种标准进行分类
  • 按发生的地点,可分成对代码段(.text)重定位和对数据段(.data)重定位。
  • 按发生的时间,可分成连接时重定位和加载时重定位(加载时重定位也称为动态重定位)。但这两步并不总是必不可少的。例如,要实现浮动代码就不能对代码段进行动态重定位,这时采取的办法是,把需要动态重定位的项搬到数据段中去,然后在代码段中引用这些项。
  • 按重定位项引用的对象,可分成数据引用和函数引用。如果引用的是静态数据或静态函数,连接器会优化生成的代码,去掉动态重定位项。
  • 从字面上讲, x86体系结构上的Linux使用了多种重定位方式,名字前缀以"R_386_",后面分别接:32、GOT32、PLT32、COPY、GLOB_DAT、JMP_SLOT、RELATIVE、GOTOFF、GOTPC。每种方式都有特定的含义。

以上几种分类中最重要的是按地点分类。而下文也将以它为主线,逐一介绍各种重定位项。首先,引入两个关键概念:GOT表和PLT表。

TOPGOT表

GOT(Global Offset Table)表中每一项都是本运行模块要引用的一个全局变量或函数的地址。可以用GOT表来间接引用全局变量、函数,也可以把GOT表的首地址作为一个基准,用相对于该基准的偏移量来引用静态变量、静态函数。由于加载器不会把运行模块加载到固定地址,在不同进程的地址空间中,各运行模块的绝对地址、相对位置都不同。这种不同反映到GOT表上,就是每个进程的每个运行模块都有独立的GOT表,所以进程间不能共享GOT表。

在x86体系结构上,本运行模块的GOT表首地址始终保存在%ebx寄存器中。编译器在每个函数入口处都生成一小段代码,用来初始化%ebx寄存器。这一步是必要的,否则,如果对该函数的调用来自另一运行模块,%ebx中就是调用者模块的GOT表地址;不重新初始化%ebx就用来引用全局变量和函数,当然出错。

TOPPLT表

PLT(Procedure Linkage Table)表每一项都是一小段代码,对应于本运行模块要引用的一个全局函数。以对函数fun的调用为例,PLT中代码片断如下:

1
2
3
.PLTfun:  jmp *fun@GOT(%ebx)
          pushl $offset
          jmp .PLT0@PC

其中引用的GOT表项被加载器初始化为下一条指令(pushl)的地址,那么该jmp指令相当于nop空指令。

用户程序中对fun的直接调用经编译连接后生成一条call fun@PLT指令,这是一条相对跳转指令(满足浮动代码的要求!),跳到.PLTfun。如果这是本运行模块中第一次调用该函数,此处的jmp等于一个空指令,继续往下执行,接着就跳到.PLT0。该PLT项保留给编译器生成的额外代码,会把程序流程引入到加载器中去。加载器计算fun的实际入口地址,填入fun@GOT表项。图示如下:

                             user program
                            --------------
                             call fun@PLT
                                   |
                                   v
              DLL             PLT table                loader
        --------------   --------------   -----------------------
        fun:           <-- jmp*fun@GOT  --> change GOT entry from
                                   |             $loader to $fun,
                                   v             then jump to there
                               GOT table
                            --------------
                            fun@GOT:$loader

第一次调用以后,GOT表项已指向函数的正确入口。以后再有对该函数的调用,跳到PLT表后,不再进入加载器,直接跳进函数正确入口了。从性能上分析,只有第一次调用才要加载器作一些额外处理,这是完全可以容忍的。还可以看出,加载时不用对相对跳转的代码进行修补,所以整个代码段都能在进程间共享。

熟悉Windows的程序员很容易注意到,GOT表、PLT表与Windows中的引入表(Import)有类似之处。其它对应关系还有: Linux的version script与Windows的.DEF文件;Linux的dynamic symbols section与Windows的输出表(Export)。不再举更多例子了。

TOP代码段重定位

需要说明,由浮动代码的要求,代码段内不应该存在重定位项。此处只是借用了"在代码段中"这个短语,实际的重定位项还是位于数据段的GOT表内。尽管如此,它与3.5节"数据段中的重定位"的区别是很明显的。

TOP装载GOT表首地址

使用GOT表当然事先要知道它的首地址,然而该首地址会随运行模块被加载的首地址不同而不同。Linux使用了一个技巧在运行时求出正确的GOT表首地址。代码片断如下,紧接其后列出的是对应的目标文件(.o)与动态连接库(.so)中的重定位项类型:

1
2
3
4
5
     call L1
L1:  popl %ebx
     addl $GOT+[.-.L1], %ebx
.o:  R_386_GOTPC
.so: NULL

如前所述,该代码片断存在于每个函数的入口处。程序第一句把当前程序计数器(IP)值推进堆栈,第二句又把它从堆栈中弹出来,结果相当于movl %eip, %ebx,只不过合法的x86指令集中不允许%eip作为操作数而已。然后第三句把%ebx加上一个GOT表与IP值的差,这个差值是个与动态连接库加载首地址无关的常数,在连接时即可求出。整个过程用类C语言描述如下:

1
2
     %ebx = %eip;
     %ebx += ($GOT - %eip)

至此%ebx等于GOT表首地址。

上述过程是编译、连接相合作的结果。编译器生成目标文件时,因为此时还不存在GOT表(每个运行模块有一个GOT表,一个PLT表,由连接器生成),所以暂时不能计算GOT表与当前IP间的差值,仅在第三句处设上一个R_386_GOTPC重定位标记而已。然后进行连接。连接器注意到GOTPC重定位项,于是计算GOT与此处IP的差值,作为addl指令的立即寻址方式操作数。以后再也不需要重定位了。

TOP引用变量、函数地址

当引用的是静态变量、静态函数或字符串常量时,使用R_386_GOTOFF重定位方式。它与GOTPC重定位方式很相似,同样首先由编译器在目标文件中设上重定位标记,然后连接器计算GOT表与被引用元素首地址的差值,作为leal指令的变址寻址方式操作数。代码片断如下:

1
2
3
leal .LC1@GOTOFF(%ebx), %eax
.o:  R_386_GOTOFF
.so: NULL

当引用的是全局变量、全局函数时,编译器会在目标文件中设上一个R_386_GOT32重定位标记。连接器会在GOT表中保留一项,注上R_386_GLOB_DAT重定位标记,用于加载器填写被引用元素的实际地址。连接器还要计算该保留项在GOT表中的偏移,作为movl指令的变址寻址方式操作数。代码片断如下:

1
2
3
movl x@GOT(%ebx), %eax
.o:  R_386_GOT32
.so: R_386_GLOB_DAT

需要指出,引用全局函数时,由GOT表读出不是全局函数的实际入口地址,而是该函数在PLT表中的入口.PLTfun(参见3.3节)。这样,无论直接调用,还是先取得函数地址再间接调用,程序流程都会转入PLT表,进而把控制权转移给加载器。加载器就是利用这个机会进行动态连接的。

TOP直接调用函数

如前所述,浮动代码中的函数调用语句会编译成相对跳转指令。首先编译器会在目标文件中设上一个R_386_PLT32重定位标记,然后视静态函数、全局函数不同而连接过程也有所不同。

如果是静态函数,调用一定来自同一运行模块,调用点相对于函数入口点的偏移量在连接时就可计算出来,作为call指令的相对当前IP偏移跳转操作数,由此直接进入函数入口,不用加载器操心。相关代码片断如下:

1
2
3
call f@PLT
.o:  R_386_PLT32
.so: NULL

如果是全局函数,连接器将生成到.PLTfun的相对跳转指令,之后就如3.3节所述,对全局函数的第一次调用会把程序流程转到加载器中去,然后计算函数的入口地址,填充fun@GOT表项。这称为R_386_JMP_SLOT重定位方式。相关代码片断如下:

1
2
3
call f@PLT
.o:  R_386_PLT32
.so: R_386_JMP_SLOT

如此一来,一个全局函数可能有多至两个重定位项。一个是必需的JMP_SLOT重定位项,加载器把它指向真正的函数入口;另一个是GLOB_DAT重定位项,加载器把它指向PLT表中的代码片断。取函数地址时,取得的总是GLOB_DAT重定位项的值,也就是指向.PLTfun,而不是真正的函数入口。

进一步考虑这样一个问题:两个动态连接库,取同一个全局函数的地址,两个结果进行比较。由前面的讨论可知,两个结果都没有指向函数的真正入口,而是分别指向两个不同的PLT表。简单进行比较,会得出"不相等"的结论,显然不正确,所以要特殊处理。

TOP数据段重定位

在数据段中的重定位是指对指针类型的静态变量、全局变量进行初始化。它与代码段中的重定位比较起来至少有以下明显不同 :

  • 在用户程序获得控制权(main函数开始执行)之前就要全部完成;
  • 不经过GOT表间接寻址,这是因为此时%ebx中还没有正确的GOT表首地址;
  • 直接修改数据段,而代码段重定位时不能修改代码段。

如果引用的是静态变量、函数、串常量,编译器会在目标文件中设上R_386_32重定位标记,并计算被引用变量、函数相对于所在段首地址的偏移量。连接器把它改成R_386_RELATIVE重定位标记,计算它相对于动态连接库首地址(通常为零)的偏移量。加载器会把运行模块真正的首地址(不为零)与该偏移量相加,结果用来初始化指针变量。代码片断如下:

1
2
3
4
5
6
.section .rodata
.LC0: .string "Ok\n"
.data
p:     .long .LC0
.o:  R_386_32 w/ section
.so: R_386_RELATIVE

如果引用的是全局变量、函数,编译器同样设上R_386_32重定位标记,并且记录引用的符号名字。连接器不必动作。最后加载器查找被引用符号,结果用来初始化指针变量。对于全局函数,查找的结果仍然是函数在PLT表中的代码片断,而不是实际入口。这与前面引用全局函数的讨论相同。代码片断如下:

1
2
3
4
.data
p:       .long printf
.o:  R_386_32 w/ symbol
.so: R_386_32 w/ symbol

1. __cdecl是C\C++的默认调用约定; VC的调用约定中并没有thiscall这个关键字,它是类成员函数默认调用约定;
C\C++中的main(或wmain)函数的调用约定必须是__cdecl,不允许更改;
默认调用约定一般能够通过编译器设置进行更改,如果你的代码依赖于调用约定,请明确指出需要使用的调用约定;
常见的函数调用约定中,只有cdecl约定需要调用者来清除堆栈; C\C++中的函数支持参数数目不定的参数列表,比如函数;
由于函数体不知道调用者在堆栈中压入了多少参数,所以函数体不能方便的知道应该怎样清除堆栈,那么最好的办法就是把清除堆栈的责任交给调用者;

© www.yifeiyang.net
net tracking

                                                                                                 stats