Reverse engineering for beginners

说明

该项目为某同性交友团体翻译某本bl文的项目地址,异性恋请勿点击

源起

两年前乌云知识库的 @瞌睡龙 菊苣发起对《Reverse engineering for beginners》本书的翻译项目,但是后期对这本书都没怎么做过修缮工作,以至于两年间Dennis Yurichev对这本书的调整修改都得不到更新。而且乌云译版里面还有着不少语句不通,格式混乱的瑕疵。

恰巧在前几天在Dennis Yurichev菊苣的主页上看到《reverse engineering for beginners》的韩语版已经出版发售。而相比之下这本书在中国的技术社区上却显得很冷清,作为之前参加过这项翻译工作我实在不忍心看着这么好的逆向书籍在中国的技术社区上被埋没。我尝试搜了一下,目前还没找到乌云译版以外的翻译版本,另外乌云的中文翻译版可以在Dennis Yurichev的blog看找到链接http://yurichev.com/blog/2015-aug-20/。

Dennis Yurichev在感谢列表上antiy公司也有一份Chinese的翻译版本,但是我没找到,如果有小伙伴知道的麻烦告诉我一声,谢谢: )

最后,我目前看过的逆向书籍不多,但是对比一下国内看雪社区的《加密与解密》,《reverse engineering for beginners》更适合作为逆向入门书籍,而且里面覆盖的范围更广,包括了x86/x64,arm两三种CPU的指令集,而且囊括了Linux和Windows的OS hack等内容,各种逆向工具也都介绍了一番。所以逆向方面我挺推荐这本书作为入门读物的。

修缮计划

  1. 对每个章节都review并修缮,把语病,格式混乱等问题给解决了。
  2. 里面大量的术语翻译不一致,格式不一致,这方面也要做改善。
  3. 因为这期间作者对原版调整了很多,有不少章节缺漏或被删,分章情况也大不相同,所以需要继续完善跟原版的衔接工作。
  4. 迁移到gitbook和看云上,并在乌云drops上开一个专题连载这本书,提供一个更好的阅读体验。
  5. LaTex版本的迁移工作,但是同时markdown版本也会进行维护,而且维护应该也把markdown作为主要的编辑工具,毕竟会的人多一些:),LaTex粉体谅一下。

任务分配计划

如果有兴趣的同学,请在我的github分支上提一个issue,并说明您要做哪一块工作,并简单说明一下您的工作计划。其它同学也对该工作有兴趣的,可以继续在这个issue上跟进和讨论。

即使提交一个逗号的修改我也会将您加入到贡献名单中。

贡献名单

第一期的翻译贡献人员

瞌睡龙、糖果、blast、magix526、Larryxi、左懒、DM_、Zing

修缮工作的贡献人员

xqin

如有缺漏请通知我一声。谢谢!

最后

最后,我在乌云zone上面发布了一个悬赏贴,里面就悬赏给第一个contributor了:)

http://zone.wooyun.org/content/27463

希望有兴趣的同学一起帮忙完善这本书吧!

因为乌云主分支的管理员比较少上线,所以跟@瞌睡龙菊苣商量后,先在我的github分支上做修缮工作,之后任务差不多了再归并到主分支上。

有兴趣的同学也可以加我的QQ:736809745。

无关翻译工作的,闲聊,技术交流都可以:)

CPU简介

CPU就是执行所有程序的工作单元。

词汇表

Instruction:CPU的原指令,例如:将数据在数据区与寄存器之间进行转移操作,对数据进行操作,算术操作。原则上每种CPU会有自己独特的一套指令构架(Instruction Set Architecture(ISA))。

Machine code: CPU的指令码(机器码),每条指令都会被译成指令码。

Assembly Language: 汇编语言,助记码和其他一些例如宏那样的特性组成的便于程序员编写的语言。

CPU register:CPU寄存器,每个CPU都有一些通用寄存器(General Purpose Registers(GPR))。X86有8个,x86-64(amd64)有16个,ARM有16个,最简单去理解寄存器的方法就是,把寄存器想成一个不需要类型的临时变量。想象你在用高级编程语言,并且只有8个32bit的变量。只用这些可以完成非常多的事情。

那么机器码跟程序语言有什么区别?对于人类来讲,使用例如C/C++, Java, Python这样编程语言会比较简单,但是CPU更喜欢低级抽象的东西。真希望有一天CPU也能直接来执行高级语言的语句,但那样CPU肯定会变得非常地复杂。人类之所以使用汇编语言会感觉不很方便是因为它非常的低级,而且很难用它写非常长的代码并不出现错误。有一种将高级语言转换到汇编语言的程序,它就是编译器。

Hello,world!

让我们用最著名的代码例子开始吧:

#include <stdio.h>
int main() {
    printf("hello, world");
    return 0;
};

2.1 x86

2.1.1 MSVC-x86

在MSVC 2010中编译一下:

cl 1.cpp /Fa1.asm

(/Fa 选项表示生产汇编列表文件)

CONST   SEGMENT
    $SG3830 DB     'hello, world', 00H
CONST   ENDS
PUBLIC  _main
EXTRN   _printf:PROC
; Function compile flags: /Odtp
_TEXT   SEGMENT
_main   PROC
    push ebp
    mov ebp, esp
    push OFFSET $SG3830
    call _printf
    add esp, 4
    xor eax, eax
    pop ebp
    ret 0
_main   ENDP
_TEXT   ENDS

MSVC生成的是Intel汇编语法。Intel语法与AT&T语法的区别将在后面讨论。

编译器会把1.obj文件连接成1.exe。

在我们的例子当中,文件包含两个部分:CONST(放数据)和_TEXT(放代码)。

字符串"hello,world"在C/C++ 类型为const char*,然而它已经丢失了自己的名称。

编译器需要处理这个字符串,就自己给他定义了一个$SG3830。

所以例子可以改写为:

#include <stdio.h>
const char *$SG3830="hello, world";
int main() {
    printf($SG3830);
    return 0;
};

我们回到汇编列表,正如我们看到的,字符串是由0字节结束的,这也是C/C++的标准。

在代码部分,_TEXT,只有一个函数:main()。

函数main()与大多数函数一样都有开始的代码与结束的代码。

函数当中的开始代码结束以后,调用了printf()函数:CALL _printf

在PUSH指令的帮助下,我们问候语字符串的地址(或指向它的指针)在被调用之前存放在栈当中。

当printf()函数执行完返回到main()函数的时候,字符串地址(或指向它的指针)仍然在堆栈中。

当我们都不再需要它的时候,堆栈指针(ESP寄存器)需要改变。

ADD ESP, 4

意思是ESP寄存器加4。

为什么是4呢?由于是32位的代码,通过栈传送地址刚好需要4个字节。

在64位系统当中它是8字节。

ADD ESP, 4实际上等同于POP register

一些编辑器(如Intel C++编译器)在同样的情况下可能会用POP ECX代替ADD(例如这样的模式可以在Oracle RDBMS代码中看到,因为它是由Intel C++编译器编译的),这条指令的效果基本相同,但是ECX的寄存器内容会被改写。

Intel C++编译器可能用POP ECX,因为这比ADD ESP, X需要的字节数更短,(1字节对应3字节)。

在调用printf()之后,在C/C++代码之后执行return 0return 0是main()函数的返回结果。

代码被编译成指令XOR EAX, EAX

XOR事实上就是异或,但是编译器经常用它来代替MOV EAX, 0原因就是它需要的字节更短(2字节对应5字节)。

有些编译器用SUB EAX, EAX 就是EXA的值减去EAX,也就是返回0。

最后的指令RET 返回给调用者,他是C/C++代码吧控制返还给操作系统。

2.1.2 GCC-x86

现在我们尝试同样的C/C++代码在linux中的GCC 4.4.1编译

gcc 1.c -o 1

下一步,在IDA反汇编的帮助下,我们看看main()函数是如何被创建的。

(IDA与MSVC一样,也是显示Intel语法)。

我也可以是GCC生成Intel语法的汇编代码,添加参数-S -masm=intel

汇编代码:

main proc near
var_10          = dword ptr -10h
    push ebp
    mov  ebp, esp
    and  esp, 0FFFFFFF0h
    sub  esp, 10h
    mov  eax, offset aHelloWorld ;` `"hello, world"
    mov [esp+10h+var_10], eax
    call _printf
    mov eax, 0
    leave
    retn
main            endp

结果几乎是相同的,"hello,world"字符串地址(保存在data段的)一开始保存在EAX寄存器当中,然后保存到栈当中。

同样的在函数开始我们看到了

AND ESP, 0FFFFFFF0h

这条指令以16字节边界大小对齐ESP寄存器,这使得所有值的地址在栈上都会有相同的对齐方式。

SUB ESP,10H在栈上分配16个字节。 这里其实只需要4个字节。

这是因为,分配堆栈的大小也被排列在一个16字节的边界。

该字符串的地址(或这个字符串指针),不使用PUSH指令,直接写入到堆栈空间。var_10,是一个局部变量,也是printf()的参数。

然后调用printf()函数。

不像MSVC,当gcc编译不开启优化,它使用MOV EAX,0清空EAX,而不是更短的代码。

最后一条指令,LEAVE相当于MOV ESP,EBP和POP EBP两条指令。

换句话说,这相当于指令将堆栈指针(ESP)恢复,EBP寄存器到其初始状态。

这是必须的,因为我们在函数的开头修改了这些寄存器的值(ESP和EBP)(执行MOV EBP,ESP/AND ESP…)。

2.1.3 GCC:AT&T 语法

我们来看一看在AT&T当中的汇编语法,这个语法在UNIX当中更普遍。

gcc -S 1_1.c

我们将得到这个:

    .file   "1_1.c"
    .section    .rodata
.LC0:
    .string "hello, world"
    .text
    .globl  main
    .type   main, @function
main:
.LFB0:
    .cfi_startproc
    pushl %ebp
    .cfi_def_cfa_offset 8
    .cfi_offset 5, -8
    movl %esp, %ebp
    .cfi_def_cfa_register 5
    andl $-16, %esp
    subl $16, %esp
    movl $.LC0, (%esp)
    call printf
    movl $0, %eax
    leave
    .cfi_restore 5
    .cfi_def_cfa 4, 4
    ret
    .cfi_endproc
.LFE0:
    .size   main, .-main
    .ident  "GCC: (Ubuntu/Linaro 4.7.3-1ubuntu1) 4.7.3"
    .section        .note.GNU-stack, "", @progbits

有很多的宏(用点开始)。现在为了简单起见,我们先不看这些。(除了 .string ,就像一个C字符串编码一个null结尾的字符序列)。然后,我们将看到这个:

.LC0:
    .string "hello, world"
main:
    pushl   %ebp
    movl %esp, %ebp
    andl $-16, %esp
    subl $16, %esp
    movl $.LC0, (%esp)
    call printf
    movl $0, %eax
    leave
    ret

在Intel与AT&T语法当中比较重要的区别就是:

操作数写在后面

在Intel语法中:<instruction> <destination operand> <source operand>
在AT&T语法中:<instruction> <source operand> <destination operand>

有一个理解它们的方法: 当你面对intel语法的时候,你可以想象把等号放到2个操作数中间,当面对AT&T语法的时候,你可以放一个右箭头(→)到两个操作数之间。

AT&T: 在寄存器名之前需要写一个百分号(%)并且在数字前面需要美元符($)。方括号被圆括号替代。 AT&T: 一些用来表示数据形式的特殊的符号

l      long(32 bits)
w      word(16bits)
b      byte(8 bits)

让我们回到上面的编译结果:它和在IDA里看到的是一样的。只有一点不同:0FFFFFFF0h 被写成了$-16,但这是一样的,10进制的16在16进制里表示为0x10。-0x10就等同于0xFFFFFFF0(这是针对于32位构架)。

外加返回值这里用的MOV来设定为0,而不是用XOR。MOV仅仅是加载(load)了变量到寄存器。指令的名称并不直观。在其他的构架上,这条指令会被称作例如”load”这样的。

2.2 x86-64

2.2.1 MSVC-x86-64

让我们来试试64-bit的MSVC:

$SG2989 DB      'hello, world', 00H
main    PROC
    sub    rsp, 40
    lea    rcx, OFFSET FLAT:$SG2923
    call   printf
    xor    eax, eax
    add    rsp, 40
    ret    0
main ENDP

在x86-64里,所有被扩展到64位的寄存器都有R-前缀。并且尽量不用栈来传递函数的参数了,大量使用寄存器来传递参数,非常类似于fastcall。

在win64里,RCX,RDX,R8,R9寄存器被用来传递函数参数,如果还有更多就使用栈,在这里我们可以看到printf()函数的参数没用通过栈来传递,而是使用了rcx。 让我们针对64位来看,作为64位寄存器会有R-前缀,并且这些寄存器向下兼容,32位的部分使用E-前缀。

如下图所示,这是RAX/EAX/AX/AL在64位x86兼容cpu里的情况 

在main()函数会返回一个int类型的值,在64位的程序里为了兼容和移植性,还是用32位的,所以可以看到EAX(寄存器的低32位部分)在函数最后替代RAX被清空成0。

2.2.2 GCC-x86-64

这次试试GCC在64位的Linux里:

    .string "hello, world"
main:
    sub    rsp, 8
    mov    edi, OFFSET FLAT:.LC0 ; "hello, world"
    xor    eax, eax  ; number of vector registers passed
    call   printf
    xor    eax, eax
    add    rsp, 8
    ret

在Linux,*BSD和Mac OS X里使用同一种方式来传递函数参数。头6个参数使用RDI,RSI,RDX,RCX,R8,R9来传递的,剩下的要靠栈。

所以在这个程序里,字串的指针被放到EDI(RDI的低32位部)。为什么不是64位寄存器RDI那?

这是一个重点,在64位模式下,对低32位进行操作的时候,会清空高32位的内容。比如 MOV EAX,011223344h将会把值写到RAX里,并且清空RAX的高32位区域。 如果我们打开编译好的对象文件(object file[.o]),我们会看到所有的指令:

Listing 2.8:GCC 4.4.6 x64

.text:00000000004004D0                     main proc near
.text:00000000004004D0 48 83 EC 08             sub rsp, 8
.text:00000000004004D4 BF E8 05 40 00          mov edi, offset format ;"hello, world"
.text:00000000004004D9 31 C0                   xor eax, eax
.text:00000000004004DB E8 D8 FE FF FF          call _printf
.text:00000000004004E0 31 C0                   xor eax, eax
.text:00000000004004E2 48 83 C4 08             add rsp, 8
.text:00000000004004E6 C3                      retn
.text:00000000004004E6                     main endp

就像看到的那样,在04004d4那行给edi写字串指针的那句花了5个bytes。如果把这句换做给rdi写指针,会花掉7个bytes.就是说GCC在试图节省空间,为此数据段(data segment)中包含的字串不会被分配到高于4GB地址的空间上。

可以看到在printf()函数调用前eax被清空了,这样做事因为要eax被用作传递向量寄存器(vector registers)的个数。

参考【21】 MichaelMatz/JanHubicka/AndreasJaeger/MarkMitchell. Systemvapplicationbinaryinterface.amdarchitecture processor supplement, . Also available as http://x86-64.org/documentation/abi.pdf.

2.3 ARM

根据作者自身对ARM处理器的经验,选择了2款在嵌入式开发流行的编译器,Keil Release 6/2013和苹果的Xcode 4.6.3 IDE(其中使用了LLVM-GCC4.2编译器),这些可以为ARM兼容处理器和系统芯片(System on Chip)(SOC))来进行编码。比如ipod/iphone/ipad,windows8 rt,并且包括raspberry pi。

2.3.1 未进行代码优化的Keil编译:ARM模式

让我们在Keil里编译我们的例子

armcc.exe –arm –c90 –O0 1.c

armcc编译器可以生成intel语法的汇编程序列表,但是里面有高级的ARM处理器相关的宏,对我们来讲更希望看到的是IDA反汇编之后的结果。

Listing 2.9: Non-optimizing Keil + ARM mode + IDA

#!bash
.text:00000000                  main
.text:00000000 10 40 2D E9              STMFD SP!, {R4,LR}
.text:00000004 1E 0E 8F E2              ADR R0, aHelloWorld ; "hello, world"
.text:00000008 15 19 00 EB              BL __2printf
.text:0000000C 00 00 A0 E3              MOV R0, #0
.text:00000010 10 80 BD E8              LDMFD SP!, {R4,PC}

.text:000001EC 68 65 6C 6C +aHelloWorld  DCB "hello, world",0 ; DATA XREF: main+4

针对ARM处理器,我们需要预备一点知识,要知道ARM处理器至少有2种模式:ARM模式和thumb模式,在ARM模式下,所有的指令都被激活并且都是32位的。在thumb模式下所有的指令都是16位的。Thumb模式比较需要注意,因为程序可能需要更为紧凑,或者当微处理器用的是16位内存地址时会执行的更快。但也存在缺陷,在thumb模式下可用的指令没ARM下多,只有8个寄存器可以访问,有时候ARM模式下一条指令就能解决的问题,thumb模式下需要多个指令来完成。

从ARMv7开始引入了thumb-2指令集。这是一个加强的thumb模式。拥有了更多的指令,通常会有误解,感觉thumb-2是ARM和thumb的混合。Thumb-2加强了处理器的特性,并且媲美ARM模式。程序可能会混合使用2种模式。其中大量的ipod/iphone/ipad程序会使用thumb-2是因为Xcode将其作为了默认模式。

在例子中,我们可以发现所有指令都是4bytes的,因为我们编译的时候选择了ARM模式,而不是thumb模式。

最开始的指令是STMFD SP!, {R4, LR},这条指令类似x86平台的PUSH指令,会写2个寄存器(R4和LR)的变量到栈里。不过在armcc编译器里输出的汇编列表里会写成PUSH {R4, LR},但这并不准确,因为PUSH命令只在thumb模式下有,所以我建议大家注意用IDA来做反汇编工具。

这指令开始会减少SP的值,已加大栈空间,并且将R4和LR写入分配好的栈里。

这条指令(类似于PUSH的STMFD)允许一次压入好几个值,非常实用。有一点跟x86上的PUSH不同的地方也很赞,就是这条指令不像x86的PUSH只能对sp操作,而是可以指定操作任意的寄存器。

ADR R0, aHelloWorld这条指令将PC寄存器的值与"hello, world"字串的地址偏移相加放入R0,为什么说要PC参与这个操作那?这是因为代码是PIC(position-independet code)的,这段代码可以独立在内存运行,而不需要更改内存地址。ADR这条指令中,指令中字串地址和字串被放置的位置是不同的。但变化是相对的,这要看系统是如何安排字串放置的位置了。这也就说明了,为何每次获取内存中字串的绝对地址,都要把这个指令里的地址加上PC寄存器里的值了。

BL __2print这条指令用于调用printf()函数,这是来说下这条指令时如何工作的:

将BL指令(0xC)后面的地址写入LR寄存器;
然后把printf()函数的入口地址写入PC寄存器,进入printf()函数。

当printf()函数完成之后,函数会通过LR寄存器保存的地址,来进行返回操作。

函数返回地址的存放位置也正是“纯”RISC处理器(例如:ARM)和CISC处理器(例如x86)的区别。

另外,一个32位地址或者偏移不能被编码到BL指令里,因为BL指令只有24bits来存放地址,所有的ARM模式下的指令都是4bytes(32bits),所以一条指令里不能放满4bytes的地址,这也就意味着最后2bits总会被设置成0,总的来说也就是有26bits的偏移(包括了最后2个bit一直被设为0)会被编码进去。这也够去访问大约±32M的了。

下面我们来看MOV R0, #0这条语句,这条语句就是把0写到了R0寄存器里,这是因为C函数返回了0,返回值当然是放在R0里的。

最后一条指令是LDMFD SP!, R4,PC,这条指令的作用跟开始的那条STMFD正好相反,这条指令将栈上的值保存到R4和PC寄存器里,并且增加SP栈寄存器的值。这非常类似x86平台里的POP指令。最前面那条STMFD指令成对保存了R4,和LR寄存器,LDMFD的时候将当时这两个值保存到了R4和PC里完成了函数的返回。

我前面也说过,函数的返回地址会保存到LD寄存器里。在函数的最开始会把他保存到栈里,这是因为main()函数里还需要调用printf()函数,这个时候就会影响LD寄存器。在函数的最后就会将LD拿出栈放入PC寄存器里,完成函数的返回操作。最后C/C++程序的main()函数会返回到类似系统加载器上或者CRT里面。

汇编代码里的DCB关键字用来定义ASCII字串数组,就像x86汇编里的DB关键字。

2.3.2未进行代码优化的Keil编译: thumb模式

让我们用下面的指令讲例程用Keil的thumb模式来编译一下。

armcc.exe –thumb –c90 –O0 1.c

我们可以在IDA里得到下面这样的代码: Listing 2.10:Non-optimizing Keil + thumb mode + IDA

.text:00000000            main
.text:00000000 10 B5          PUSH {R4,LR}
.text:00000002 C0 A0          ADR R0, aHelloWorld ; "hello, world"
.text:00000004 06 F0 2E F9    BL __2printf
.text:00000008 00 20          MOVS R0, #0
.text:0000000A 10 BD          POP {R4,PC}
.text:00000304 68 65 6C 6C +aHelloWorld  DCB "hello, world",0 ; DATA XREF: main+2

我们首先就能注意到指令都是2bytes(16bits)的了,这正是thumb模式的特征,BL指令作为特例是2个16bits来构成的。只用16bits没可能加载printf()函数的入口地址到PC寄存器。所以前面的16bits用来加载函数偏移的高10bits位,后面的16bits用来加载函数偏移的低11bits位,正如我说过的,所有的thumb模式下的指令都是2bytes(16bits)。但是这样的话thumb指令就没法使用更大的地址。就像上面那样,最后一个bits的地址将会在编码指令的时候省略。总的来讲,BL在thumb模式下可以访问自身地址大于±2M大的周边的地址。

至于其他指令:PUSH和POP,它们跟上面讲到的STMFD跟LDMFD很类似,但这里不需要指定SP寄存器,ADR指令也跟上面的工作方式相同。MOVS指令将函数的返回值0写到了R0里,最后函数返回。

2.3.3开启代码优化的Xcode(LLVM)编译: ARM模式

Xcode 4.6.3不开启代码优化的情况下,会产生非常多冗余的代码,所以我们学习一个尽量小的版本。

开启-O3编译选项

Listing2.11:Optimizing Xcode(LLVM)+ARM mode

__text:000028C4         _hello_world
__text:000028C4 80 40 2D E9                     STMFD   SP!, {R7,LR}
__text:000028C8 86 06 01 E3                     MOV     R0, #0x1686
__text:000028CC 0D 70 A0 E1                     MOV     R7, SP
__text:000028D0 00 00 40 E3                     MOVT    R0, #0
__text:000028D4 00 00 8F E0                     ADD     R0, PC, R0
__text:000028D8 C3 05 00 EB                     BL      _puts
__text:000028DC 00 00 A0 E3                     MOV     R0, #0
__text:000028E0 80 80 BD E8                     LDMFD   SP!, {R7,PC}
__cstring:00003F62 48 65 6C 6C +aHelloWorld_0    DCB "Hello world!", 0

STMFD和LDMFD对我们来说已经非常熟悉了。

MOV指令就是将0x1686写入R0寄存器里。这个值也正是字串”Hello world!”的指针偏移。

R7寄存器里放入了栈地址,我们继续。

MOVT R0, #0指令时将R0的高16bits写入0。这是因为普通情况下MOV这条指令在ARM模式下,只对低16bits进行操作。需要记住的是所有在ARM模式下的指令都被限定在32bits内。当然这个限制并不影响2个寄存器直接的操作。这也是MOVT这种写高16bits指令存在的意义。其实这样写的代码会感觉有点多余,因为MOVS R0,#0x1686这条指令也能把高16位清0。或许这就是相对于人脑来说编译器的不足。

ADD R0,PC,R0指令把R0寄存器的值与PC寄存器的值进行相加并且保存到R0寄存器里面,用来计算"Hello world!"这个字串的绝对地址。上面已经介绍过了,这是因为代码是PIC(Position-independent code)的,所以这里需要这么做。

BL指令用来调用printf()的替代函数puts()函数。

GCC将printf()函数替换成了puts()。因为printf()函数只有一个参数的时候跟puts()函数是类似的。

printf()函数的字串参数里存在特殊控制符(例如 “%s”,“”,需要注意的是,程序里字串里没有“”,因为在puts()函数里这是不需要的)的时候,两个函数的功效就会不同。

为什么编译器会替换printf()到puts()那?因为puts()函数更快。

puts()函数效率更快是因为它只是做了字串的标准输出(stdout)并不用比较%符号。

下面,我们可以看到非常熟悉的"MOV R0, #0"指令,用来将R0寄存器设为0。

2.3.4 开启代码优化的Xcode(LLVM)编译thumb-2模式

在默认情况下,Xcode4.6.3会生成如下的thumb-2代码

Listing 2.12:Optimizing Xcode(LLVM)+thumb-2 mode

__text:00002B6C                _hello_world
__text:00002B6C 80 B5          PUSH    {R7,LR}
__text:00002B6E 41 F2 D8 30    MOVW    R0, #0x13D8
__text:00002B72 6F 46          MOV     R7, SP
__text:00002B74 C0 F2 00 00    MOVT.W  R0, #0
__text:00002B78 78 44          ADD     R0, PC
__text:00002B7A 01 F0 38 EA    BLX     _puts
__text:00002B7E 00 20          MOVS    R0, #0
__text:00002B80 80 BD          POP     {R7,PC}
...
__cstring:00003E70 48 65 6C 6C 6F 20 +aHelloWorld DCB "Hello world!",0xA,0

BL和BLX指令在thumb模式下情况需要我们回忆下刚才讲过的,它是由一对16-bit的指令来构成的。在thumb-2模式下这条指令跟thumb一样被编码成了32-bit指令。非常容易观察到的是,thumb-2的指令的机器码也是从0xFx或者0xEx的。对于thumb和thumb-2模式来说,在IDA的结果里机器码的位置和这里是交替交换的。对于ARM模式来说4个byte也是反向的,这是因为他们用了不同的字节序。所以我们可以知道,MOVW,MOVT.W和BLX这几个指令的开始都是0xFx。

在thumb-2指令里有一条是“MOVW R0, #0x13D8”,它的作用是写数据到R0的低16位里面。

MOVT.W R0, #0的作用类似与前面讲到的MOVT指令,但它可以工作在thumb-2模式下。

还有些跟上面不同的地方,比如BLX指令替代了上面用到的BL指令,这条指令不仅将控制puts()函数返回的地址放入了LR寄存器里,并且讲代码从thumb模式转换到了ARM模式(或者ARM转换到thumb(根据现有情况判断))。这条指令跳转到下面这样的位置(下面的代码是ARM编码模式)。

__symbolstub1:00003FEC              _puts  ; CODE XREF: _hello_world+E
__symbolstub1:00003FEC 44 F0 9F E5  LDR PC, =__imp__puts

可能会有细心的读者要问了:为什么不直接跳转到puts()函数里面去?

因为那样做会浪费内存空间。

很多程序都会使用额外的动态库(dynamic libraries)(Windows里面的DLL,还有*NIX里面的.so,MAC OS X里面的.dylib),通常使用的库函数会被放入动态库中,当然也包括标准C函数puts()。

在可执行的二进制文件里(Windows的PE里的.exe文件,ELF和Mach-O文件)都会有输入表段。它是一个用来引入额外模块里模块名称和符号(函数或者全局变量)的列表。

系统加载器(OS loader)会加载所有需要的模块,当在主模块里枚举输入符号的时候,会把每个符号正确的地址与相应的符号确立起来。

在我们的这个例子里,__imp__puts就是一个系统加载器加载额外模块的32位的地址值。LDR指令只需要把这个值加载到PC里面去,就可以控制程序流程到puts()函数里去。

所以只需要在系统加载器里的时候,一次性的就能将每个符号所对应的地址确定下来,这是个提高效率的好方式。

外加,我们前面也指出过,我们没办法只用一条指令并且不做内存操作的情况下就将一个32bit的值保存到寄存器里,ARM并不是唯一的模式的情况下,程序里去跳入动态库中的某个函数里,最好的办法就是这样做一些类似与上面这样单一指令的函数(称做thunk function),然后从thumb模式里也能去调用。

在上面的例子(ARM编译的那个例子)中BL指令也是跳转到了同一个thunk function里。尽管没有进行模式的转变(所以指令里不存在那个”X”)。

函数的序幕,是像下面这样的代码片段:

push  ebp
mov   ebp, esp
sub   esp, X

这些指令做了什么:将寄存器EBP的值入栈,将ESP赋值给EBP,在栈中分配空间, 用来保存局部变量。

在函数执行过程中,EBP是固定的,可以用来访问局部变量和函数参数。也可以使用 ESP,但在函数运行过程中,ESP会变化,使用起来不方便。

mov  esp, ebp
pop  ebp
ret  0

3.1 递归

函数调用开始和结束使递归变得难以理解。

举个例子,有一次我写了一个函数遍历二叉树右侧节点。使用了看起来很高⼤上的递归函数,但由于每次函数调用开始和结束都需要花费很长时间,它运行速度比迭代方 式要慢好多倍。

顺便提一下,这就是尾部调用存在的原因。

栈在计算科学中是最重要并且是最基本的数据结构。

严格的来说,它只是在x86中被ESP,或x64中被RSP,或ARM中被SP所指向的一段程序内存区域。访问栈内存,最常使用的指令是PUSH和POP(在x86和ARM Thumb模式中)。

PUSH指令在32位模式下,会将ESP/RSP/SP的值减去4(在64位系统中,会减去8),然后将操作数写入到ESP/RSP/SP指向的内存地址。

POP是相反的操作运算:从SP指向的内存地址中获取数据,存入操作数(一般为寄存器), 然后将SP(栈指针)加4(或8)。

4.1 为什么栈反向增长?

按正常思维来说,我们会认为像其它数据结构一样,栈是正向增长的,比如:栈指针会指向高地址。

我们知道:

映像文件的划分为三个部分,程度代码段在内存空闲部分运行。在运行过程中,这部分是具有写保护的,所有进程都可以共享访问这个程序。在内存空间中,程序text区段开始的8k字节是不能共享的可写区段,这个大小可以使用系统函数来扩大。在内存高位地址是可以像硬件指针可以自由活动向下增长的栈区段。

4.2 栈可以用来做什么?

4.2.1 保存函数返回地址以便在函数执行完成时返回控制权

x86

当使用CALL指令去调用一个函数时,CALL后面一条指令的地址会被保存到栈中,使用无条件跳转指令跳转到CALL中执行。  CALL指令等价于PUSH函数返回地址和JMP跳转。

void f()
{
    f();
};

MSVC 2008显示了一些报错信息:

c:\tmp6>cl ss.cpp /Fass.asm
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.
ss.cpp
c:\tmp6\ss.cpp(4) : warning C4717: ’f’ : recursive on all control paths, function will cause
    runtime stack overflow

但无论如何还是生成了正确的代码:

?f@@YAXXZ PROC                                          ; f
; File c:\tmp6\ss.cpp
; Line 2
        push ebp
        mov     ebp, esp
; Line 3
        call    ?f@@YAXXZ                               ; f
; Line 4
        pop ebp
        ret     0
?f@@YAXXZ ENDP  ; f

如果我们设置优化(/0x)标识,生成的代码将不会出现栈溢出,并且会运行的很好。

?f@@YAXXZ PROC                                          ; f
; File c:\tmp6\ss.cpp
; Line 2
$LL3@f:
; Line 3
        jmp     SHORT $LL3@f
?f@@YAXXZ ENDP                                          ; f

GCC 4.4.1 在这两种条件下,会生成同样的代码,而且不会有任何警告。

ARM

ARM程序员经常使用栈来保存返回地址,但有些不同。像是提到的“Hello,World!(2.3), RA保存在LR(链接寄存器)。然而,如果需要调用另外一个函数,需要多次使用LR寄存器,它的值会被保存起来。通常会在函数开始的时候保存。像我们经常看到的指令“PUSH R4-R7, LR”,在函数结尾处的指令“POP R4-R7, PC”,在函数中使⽤用到的寄存器会被保存到栈中,包括LR。

尽管如此,如果一个函数从未调用其它函数,在ARM专用术语中被叫称作叶子函数。因此,叶⼦函数不需要LR寄存器。如果一个函数很小并使用了少量的寄存器,可能不会⽤到栈。因此,是可以不使用栈而实现调用叶子函数的。在扩展ARM上不使用栈,这样就会比在x86上运行要快。在未分配栈内存或栈内存不可用的情况下,这种方式是非常有用的。

4.2.2 传递函数参数

在x86中,最常见的传参方式是cdecl:

push arg3
push arg2
push arg1
call f
add esp, 4*3

被调用函数通过栈指针访问函数参数。因此,这就是为什么要在函数f()执行之前将数据放入栈中的原因。

来看一下其它调用约定。没有意义也没有必要强迫程序员一定要使用栈来传递参数。

这不是必需的,可以不使用栈,通过其它方式来实现。

例如,可以为参数分配一部分堆空间,存入参数,将指向这部分内存的指针存入EAX,这样就可以了。然而,在x86和ARM中,使用栈传递参数还是更加方便的。

另外一个问题,被调用的函数并不知道有多少参数被传递进来。有些函数可以传递不同个数的参数(如:printf()),通过一些说明性的字符(以%开始)才可以判断。如果我们这样调用函数

printf("%d %d %d", 1234);

printf()会传⼊入1234,然后另外传入栈中的两个随机数字。这就让我们使用哪种方式调用 main()函数变得不重要,像main(),main(int argc, char *argv[])main(int argc, char *argv[], char *envp[])

事实上,CRT函数在调用main()函数时,使用了下面的指令:  #!bash push envp push argv push argc call main ...

如果你使用了没有参数的main()函数,尽管如此,但它们仍然在栈中,只是无法使用。如果你使用了main(int argc, char *argv[]),你可以使用两个参数,第三个参数对你的函数是“不可见的”。如果你使用main(int argc)这种方式,同样是可以正常运行的。

4.2.3 局部变量存放

局部变量存放到任何你想存放的地方,但传统上来说,大家更喜欢通过将栈指针移动到栈底,来存放局部变量,当然,这不是必需的。

4.2.4 x86: alloca() 函数

对alloca()函数并没有值得学习的。

该函数的作用像malloc()一样,但只会在栈上分配内存。

它分配的内存并不需要在函数结束时,调用像free()这样的函数来释放,当函数运行结束,ESP的值还原时,这部分内存会自动释放。对alloca()函数的实现也没有什么值得介绍的。

这个函数,如果精简一下,就是将ESP指针指向栈底,根据你所需要的内存大小将ESP指向所分配的内存块。让我们试一下:

#include <malloc.h>
#include <stdio.h>
void f() {
    char *buf=(char*)alloca (600);
    _snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3);
    puts (buf);
};

(snprintf()函数作用与printf()函数基本相同,不同的地方是printf()会将结果输出到的标准输出stdout,snprintf()会将结果保存到内存中,后面两⾏代码可以使用printf()替换,但我想说明小内存的使用习惯。)

MSVC

让我们来编译 (MSVC 2010):

...
        mov    eax, 600         ; 00000258H
        call   __alloca_probe_16
        mov    esi, esp
 
        push   3
        push   2
        push   1
        push   OFFSET $SG2672
        push   600              ; 00000258H
        push   esi
        call   __snprintf
 
        push   esi
        call   _puts
        add    esp, 28          ; 0000001cH
...

这唯一的函数参数是通过EAX(未使用栈)传递。在函数调用结束时,ESP会指向 600字节的内存,我们可以像使用一般内存一样来使用它做为缓冲区。

GCC + Intel格式

GCC 4.4.1不需要调用函数就可以实现相同的功能:

.LC0:
           .string "hi! %d, %d, %d\n"
f:
           push    ebp
           mov     ebp, esp
           push    ebx
           sub     esp, 660
           lea     ebx, [esp+39]
           and     ebx, -16                             ; align pointer by 16-bit border
           mov     DWORD PTR [esp], ebx                 ; s
           mov     DWORD PTR [esp+20], 3
           mov     DWORD PTR [esp+16], 2
           mov     DWORD PTR [esp+12], 1
           mov     DWORD PTR [esp+8], OFFSET FLAT:.LC0  ; "hi! %d, %d, %d\n"
           mov     DWORD PTR [esp+4], 600               ; maxlen
           call    _snprintf
           mov     DWORD PTR [esp], ebx
           call    puts
           mov     ebx, DWORD PTR [ebp-4]
           leave
           ret

### GCC + AT&T 格式

我们来看相同的代码,但使用了AT&T格式:

.LC0:
        .string "hi! %d, %d, %d\n"
f:
        pushl %ebp
        movl    %esp, %ebp
        pushl   %ebx
        subl    $660, %esp
        leal    39(%esp), %ebx
        andl    $-16, %ebx
        movl    %ebx, (%esp)
        movl    $3, 20(%esp)
        movl    $2, 16(%esp)
        movl    $1, 12(%esp)
        movl    $.LC0, 8(%esp)
        movl    $600, 4(%esp)
        call    _snprintf
        movl    %ebx, (%esp)
        call    puts
        movl    -4(%ebp), %ebx
        leave
        ret

代码与上面的那个图是相同的。

例如:movl $3, 20(%esp)与mov DWORD PTR [esp + 20],3是等价的,Intel的内存地址增加是使用register+offset,而AT&T使用的是offset(%register)。

4.2.5 (Windows) 结构化异常处理

SEH也是存放在栈中的(如果存在的话)。 想了解更多,请等待后续翻译在(51.3)。

4.2.6 缓冲区溢出保护

想了解更多,请等待后续翻译,在(16.2)。

4.3 典型的内存布局

在32位系统中,函数开始时,栈的布局:

Chapter 5 printf() 与参数处理

现在让我们扩展“hello, world”(2)中的示例,将其中main()函数中printf的部分替换成这样

#include <stdio.h>
int main()
{
    printf("a=%d; b=%d; c=%d", 1, 2, 3);
    return 0;
};

5.1 x86: 3个参数

5.1.1 MSVC

在我们用MSVC 2010 Express编译后可以看到:

$SG3830 DB ’a=%d; b=%d; c=%d’, 00H
...
        push 3
        push 2
        push 1
        push OFFSET $SG3830
        call _printf
        add esp, 16        ; 00000010H

这和之前的代码几乎一样,但是我们现在可以看到printf()的参数被反序压入了栈中。第一个参数被最后压入。

另外,在32bit的环境下int类型变量占4 bytes。

那么,这里有4个参数 4*4=16 —— 恰好在栈中占据了16bytes:一个32bit字符串指针,和3个int类型变量。

当函数执行完后,执行"ADD ESP, X"指令恢复栈指针寄存器(ESP 寄存器)。通常可以在这里推断函数参数的个数:用 X除以4。

当然,这只涉及__cdecl函数调用方式。

也可以在最后一个函数调用后,把几个ADD ESP, X指令合并成一个。

push a1
push a2
call ...
...
push a1
call ...
...
push a1
push a2
push a3
call ...
add esp, 24

5.1.2 MSVC 与 ollyDbg

现在我们来在OllyDbg中加载这个范例。我们可以尝试在MSVC 2012 加 /MD 参数编译这个示例,也就是链接MSVCR*.dll,那么我们就可以在debugger中清楚的看到调用的函数。

在OllyDbg中载入程序,最开始的断点在ntdll.dll中,接着按F9(run),然后第二个断点在CRT-code中。现在我们来找main()函数。

往下滚动屏幕,找到下图这段代码(MSVC把main()函数分配在代码段开始处) 见图5.3

点击 PUSH EBP指令,按下F2(设置断点)然后按下F9(run),通过这些操作来跳过CRT-code,因为我们现在还不必关注这部分。

按6次F8(step over)。见图5.4 现在EIP 指向了CALL printf的指令。和其他调试器一样,OllyDbg高亮了有值改变的寄存器。所以每次你按下F8,EIP都在改变然后它看起来便是红色的。ESP同时也在改变,因为它是指向栈的

栈中的数据又在哪?那么看一下调试器右下方的窗口:

图 5.1

然后我们可以看到有三列,栈的地址,元组数据,以及一些OllyDbg的注释,OllyDbg可以识别像printf()这样的字符串,以及后面的三个值。

右击选中字符串,然后点击”follow in dump”,然后字符串就会出现在左侧显示内存数据的地方,这些内存的数据可以被编辑。我们可以修改这些字符串,之后这个例子的结果就会变的不同,现在可能并不是很实用。但是作为练习却非常好,可以体会每部分是如何工作的。

再按一次F8(step over)

然后我们就可以看到输出

图5.2 执行printf()函数

让我们看看寄存器和栈是怎样变化的 见图5.5

EAX寄存器现在是0xD(13).这是正确的,printf()返回打印的字符,EIP也变了——

事实上现在指向CALL printf之后下一条指令的地址.ECX和EDX的值也改变了。显然,printf()函数的内部机制对它们进行了使用。

很重要的一点ESP的值并没有发生变化,栈的状态也是!我们可以清楚地看到字符串和相应的3个值还是在那里,实际上这就是cdecl调用方式。被调用的函数并不清楚栈中参数,因为这是调用体的任务。

再按一下F8执行ADD ESP, 0见图5.6

ESP改变了,但是值还是在栈中!当然 没有必要用0或者别的数据填充这些值。

因为在栈指针寄存器之上的数据都是无用的。

图5.3 OllyDbg:main()初始处

图5.4 OllyDbg:printf()执行时

图5.5 Ollydbg:printf()执行后

图5.6 OllyDbg ADD ESP, 10执行完后

5.1.3 GCC

现在我们将同样的程序在linux下用GCC4.4.1编译后放入IDA看一下:

main            proc near
 
var_10          = dword ptr -10h
var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4
 
                push    ebp
                mov     ebp, esp
                and     esp, 0FFFFFFF0h
                sub     esp, 10h
                mov     eax, offset aADBDCD ; "a=%d; b=%d; c=%d"
                mov     [esp+10h+var_4], 3
                mov     [esp+10h+var_8], 2
                mov     [esp+10h+var_C], 1
                mov     [esp+10h+var_10], eax
                call    _printf
                mov     eax, 0
                leave
                retn
main            endp

MSVC与GCC编译后代码的不同点只是参数入栈的方法不同,这里GCC不用PUSH/POP而是直接对栈操作。

5.1.4 GCC与GDB

接着我们尝试在linux中用GDB运行下这个示例程序。

-g 表示将debug信息插入可执行文件中

$ gcc 1.c -g -o 1

反编译:

$ gdb 1
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/1...done.

表5.1 在printf()处设置断点

(gdb) b printf
Breakpoint 1 at 0x80482f0

Run 这里没有printf()函数的源码,所以GDB没法显示出源码,但是却可以这样做

(gdb) run
Starting program: /home/dennis/polygon/1
 
Breakpoint 1, __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
29 printf.c: No such file or directory.

打印10组栈中的元组数据,左边是栈中的地址

(gdb) x/10w $esp
0xbffff11c: 0x0804844a 0x080484f0 0x00000001 0x00000002
0xbffff12c: 0x00000003 0x08048460 0x00000000 0x00000000
0xbffff13c: 0xb7e29905 0x00000001

最开始的是返回地址(0x0804844a),我们可以确定在这里,于是可以反汇编这里的代码

(gdb) x/5i 0x0804844a
0x804844a <main+45>: mov $0x0,%eax
0x804844f <main+50>: leave
0x8048450 <main+51>: ret
0x8048451: xchg %ax,%ax
0x8048453: xchg %ax,%ax

两个XCHG指令,明显是一些垃圾数据,可以忽略 第二个(0x080484f0)是一处格式化字符串

(gdb) x/s 0x080484f0
0x80484f0: "a=%d; b=%d; c=%d"

而其他三个则是printf()函数的参数,另外的可能只是栈中的垃圾数据,但是也可能是其他函数的数据,例如它们的本地变量。这里可以忽略。 执行 finish ,表示执行到函数结束。在这里是执行到printf()完。

(gdb) finish
Run till exit from #0 __printf (format=0x80484f0 "a=%d; b=%d; c=%d") at printf.c:29
main () at 1.c:6
6 return 0;
Value returned is $2 = 13

GDB显示了printf()函数在eax中的返回值,这是打印字符的数量,就像在OllyDbg中一样。

我们同样看到了return 0;及这在1.c文件中第6行所代表的含义。1.c文件就在当前目录下,GDB就在那找到了字符串。但是GDB又是怎么知道当前执行到了哪一行?

事实上这和编译器有关,当生成调试信息时,同样也保存了一张代码行号与指令地址的关系表。

查看EAX中储存的13:

(gdb) info registers
eax            0xd      13
ecx            0x0      0
edx            0x0      0
ebx            0xb7fc0000       -1208221696
esp            0xbffff120       0xbffff120
ebp            0xbffff138       0xbffff138
esi            0x0      0
edi            0x0      0
eip            0x804844a        0x804844a <main+45>
...

反汇编当前的指令

(gdb) disas
Dump of assembler code for function main:
    0x0804841d <+0>:    push    %ebp
    0x0804841e <+1>:    mov     %esp,%ebp
    0x08048420 <+3>:    and     $0xfffffff0,%esp
    0x08048423 <+6>:    sub     $0x10,%esp
    0x08048426 <+9>:    movl    $0x3,0xc(%esp)
    0x0804842e <+17>:   movl    $0x2,0x8(%esp)
    0x08048436 <+25>:   movl    $0x1,0x4(%esp)
    0x0804843e <+33>:   movl    $0x80484f0,(%esp)
    0x08048445 <+40>:   call    0x80482f0 <printf@plt>
=>  0x0804844a <+45>:   mov     $0x0,%eax
    0x0804844f <+50>:   leave
    0x08048450 <+51>:   ret
End of assembler dump.

GDB默认使用AT&T语法显示,当然也可以转换至intel:

(gdb) set disassembly-flavor intel
(gdb) disas
Dump of assembler code for function main:
    0x0804841d <+0>:    push    ebp
    0x0804841e <+1>:    mov     ebp,esp
    0x08048420 <+3>:    and     esp,0xfffffff0
    0x08048423 <+6>:    sub     esp,0x10
    0x08048426 <+9>:    mov     DWORD PTR [esp+0xc],0x3
    0x0804842e <+17>:   mov     DWORD PTR [esp+0x8],0x2
    0x08048436 <+25>:   mov     DWORD PTR [esp+0x4],0x1
    0x0804843e <+33>:   mov     DWORD PTR [esp],0x80484f0
    0x08048445 <+40>:   call    0x80482f0 <printf@plt>
=>  0x0804844a <+45>:   mov     eax,0x0
    0x0804844f <+50>:   leave
    0x08048450 <+51>:   ret
End of assembler dump.

执行下一条指令,GDB显示了结束大括号,代表着这里是函数结束部分。

(gdb) step
7 };

在执行完MOV EAX, 0后我们可以看到EAX就已经变为0了。

(gdb) info registers
eax 0x0 0
ecx 0x0 0
edx 0x0 0
ebx 0xb7fc0000 -1208221696
esp 0xbffff120 0xbffff120
ebp 0xbffff138 0xbffff138
esi 0x0 0
edi 0x0 0
eip 0x804844f 0x804844f <main+50>
...

5.2 x64: 8个参数

为了看其他参数如何通过栈传递的,我们再次修改代码将参数个数增加到9个(printf()格式化字符串和8个int 变量)

#include <stdio.h>
int main() {
        printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
", 1, 2, 3, 4, 5, 6, 7, 8);
        return 0;
};

5.2.1 MSVC

正如我们之前所见,在win64下开始的4个参数传递至RCX,RDX,R8,R9寄存器,

然而 MOV指令,替代PUSH指令。用来准备栈数据,所以值都是直接写入栈中

$SG2923 DB ’a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d’, 0aH, 00H
 
main    PROC
        sub     rsp, 88
 
        mov     DWORD PTR [rsp+64], 8
        mov     DWORD PTR [rsp+56], 7
        mov     DWORD PTR [rsp+48], 6
        mov     DWORD PTR [rsp+40], 5
        mov     DWORD PTR [rsp+32], 4
        mov     r9d, 3
        mov     r8d, 2
        mov     edx, 1
        lea     rcx, OFFSET FLAT:$SG2923
        call    printf
 
        ; return 0
        xor eax, eax
 
        add     rsp, 88
        ret     0
main ENDP
_TEXT ENDS
END

表5.2:msvc 2010 x64

5.2.2 GCC

在*NIX系统,对于x86-64这也是同样的原理,除了前6个参数传递给了RDI,RSI,RDX,RCX,R8,R9寄存器。GCC将生成的代码字符指针写入了EDI而不是RDI(如果有的话)——我们在2.2.2节看到过这部分

同样我们也看到在寄存器EAX被清零前有个printf() call:

.LC0:
    .string "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
"

main:
    sub     rsp, 40

    mov     r9d, 5
    mov     r8d, 4
    mov     ecx, 3
    mov     edx, 2
    mov     esi, 1
    mov     edi, OFFSET FLAT:.LC0
    xor     eax, eax ; number of vector registers passed
    mov     DWORD PTR [rsp+16], 8
    mov     DWORD PTR [rsp+8], 7
    mov     DWORD PTR [rsp], 6
    call    printf

    ; return 0

    xor     eax, eax
    add     rsp, 40
    ret

表5.3:GCC 4.4.6 –o 3 x64

5.2.3 GCC + GDB

让我们在GDB中尝试这个例子。

$ gcc -g 2.c -o 2

反编译:

$ gdb 2
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/2...done.

表5.4:在printf()处下断点,然后run

(gdb) b printf
Breakpoint 1 at 0x400410
(gdb) run
Starting program: /home/dennis/polygon/2
Breakpoint 1, __printf (format=0x400628 "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
") at
printf.c:29
29 printf.c: No such file or directory.

寄存器RSI/RDX/RCX/R8/R9都有应有的值,RIP则是printf()函数地址

(gdb) info registers
rax     0x0     0
rbx     0x0     0
rcx     0x3     3
rdx     0x2     2
rsi     0x1     1
rdi     0x400628 4195880
rbp     0x7fffffffdf60 0x7fffffffdf60
rsp     0x7fffffffdf38 0x7fffffffdf38
r8      0x4     4
r9      0x5     5
r10     0x7fffffffdce0 140737488346336
r11     0x7ffff7a65f60 140737348263776
r12     0x400440 4195392
r13     0x7fffffffe040 140737488347200
r14     0x0     0
r15     0x0     0
rip     0x7ffff7a65f60 0x7ffff7a65f60 <__printf>
...

表5.5 检查格式化字符串

(gdb) x/s $rdi
0x400628: "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
"

用x/g命令显示栈内容

(gdb) x/10g $rsp
0x7fffffffdf38: 0x0000000000400576 0x0000000000000006
0x7fffffffdf48: 0x0000000000000007 0x00007fff00000008
0x7fffffffdf58: 0x0000000000000000 0x0000000000000000
0x7fffffffdf68: 0x00007ffff7a33de5 0x0000000000000000
0x7fffffffdf78: 0x00007fffffffe048 0x0000000100000000

与之前一样,第一个栈元素是返回地址,我们也同时也看到在高32位的8也没有被清除。 0x00007fff00000008,这是因为是32位int类型的,因此,高寄存器或堆栈部分可能包含一些随机垃圾数值。

printf()函数执行之后将返回控制,GDB会显示整个main()函数。

(gdb) set disassembly-flavor intel
(gdb) disas 0x0000000000400576
Dump of assembler code for function main:
    0x000000000040052d <+0>:    push    rbp
    0x000000000040052e <+1>:    mov     rbp,rsp
    0x0000000000400531 <+4>:    sub     rsp,0x20
    0x0000000000400535 <+8>:    mov     DWORD PTR [rsp+0x10],0x8
    0x000000000040053d <+16>:   mov     DWORD PTR [rsp+0x8],0x7
    0x0000000000400545 <+24>:   mov     DWORD PTR [rsp],0x6
    0x000000000040054c <+31>:   mov     r9d,0x5
    0x0000000000400552 <+37>:   mov     r8d,0x4
    0x0000000000400558 <+43>:   mov     ecx,0x3
    0x000000000040055d <+48>:   mov     edx,0x2
    0x0000000000400562 <+53>:   mov     esi,0x1
    0x0000000000400567 <+58>:   mov     edi,0x400628
    0x000000000040056c <+63>:   mov     eax,0x0
    0x0000000000400571 <+68>:   call    0x400410 <printf@plt>
    0x0000000000400576 <+73>:   mov     eax,0x0
    0x000000000040057b <+78>:   leave
    0x000000000040057c <+79>:   ret
End of assembler dump.

执行完printf()后,就会清零EAX,然后发现EAX早已为0,RIP现在则指向LEAVE指令。

(gdb) finish
Run till exit from #0 __printf (format=0x400628 "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
n") at printf.c:29
a=1; b=2; c=3; d=4; e=5; f=6; g=7; h=8
main () at 2.c:6
6       return 0;
Value returned is $1 = 39
(gdb) next
7 };
(gdb) info registers
rax     0x0     0
rbx     0x0     0
rcx     0x26    38
rdx     0x7ffff7dd59f0 140737351866864
rsi     0x7fffffd9 2147483609
rdi     0x0     0
rbp     0x7fffffffdf60 0x7fffffffdf60
rsp     0x7fffffffdf40 0x7fffffffdf40
r8      0x7ffff7dd26a0 140737351853728
r9      0x7ffff7a60134 140737348239668
r10     0x7fffffffd5b0 140737488344496
r11     0x7ffff7a95900 140737348458752
r12     0x400440 4195392
r13     0x7fffffffe040 140737488347200
r14     0x0     0
r15     0x0     0
rip     0x40057b 0x40057b <main+78>
...

5.3 ARM:3个参数

习惯上,ARM传递参数的规则(参数调用)如下:前4个参数传递给了R0-R3寄存器,其余的参数则在栈中。这和fastcall或者win64传递参数很相似

5.3.1 Non-optimizing Keil + ARM mode(非优化keil编译模式 + ARM环境)

.text:00000014            printf_main1
.text:00000014 10 40 2D E9       STMFD   SP!, {R4,LR}
.text:00000018 03 30 A0 E3       MOV     R3, #3
.text:0000001C 02 20 A0 E3       MOV     R2, #2
.text:00000020 01 10 A0 E3       MOV     R1, #1
.text:00000024 1D 0E 8F E2       ADR     R0, aADBDCD ; "a=%d; b=%d; c=%d
"
.text:00000028 0D 19 00 EB       BL      __2printf
.text:0000002C 10 80 BD E8       LDMFD   SP!, {R4,PC}

所以 前四个参数按照它们的顺序传递给了R0-R3, printf()中的格式化字符串指针在R0中,然后1在R1,2在R2,3在R3. 到目前为止没有什么不寻常的。

5.3.2 Optimizing Keil + ARM mode(优化的keil编译模式 + ARM环境)

.text:00000014     EXPORT printf_main1
.text:00000014     printf_main1
.text:00000014 03 30 A0 E3     MOV    R3, #3
.text:00000018 02 20 A0 E3     MOV    R2, #2
.text:0000001C 01 10 A0 E3     MOV    R1, #1
.text:00000020 1E 0E 8F E2     ADR    R0, aADBDCD ; "a=%d; b=%d; c=%d
"
.text:00000024 CB 18 00 EA     B     __2printf

表5.7: Optimizing Keil + ARM mode

这是在针对ARM optimized (-O3)版本下的,我们可以B作为最后一个指令而不是熟悉的BL。另外一个不同之处在optimized与之前的(compiled without optimization)对比发现函数prologue 和 epilogue(储存R0和LR值的寄存器),B指令仅仅跳向另一处地址,没有任何关于LR寄存器的操作,也就是说它和x86中的jmp相似,为什么会这样?因为代码就是这样,事实上,这和前面相似,主要有两点原因 1)不管是栈还是SP(栈指针),都有被修改。2)printf()的调用是最后的指令,所以之后便没有了。完成之后,printf()函数就返回到LR储存的地址处。但是指针地址从函数调用的地方转移到了LR中!接着就会从printf()到那里。结果,我们不需要保存LR,因为我们没有必要修改LR。因为除了printf()函数外没有其他函数了。另外,除了这个调用外,我们不需要再做别的。这就是为什么这样编译是可行的。

5.3.3 Optimizing Keil + thumb mode

.text:0000000C     printf_main1
.text:0000000C 10 B5           PUSH {R4,LR}
.text:0000000E 03 23           MOVS R3, #3
.text:00000010 02 22           MOVS R2, #2
.text:00000012 01 21           MOVS R1, #1
.text:00000014 A4 A0           ADR R0, aADBDCD ; "a=%d; b=%d; c=%d
"
.text:00000016 06 F0 EB F8     BL __2printf
.text:0000001A 10 BD           POP {R4,PC}

表5.8:Optimizing Keil + thumb mode

和non-optimized for ARM mode代码没什么明显的区别

5.4 ARM: 8 arguments

我们再用之前9个参数的那个例子

void printf_main2()
{
    printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d
", 1, 2, 3, 4, 5, 6, 7, 8);
};

5.4.1 Optimizing Keil: ARM mode

.text:00000028      printf_main2
.text:00000028
.text:00000028      var_18 = -0x18
.text:00000028      var_14 = -0x14
.text:00000028      var_4 = -4
.text:00000028
.text:00000028 04 E0 2D E5      STR     LR, [SP,#var_4]!
.text:0000002C 14 D0 4D E2      SUB     SP, SP, #0x14
.text:00000030 08 30 A0 E3      MOV     R3, #8
.text:00000034 07 20 A0 E3      MOV     R2, #7
.text:00000038 06 10 A0 E3      MOV     R1, #6
.text:0000003C 05 00 A0 E3      MOV     R0, #5
.text:00000040 04 C0 8D E2      ADD     R12, SP, #0x18+var_14
.text:00000044 0F 00 8C E8      STMIA   R12, {R0-R3}
.text:00000048 04 00 A0 E3      MOV     R0, #4
.text:0000004C 00 00 8D E5      STR     R0, [SP,#0x18+var_18]
.text:00000050 03 30 A0 E3      MOV     R3, #3
.text:00000054 02 20 A0 E3      MOV     R2, #2
.text:00000058 01 10 A0 E3      MOV     R1, #1
.text:0000005C 6E 0F 8F E2      ADR     R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d;
e=%d; f=%d; g=%"...
.text:00000060 BC 18 00 EB      BL      __2printf
.text:00000064 14 D0 8D E2      ADD     SP, SP, #0x14
.text:00000068 04 F0 9D E4      LDR     PC, [SP+4+var_4],#4

这些代码可以分成几个部分:

Function prologue:

最开始的”STR LR, [SP,#var_4]!”指令将LR储存在栈中,因为我们将用这个寄存器调用printf()。

第二个” SUB SP, SP, #0x14”指令减了SP(栈指针),为了在栈上分配0x14(20)bytes的内存,实际上我们需要传递5个 32-bit的数据通过栈传递给printf()函数,而且每个占4bytes,也就是5*4=20。另外4个32-bit的数据将会传递给寄存器。

通过栈传递5,6,7和8:

然后,5,6,7,8分别被写入了R0,R1,R2及R3寄存器。然后ADD R12, SP,#0x18+var_14指令将栈中指针的地址写入,并且在这里会向R12写入4个值,var_14是一个汇编宏,相当于0x14,这些都由IDA简明的创建表示访问栈的变量,var_?在IDA中表示栈中的本地变量,所以SP+4将被写入R12寄存器。下一步的”STMIA R12, R0-R3”指令将R0-R3寄存器的内容写在了R2指向的指针处。STMIA指令指Store Multiple Increment After, Increment After指R12寄存器在有值写入后自增4。

通过栈传递4:

4存在R0中,然后这个值在” STR R0, [SP,#0x18+var_18]”指令帮助下,存在了栈上,var_18是0x18,偏移量为0.所以R0寄存器中的值将会写在SP指针指向的指针处。

通过寄存器传递1,2,3:

开始3个数(a,b,c)(分别是1,2,3)正好在printf()函数调用前被传递到了R1,R2,R3寄存器中。 然后另外5个值通过栈传递。

printf() 调用:

“ADD SP, SP, #0x14”指令将SP指针返回到之前的指针处,因此清除了栈,当然,栈中之前写入的数据还在那,但是当后来的函数被调用时那里则会被重写。 “LDR PC, [SP+4+var_4],#4”指令将LR中储存的值载入到PC指针,因此函数结束。

5.4.2 Optimizing Keil: thumb mode

.text:0000001C      printf_main2
.text:0000001C
.text:0000001C      var_18 = -0x18
.text:0000001C      var_14 = -0x14
.text:0000001C      var_8 = -8
.text:0000001C
.text:0000001C 00 B5        PUSH    {LR}
.text:0000001E 08 23        MOVS    R3, #8
.text:00000020 85 B0        SUB     SP, SP, #0x14
.text:00000022 04 93        STR     R3, [SP,#0x18+var_8]
.text:00000024 07 22        MOVS    R2, #7
.text:00000026 06 21        MOVS    R1, #6
.text:00000028 05 20        MOVS    R0, #5
.text:0000002A 01 AB        ADD     R3, SP, #0x18+var_14
.text:0000002C 07 C3        STMIA   R3!, {R0-R2}
.text:0000002E 04 20        MOVS    R0, #4
.text:00000030 00 90        STR     R0, [SP,#0x18+var_18]
.text:00000032 03 23        MOVS    R3, #3
.text:00000034 02 22        MOVS    R2, #2
.text:00000036 01 21        MOVS    R1, #1
.text:00000038 A0 A0        ADR     R0, aADBDCDDDEDFDGD ; "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"...
.text:0000003A 06 F0 D9 F8  BL __2printf
.text:0000003E
.text:0000003E              loc_3E ; CODE XREF: example13_f+16
.text:0000003E 05 B0        ADD SP, SP, #0x14
.text:00000040 00 BD        POP {PC}

几乎和之前的例子是一样的,然后这是thumb 代码,值入栈的确不同:先是8,然后5,6,7,第三个是4。

5.4.3 Optimizing Xcode (LLVM): ARM mode

__text:0000290C     _printf_main2
__text:0000290C
__text:0000290C     var_1C = -0x1C
__text:0000290C     var_C = -0xC
__text:0000290C
__text:0000290C 80 40 2D E9     STMFD   SP!, {R7,LR}
__text:00002910 0D 70 A0 E1     MOV     R7, SP
__text:00002914 14 D0 4D E2     SUB     SP, SP, #0x14
__text:00002918 70 05 01 E3     MOV     R0, #0x1570
__text:0000291C 07 C0 A0 E3     MOV     R12, #7
__text:00002920 00 00 40 E3     MOVT    R0, #0
__text:00002924 04 20 A0 E3     MOV     R2, #4
__text:00002928 00 00 8F E0     ADD     R0, PC, R0
__text:0000292C 06 30 A0 E3     MOV     R3, #6
__text:00002930 05 10 A0 E3     MOV     R1, #5
__text:00002934 00 20 8D E5     STR     R2, [SP,#0x1C+var_1C]
__text:00002938 0A 10 8D E9     STMFA   SP, {R1,R3,R12}
__text:0000293C 08 90 A0 E3     MOV     R9, #8
__text:00002940 01 10 A0 E3     MOV     R1, #1
__text:00002944 02 20 A0 E3     MOV     R2, #2
__text:00002948 03 30 A0 E3     MOV     R3, #3
__text:0000294C 10 90 8D E5     STR     R9, [SP,#0x1C+var_C]
__text:00002950 A4 05 00 EB     BL      _printf
__text:00002954 07 D0 A0 E1     MOV     SP, R7
__text:00002958 80 80 BD E8     LDMFD   SP!, {R7,PC}

几乎和我们之前遇到的一样,除了STMFA(Store Multiple Full Ascending)指令,它和STMIB(Store Multiple Increment Before)指令一样,这个指令直到下个寄存器的值写入内存时会增加SP寄存器中的值,但是反过来却不同。

另外一个地方我们可以轻松的发现指令是随机分布的,例如,R0寄存器中的值在三个地方初始,在0x2918,0x2920,0x2928。而这一个指令就可以搞定。然而,optimizing compiler有它自己的原因,对于如何更好的放置指令,通常,处理器尝试同时执行并行的指令,例如像” MOVT R0, #0”和” ADD R0, PC,R0”就不能同时执行了,因为它们同时都在修改R0寄存器,另一方面”MOVT R0, #0”和”MOV R2, #4”指令却可以同时执行,因为执行效果并没有任何冲突。 大概,编译器就是这样尝试编译的,可能。

5.4.4 Optimizing Xcode (LLVM): thumb-2 mode

__text:00002BA0     _printf_main2
__text:00002BA0
__text:00002BA0     var_1C = -0x1C
__text:00002BA0     var_18 = -0x18
__text:00002BA0     var_C = -0xC
__text:00002BA0
__text:00002BA0 80 B5           PUSH    {R7,LR}
__text:00002BA2 6F 46           MOV     R7, SP
__text:00002BA4 85 B0           SUB     SP, SP, #0x14
__text:00002BA6 41 F2 D8 20     MOVW    R0, #0x12D8
__text:00002BAA 4F F0 07 0C     MOV.W   R12, #7
__text:00002BAE C0 F2 00 00     MOVT.W  R0, #0
__text:00002BB2 04 22           MOVS    R2, #4
__text:00002BB4 78 44           ADD     R0, PC ; char *
__text:00002BB6 06 23           MOVS    R3, #6
__text:00002BB8 05 21           MOVS    R1, #5
__text:00002BBA 0D F1 04 0E     ADD.W   LR, SP, #0x1C+var_18
__text:00002BBE 00 92           STR     R2, [SP,#0x1C+var_1C]
__text:00002BC0 4F F0 08 09     MOV.W   R9, #8
__text:00002BC4 8E E8 0A 10     STMIA.W LR, {R1,R3,R12}
__text:00002BC8 01 21           MOVS    R1, #1
__text:00002BCA 02 22           MOVS    R2, #2
__text:00002BCC 03 23           MOVS    R3, #3
__text:00002BCE CD F8 10 90     STR.W   R9, [SP,#0x1C+var_C]
__text:00002BD2 01 F0 0A EA     BLX     _printf
__text:00002BD6 05 B0           ADD     SP, SP, #0x14
__text:00002BD8 80 BD           POP     {R7,PC}

几乎和前面的例子相同,除了thumb-instructions在这里被替代使用了

5.5 by the way

值得一提的是,这些x86,x64,fastcall和ARM传递参数的不同表现了CPU并不在意函数参数是怎样传递的,同样也假想编译器可能用特殊的结构传送参数而一点也不是通过栈。

Chapter 6 scanf()

现在我们来使用scanf()。

#include <stdio.h>
int main() 
{
    int x;
    printf ("Enter X:
");
    scanf ("%d", &x);
    printf ("You entered %d...
", x);
    return 0; 
};

好吧,我承认现在使用scanf()是不明智的,但是我想说明如何把指针传递给int变量。

6.1 关于指针

这是计算机科学中最基础的概念之一。通常,大数组、结构或对象经常被传递给其它函数,而传递它们的地址要更加简单。更重要的是:如果调用函数要修改数组或结构中的数据,并且作为整体返回,那么最简单的办法就是把数组或结构的地址传递给函数,让函数进行修改。

在C/C++中指针就是某处内存的地址。

在x86中,地址是以32位数表示的(占4字节);在x86-64中是64位数(占8字节)。顺便一说,这也是为什么有些人在改用x86-64时感到愤怒——x64架构中所有的指针需要的空间是原来的两倍。

通过某种方法,只使用无类型指针也是可行的。例如标准C函数memcpy(),用于把一个区块复制到另外一个区块上,需要两个void*型指针作为输入,因为你无法预知,也无需知道要复制区块的类型,区块的大小才是重要的。

当函数需要一个以上的返回值时也经常用到指针(等到第九章再讲)。scanf()就是这样,函数除了要显示成功读入的字符个数外,还要返回全部值。

在C/C++中,指针类型只是用于在编译阶段进行类型检查。本质上,在已编译的代码中并不包含指针类型的信息。

6.2 x86

6.2.1 MSVC

MVSC 2010编译后得到下面代码

CONST SEGMENT
$SG3831 DB ’Enter X:’, 0aH, 00H
$SG3832 DB ’%d’, 00H
35
6.2. X86 CHAPTER 6. SCANF()
$SG3833 DB ’You entered %d...’, 0aH, 00H
CONST ENDS
PUBLIC _main
EXTRN _scanf:PROC
EXTRN _printf:PROC
; Function compile flags: /Odtp
_TEXT SEGMENT
_x$ = -4 ; size = 4
_main PROC
        push    ebp
        mov     ebp, esp
        push    ecx
        push    OFFSET $SG3831 ; ’Enter X:’
        call    _printf
        add     esp, 4
        lea     eax, DWORD PTR _x$[ebp]
        push    eax
        push    OFFSET $SG3832 ; ’%d’
        call    _scanf
        add     esp, 8
        mov     ecx, DWORD PTR _x$[ebp]
        push    ecx
        push    OFFSET $SG3833 ; ’You entered %d...’
        call    _printf
        add     esp, 8
        ; return 0
        xor     eax, eax
        mov     esp, ebp
        pop     ebp
        ret     0
_main ENDP
_TEXT ENDS

X是局部变量。

C/C++标准告诉我们它只对函数内部可见,无法从外部访问。习惯上,局部变量放在栈中。也可能有其他方法,但在x86中是这样。

函数序言后下一条指令PUSH ECX目的并不是要存储ECX的状态(注意程序结尾没有与之相对的POP ECX)。

事实上这条指令仅仅是在栈中分配了4字节用于存储变量x。

变量x可以用宏 _x$ 来访问(等于-4),EBP寄存器指向当前栈帧。

在一个函数执行完之后,EBP将指向当前栈帧,就无法通过EBP+offset来访问局部变量和函数参数了。

也可以使用ESP寄存器,但由于它经常变化所以使用不方便。所以说在函数刚开始时,EBP的值保存了此时ESP的值。

下面是一个非常典型的32位栈帧结构

...
EBP-8 local variable #2, marked in IDA as var_8
EBP-4 local variable #1, marked in IDA as var_4
EBP saved value of EBP
EBP+4 return address
EBP+8 argument#1, marked in IDA as arg_0
EBP+0xC argument#2, marked in IDA as arg_4
EBP+0x10 argument#3, marked in IDA as arg_8
...

在我们的例子中,scanf()有两个参数。

第一个参数是指向“%d”的字符串指针,第二个是变量x的地址。

首先,lea eax, DWORD PTR _x$[ebp]指令将变量x的地址放入EAX寄存器。LEA作用是“取有效地址”,然而之后的主要用途有所变化(b.6.2)。

可以说,LEA在这里只是把EBP的值与宏 _x$的值相乘,并存储在EAX寄存器中。

lea eax, [ebp-4]也是一样。

EBP的值减去4,结果放在EAX寄存器中。接着EAX寄存器的值被压入栈中,再调用printf()。

之后,printf()被调用。第一个参数是一个字符串指针:“You entered %d …”。

第二个参数是通过mov ecx, [ebp-4]使用的,这个指令把变量x的内容传给ECX而不是它的地址。

然后,ECX的值放入栈中,接着最后一次调用printf()。

6.2.2 MSVC+OllyDbg

让我们在OllyDbg中使用这个例子。首先载入程序,按F8直到进入我们的可执行文件而不是ntdll.dll。往下滚动屏幕找到main()。点击第一条指令(PUSH EBP),按F2,再按F9,触发main()开始处的断点。

让我们来跟随到准备变量x的地址的位置。图6.2

可以右击寄存器窗口的EAX,再点击“堆栈窗口中跟随”。这个地址会在堆栈窗口中显示。观察,这是局部栈中的一个变量。我在图中用红色箭头标出。这里是一些无用数据(0x77D478)。PUSH指令将会把这个栈元素的地址压入栈中。然后按F8直到scanf()函数执行完。在scanf()执行时,我们要在命令行窗口中输入,例如输入123。

图6.1 命令行输出

scanf()在这里执行。图6.3。scanf()在EAX中返回1,这意味着成功读入了一个值。现在我们关心的那个栈元素中的值是0x7B(123)。

接下来,这个值从栈中复制到ECX寄存器中,然后传递给printf()。图6.4

图6.2 OllyDbg:计算局部变量的地址

图6.3:OllyDbg:scanf()执行

图6.4:OllyDbg:准备把值传递给printf()

6.2.3 GCC

让我们在Linux GCC 4.4.1下编译这段代码

GCC把第一个调用的printf()替换成了puts(),原因在2.3.3节中讲过了。

和之前一样,参数都是用MOV指令放入栈中。

6.3 x64

和原来一样,只是传递参数时不使用栈而使用寄存器。

6.3.1 MSVC

_DATA   SEGMENT
$SG1289 DB ’Enter X:’, 0aH, 00H
$SG1291 DB ’%d’, 00H
$SG1292 DB ’You entered %d...’, 0aH, 00H
_DATA   ENDS
 
_TEXT   SEGMENT
x$ = 32
main    PROC
$LN3:
        sub rsp, 56
        lea rcx, OFFSET FLAT:$SG1289 ; ’Enter X:’
        call printf
        lea rdx, QWORD PTR x$[rsp]
        lea rcx, OFFSET FLAT:$SG1291 ; ’%d’
        call scanf
        mov edx, DWORD PTR x$[rsp]
        lea rcx, OFFSET FLAT:$SG1292 ; ’You entered %d...’
        call printf
        ; return 0
        xor eax, eax
        add rsp, 56
        ret 0
main    ENDP
_TEXT   ENDS

6.3.2 GCC

.LC0:
        .string "Enter X:"
.LC1:
        .string "%d"
.LC2:
        .string "You entered %d...
"
main:
        sub     rsp, 24
        mov     edi, OFFSET FLAT:.LC0 ; "Enter X:"
        call    puts
        lea     rsi, [rsp+12]
        mov     edi, OFFSET FLAT:.LC1 ; "%d"
        xor     eax, eax
        call    __isoc99_scanf
        mov     esi, DWORD PTR [rsp+12]
        mov     edi, OFFSET FLAT:.LC2 ; "You entered %d...
"
        xor     eax, eax
        call    printf
        ; return 0
        xor     eax, eax
        add     rsp, 24
        ret

6.4 ARM

6.4.1 keil优化+thumb mode

.text:00000042      scanf_main
.text:00000042
.text:00000042      var_8 = -8
.text:00000042
.text:00000042 08 B5            PUSH    {R3,LR}
.text:00000044 A9 A0            ADR     R0, aEnterX ; "Enter X:
"
.text:00000046 06 F0 D3 F8      BL      __2printf
.text:0000004A 69 46            MOV     R1, SP
.text:0000004C AA A0            ADR     R0, aD ; "%d"
.text:0000004E 06 F0 CD F8      BL      __0scanf
.text:00000052 00 99            LDR     R1, [SP,#8+var_8]
.text:00000054 A9 A0            ADR     R0, aYouEnteredD___ ; "You entered %d...
"
.text:00000056 06 F0 CB F8      BL      __2printf
.text:0000005A 00 20            MOVS    R0, #0
.text:0000005C 08 BD            POP     {R3,PC}

必须把一个指向int变量的指针传递给scanf(),这样才能通过这个指针返回一个值。Int是一个32位的值,所以我们在内存中需要4字节存储,并且正好符合32位的寄存器。局部变量x的空间分配在栈中,IDA把他命名为var_8。然而并不需要分配空间,因为栈指针指向的空间可以被立即使用。所以栈指针的值被复制到R1寄存器中,然后和格式化字符串一起送入scanf()。然后LDR指令将这个值从栈中送入R1寄存器,用以送入printf()中。

用ARM-mode和Xcode LLVM编译的代码区别不大,这里略去。

6.5 Global Variables

如果之前的例子中的x变量不再是本地变量而是全局变量呢?那么就有机会接触任何指针,不仅仅是函数体,全局变量被认为anti-pattern(通常被认为是一个不好的习惯),但是为了试验,我们可以这样做。

#include <stdio.h>
int x;
int main()
{
    printf ("Enter X:
");
    scanf ("%d", &x);
    printf ("You entered %d...
", x);
    return 0;
};

6.5.1 MSVC: x86

_DATA       SEGMENT
COMM        _x:DWORD
$SG2456     DB      ’Enter X:’, 0aH, 00H
$SG2457     DB      ’%d’, 00H
$SG2458     DB      ’You entered %d...’, 0aH, 00H
_DATA   ENDS
PUBLIC  _main
EXTRN   _scanf:PROC
EXTRN   _printf:PROC
; Function compile flags: /Odtp
_TEXT   SEGMENT
_main   PROC
    push    ebp
    mov     ebp, esp
    push    OFFSET $SG2456
    call    _printf
    add     esp, 4
    push    OFFSET _x
    push    OFFSET $SG2457
    call    _scanf
    add     esp, 8
    mov     eax, DWORD PTR _x
    push    eax
    push    OFFSET $SG2458
    call    _printf
    add     esp, 8
    xor     eax, eax
    pop     ebp
    ret     0
_main ENDP
_TEXT ENDS

现在x变量被定义为在_DATA部分,局部堆栈不允许再分配任何内存,除了直接访问内存所有通过栈的访问都不被允许。在执行的文件中全局变量还未初始化(实际上,我们为什么要在执行文件中为未初始化的变量分配一块?)但是当访问这里时,系统会在这里分配一块0值。

现在让我们来分析变量的分配。

int x=10; // default value

我们得到:

_DATA   SEGMENT
_x      DD      0aH
...

这里我们看见一个双字节的值0xA(DD 表示双字节 = 32bit)

如果你在IDA中打开compiled.exe,你会发现x变量被放置在_DATA块的开始处,接着你就会看见文本字符串。

如果你在IDA中打开之前例子中的compiled.exe中X变量没有定义的地方,你就会看见像这样的东西:

.data:0040FA80 _x               dd ?        ; DATA XREF: _main+10
.data:0040FA80                              ; _main+22
.data:0040FA84 dword_40FA84     dd ?        ; DATA XREF: _memset+1E
.data:0040FA84                              ; unknown_libname_1+28
.data:0040FA88 dword_40FA88     dd ?        ; DATA XREF: ___sbh_find_block+5
.data:0040FA88                              ; ___sbh_free_block+2BC
.data:0040FA8C ; LPVOID lpMem
.data:0040FA8C lpMem            dd ?        ; DATA XREF: ___sbh_find_block+B
.data:0040FA8C                              ; ___sbh_free_block+2CA
.data:0040FA90 dword_40FA90     dd ?        ; DATA XREF: _V6_HeapAlloc+13
.data:0040FA90                              ; __calloc_impl+72
.data:0040FA94 dword_40FA94     dd ?        ; DATA XREF: ___sbh_free_block+2FE

被_x替换了?其它变量也并未要求初始化,这也就是说在载入exe至内存后,在这里有一块针对所有变量的空间,并且还有一些随机的垃圾数据。但在在exe中这些没有初始化的变量并不影响什么,比如它适合大数组。

6.5.2 MSVC: x86 + OllyDbg

到这里事情就变得简单了(见表6.5),变量都在data部分,顺便说一句,在PUSH指令后,压入x的地址,被执行后,地址将会在栈中显示,那么右击元组数据,点击“Fllow in dump”,然后变量就会在左侧内存窗口显示.

在命令行窗口中输入123后,这里就会显示0x7B

但是为什么第一个字节是7B?合理的猜测,这里会有一组00 00 7B,被称为是字节顺序,然后在x86中使用的是小端,也就是说低位数据先写,高位数据后写。

不一会,这里的32-bit值就会载入到EAX中,然后被传递给printf().

X变量地址是0xDC3390.在OllyDbg中我们看进程内存映射(Alt-M),然后发现这个地在PE文件.data结构处。见表6.6

表6.5 OllyDbg: scanf()执行后

表6.6: OllyDbg 进程内存映射

6.5.3 GCC: x86

这和linux中几乎是一样的,除了segment的名称和属性:未初始化变量被放置在_bss部分。

在ELF文件格式中,这部分数据有这样的属性:

; Segment type: Uninitialized
; Segment permissions: Read/Write

如果静态的分配一个值,比如10,它将会被放在_data部分,这部分有下面的属性:

; Segment type: Pure data
; Segment permissions: Read/Write

6.5.4 MSVC: x64

_DATA       SEGMENT
COMM        x:DWORD
$SG2924     DB      ’Enter X:’, 0aH, 00H
$SG2925     DB      ’%d’, 00H
$SG2926     DB      ’You entered %d...’, 0aH, 00H
_DATA       ENDS

_TEXT       SEGMENT
main        PROC
$LN3:
            sub     rsp, 40
            lea     rcx, OFFSET FLAT:$SG2924 ; ’Enter X:’
            call    printf
            lea     rdx, OFFSET FLAT:x
            lea     rcx, OFFSET FLAT:$SG2925 ; ’%d’
            call    scanf
            mov     edx, DWORD PTR x
            lea     rcx, OFFSET FLAT:$SG2926 ; ’You entered %d...’
            call    printf
            ; return 0
            xor     eax, eax
            add     rsp, 40
            ret     0
main ENDP
_TEXT ENDS

几乎和x86中的代码是一样的,发现x变量的地址传递给scanf()用的是LEA指令,尽管第二处传递给printf()变量时用的是MOV指令,“DWORD PTR”——是汇编语言中的一部分(和机器码没有联系)。这就表示变量数据类型是32-bit,于是MOV指令就被编码了。

6.5.5 ARM:Optimizing Keil + thumb mode

.text:00000000 ; Segment type: Pure code
.text:00000000          AREA .text, CODE
...
.text:00000000 main
.text:00000000                  PUSH    {R4,LR}
.text:00000002                  ADR     R0, aEnterX         ; "Enter X:
"
.text:00000004                  BL      __2printf
.text:00000008                  LDR     R1, =x
.text:0000000A                  ADR     R0, aD              ; "%d"
.text:0000000C                  BL      __0scanf
.text:00000010                  LDR     R0, =x
.text:00000012                  LDR     R1, [R0]
.text:00000014                  ADR     R0, aYouEnteredD___ ; "You entered %d...
"
.text:00000016                  BL      __2printf
.text:0000001A                  MOVS    R0, #0
.text:0000001C                  POP     {R4,PC}
...
.text:00000020 aEnterX          DCB     "Enter X:",0xA,0    ; DATA XREF: main+2
.text:0000002A                  DCB     0
.text:0000002B                  DCB     0
.text:0000002C off_2C           DCD x                       ; DATA XREF: main+8
.text:0000002C                                      ; main+10
.text:00000030 aD               DCB     "%d",0              ; DATA XREF: main+A
.text:00000033                  DCB 0
.text:00000034 aYouEnteredD___  DCB "You entered %d...",0xA,0 ; DATA XREF: main+14
.text:00000047                  DCB 0
.text:00000047 ; .text          ends
.text:00000047
...
.data:00000048 ; Segment type:  Pure data
.data:00000048                  AREA .data, DATA
.data:00000048                  ; ORG 0x48
.data:00000048                  EXPORT x
.data:00000048 x                DCD 0xA                     ; DATA XREF: main+8
.data:00000048                                              ; main+10
.data:00000048                                              ; .data ends

那么,现在x变量以某种方式变为全局的,现在被放置在另一个部分中。命名为data块(.data)。有人可能会问,为什么文本字符串被放在了代码块(.text),而且x可以被放在这?因为这是变量,而且根据它的定义,它可以变化,也有可能会频繁变化,不频繁变化的代码块可以被放置在ROM中,变化的变量在RAM中,当有ROM时在RAM中储存不变的变量是不利于节约资源的。

此外,RAM中数据部分常量必须在之前初始化,因为在RAM使用后,很明显,将会包含杂乱的信息。

继续向前,我们可以看到,在代码片段,有个指针指向X变量(0ff_2C)。然后所有关于变量的操作都是通过这个指针。这也是x变量可以被放在远离这里地方的原因。所以他的地址一定被存在离这很近的地方。LDR指令在thumb模式下只可访问指向地址在1020bytes内的数据。同样的指令在ARM模式下——范围就达到了4095bytes,也就是x变量地址一定要在这附近的原因。因为没法保证链接时会把这个变量放在附近。

另外,如果变量以const声明,Keil编译环境下则会将变量放在.constdata部分,大概从那以后,链接时就可以把这部分和代码块放在ROM里了。

6.6 scanf()结果检查

正如我之前所见的,现在使用scanf()有点过时了,但是如过我们不得不这样做时,我们需要检查scanf()执行完毕时是否发生了错误。

#include <stdio.h>
int main()
{
    int x;
    printf ("Enter X:
");

    if (scanf ("%d", &x)==1)
        printf ("You entered %d...
", x);
    else
        printf ("What you entered? Huh?
");

    return 0;
};

按标准,scanf()函数返回成功获取的字段数。

在我们的例子中,如果事情顺利,用户输入一个数字,scanf()将会返回1或0或者错误情况下返回EOF.

这里,我们添加了一些检查scanf()结果的c代码,用来打印错误信息:

按照预期的回显:

C:...>ex3.exe
Enter X:
123
You entered 123...
 
C:...>ex3.exe
Enter X:
ouch
What you entered? Huh?

6.6.1 MSVC: x86

我们可以得到这样的汇编代码(msvc2010):

        lea     eax, DWORD PTR _x$[ebp]
        push    eax
        push    OFFSET $SG3833 ; ’%d’, 00H
        call    _scanf
        add     esp, 8
        cmp     eax, 1
        jne     SHORT $LN2@main
        mov     ecx, DWORD PTR _x$[ebp]
        push    ecx
        push    OFFSET $SG3834 ; ’You entered %d...’, 0aH, 00H
        call    _printf
        add     esp, 8
        jmp     SHORT $LN1@main
$LN2@main:
        push    OFFSET $SG3836 ; ’What you entered? Huh?’, 0aH, 00H
        call    _printf
        add     esp, 4
$LN1@main:
        xor     eax, eax

调用函数(main())必须能够访问到被调用函数(scanf())的结果,所以callee把这个值留在了EAX寄存器中。

然后我们在“CMP EAX, 1”指令的帮助下,换句话说,我们将eax中的值与1进行比较。

JNE根据CMP的结果判断跳至哪,JNE表示(jump if Not Equal)

所以,如果EAX中的值不等于1,那么处理器就会将执行流程跳转到JNE指向的,在我们的例子中是$LN2@main,当流程跳到这里时,CPU将会带着参数“What you entered? Huh?”执行printf(),但是执行正常,就不会发生跳转,然后另外一个printf()就会执行,两个参数为“You entered %d…”及x变量的值。

因为第二个printf()并没有被执行,后面有一个JMP(无条件跳转),就会将执行流程到第二个printf()后“XOR EAX, EAX”前,执行完返回0。

那么,可以这么说,比较两个值通常使用CMP/Jcc这对指令,cc是条件码,CMP比较两个值,然后设置processor flag,Jcc检查flags然后判断是否跳。

但是事实上,这却被认为是诡异的。但是CMP指令事实上,但是CMP指令实际上是SUB(subtract),所有算术指令都会设置processor flags,不仅仅只有CMP,当我们比较1和1时,1结果就变成了0,ZF flag就会被设定(表示最后一次的比较结果为0),除了两个数相等以外,再没有其他情况了。JNE 检查ZF flag,如果没有设定就会跳转。JNE实际上就是JNZ(Jump if Not Zero)指令。JNE和JNZ的机器码都是一样的。所以CMP指令可以被SUB指令代替,几乎一切的都没什么变化。但是SUB会改变第一个数,CMP是“SUB without saving result”.

6.6.2 MSVC: x86:IDA

现在是时候打开IDA然后尝试做些什么了,顺便说一句。对于初学者来说使用在MSVC中使用/MD是个非常好的主意。这样所有独立的函数不会从可执行文件中link,而是从MSVCR*.dll。因此这样可以简单明了的发现函数在哪里被调用。

当在IDA中分析代码时,建议一定要做笔记。比如在分析这个例子的时候,我们看到了JNZ将要被设置为error,所以点击标注,然后标注为“error”。另外一处标注在“exit”:

.text:00401000 _main proc near
.text:00401000
.text:00401000 var_4        = dword ptr -4
.text:00401000 argc         = dword ptr 8
.text:00401000 argv         = dword ptr 0Ch
.text:00401000 envp         = dword ptr 10h
.text:00401000
.text:00401000              push    ebp
.text:00401001              mov     ebp, esp
.text:00401003              push    ecx
.text:00401004              push    offset Format   ; "Enter X:
"
.text:00401009              call    ds:printf
.text:0040100F              add     esp, 4
.text:00401012              lea     eax, [ebp+var_4]
.text:00401015              push    eax
.text:00401016              push    offset aD       ; "%d"
.text:0040101B              call    ds:scanf
.text:00401021              add     esp, 8
.text:00401024              cmp     eax, 1
.text:00401027              jnz     short error
.text:00401029              mov     ecx, [ebp+var_4]
.text:0040102C              push    ecx
.text:0040102D              push    offset aYou     ; "You entered %d...
"
.text:00401032              call    ds:printf
.text:00401038              add     esp, 8
.text:0040103B              jmp     short exit
.text:0040103D ; ---------------------------------------------------------------------------
.text:0040103D
.text:0040103D error:                               ; CODE XREF: _main+27
.text:0040103D              push    offset aWhat    ; "What you entered? Huh?
"
.text:00401042              call    ds:printf
.text:00401048              add     esp, 4
.text:0040104B
.text:0040104B exit:                                ; CODE XREF: _main+3B
.text:0040104B              xor     eax, eax
.text:0040104D              mov     esp, ebp
.text:0040104F              pop     ebp
.text:00401050              retn
.text:00401050 _main   endp

现在理解代码就变得非常简单了。然而过分的标注指令却不是一个好主意。

函数的一部分有可能也会被IDA隐藏:

我隐藏了两部分然后分别给它们命名:

.text:00401000 _text        segment para public ’CODE’ use32
.text:00401000              assume cs:_text
.text:00401000              ;org 401000h
.text:00401000 ; ask for X
.text:00401012 ; get X
.text:00401024              cmp     eax, 1
.text:00401027              jnz     short error
.text:00401029 ; print result
.text:0040103B              jmp     short exit
.text:0040103D ; ---------------------------------------------------------------------------
.text:0040103D
.text:0040103D error:                               ; CODE XREF: _main+27
.text:0040103D              push    offset aWhat    ; "What you entered? Huh?
"
.text:00401042              call    ds:printf
.text:00401048              add     esp, 4
.text:0040104B
.text:0040104B exit:                                ; CODE XREF: _main+3B
.text:0040104B              xor     eax, eax
.text:0040104D              mov     esp, ebp
.text:0040104F              pop     ebp
.text:00401050              retn
.text:00401050 _main        endp

如果要显示这些隐藏的部分,我们可以点击数字上的+。

为了压缩“空间”,我们可以看到IDA怎样用图表代替一个函数的(见图6.7),然后在每个条件跳转处有两个箭头,绿色和红色。绿色箭头代表如果跳转触发的方向,红色则相反。

当然可以折叠节点,然后备注名称,我像这样处理了3块(见图 6.8):

这个非常的有用。可以这么说,逆向工程师很重要的一点就是缩小他所有的信息。

图6.7: IDA 图形模式

图6.8: Graph mode in IDA with 3 nodes folded

6.6.3 MSVC: x86 + OllyDbg

让我们继续在OllyDbg中看这个范例程序,使它认为scanf()怎么运行都不会出错。

当本地变量地址被传递给scanf()时,这个变量还有一些垃圾数据。这里是0x4CD478:见图6.10

当scanf()执行时,我在命令行窗口输入了一些不是数字的东西,像“asdasd”.scanf()结束后eax变为了0.也就意味着有错误发生:见图6.11

我们也可以发现栈中的本地变量并没有发生变化,scanf()会在那里写入什么呢?其实什么都没有,只是返回了0.

现在让我们尝试修改这个程序,右击EAX,在选项中有个“set to 1”,这正是我们所需要的。

现在EAX是1了。那么接下来的检查就会按照我们的需求执行,然后printf()将会打印出栈上的变量。

按下F9我们可以在窗口中看到:

图6.9

实际上,5035128是栈上一个数据(0x4CD478)的十进制表示!

图6.10

图6.11

6.6.4 MSVC: x86 + Hlew

这也是一个关于可执行文件patch的简单例子,我们之前尝试patch程序,所以程序总是打印数字,不管我们输入什么。

假设编译时并没有使用/MD,我们可以在.text开始的地方找到main()函数,现在让我们在Hiew中打开执行文件。找到.text的开始处(enter,F8,F6,enter,enter)

我们可以看到这个:表6.13

然后按下F9(update),现在文件保存在了磁盘中,就像我们想要的。

两个NOP可能看起来并不是那么完美,另一个方法是把0写在第二处(jump offset),所以JNZ就可以总是跳到下一个指令了。

另外我们也可以这样做:替换第一个字节为EB,这样就不修改第二处(jump offset),这样就会无条件跳转,不管我们输入什么,错误信息都可以打印出来了。

图6.12:main()函数

图6.13:Hiew 用两个NOP替换JNZ

6.6.5 GCC: x86

生成的代码和gcc 4.4.1是一样的,除了我们之前已经考虑过的

6.6.6 MSVC: x64

因为我们这里处理的是无整型变量。在x86-64中还是32bit,我们可以看出32bit的寄存器(前缀为E)在这种情况下是怎样使用的,然而64bit的寄存也有被使用(前缀R)

_DATA       SEGMENT
$SG2924     DB      ’Enter X:’, 0aH, 00H
$SG2926     DB      ’%d’, 00H
$SG2927     DB      ’You entered %d...’, 0aH, 00H
$SG2929     DB      ’What you entered? Huh?’, 0aH, 00H
_DATA       ENDS
 
_TEXT       SEGMENT
x$ = 32
main        PROC
$LN5:
            sub         rsp, 56
            lea         rcx, OFFSET FLAT:$SG2924 ; ’Enter X:’
            call        printf
            lea         rdx, QWORD PTR x$[rsp]
            lea         rcx, OFFSET FLAT:$SG2926 ; ’%d’
            call        scanf
            cmp         eax, 1
            jne         SHORT $LN2@main
            mov         edx, DWORD PTR x$[rsp]
            lea         rcx, OFFSET FLAT:$SG2927 ; ’You entered %d...’
            call        printf
            jmp         SHORT $LN1@main
$LN2@main:
            lea rcx, OFFSET FLAT:$SG2929 ; ’What you entered? Huh?’
            call printf
$LN1@main:
            ; return 0
            xor         eax, eax
            add         rsp, 56
            ret         0
main        ENDP
_TEXT       ENDS
END

6.6.7 ARM:Optimizing Keil + thumb mode

var_8       = -8
 
            PUSH    {R3,LR}
            ADR     R0, aEnterX         ; "Enter X:
"
            BL      __2printf
            MOV     R1, SP
            ADR     R0, aD              ; "%d"
            BL      __0scanf
            CMP     R0, #1
            BEQ     loc_1E
            ADR     R0, aWhatYouEntered ; "What you entered? Huh?
"
            BL      __2printf
loc_1A                                  ; CODE XREF: main+26
            MOVS    R0, #0
            POP     {R3,PC}
 
loc_1E                                  ; CODE XREF: main+12
            LDR     R1, [SP,#8+var_8]
            ADR     R0, aYouEnteredD___ ; "You entered %d...
"
            BL      __2printf
            B       loc_1A

这里有两个新指令CMP 和BEQ.

CMP和x86指令中的相似,它会用一个参数减去另外一个参数然后保存flag.

BEQ是跳向另一处地址,如果数相等就会跳,如果最后一次比较结果为0,或者Z flag是1。和x86中的JZ是一样的。

其他的都很简单,执行流程分为两个方向,当R0被写入0后,两个方向则会合并,作为函数的返回值,然后函数结束。

访问传递参数

现在我们来看函数调用者通过栈把参数传递到被调用函数。被调用函数是如何访问这些参数呢?

#include <stdio.h>
int f (int a, int b, int c)
{
        return a*b+c;
};
int main()
{
        printf ("%d
", f(1, 2, 3));
        return 0;
};

7.1 X86

7.1.1 MSVC

如下为相应的反汇编代码(MSVC 2010 Express)

Listing 7.2 MSVC 2010 Express

_TEXT   SEGMENT
_a$ = 8                                  ; size = 4
_b$ = 12                                 ; size = 4
_c$ = 16                                 ; size = 4
_f      PROC
        push ebp
        mov ebp, esp
        mov eax, DWORD PTR _a$[ebp]
        imul eax, DWORD PTR _b$[ebp]
        add eax, DWORD PTR _c$[ebp]
        pop ebp
        ret 0
_f      ENDP
 
_main   PROC
        push ebp
        mov ebp, esp
        push 3 ; 3rd argument
        push 2 ; 2nd argument
        push 1 ; 1st argument
        call _f
        add esp, 12
        push eax
        push OFFSET $SG2463 ; ’%d’, 0aH, 00H
        call _printf
        add esp, 8
        ; return 0
        xor eax, eax
        pop ebp
        ret 0
_main ENDP

我们可以看到函数main()中3个数字被圧栈,然后函数f(int, int, int)被调用。函数f()内部访问参数时使用了像_ a = 8访使访)。EBPa的值指向压栈参数。

a[ebp]eaxIMULeaxeaxb[ebp]的乘积,然后eax与c$[ebp]的值相加并将和放入eax寄存器中,之后返回eax的值。返回值作为printf()的参数。

7.1.2 MSVC+OllyDbg

我们在OllyDbg中观察,跟踪到函数f()使用第一个参数的位置,可以看到寄存器EBP指向栈底,图中使用红色箭头标识。栈帧中第一个被保存的是EBP的值,第二个是返回地址(RA),第三个是参数1,接下来是参数2,以此类推。因此,当我们访问第一个参数时EBP应该加8(2个32-bit字节宽度)。

Figure 7.1: OllyDbg: 函数f()内部

7.1.3 GCC

使用GCC4.4.1编译后在IDA中查看

Listing 7.3: GCC 4.4.1

            public f
f           proc near
 
arg_0       = dword ptr 8
arg_4       = dword ptr 0Ch
arg_8       = dword ptr 10h
 
            push    ebp
            mov     ebp, esp
            mov     eax, [ebp+arg_0]  ; 1st argument
            imul    eax, [ebp+arg_4]  ; 2nd argument
            add     eax, [ebp+arg_8]  ; 3rd argument
            pop     ebp
            retn
f           endp
 
            public main
main        proc near
 
var_10      = dword ptr -10h
var_C       = dword ptr -0Ch
var_8       = dword ptr -8
 
            push    ebp
            mov     ebp, esp
            and     esp, 0FFFFFFF0h
            sub     esp, 10h
            mov     [esp+10h+var_8], 3  ; 3rd argument
            mov     [esp+10h+var_C], 2  ; 2nd argument
            mov     [esp+10h+var_10], 1  ; 1st argument
            call    f
            mov     edx, offset aD ; "%d
"
            mov     [esp+10h+var_C], eax
            mov     [esp+10h+var_10], edx
            call    _printf
            mov     eax, 0
            leave
            retn
main    endp

几乎相同的结果。

执行两个函数后栈指针ESP并没有显示恢复,因为倒数第二个指令LEAVE(B.6.2)会自动恢复栈指针。

7.2 X64

x86-64架构下有点不同,函数参数(4或6)使用寄存器传递,被调用函数通过访问寄存器来访问传递进来的参数。

7.2.1 MSVC

MSVC优化后:

Listing 7.4: MSVC 2012 /Ox x64

$SG2997     DB      ’%d’, 0aH, 00H
 
main        PROC
            sub     rsp, 40
            mov     edx, 2
            lea     r8d, QWORD PTR [rdx+1]  ; R8D=3
            lea     ecx, QWORD PTR [rdx-1]  ; ECX=1
            call    f
            lea     rcx, OFFSET FLAT:$SG2997  ; ’%d’
            mov     edx, eax
            call    printf
            xor     eax, eax
            add     rsp, 40
            ret     0
main        ENDP
 
f           PROC
            ; ECX - 1st argument
            ; EDX - 2nd argument
            ; R8D - 3rd argument
            imul    ecx, edx
            lea     eax, DWORD PTR [r8+rcx]
            ret     0
f           ENDP

我们可以看到函数f()直接使用寄存器来操作参数,LEA指令用来做加法,编译器认为使用LEA比使用ADD指令要更快。在mian()中也使用了LEA指令,编译器认为使用LEA比使用MOV指令效率更高。

我们来看看MSVC没有优化的情况:

Listing 7.5: MSVC 2012 x64

f           proc near
 
; shadow space:
arg_0       = dword ptr 8
arg_8       = dword ptr 10h
arg_10      = dword ptr 18h
 
            ; ECX - 1st argument
            ; EDX - 2nd argument
            ; R8D - 3rd argument
            mov     [rsp+arg_10], r8d
            mov     [rsp+arg_8], edx
            mov     [rsp+arg_0], ecx
            mov     eax, [rsp+arg_0]
            imul    eax, [rsp+arg_8]
            add     eax, [rsp+arg_10]
            retn
f endp
 
main        proc    near
            sub     rsp, 28h
            mov     r8d, 3 ; 3rd argument
            mov     edx, 2 ; 2nd argument
            mov     ecx, 1 ; 1st argument
            call    f
            mov     edx, eax
            lea     rcx, $SG2931 ; "%d
"
            call    printf
 
            ; return 0
            xor     eax, eax
            add     rsp, 28h
            retn
main        endp

这里从寄存器传递进来的3个参数因为某种情况又被保存到栈里。这就是所谓的“shadow space”2:每个Win64通常(不是必需)会保存所有4个寄存器的值。这样做由两个原因:1)为输入参数分配所有寄存器(即使是4个)太浪费,所以要通过堆栈来访问;2)每次中断下来调试器总是能够定位函数参数3。

调用者负责在栈中分配“shadow space”。

7.2.2 GCC

GCC优化后的代码:

Listing 7.6: GCC 4.4.6 -O3 x64

f:
        ; EDI - 1st argument
        ; ESI - 2nd argument
        ; EDX - 3rd argument
        imul    esi, edi
        lea     eax, [rdx+rsi]
        ret
 
main:
        sub     rsp, 8
        mov     edx, 3
        mov     esi, 2
        mov     edi, 1
        call    f
        mov     edi, OFFSET FLAT:.LC0 ; "%d
"
        mov     esi, eax
        xor     eax, eax ; number of vector registers passed
        call    printf
        xor     eax, eax
        add     rsp, 8
        ret

GCC无优化代码:

Listing 7.7: GCC 4.4.6 x64

f:
        ; EDI - 1st argument
        ; ESI - 2nd argument
        ; EDX - 3rd argument
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     DWORD PTR [rbp-8], esi
        mov     DWORD PTR [rbp-12], edx
        mov     eax, DWORD PTR [rbp-4]
        imul    eax, DWORD PTR [rbp-8]
        add     eax, DWORD PTR [rbp-12]
        leave
        ret
 
main:
        push    rbp
        mov     rbp, rsp
        mov     edx, 3
        mov     esi, 2
        mov     edi, 1
        call    f
        mov     edx, eax
        mov     eax, OFFSET FLAT:.LC0 ; "%d
"
        mov     esi, edx
        mov     rdi, rax
        mov     eax, 0 ; number of vector registers passed
        call    printf
        mov     eax, 0
        leave
        ret

System V *NIX [21]没有“shadow space”,但被调用者可能会保存参数,这也是造成寄存器短缺的原因。

7.2.3 GCC: uint64_t instead int

我们例子使用的是32位int,寄存器也为32位寄存器(前缀为E-)。

为处理64位数值内部会自动调整为64位寄存器:

#include <stdio.h>
#include <stdint.h>
 
uint64_t f (uint64_t a, uint64_t b, uint64_t c)
{
    return a*b+c;
};
int main()
{
    printf ("%lld
", f(0x1122334455667788,0x1111111122222222,0x3333333344444444));
    return 0;
};

Listing 7.8: GCC 4.4.6 -O3 x64

f       proc near
        imul    rsi, rdi
        lea     rax, [rdx+rsi]
        retn
f       endp
 
main    proc near
        sub     rsp, 8
        mov     rdx, 3333333344444444h ; 3rd argument
        mov     rsi, 1111111122222222h ; 2nd argument
        mov     rdi, 1122334455667788h ; 1st argument
        call    f
        mov     edi, offset format ; "%lld
"
        mov     rsi, rax
        xor     eax, eax ; number of vector registers passed
        call    _printf
        xor     eax, eax
        add     rsp, 8
        retn
main    endp

代码非常相似,只是使用了64位寄存器(前缀为R)。

7.3 ARM

7.3.1 未优化的Keil + ARM mode

.text:000000A4 00 30 A0 E1              MOV     R3, R0
.text:000000A8 93 21 20 E0              MLA     R0, R3, R1, R2
.text:000000AC 1E FF 2F E1              BX      LR
...
.text:000000B0             main
.text:000000B0 10 40 2D E9              STMFD   SP!, {R4,LR}
.text:000000B4 03 20 A0 E3              MOV     R2, #3
.text:000000B8 02 10 A0 E3              MOV     R1, #2
.text:000000BC 01 00 A0 E3              MOV     R0, #1
.text:000000C0 F7 FF FF EB              BL      f
.text:000000C4 00 40 A0 E1              MOV     R4, R0
.text:000000C8 04 10 A0 E1              MOV     R1, R4
.text:000000CC 5A 0F 8F E2              ADR     R0, aD_0  ; "%d
"
.text:000000D0 E3 18 00 EB              BL      __2printf
.text:000000D4 00 00 A0 E3              MOV     R0, #0
.text:000000D8 10 80 BD E8              LDMFD   SP!, {R4,PC}

main()函数里调用了另外两个函数,3个值被传递到f();

正如前面提到的,ARM通常使用前四个寄存器(R0-R4)传递前四个值。

f()函数使用了前三个寄存器(R0-R2)作为参数。

MLA (Multiply Accumulate)指令将R3寄存器和R1寄存器的值相乘,然后再将乘积与R2寄存器的值相加将结果存入R0,函数返回R0。

一条指令完成乘法和加法4,如果不包括SIMD新的FMA指令5,通常x86下没有这样的指令。

第一条指令MOV R3,R0,看起来冗余是因为该代码是非优化的。

BX指令返回到LR寄存器存储的地址,处理器根据状态模式从Thumb状态转换到ARM状态,或者反之。函数f()可以被ARM代码或者Thumb代码调用,如果是Thumb代码调用BX将返回到调用函数并切换到Thumb模式,或者反之。

7.3.2 Optimizing Keil + ARM mode

.text:00000098            f
.text:00000098 91 20 20 E0                MLA R0, R1, R0, R2
.text:0000009C 1E FF 2F E1                BX  LR

这里f()编译时使用完全优化模式(-O3),MOV指令被优化,现在MLA使用所有输入寄存器并将结果置入R0寄存器。

7.3.3 Optimizing Keil + thumb mode

.text:0000005E 48 43                  MULS R0, R1
.text:00000060 80 18                  ADDS R0, R0, R2
.text:00000062 70 47                  BX   LR

Thumb模式下没有MLA指令,编译器做了两次间接处理,MULS指令使R0寄存器的值与R1寄存器的值相乘并将结果存入R0。ADDS指令将R0与R2的值相加并将结果存入R0。

一个或者多个字的返回值

X86架构下通常返回EAX寄存器的值,如果是单字节char,则只使用EAX的低8位AL。如果返回float类型则使用FPU寄存器ST(0)。ARM架构下通常返回寄存器R0。

假如main()函数的返回值是void而不是int会怎么样?

通常启动函数调用main()为:

push envp
push argv
push argc
call main
push eax
call exit

换句话说为

exit(main(argc,argv,envp));

如果main()声明为void类型并且函数没有明确返回状态值,通常在main()结束时EAX寄存器的值被返回,然后作为exit()的参数。大多数情况下函数返回的是随机值。这种情况下程序的退出代码为伪随机的。

我们看一个实例,注意main()是void类型:

#include <stdio.h>
void main()
{
    printf ("Hello, world!");
};

我们在linux下编译。

GCC 4.8.1会使用puts()替代printf()(看前面章节2.3.3),没有关系,因为puts()会返回打印的字符数,就行printf()一样。请注意,main()结束时EAX寄存器的值是非0的,这意味着main()结束时保留puts()返回时EAX的值。

Listing 8.1: GCC 4.8.1

.LC0:
        .string "Hello, world!"
main:
        push    ebp
        mov     ebp, esp
        and     esp, -16
        sub     esp, 16
        mov     DWORD PTR [esp], OFFSET FLAT:.LC0
        call    puts
        leave
        ret

我们写bash脚本来看退出状态:

Listing 8.2: tst.sh

#!/bin/sh
./hello_world
echo $?

运行:

$ tst.sh
Hello, world!
14

14为打印的字符数。

回到返回值是EAX寄存器值的事实,这也就是为什么老的C编译器不能够创建返回信息无法拟合到一个寄存器(通常是int型)的函数。如果必须这样,应该通过指针来传递。现在可以这样,比如返回整个结构体,这种情况应该避免。如果必须要返回大的结构体,调用者必须开辟存储空间,并通过第一个参数传递指针,整个过程对程序是透明的。像手动通过第一个参数传递指针一样,只是编译器隐藏了这个过程。

小例子:

struct s
{
    int a;
    int b;
    int c;
};

struct s get_some_values (int a)
{
    struct s rt;
    rt.a=a+1;
    rt.b=a+2;
    rt.c=a+3;

    return rt;
};

…我们可以得到(MSVC 2010 /Ox):

$T3853 = 8                  ; size = 4
_a$ = 12                    ; size = 4
?get_some_values@@YA?AUs@@H@Z PROC      ; get_some_values
    mov     ecx, DWORD PTR _a$[esp-4]
    mov     eax, DWORD PTR $T3853[esp-4]
    lea     edx, DWORD PTR [ecx+1]
    mov     DWORD PTR [eax], edx
    lea     edx, DWORD PTR [ecx+2]
    add     ecx, 3
    mov     DWORD PTR [eax+4], edx
    mov     DWORD PTR [eax+8], ecx
    ret     0
?get_some_values@@YA?AUs@@H@Z ENDP      ; get_some_values

内部变量传递指针到结构体的宏为$T3853。

这个例子可以用C99语言扩展来重写:

struct s
{
    int a;
    int b;
    int c;
};

struct s get_some_values (int a)
{
    return (struct s){.a=a+1, .b=a+2, .c=a+3};
};

Listing 8.3: GCC 4.8.1

_get_some_values proc near

ptr_to_struct   = dword ptr 4
a               = dword ptr 8
                mov     edx, [esp+a]
                mov     eax, [esp+ptr_to_struct]
                lea     ecx, [edx+1]
                mov     [eax], ecx
                lea     ecx, [edx+2]
                add     edx, 3
                mov     [eax+4], ecx
                mov     [eax+8], edx
                retn
_get_some_values endp

我们可以看到,函数仅仅填充调用者申请的结构体空间的相应字段。因此没有性能缺陷。 # 指针

指针通常被用作函数返回值(recall scanf() case (6)).例如,当函数返回两个值时。

9.1 Global variables example

#include <stdio.h>

void f1 (int x, int y, int *sum, int *product)
{
    *sum=x+y;
    *product=x*y;
};

int sum, product;

void main()
{
    f1(123, 456, &sum, &product);
    printf ("sum=%d, product=%d", sum, product);
};

编译后

Listing 9.1: Optimizing MSVC 2010 (/Ox /Ob0)

COMM        _product:DWORD
COMM        _sum:DWORD
$SG2803 DB              ’sum=%d, product=%d’, 0aH, 00H
 
_x$ = 8                                     ; size = 4
_y$ = 12                                    ; size = 4
_sum$ = 16                                  ; size = 4
_product$ = 20                              ; size = 4
_f1         PROC
            mov     ecx, DWORD PTR _y$[esp-4]
            mov     eax, DWORD PTR _x$[esp-4]
            lea     edx, DWORD PTR [eax+ecx]
            imul    eax, ecx
            mov     ecx, DWORD PTR _product$[esp-4]
            push    esi
            mov     esi, DWORD PTR _sum$[esp]
            mov     DWORD PTR [esi], edx
            mov     DWORD PTR [ecx], eax
            pop     esi
            ret     0
_f1         ENDP
 
_main       PROC
            push    OFFSET _product
            push    OFFSET _sum
            push    456                     ; 000001c8H
            push    123                     ; 0000007bH
            call    _f1
            mov     eax, DWORD PTR _product
            mov     ecx, DWORD PTR _sum
            push    eax
            push    ecx
            push    OFFSET $SG2803
            call    DWORD PTR __imp__printf
            add     esp, 28                 ; 0000001cH
            xor     eax, eax
            ret     0
_main   ENDP

让我们在OD中查看:图9.1。首先全局变量地址被传递进f1()。我们在堆栈元素点击“数据窗口跟随”,可以看到数据段上分配两个变量的空间。这些变量被置0,因为未初始化数据(BSS1)在程序运行之前被清理为0。这些变量属于数据段,我们按Alt+M可以查看内存映射fig. 9.5.

让我们跟踪(F7)到f1()fig. 9.2.在堆栈中为456 (0x1C8) 和 123 (0x7B),接着是两个全局变量的地址。

让我们跟踪到f1()结尾,可以看到两个全局变量存放了计算结果。

现在两个全局变量的值被加载到寄存器传递给printf(): fig. 9.4.

Figure 9.1: OllyDbg: 全局变量地址被传递进f1()

Figure 9.2: OllyDbg: f1()开始

Figure 9.3: OllyDbg: f1()完成

Figure 9.4: OllyDbg: 全局变量被传递进printf()

Figure 9.5: OllyDbg: memory map

9.2 Local variables example

让我们修改一下例子:

Listing 9.2: 局部变量

void main()
{
    int sum, product; // now variables are here
 
    f1(123, 456, &sum, &product);
    printf ("sum=%d, product=%d
", sum, product);
};

f1()函数代码没有改变。仅仅main()代码作了修改。

Listing 9.3: Optimizing MSVC 2010 (/Ox /Ob0)

_product$ = -8              ; size = 4
_sum$ = -4                  ; size = 4
_main   PROC
; Line 10
        sub     esp, 8
; Line 13
        lea     eax, DWORD PTR _product$[esp+8]
        push    eax
        lea     ecx, DWORD PTR _sum$[esp+12]
        push    ecx
        push    456         ; 000001c8H
        push    123         ; 0000007bH
        call    _f1
; Line 14
        mov     edx, DWORD PTR _product$[esp+24]
        mov     eax, DWORD PTR _sum$[esp+24]
        push    edx
        push    eax
        push    OFFSET $SG2803
        call    DWORD PTR __imp__printf
; Line 15
        xor     eax, eax
        add     esp, 36     ; 00000024H
        ret     0

我们在OD中查看,局部变量地址在堆栈中是0x35FCF4和0x35FCF8。我们可以看到是如何圧栈的fig. 9.6.

f1()开始的时候,随机栈地址为0x35FCF4和0x35FCF8 fig. 9.7.

f1()完成时结果0xDB18和0x243存放在地址0x35FCF4和0x35FCF8。

Figure 9.6: OllyDbg: 局部变量地址被圧栈

Figure 9.7: OllyDbg: f1()starting

Figure 9.8: OllyDbg: f1()finished

9.3 小结

f1()可以返回结果到内存的任何地方,这是指针的本质和特性。顺便提一下,C++引用的工作方式和这个类似。详情阅读相关内容(33)。

条件跳转

现在我们来了解条件跳转。

#include <stdio.h>

void f_signed (int a, int b)
{
    if (a>b)
        printf ("a>b");
    if (a==b)
        printf ("a==b");
    if (a<b)
        printf ("a<b");
};

void f_unsigned (unsigned int a, unsigned int b)
{
    if (a>b)
        printf ("a>b");
    if (a==b)
        printf ("a==b");
    if (a<b)
        printf ("a<b");
};

int main()
{
    f_signed(1, 2);
    f_unsigned(1, 2);
    return 0;
};

10.1 x86

10.1.1 x86 + MSVC

f_signed() 函数:

Listing 10.1: 非优化MSVC 2010

_a$ = 8
_b$ = 12
_f_signed   PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _a$[ebp]
        cmp     eax, DWORD PTR _b$[ebp]
        jle     SHORT $LN3@f_signed
        push    OFFSET $SG737       ; "a>b"
        call    _printf
        add     esp, 4
$LN3@f_signed:
        mov     ecx, DWORD PTR _a$[ebp]
        cmp     ecx, DWORD PTR _b$[ebp]
        jne     SHORT $LN2@f_signed
        push    OFFSET $SG739       ; "a==b"
        call    _printf
        add     esp, 4
$LN2@f_signed:
        mov     edx, DWORD PTR _a$[ebp]
        cmp     edx, DWORD PTR _b$[ebp]
        jge     SHORT $LN4@f_signed
        push    OFFSET $SG741       ; "a<b"
        call    _printf
        add     esp, 4
        $LN4@f_signed:
        pop     ebp
        ret     0
_f_signed   ENDP

第一个指令JLE意味如果小于等于则跳转。换句话说,第二个操作数大于或者等于第一个操作数,控制流将传递到指定地址或者标签。否则(第二个操作数小于第一个操作数)第一个printf()将被调用。第二个检测JNE:如果不相等则跳转。如果两个操作数相等控制流则不变。第三个检测JGE:大于等于跳转,当第一个操作数大于或者等于第二个操作数时跳转。如果三种情况都没有发生则无printf()被调用,事实上,如果没有特殊干预,这种情况几乎不会发生。

f_unsigned()函数类似,只是JBE和JAE替代了JLE和JGE,我们来看f_unsigned()函数

Listing 10.2: GCC

_a$ = 8                                 ; size = 4
_b$ = 12                                ; size = 4
_f_unsigned     PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _a$[ebp]
        cmp     eax, DWORD PTR _b$[ebp]
        jbe     SHORT $LN3@f_unsigned
        push    OFFSET $SG2761 ; "a>b"
        call    _printf
        add     esp, 4
$LN3@f_unsigned:
        mov     ecx, DWORD PTR _a$[ebp]
        cmp     ecx, DWORD PTR _b$[ebp]
        jne     SHORT $LN2@f_unsigned
        push    OFFSET $SG2763 ; "a==b"
        call    _printf
        add     esp, 4
$LN2@f_unsigned:
        mov     edx, DWORD PTR _a$[ebp]
        cmp     edx, DWORD PTR _b$[ebp]
        jae     SHORT $LN4@f_unsigned
        push    OFFSET $SG2765 ; "a<b"
        call    _printf
        add     esp, 4
$LN4@f_unsigned:
        pop     ebp
        ret     0
_f_unsigned     ENDP

几乎是相同的,不同的是:JBE-小于等于跳转和JAE-大于等于跳转。这些指令(JA/JAE/JBE/JBE)不同于JG/JGE/JL/JLE,它们使用无符号值。

我们也可以看到有符号值的表示(35)。因此我们看JG/JL代替JA/JBE的用法或者相反,我们几乎可以确定变量的有符号或者无符号类型。

main()函数没有什么新的内容:

Listing 10.3: main()

_main   PROC
        push    ebp
        mov     ebp, esp
        push    2
        push    1
        call    _f_signed
        add     esp, 8
        push    2
        push    1
        call    _f_unsigned
        add     esp, 8
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP

10.1.2 x86 + MSVC + OllyDbg

我们在OD里允许例子来查看标志寄存器。我们从f_unsigned()函数开始。CMP执行了三次,每次的参数都相同,所以标志位也相同。

第一次比较的结果:fig. 10.1.标志位:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.标志位名称为OD对其的简称。

当CF=1 or ZF=1时JBE将被触发,此时将跳转。

接下来的条件跳转:fig. 10.2.当ZF=0(zero flag)时JNZ则被触发

第三个条件跳转:fig. 10.3.我们可以发现14当CF=0 (carry flag)时,JNB将被触发。在该例中条件不为真,所以第三个printf()将被执行。

Figure 10.1: OllyDbg: f_unsigned(): 第一个条件跳转

Figure 10.2: OllyDbg: f_unsigned(): 第二个条件跳转

Figure 10.3: OllyDbg: f_unsigned(): 第三个条件跳转

现在我们在OD中看f_signed()函数使用有符号值。

可以看到标志寄存器:C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0.

第一种条件跳转JLE将被触发fig. 10.4.我们可以发现14,当ZF=1 or SF≠OF。该例中SF≠OF,所以跳转将被触发。

下一个条件跳转将被触发:如果ZF=0 (zero flag): fig. 10.5.

第三个条件跳转将不会被触发,因为仅有SF=OF,该例中不为真: fig. 10.6.

Figure 10.4: OllyDbg: f_signed(): 第一个条件跳转

Figure 10.5: OllyDbg: f_signed(): 第二个条件跳转

Figure 10.6: OllyDbg: f_signed(): 第三个条件跳转

10.1.3 x86 + MSVC + Hiew

我们可以修改这个可执行文件,使其无论输入的什么值f_unsigned()函数都会打印“a==b”。

在Hiew中查看:fig. 10.7.

我们要完成以下3个任务:

  1. 使第一个跳转一直被触发;
  2. 使第二个跳转从不被触发;
  3. 使第三个跳转一直被触发。 我们需要使代码流进入第二个printf(),这样才一直打印“a==b”。

三个指令(或字节)应该被修改:

  1. 第一个跳转修改为JMP,但跳转偏移值不变。
  2. 第二个跳转有时可能被触发,我们修改跳转偏移值为0后,无论何种情况,程序总是跳向下一条指令。跳转地址等于跳转偏移值加上下一条指令地址,当跳转偏移值为0时,跳转地址就为下一条指令地址,所以无论如何下一条指令总被执行。
  3. 第三个跳转我们也修改为JMP,这样跳转总被触发。 修改后:fig. 10.8.

如果忘了这些跳转,printf()可能会被多次调用,这种行为可能是我们不需要的。

Figure 10.7: Hiew: f_unsigned() 函数

Figure 10.8: Hiew:我们修改 f_unsigned() 函数

10.1.4 Non-optimizing GCC

GCC 4.4.1非优化状态产生的代码几乎一样,只是用puts() (2.3.3) 替代 printf()。

10.1.5 Optimizing GCC

细心的读者可能会问,为什么要多次执行CMP,如果标志寄存器每次都相同呢?可能MSVC不会做这样的优化,但是GCC 4.8.1可以做这样的深度优化:

Listing 10.4: GCC 4.8.1 f_signed()

f_signed:
        mov     eax, DWORD PTR [esp+8]
        cmp     DWORD PTR [esp+4], eax
        jg      .L6
        je      .L7
        jge     .L1
        mov     DWORD PTR [esp+4], OFFSET FLAT:.LC2 ; "a<b"
        jmp     puts
.L6:
        mov     DWORD PTR [esp+4], OFFSET FLAT:.LC0 ; "a>b"
        jmp     puts
.L1:
        rep     ret
.L7:
        mov     DWORD PTR [esp+4], OFFSET FLAT:.LC1 ; "a==b"
        jmp     puts

我们可以看到JMP puts替代了CALL puts/RETN。稍后我们介绍这种情况11.1.1.。

不用说,这种类型的x86代码是很少见的。MSVC2012似乎不会这样做。其他情况下,汇编程序能意识到此类使用。如果你在其它地方看到此类代码,更可能是手工构造的。

f_unsigned()函数代码:

Listing 10.5: GCC 4.8.1 f_unsigned()

f_unsigned:
        push    esi
        push    ebx
        sub     esp, 20
        mov     esi, DWORD PTR [esp+32]
        mov     ebx, DWORD PTR [esp+36]
        cmp     esi, ebx
        ja      .L13
        cmp     esi, ebx ; instruction may be removed
        je      .L14
.L10:
        jb      .L15
        add     esp, 20
        pop     ebx
        pop     esi
        ret
.L15:
        mov     DWORD PTR [esp+32], OFFSET FLAT:.LC2 ; "a<b"
        add     esp, 20
        pop     ebx
        pop     esi
        jmp     puts
.L13:
        mov     DWORD PTR [esp], OFFSET FLAT:.LC0 ; "a>b"
        call    puts
        cmp     esi, ebx
        jne     .L10
.L14:
        mov     DWORD PTR [esp+32], OFFSET FLAT:.LC1 ; "a==b"
        add     esp, 20
        pop     ebx
        pop     esi
        jmp     puts

因此,GCC 4.8.1的优化算法并不总是完美的。

10.2 ARM

10.2.1 Keil + ARM mode优化后

Listing 10.6: Optimizing Keil + ARM mode

.text:000000B8                          EXPORT f_signed
.text:000000B8              f_signed                ; CODE XREF: main+C
.text:000000B8 70 40 2D E9              STMFD   SP!, {R4-R6,LR}
.text:000000BC 01 40 A0 E1              MOV     R4, R1
.text:000000C0 04 00 50 E1              CMP     R0, R4
.text:000000C4 00 50 A0 E1              MOV     R5, R0
.text:000000C8 1A 0E 8F C2              ADRGT   R0, aAB ; "a>b"
.text:000000CC A1 18 00 CB              BLGT    __2printf
.text:000000D0 04 00 55 E1              CMP     R5, R4
.text:000000D4 67 0F 8F 02              ADREQ   R0, aAB_0 ; "a==b"
.text:000000D8 9E 18 00 0B              BLEQ    __2printf
.text:000000DC 04 00 55 E1              CMP     R5, R4
.text:000000E0 70 80 BD A8              LDMGEFD SP!, {R4-R6,PC}
.text:000000E4 70 40 BD E8              LDMFD   SP!, {R4-R6,LR}
.text:000000E8 19 0E 8F E2              ADR     R0, aAB_1 ; "a<b"
.text:000000EC 99 18 00 EA              B       __2printf
.text:000000EC              ; End of function f_signed

ARM下很多指令只有某些标志位被设置时才会被执行。比如做数值比较时。

举个例子,ADD实施上是ADDAL,这里的AL是Always,即总被执行。判定谓词是32位ARM指令的高4位(条件域)。无条件跳转的B指令其实是有条件的,就行其它任何条件跳转一样,只是条件域为AL,这意味着总是被执行,忽略标志位。

ADRGT指令就像和ADR一样,只是该指令前面为CMP指令,并且只有前面数值大于另一个数值时(Greater Than)时才被执行。

接下来的BLGT行为和BL一样,只有比较结果符合条件才能出发(Greater Than)。ADRGT把字符串“a>b”的地址写入R0,然后BLGT调用printf()。因此,这些指令都带有GT后缀,只有当R0(a值)大于R4(b值)时指令才会被执行。

然后我们看ADREQ和BLEQ,这些指令动作和ADR及BL一样,只有当两个操作数对比后相等时才会被执行。这些指令前面是CMP(因为printf()调用可能会修改状态标识)。 然后我们看LDMGEFD,该指令行为和LDMFD指令一样1,仅仅当第一个值大于等于另一个值时(Greater Than),指令才会被执行。

“LDMGEFD SP!, {R4-R6,PC}”恢复寄存器并返回,只是当a>=b时才被触发,这样之后函数才执行完成。但是如果 a<b,触发条件不成立是将执行下一条指令LDMFD SP!, {R4-R6,LR},该指令保存R4-R6寄存器,使用LR而不是PC,函数并不返回。最后两条指令是执行printf()(5.3.2)。

f_unsigned与此一样只是使用对应的指令为ADRHI, BLHI及LDMCSFD,判断谓词(HI = Unsigned higher, CS = Carry Set (greater than or equal))请类比之前的说明,另外就是函数内部使用无符号数值。

我们来看一下main()函数:

Listing 10.7: main()

.text:00000128                              EXPORT main
.text:00000128          main
.text:00000128 10 40 2D E9                  STMFD SP!, {R4,LR}
.text:0000012C 02 10 A0 E3                  MOV R1, #2
.text:00000130 01 00 A0 E3                  MOV R0, #1
.text:00000134 DF FF FF EB                  BL f_signed
.text:00000138 02 10 A0 E3                  MOV R1, #2
.text:0000013C 01 00 A0 E3                  MOV R0, #1
.text:00000140 EA FF FF EB                  BL f_unsigned
.text:00000144 00 00 A0 E3                  MOV R0, #0
.text:00000148 10 80 BD E8                  LDMFD SP!, {R4,PC}
.text:00000148          ; End of function main

这就是ARM模式如何避免使用条件跳转。

这样做有什么好处呢?因为ARM使用精简指令集(RISC)。简言之,处理器流水线技术受到跳转的影响,这也是分支预测重要的原因。程序使用的条件或者无条件跳转越少越好,使用断言指令可以减少条件跳转的使用次数。

x86没有这也的功能,通过使用CMP设置相应的标志位来触发指令。

10.2.2 Optimizing Keil + thumb mode

Listing 10.8: Optimizing Keil + thumb mode

.text:00000072      f_signed                        ; CODE XREF: main+6
.text:00000072 70 B5                PUSH    {R4-R6,LR}
.text:00000074 0C 00                MOVS    R4, R1
.text:00000076 05 00                MOVS    R5, R0
.text:00000078 A0 42                CMP     R0, R4
.text:0000007A 02 DD                BLE     loc_82
.text:0000007C A4 A0                ADR     R0, aAB         ; "a>b"
.text:0000007E 06 F0 B7 F8          BL      __2printf
.text:00000082
.text:00000082      loc_82                      ; CODE XREF: f_signed+8
.text:00000082 A5 42                CMP     R5, R4
.text:00000084 02 D1                BNE     loc_8C
.text:00000086 A4 A0                ADR     R0, aAB_0   ; "a==b"
.text:00000088 06 F0 B2 F8          BL      __2printf
.text:0000008C
.text:0000008C      loc_8C                      ; CODE XREF: f_signed+12
.text:0000008C A5 42                CMP     R5, R4
.text:0000008E 02 DA                BGE     locret_96
.text:00000090 A3 A0                ADR     R0, aAB_1   ; "a<b"
.text:00000092 06 F0 AD F8          BL      __2printf
.text:00000096
.text:00000096      locret_96                   ; CODE XREF: f_signed+1C
.text:00000096 70 BD                POP     {R4-R6,PC}
.text:00000096      ; End of function f_signed

仅仅Thumb模式下的B指令可能需要条件代码辅助,所以thumb代码看起来更普通一些。

BLE通常是条件跳转小于或等于(Less than or Equal),BNE—不等于(Not Equal),BGE—大于或等于(Greater than or Equal)。

f_unsigned函数是同样的,只是使用的指令用来处理无符号数值:BLS (Unsigned lower or same) 和BCS (Carry Set (Greater than or equal)).

选择结构switch()/case/default

11.1 一些例子

void f (int a)
{
    switch (a)
    {
        case 0: printf ("zero"); break;
        case 1: printf ("one"); break;
        case 2: printf ("two"); break;
        default: printf ("something unknown"); break;
    };
};

11.1.1 X86

反汇编结果如下(MSVC 2010):

清单11.1: MSVC 2010

tv64 = -4       ; size = 4
_a$ = 8         ; size = 4
_f  PROC
    push    ebp
    mov     ebp, esp
    push    ecx
    mov     eax, DWORD PTR _a$[ebp]
    mov     DWORD PTR tv64[ebp], eax
    cmp     DWORD PTR tv64[ebp], 0
    je      SHORT $LN4@f
    cmp     DWORD PTR tv64[ebp], 1
    je      SHORT $LN3@f
    cmp     DWORD PTR tv64[ebp], 2
    je      SHORT $LN2@f
    jmp     SHORT $LN1@f
$LN4@f:
    push    OFFSET $SG739 ; ’zero’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN7@f
$LN3@f:
    push    OFFSET $SG741 ; ’one’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN7@f
$LN2@f:
    push    OFFSET $SG743 ; ’two’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN7@f
$LN1@f:
    push    OFFSET $SG745 ; ’something unknown’, 0aH, 00H
    call    _printf
    add     esp, 4
$LN7@f:
    mov     esp, ebp
    pop     ebp
    ret     0
_f    ENDP

输出函数的switch中有一些case选择分支,事实上,它是和下面这个形式等价的:

void f (int a)
{
    if (a==0)
        printf ("zero");
    else if (a==1)
        printf ("one");
    else if (a==2)
        printf ("two");
    else
        printf ("something unknown");
};

当switch()中有一些case分支时,我们可以看到此类代码,虽然不能确定,但是,事实上switch()在机器码级别上就是对if()的封装。这也就是说,switch()其实只是对有一大堆类似条件判断的if()的一个语法糖。

在生成代码时,除了编译器把输入变量移动到一个临时本地变量tv64中之外,这块代码对我们来说并无新意。

如果是在GCC 4.4.1下编译同样的代码,我们得到的结果也几乎一样,即使你打开了最高优化(-O3)也是如此。

让我们在微软VC编译器中打开/Ox优化选项: cl 1.c /Fa1.asm /Ox

清单11.2: MSVC

_a$ = 8                 ; size = 4
_f  PROC
    mov     eax, DWORD PTR _a$[esp-4]
    sub     eax, 0
    je      SHORT $LN4@f
    sub     eax, 1
    je      SHORT $LN3@f
    sub     eax, 1
    je      SHORT $LN2@f
    mov     DWORD PTR _a$[esp-4], OFFSET $SG791 ; ’something unknown’, 0aH, 00H
    jmp     _printf
$LN2@f:
    mov     DWORD PTR _a$[esp-4], OFFSET $SG789 ; ’two’, 0aH, 00H
    jmp     _printf
$LN3@f:
    mov     DWORD PTR _a$[esp-4], OFFSET $SG787 ; ’one’, 0aH, 00H
    jmp     _printf
$LN4@f:
    mov     DWORD PTR _a$[esp-4], OFFSET $SG785 ; ’zero’, 0aH, 00H
    jmp     _printf
_f ENDP

我们可以看到浏览器做了更多的难以阅读的优化(Dirty hacks)。

首先,变量的值会被放入EAX,接着EAX减0。听起来这很奇怪,但它之后是需要检查先前EAX寄存器的值是否为0的,如果是,那么程序会设置上零标志位ZF(这也表示了减去0之后,结果依然是0),第一个条件跳转语句JE(Jump if Equal 或者同义词 JZ - Jump if Zero)会因此触发跳转。如果这个条件不满足,JE没有跳转的话,输入值将减去1,之后就和之前的一样了,如果哪一次值是0,那么JE就会触发,从而跳转到对应的处理语句上。

(译注:SUB操作会重置零标志位ZF,但是MOV不会设置标志位,而JE将只有在ZF标志位设置之后才会跳转。如果需要基于EAX的值来做JE跳转的话,是需要用这个方法设置标志位的)。

并且,如果没有JE语句被触发,最终,printf()函数将收到“something unknown”的参数。

其次:我们看到了一些不寻常的东西——字符串指针被放在了变量里,然后printf()并没有通过CALL,而是通过JMP来调用的。 这个可以很简单的解释清楚,调用者把参数压栈,然后通过CALL调用函数。CALL通过把返回地址压栈,然后做无条件跳转来跳到我们的函数地址。我们的函数在执行时,不管在任何时候都有以下的栈结构(因为它没有任何移动栈指针的语句):

· ESP —— 指向返回地址
· ESP+4 —— 指向变量a (也即参数)

另一方面,当我们这儿调用printf()函数的时候,它也需要有与我们这个函数相同的栈结构,不同之处只在于printf()的第一个参数是指向一个字符串的。 这也就是你之前看到的我们的代码所做的事情。

我们的代码把第一个参数的地址替换了,然后跳转到printf(),就像第一个没有调用我们的函数f()而是先调用了printf()一样。 printf()把一串字符输出到stdout 中,然后执行RET语句, 这一句会从栈上弹出返回地址,因此,此时控制流会返回到调用f()的函数上,而不是f()上。

这一切之所以能发生,是因为printf()在f()的末尾。在一些情况下,这有些类似于longjmp()函数。当然,这一切只是为了提高执行速度。

ARM编译器也有类似的优化,请见5.3.2节“带有多个参数的printf()函数调用”。

11.1.2 ARM: 优化后的 Keil + ARM 模式

.text:0000014C             f1
.text:0000014C 00 00 50 E3          CMP R0, #0
.text:00000150 13 0E 8F 02          ADREQ R0, aZero     ; "zero
"
.text:00000154 05 00 00 0A          BEQ loc_170
.text:00000158 01 00 50 E3          CMP R0, #1
.text:0000015C 4B 0F 8F 02          ADREQ R0, aOne      ; "one
"
.text:00000160 02 00 00 0A          BEQ loc_170
.text:00000164 02 00 50 E3          CMP R0, #2
.text:00000168 4A 0F 8F 12          ADRNE R0, aSomethingUnkno ; "something unknown
"
.text:0000016C 4E 0F 8F 02          ADREQ R0, aTwo      ; "two
"
.text:00000170
.text:00000170                      loc_170             ; CODE XREF: f1+8
.text:00000170                                          ; f1+14
.text:00000170 78 18 00 EA          B __2printf

我们再一次看看这个代码,我们不能确定的说这就是源代码里面的switch()或者说它是if()的封装。

但是,我们可以看到这里它也在试图预测指令(像是ADREQ(相等)),这里它会在R0=0的情况下触发,并且字符串“zero”的地址将被加载到R0中。如果R0=0,下一个指令BEQ将把控制流定向到loc_170处。顺带一说,机智的读者们可能会文,之前的ADREQ已经用其他值填充了R0寄存器了,那么BEQ会被正确触发吗?答案是“是”。因为BEQ检查的是CMP所设置的标记位,但是ADREQ根本没有修改标记位。

还有,在ARM中,一些指令还会加上-S后缀,这表明指令将会根据结果设置标记位。如果没有-S的话,表明标记位并不会被修改。比如,ADD(而不是ADDS)将会把两个操作数相加,但是并不会涉及标记位。这类指令对使用CMP设置标记位之后使用标记位的指令,例如条件跳转来说非常有用。

其他指令对我们来说已经很熟悉了。这里只有一个调用指向printf(),在末尾,我们已经知道了这个小技巧(见5.3.2节)。在末尾处有三个指向printf()的地址。 还有,需要注意的是如果a=2但是a并不在它的选择分支给定的常数中时,“CMP R0, #2”指令在这个情况下就需要知道a是否等于2。如果结果为假,ADRNE将会读取字符串“something unknown”到R0中,因为a在之前已经和0、1做过是否相等的判断了,这里我们可以假定a并不等于0或者1。并且,如果R0=2,a指向的字符串“two”将会被ADREQ载入R0。

11.1.3 ARM: 优化后的 Keil + thumb 模式

.text:000000D4          f1
.text:000000D4 10 B5            PUSH    {R4,LR}
.text:000000D6 00 28            CMP     R0, #0
.text:000000D8 05 D0            BEQ     zero_case
.text:000000DA 01 28            CMP     R0, #1
.text:000000DC 05 D0            BEQ     one_case
.text:000000DE 02 28            CMP     R0, #2
.text:000000E0 05 D0            BEQ     two_case
.text:000000E2 91 A0            ADR     R0, aSomethingUnkno ; "something unknown
"
.text:000000E4 04 E0            B       default_case
.text:000000E6 ;
-------------------------------------------------------------------------
.text:000000E6          zero_case                           ; CODE XREF: f1+4
.text:000000E6 95 A0            ADR     R0, aZero           ; "zero
"
.text:000000E8 02 E0            B       default_case
.text:000000EA ;
-------------------------------------------------------------------------
.text:000000EA          one_case                            ; CODE XREF: f1+8
.text:000000EA 96 A0            ADR     R0, aOne            ; "one
"
.text:000000EC 00 E0            B       default_case
.text:000000EE          ;
-------------------------------------------------------------------------
.text:000000EE          two_case                            ; CODE XREF: f1+C
.text:000000EE 97 A0            ADR     R0, aTwo            ; "two
"
.text:000000F0                  default_case                ; CODE XREF: f1+10
.text:000000F0                                              ; f1+14
.text:000000F0 06 F0 7E F8      BL      __2printf
.text:000000F4 10 BD            POP     {R4,PC}
.text:000000F4           ; End of function f1

正如我之前提到的,在thumb模式下并没有什么功能来连接预测结果,所以这里的thumb代码有点像容易理解的x86 CISC代码。

11.2 多case情况的例子

在有许多case分支的switch()语句中,对编译器来说,转换出一大堆JE/JNE语句并不是太方便。

void f (int a)
{
    switch (a)
    {
        case 0: printf ("zero"); break;
        case 1: printf ("one"); break;
        case 2: printf ("two"); break;
        case 3: printf ("three"); break;
        case 4: printf ("four"); break;
        default: printf ("something unknown"); break;
    };
};

 11.2.1 x86

反汇编结果如下(MSVC 2010):

清单11.3: MSVC 2010

tv64 = -4           ; size = 4
_a$ = 8             ; size = 4
_f      PROC
    push    ebp
    mov     ebp, esp
    push    ecx
    mov     eax, DWORD PTR _a$[ebp]
    mov     DWORD PTR tv64[ebp], eax
    cmp     DWORD PTR tv64[ebp], 4
    ja      SHORT $LN1@f
    mov     ecx, DWORD PTR tv64[ebp]
    jmp     DWORD PTR $LN11@f[ecx*4]
$LN6@f:
    push    OFFSET $SG739 ; ’zero’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN5@f:
    push    OFFSET $SG741 ; ’one’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN4@f:
    push    OFFSET $SG743 ; ’two’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN3@f:
    push    OFFSET $SG745 ; ’three’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN2@f:
    push    OFFSET $SG747 ; ’four’, 0aH, 00H
    call    _printf
    add     esp, 4
    jmp     SHORT $LN9@f
$LN1@f:
    push    OFFSET $SG749 ; ’something unknown’, 0aH, 00H
    call    _printf
    add     esp, 4
$LN9@f:
    mov     esp, ebp
    pop     ebp
    ret     0
    npad    2
$LN11@f:
    DD  $LN6@f ; 0
    DD  $LN5@f ; 1
    DD  $LN4@f ; 2
    DD  $LN3@f ; 3
    DD  $LN2@f ; 4
_f     ENDP

好的,我们可以看到这儿有一组不同参数的printf()调用。 它们不仅有内存中的地址,编译器还给它们带上了符号信息。顺带一提,这些符号标签也都存在于$LN11@f内部函数表中。

在函数最开始,如果a大于4,控制流将会被传递到标签$LN1@f上,这儿会有一个参数为“something unknown”的printf()调用。

如果a值小于等于4,然后我们把它乘以4,[email protected]�址的方法,这样可以正好指向我们需要的元素。比如a等于2。 那么,2×4=8(在32位进程下,所有的函数表元素的长度都只有4字节),$LN11@f的函数表地址+8——这样就能取得$LN4@f标签的位置。 JMP将从函数表中获得$LN4@f的地址,然后跳转向它。

这个函数表,有时候也叫做跳转表(jumptable)。

然后,对应的,printf()的参数就是“two”了。 字面意思, JMP DWORD PTR $LN11@f[ECX*4] 指令意味着“ 跳转到存储在$LN11@f + ecx * 4 地址上的双字”。 npad(64)是一个编译时语言宏,它用于对齐下一个标签,这样存储的地址就会按照4字节(或者16字节)对齐。这个对于处理器来说是十分合适的,因为通过内存总线、缓存从内存中获取32位的值是非常方便而且有效率的。

让我们看看GCC 4.4.1 生成的代码:

清单11.4: GCC 4.4.1

        public f
f       proc near ; CODE XREF: main+10
 
var_18  = dword ptr -18h
arg_0   = dword ptr 8
        push    ebp
        mov     ebp, esp
        sub     esp, 18h ; char *
        cmp     [ebp+arg_0], 4
        ja      short loc_8048444
        mov     eax, [ebp+arg_0]
        shl     eax, 2
        mov     eax, ds:off_804855C[eax]
        jmp     eax
loc_80483FE:                    ; DATA XREF: .rodata:off_804855C
        mov     [esp+18h+var_18], offset aZero ; "zero"
        call    _puts
        jmp     short locret_8048450
loc_804840C:                    ; DATA XREF: .rodata:08048560
        mov     [esp+18h+var_18], offset aOne ; "one"
        call    _puts
        jmp     short locret_8048450
loc_804841A:                    ; DATA XREF: .rodata:08048564
        mov     [esp+18h+var_18], offset aTwo ; "two"
        call    _puts
        jmp     short locret_8048450
loc_8048428:                    ; DATA XREF: .rodata:08048568
        mov     [esp+18h+var_18], offset aThree ; "three"
        call    _puts
        jmp     short locret_8048450
loc_8048436:                    ; DATA XREF: .rodata:0804856C
        mov     [esp+18h+var_18], offset aFour ; "four"
        call    _puts
        jmp     short locret_8048450
loc_8048444:                    ; CODE XREF: f+A
        mov     [esp+18h+var_18], offset aSomethingUnkno ; "something unknown"
        call    _puts
locret_8048450:                 ; CODE XREF: f+26
                                ; f+34...
        leave
        retn
f       endp
 
off_804855C dd offset loc_80483FE ; DATA XREF: f+12
            dd offset loc_804840C
            dd offset loc_804841A
            dd offset loc_8048428
            dd offset loc_8048436

基本和VC生成的相同,除了少许的差别:参数arg_0的乘以4操作被左移2位替换了(这集合和乘以4一样)(见17.3.1节)。 然后标签地址从off_804855C处的数组获取,地址计算之后存储到EAX中,然后通过JMP EAX跳转到实际的地址上。

11.2.2 ARM: 优化后的 Keil + ARM 模式

00000174                f2
00000174 05 00 50 E3            CMP     R0, #5                  ; switch 5 cases
00000178 00 F1 8F 30            ADDCC   PC, PC, R0,LSL#2        ; switch jump
0000017C 0E 00 00 EA            B       default_case            ; jumptable 00000178 default case
00000180                ; -------------------------------------------------------------------------
00000180
00000180                loc_180                         ; CODE XREF: f2+4
00000180 03 00 00 EA            B       zero_case       ; jumptable 00000178 case 0
00000184                ; -------------------------------------------------------------------------
00000184
00000184                loc_184                         ; CODE XREF: f2+4
00000184 04 00 00 EA            B       one_case        ; jumptable 00000178 case 1
00000188                ; -------------------------------------------------------------------------
00000188
00000188                loc_188                         ; CODE XREF: f2+4
00000188 05 00 00 EA            B       two_case        ; jumptable 00000178 case 2
0000018C                ; -------------------------------------------------------------------------
0000018C
0000018C                loc_18C                         ; CODE XREF: f2+4
0000018C 06 00 00 EA            B       three_case      ; jumptable 00000178 case 3
00000190                ; -------------------------------------------------------------------------
00000190
00000190                loc_190                         ; CODE XREF: f2+4
00000190 07 00 00 EA            B       four_case       ; jumptable 00000178 case 4
00000194                ; -------------------------------------------------------------------------
00000194
00000194                zero_case                       ; CODE XREF: f2+4
00000194                                                ; f2:loc_180
00000194 EC 00 8F E2            ADR     R0, aZero       ; jumptable 00000178 case 0
00000198 06 00 00 EA            B       loc_1B8
0000019C                ; -------------------------------------------------------------------------
0000019C
0000019C one_case                                       ; CODE XREF: f2+4
0000019C                                                ; f2:loc_184
0000019C EC 00 8F E2            ADR     R0, aOne        ; jumptable 00000178 case 1
000001A0 04 00 00 EA            B       loc_1B8
000001A4                ; -------------------------------------------------------------------------
000001A4
000001A4                two_case                        ; CODE XREF: f2+4
000001A4                                                ; f2:loc_188
000001A4 01 0C 8F E2            ADR     R0, aTwo        ; jumptable 00000178 case 2
000001A8 02 00 00 EA            B       loc_1B8
000001AC                ; -------------------------------------------------------------------------
000001AC
000001AC                three_case                      ; CODE XREF: f2+4
000001AC                                                ; f2:loc_18C
000001AC 01 0C 8F E2            ADR     R0, aThree ; jumptable 00000178 case 3
000001B0 00 00 00 EA            B       loc_1B8
000001B4 ; -------------------------------------------------------------------------
000001B4
000001B4                four_case                       ; CODE XREF: f2+4
000001B4                                                ; f2:loc_190
000001B4 01 0C 8F E2            ADR     R0, aFour       ; jumptable 00000178 case 4
000001B8
000001B8                loc_1B8                         ; CODE XREF: f2+24
000001B8                                                ; f2+2C
000001B8 66 18 00 EA            B       __2printf
000001BC ; -------------------------------------------------------------------------
000001BC
000001BC                default_case                    ; CODE XREF: f2+4
000001BC                                                ; f2+8
000001BC D4 00 8F E2            ADR     R0, aSomethingUnkno ; jumptable 00000178 default case
000001C0 FC FF FF EA            B       loc_1B8
000001C0                ; End of function f2

这个代码利用了ARM的特性,这里ARM模式下所有指令都是4个字节。

让我们记住a的最大值是4,任何更大额值都会导致它输出“something unknown”。

最开始的“CMP R0, #5”指令将a的值与5比较。

下一个“ADDCC PC, PC, R0, LSL#2”指令将仅在R0<5的时候执行(CC = Carry clear , 小于)。所以,如果ADDCC并没有触发(R0>=5时),它将会跳转到default _case标签上。

但是,如果R0<5,而且ADDCC触发了,将会发生下列事情:

R0中的值会乘以4,事实上,LSL#2代表着“左移2位”,但是像我们接下来(见17.3.1节)要看到的“移位”一样,左移2位代表乘以4。

然后,我们得到了R0 * 4的值,这个值将会和PC中现有的值相加,因此跳转到下述其中一个B(Branch 分支)指令上。

在ADDCC执行时,PC中的值(0x180)比ADDCC指令的值(0x178)提前8个字节,换句话说,提前2个指令。

这也就是为ARM处理器通道工作的方式:当ADDCC指令执行的时候,此时处理器将开始处理下一个指令,这也就是PC会指向这里的原因。

如果a=0,那么PC将不会和任何值相加,PC中实际的值将写入PC中(它相对之领先8个字节),然后跳转到标签loc_180处。这就是领先ADDCC指令8个字节的地方。

在a=1时,PC+8+a4 = PC+8+14 = PC+16= 0x184 将被写入PC中,这是loc_184标签的地址。

每当a上加1,PC都会增加4,4也是ARM模式的指令长度,而且也是B指令的长度。这组里面有5个这样的指令。

这5个B指令将传递控制流,也就是传递switch()中指定的字符串和对应的操作等等。

11.2.3 ARM: 优化后的 Keil + thumb 模式

000000F6                        EXPORT  f2
000000F6                f2
000000F6 10 B5                  PUSH    {R4,LR}
000000F8 03 00                  MOVS    R3, R0
000000FA 06 F0 69 F8            BL      __ARM_common_switch8_thumb ; switch 6 cases
000000FA                        ;-------------------------------------------------------------------------
000000FE 05                     DCB 5
000000FF 04 06 08 0A 0C 10      DCB 4, 6, 8, 0xA, 0xC, 0x10 ; jump table for switch
statement
00000105 00                     ALIGN 2
00000106
00000106                zero_case                       ; CODE XREF: f2+4
00000106 8D A0                  ADR       R0, aZero       ; jumptable 000000FA case 0
00000108 06 E0                  B       loc_118
0000010A                        ;-------------------------------------------------------------------------
0000010A
0000010A                    one_case                    ; CODE XREF: f2+4
0000010A 8E A0                  ADR       R0, aOne        ; jumptable 000000FA case 1
0000010C 04 E0                  B       loc_118
0000010E                        ;-------------------------------------------------------------------------
0000010E
0000010E                    two_case                    ; CODE XREF: f2+4
0000010E 8F A0                  ADR       R0, aTwo        ; jumptable 000000FA case 2
00000110 02 E0                  B       loc_118
00000112                        ;-------------------------------------------------------------------------
00000112
00000112                    three_case                  ; CODE XREF: f2+4
00000112 90 A0                  ADR       R0, aThree      ; jumptable 000000FA case 3
00000114 00 E0                  B       loc_118
00000116                        ;-------------------------------------------------------------------------
00000116
00000116                    four_case                   ; CODE XREF: f2+4
00000116 91 A0                  ADR       R0, aFour       ; jumptable 000000FA case 4
00000118
00000118                    loc_118                     ; CODE XREF: f2+12
00000118                                                ; f2+16
00000118 06 F0 6A F8            BL        __2printf
0000011C 10 BD                  POP       {R4,PC}
0000011E                        ;-------------------------------------------------------------------------
0000011E
0000011E                    default_case                ; CODE XREF: f2+4
0000011E 82 A0                  ADR       R0, aSomethingUnkno ; jumptable 000000FA default
case
00000120 FA E7                  B         loc_118
 
000061D0                        EXPORT __ARM_common_switch8_thumb
000061D0                    __ARM_common_switch8_thumb ; CODE XREF: example6_f2+4
000061D0 78 47                  BX          PC
000061D0                    ;---------------------------------------------------------------------------
000061D2 00 00                  ALIGN 4
000061D2                    ; End of function __ARM_common_switch8_thumb
000061D2
000061D4                        CODE32
000061D4
000061D4                    ; =============== S U B R O U T I N E =======================================
000061D4
000061D4
000061D4                    __32__ARM_common_switch8_thumb  ; CODE XREF:
    __ARM_common_switch8_thumb
000061D4 01 C0 5E E5            LDRB    R12, [LR,#-1]
000061D8 0C 00 53 E1            CMP     R3, R12
000061DC 0C 30 DE 27            LDRCSB  R3, [LR,R12]
000061E0 03 30 DE 37            LDRCCB  R3, [LR,R3]
000061E4 83 C0 8E E0            ADD     R12, LR, R3,LSL#1
000061E8 1C FF 2F E1            BX      R12
000061E8                ; End of function __32__ARM_common_switch8_thumb

一个不能确定的事实是thumb、thumb-2中的所有指令都有同样的大小。甚至可以说是在这些模式下,指令的长度是可变的,就像x86一样。

所以这一定有一个特别的表单,里面包含有多少个case(除了默认的case),然后和它们的偏移,并且给他们每个都加上一个标签,这样控制流就可以传递到正确的位置。 这里有一个特别的函数来处理表单和处理控制流,被命名为_ARMcommon_switch8_thumb。它由“BX PC”指令开始,这个函数用来将处理器切换到ARM模式,然后你就可以看到处理表单的函数。不过对我们来说,在这里解释它太复杂了,所以我们将省去一些细节。

但是有趣的是,这个函数使用LR寄存器作为表单的指针。还有,在这个函数调用后,LR将包含有紧跟着“BL __ARM_common_switch8_thumb”指令的地址,然后表单就由此开始。

当然,这里也不值得去把生成的代码作为单独的函数,然后再去重用它们。因此在switch()处理相似的位置、相似的case时编译器并不会生成相同的代码。

IDA成功的发觉到它是一个服务函数以及函数表,然后给各个标签加上了合适的注释,比如jumptable 000000FA case 0。 # 循环结构

12.1 x86

在x86指令集中,有一些独特的LOOP指令,它们会检查ECX中的值,如果它不是0的话,它会逐渐递减ECX的值(减一),然后把控制流传递给LOOP操作符提供的标签处。也许,这个指令并不是多方便,所以,我没有看到任何现代编译器自动使用它。如果你看到哪里的代码用了这个结构,那它很有可能是程序员手写的汇编代码。

顺带一提,作为家庭作业,你可以试着解释以下为什么这个指令如此不方便。

C/C++循环操作是由for()、while()、do/while()命令发起的。

让我们从for()开始吧。

这个命令定义了循环初始值(为循环计数器设置初值),循环条件(比如,计数器是否大于一个阈值?),以及在每次迭代(增/减)时和循环体中做什么。

for(初始化; 条件; 每次迭代时执行的语句)
{
    循环体;
}

所以,它生成的代码也将被考虑为4个部分。

让我们从一个简单的例子开始吧:

#include <stdio.h>
void f(int i)
{
    printf ("f(%d)
", i);
};
int main()
{
    int i;
    for (i=2; i<10; i++)
    f(i);
    return 0;
};

反汇编结果如下(MSVC 2010):

清单12.1: MSVC 2010

_i$ = -4
_main     PROC
        push    ebp
        mov     ebp, esp
        push    ecx
        mov     DWORD PTR _i$[ebp], 2       ; loop initialization
        jmp     SHORT $LN3@main
$LN2@main:
        mov     eax, DWORD PTR _i$[ebp]     ; here is what we do after each iteration:
        add     eax, 1                      ; add 1 to i value
        mov     DWORD PTR _i$[ebp], eax
$LN3@main:
        cmp     DWORD PTR _i$[ebp], 10      ; this condition is checked *before* each iteration
        jge     SHORT $LN1@main             ; if i is biggest or equals to 10, let’s finish loop
        mov     ecx, DWORD PTR _i$[ebp]     ; loop body: call f(i)
        push    ecx
        call    _f
        add     esp, 4
        jmp     SHORT $LN2@main             ; jump to loop begin
$LN1@main:                                  ; loop end
        xor     eax, eax
        mov     esp, ebp
        pop     ebp
        ret     0
_main ENDP

看起来没什么特别的。

GCC 4.4.1生成的代码也基本相同,只有一些微妙的区别。

清单12.1: GCC 4.4.1

main        proc near           ; DATA XREF: _start+17
var_20      = dword ptr -20h
var_4       = dword ptr -4
            push    ebp
            mov     ebp, esp
            and     esp, 0FFFFFFF0h
            sub     esp, 20h
            mov     [esp+20h+var_4], 2 ; i initializing
            jmp     short loc_8048476
loc_8048465:
            mov     eax, [esp+20h+var_4]
            mov     [esp+20h+var_20], eax
            call    f
            add     [esp+20h+var_4], 1 ; i increment
loc_8048476:
            cmp     [esp+20h+var_4], 9
            jle     short loc_8048465 ; if i<=9, continue loop
            mov     eax, 0
            leave
            retn
main        endp

现在,让我们看看如果我们打开了优化开关会得到什么结果(/Ox):

清单12.3: 优化后的 MSVC

_main PROC
    push esi
    mov esi, 2
$LL3@main:
    push esi
    call _f
    inc esi
    add esp, 4
    cmp esi, 10 ; 0000000aH
    jl SHORT $LL3@main
    xor eax, eax
    pop esi
    ret 0
_main ENDP

要说它做了什么,那就是:本应在栈上分配空间的变量i被移动到了寄存器ESI里面。因为我们这样一个小函数并没有这么多的本地变量,所以它才可以这么做。 这么做的话,一个重要的条件是函数f()不能改变ESI的值。我们的编译器在这里倒是非常确定。假设编译器决定在f()中使用ESI寄存器的话,ESI的值将在函数的初始化阶段被压入栈保存,并且在函数的收尾阶段将其弹出(注:即还原现场,保证程序片段执行前后某个寄存器值不变)。这个操作有点像函数开头和结束时的PUSH ESI/ POP ESI操作对。

让我们试一试开启了最高优化的GCC 4.4.1(-03优化)。

清单12.4: 优化后的GCC 4.4.1

main    proc near
var_10  = dword ptr -10h
        push    ebp
        mov     ebp, esp
        and     esp, 0FFFFFFF0h
        sub     esp, 10h
        mov     [esp+10h+var_10], 2
        call    f
        mov     [esp+10h+var_10], 3
        call    f
        mov     [esp+10h+var_10], 4
        call    f
        mov     [esp+10h+var_10], 5
        call    f
        mov     [esp+10h+var_10], 6
        call    f
        mov     [esp+10h+var_10], 7
        call    f
        mov     [esp+10h+var_10], 8
        call    f
        mov     [esp+10h+var_10], 9
        call    f
        xor     eax, eax
        leave
        retn
main endp

GCC直接把我们的循环给分解成顺序结构了。

循环分解(Loop unwinding)对这些没有太多迭代次数的循环结构来说是比较有利的,移除所有循环结构之后程序的效率会得到提升。但是,这样生成的代码明显会变得很大。

好的,现在我们把循环的最大值改为100。GCC现在生成如下:

清单12.5: GCC

        public main
main    proc near
var_20  = dword ptr -20h
        push    ebp
        mov     ebp, esp
        and     esp, 0FFFFFFF0h
        push    ebx
        mov     ebx, 2 ; i=2
        sub     esp, 1Ch
        nop     ; aligning label loc_80484D0 (loop body begin) by 16-byte border
loc_80484D0:
        mov     [esp+20h+var_20], ebx ; pass i as first argument to f()
        add     ebx, 1 ; i++
        call    f
        cmp     ebx, 64h ; i==100?
        jnz     short loc_80484D0 ; if not, continue
        add     esp, 1Ch
        xor     eax, eax ; return 0
        pop     ebx
        mov     esp, ebp
        pop     ebp
        retn
main endp

这时,代码看起来非常像MSVC 2010开启/Ox优化后生成的代码。除了这儿它用了EBX来存储变量i。 GCC也确信f()函数中不会修改EBX的值,假如它要用到EBX的话,它也一样会在函数初始化和收尾时保存EBX和还原EBX,就像这里main()函数做的事情一样。

12.1.1 OllyDbg

让我们通过/Ox和/Ob0编译程序,然后放到OllyDbg里面查看以下结果。

看起来OllyDbg能够识别简单的循环,然后把它们放在一块,为了演示方便,大家可以看图12.1。

通过跟踪代码(F8, 步过)我们可以看到ESI是如何递增的。这里的例子是ESI = i = 6: 图12.2。

9是i的最后一个循环制,这也就是为什么JL在递增的最后不会触发,之后函数结束,如图12.3。

图12.1: OllyDbg main()开始

图12.2: OllyDbg: 循环体刚刚递增了i,现在i=6

图12.3: OllyDbg中ESI=10,循环终止

12.1.2 跟踪

像我们所见的一样,手动在调试器里面跟踪代码并不是一件方便的事情。这也就是我给自己写了一个跟踪程序的原因。

我在IDA中打开了编译后的例子,然后找到了PUSH ESI指令(作用:给f()传递唯一的参数)的地址,对我的机器来说是0x401026,然后我运行了跟踪器:

tracer.exe -l:loops_2.exe bpx=loops_2.exe!0x00401026

BPX的作用只是在对应地址上设置断点然后输出寄存器状态。

在tracer.log中我看到执行后的结果:

PID=12884|New process loops_2.exe
(0) loops_2.exe!0x401026
EAX=0x00a328c8 EBX=0x00000000 ECX=0x6f0f4714 EDX=0x00000000
ESI=0x00000002 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=PF ZF IF
(0) loops_2.exe!0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000003 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
(0) loops_2.exe!0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000004 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
(0) loops_2.exe!0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000005 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF AF SF IF
(0) loops_2.exe!0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000006 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
(0) loops_2.exe!0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000007 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF AF SF IF
(0) loops_2.exe!0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000008 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF AF SF IF
(0) loops_2.exe!0x401026
EAX=0x00000005 EBX=0x00000000 ECX=0x6f0a5617 EDX=0x000ee188
ESI=0x00000009 EDI=0x00333378 EBP=0x0024fbfc ESP=0x0024fbb8
EIP=0x00331026
FLAGS=CF PF AF SF IF
PID=12884|Process loops_2.exe exited. ExitCode=0 (0x0)

我们可以看到ESI寄存器是如何从2变为9的。

甚至于跟踪器可以收集某个函数调用内所有寄存器的值,所以它被叫做跟踪器(a trace)。每个指令都会被它跟踪上,所有感兴趣的寄存器值都会被它提示出来,然后收集下来。 然后可以生成IDA能用的.idc-script。所以,在IDA中我知道了main()函数地址是0x00401020,然后我执行了:

tracer.exe -l:loops_2.exe bpf=loops_2.exe!0x00401020,trace:cc

bpf的意思是在函数上设置断点。

结果是我得到了loops_2.exe.idc和loops_2.exe_clear.idc两个脚本。我加载loops_2.idc到IDA中,然后可以看到图12.4所示的内容。

我们可以看到ESI在循环体开始时从2变化为9,但是在递增完之后,它的值从9(译注:作者原文是3,但是揣测是笔误,应为9。)变为了0xA(10)。我们也可以看到main()函数结束时EAX被设置为了0。

编译器也生成了loops_2.exe.txt,包含有每个指令执行了多少次和寄存器值的一些信息:

清单12.6: loops_2.exe.txt

0x401020 (.text+0x20), e= 1 [PUSH ESI] ESI=1
0x401021 (.text+0x21), e= 1 [MOV ESI, 2]
0x401026 (.text+0x26), e= 8 [PUSH ESI] ESI=2..9
0x401027 (.text+0x27), e= 8 [CALL 8D1000h] tracing nested maximum level (1) reached,
skipping this CALL 8D1000h=0x8d1000
0x40102c (.text+0x2c), e= 8 [INC ESI] ESI=2..9
0x40102d (.text+0x2d), e= 8 [ADD ESP, 4] ESP=0x38fcbc
0x401030 (.text+0x30), e= 8 [CMP ESI, 0Ah] ESI=3..0xa
0x401033 (.text+0x33), e= 8 [JL 8D1026h] SF=false,true OF=false
0x401035 (.text+0x35), e= 1 [XOR EAX, EAX]
0x401037 (.text+0x37), e= 1 [POP ESI]
0x401038 (.text+0x38), e= 1 [RETN] EAX=0

生成的代码可以在此使用:

图12.4: IDA加载了.idc-script之后的内容

12.2 ARM

12.2.1 无优化 Keil + ARM模式

main
    STMFD   SP!, {R4,LR}
    MOV     R4, #2
    B       loc_368
; ---------------------------------------------------------------------------
 
loc_35C                 ; CODE XREF: main+1C
    MOV     R0, R4
    BL      f
    ADD     R4, R4, #1
loc_368                 ; CODE XREF: main+8
    CMP     R4, #0xA
    BLT     loc_35C
    MOV     R0, #0
    LDMFD   SP!, {R4,PC}

迭代计数器i存储到了R4寄存器中。

MOV R4,#2初始化i。
MOV R0,R4 和 BL f 指令组成循环体,第一个指令为f()准备参数,第二个用来调用它。
ADD R4,R4, #1 指令在每次迭代中为i加一。
CMP R4,#0xA 将i和0xA(10)比较,下一个指令BLT(Branch Less Than,分支小于)将在i<10时跳转。
否则, R0将会被写入0(因为我们的函数返回0),然后函数执行终止。

12.2.2 优化后的 Keil + ARM模式

_main
        PUSH    {R4,LR}
        MOVS    R4, #2

loc_132             ; CODE XREF: _main+E
        MOVS    R0, R4
        BL      example7_f
        ADDS    R4, R4, #1
        CMP     R4, #0xA
        BLT     loc_132
        MOVS    R0, #0
        POP     {R4,PC}

事实上,是一样的。

12.2.3 优化后的 Xcode(LLVM) + thumb-2 模式

_main
    PUSH    {R4,R7,LR}
    MOVW    R4, #0x1124 ; "%d
"
    MOVS    R1, #2
    MOVT.W  R4, #0
    ADD     R7, SP, #4
    ADD     R4, PC
    MOV     R0, R4
    BLX     _printf
    MOV     R0, R4
    MOVS    R1, #3
    BLX     _printf
    MOV     R0, R4
    MOVS    R1, #4
    BLX     _printf
    MOV     R0, R4
    MOVS    R1, #5
    BLX     _printf
    MOV     R0, R4
    MOVS    R1, #6
    BLX     _printf
    MOV     R0, R4
    MOVS    R1, #7
    BLX     _printf
    MOV     R0, R4
    MOVS    R1, #8
    BLX     _printf
    MOV     R0, R4
    MOVS    R1, #9
    BLX     _printf
    MOVS    R0, #0
    POP     {R4,R7,PC}

事实上,printf是在我的f()函数里调用的:

void f(int i)
{
   // do something here
   printf ("%d", i);
};

所以,LLVM不仅仅是拆解了(unroll)循环,而且还把我的短函数f()给作为内联函数看待了,这样,它把它的函数体内插了8遍,而不是用一个循环来解决。对于我们这种简短的函数来说,编译器这样做是有可能的。

12.3 更多的一些事情

在编译器生成的代码里面,我们可以发现在i初始化之后,循环体并不会被执行,转而是先检查i的条件,在这之后才开始执行循环体。这么做是正确的,因为,如果循环条件在一开始就不满足,那么循环体是不应当被执行的。比如,在下面的例子中,就可能出现这个情况:

for (i=0; i<total_entries_to_process; i++)
    loop_body;

如果total_entries_to_process等于0,那么循环体就不应该被执行。这就是为什么应当在循环体被执行之前检查循环条件。 但是,开启编译器优化之后,如果编译器确定不会出现上面这种情况的话,那么条件检查和循环体的语句可能会互换(比如我们上面提到的简单的例子以及Keil、Xcode(LLVM)、MSVC的优化模式)。

strlen()

现在,让我们再看一眼循环结构。通常,strlen()函数是由while()来实现的。这就是MSVC标准库中strlen的做法:

int my_strlen (const char * str)
{
    const char *eos = str;
    while( *eos++ ) ;
    return( eos - str - 1 );
}
int main()
{
    // test
    return my_strlen("hello!");
};

13.1 x86

13.1.1 无优化的 MSVC

让我们编译一下:

_eos$ = -4                  ; size = 4
_str$ = 8                   ; size = 4
_strlen PROC
    push    ebp
    mov     ebp, esp
    push    ecx
    mov     eax, DWORD PTR _str$[ebp]   ; place pointer to string from str
    mov     DWORD PTR _eos$[ebp], eax   ; place it to local varuable eos
$LN2@strlen_:
    mov     ecx, DWORD PTR _eos$[ebp]   ; ECX=eos
 
    ; take 8-bit byte from address in ECX and place it as 32-bit value to EDX with sign extension
 
    movsx   edx, BYTE PTR [ecx]
    mov     eax, DWORD PTR _eos$[ebp]   ; EAX=eos
    add     eax, 1 ; increment EAX
    mov     DWORD PTR _eos$[ebp], eax   ; place EAX back to eos
    test    edx, edx                    ; EDX is zero?
    je      SHORT $LN1@strlen_          ; yes, then finish loop
    jmp     SHORT $LN2@strlen_          ; continue loop
$LN1@strlen_:
 
    ; here we calculate the difference between two pointers
 
    mov     eax, DWORD PTR _eos$[ebp]
    sub     eax, DWORD PTR _str$[ebp]
    sub     eax, 1                      ; subtract 1 and return result
    mov     esp, ebp
    pop     ebp
    ret     0
_strlen_ ENDP

我们看到了两个新的指令:MOVSX(见13.1.1节)和TEST。

关于第一个:MOVSX用来从内存中取出字节然后把它放到一个32位寄存器中。MOVSX意味着MOV with Sign-Extent(带符号扩展的MOV操作)。MOVSX操作下,如果复制源是负数,从第8到第31的位将被设为1,否则将被设为0。

现在解释一下为什么要这么做。

C/C++标准将char(译注:1字节)类型定义为有符号的。如果我们有2个值,一个是char,另一个是int(int也是有符号的),而且它的初值是-2(被编码为0xFE),我们将这个值拷贝到int(译注:一般是4字节)中时,int的值将是0x000000FE,这时,int的值将是254而不是-2。因为在有符号数中,-2被编码为0xFFFFFFFE。 所以,如果我们需要将0xFE从char类型转换为int类型,那么,我们就需要识别它的符号并扩展它。这就是MOVSX所做的事情。

请参见章节“有符号数表示方法”。(35章)

我不太确定编译器是否需要将char变量存储在EDX中,它可以使用其中8位(我的意思是DL部分)。显然,编译器的寄存器分配器就是这么工作的。

然后我们可以看到TEST EDX, EDX。关于TEST指令,你可以阅读一下位这一节(17章)。但是现在我想说的是,这个TEST指令只是检查EDX的值是否等于0。

13.1.2 无优化的 GCC

让我们在GCC 4.4.1下测试:

        public strlen
strlen  proc near
 
eos     = dword ptr -4
arg_0   = dword ptr 8
 
        push    ebp
        mov     ebp, esp
        sub     esp, 10h
        mov     eax, [ebp+arg_0]
        mov     [ebp+eos], eax
 
loc_80483F0:
        mov     eax, [ebp+eos]
        movzx   eax, byte ptr [eax]
        test    al, al
        setnz   al
        add     [ebp+eos], 1
        test    al, al
        jnz     short loc_80483F0
        mov     edx, [ebp+eos]
        mov     eax, [ebp+arg_0]
        mov     ecx, edx
        sub     ecx, eax
        mov     eax, ecx
        sub     eax, 1
        leave
        retn
strlen  endp

可以看到它的结果和MSVC几乎相同,但是这儿我们可以看到它用MOVZX代替了MOVSX。 MOVZX代表着MOV with Zero-Extend(0位扩展MOV)。这个指令将8位或者16位的值拷贝到32位寄存器,然后将剩余位设置为0。事实上,这个指令比较方便的原因是它将两条指令组合到了一起:xor eax,eax / mov al, […]。

另一方面来说,显然这里编译器可以产生如下代码: mov al, byte ptr [eax] / test al, al,这几乎是一样的,但是,EAX高位将还是会有随机的数值存在。 但是我们想一想就知道了,这正是编译器的劣势所在——它不能产生更多能让人容易理解的代码。严格的说, 事实上编译器也并没有义务为人类产生易于理解的代码。

还有一个新指令,SETNZ。这里,如果AL包含非0, test al, al将设置ZF标记位为0。 但是SETNZ中,如果ZF == 0(NZ的意思是非零,Not Zero),AL将设置为1。用自然语言描述一下,如果AL非0,我们就跳转到loc_80483F0。编译器生成了少量的冗余代码,不过不要忘了我们已经把优化给关了。

13.1.3 优化后的 MSVC

让我们在MSVC 2012下编译,打开优化选项/Ox:

清单13.1: MSVC 2010 /Ox /Ob0

_str$ = 8               ; size = 4
_strlen     PROC
            mov     edx, DWORD PTR _str$[esp-4] ; EDX -> 指向字符的指针
            mov     eax, edx                    ; 移动到 EAX
$LL2@strlen:
            mov     cl, BYTE PTR [eax]          ; CL = *EAX
            inc     eax                         ; EAX++
            test    cl, cl                      ; CL==0?
            jne     SHORT $LL2@strlen           ; 否,继续循环
            sub     eax, edx                    ; 计算指针差异
            dec     eax                         ; 递减 EAX
            ret     0
_strlen ENDP

现在看起来就更简单点了。但是没有必要去说编译器能在这么小的函数里面,如此有效率的使用如此少的本地变量,特殊情况而已。

INC / DEC是递增 / 递减指令,或者换句话说,给变量加一或者减一。

13.1.4 优化后的 MSVC + OllyDbg

我们可以在OllyDbg中试试这个(优化过的)例子。这儿有一个简单的最初的初始化:图13.1。 我们可以看到OllyDbg

找到了一个循环,然后为了方便观看,OllyDbg把它们环绕在一个方格区域中了。在EAX上右键点击,我们可以选择“Follow in Dump”,然后内存窗口的位置将会跳转到对应位置。我们可以在内存中看到这里有一个“hello!”的字符串。 在它之后至少有一个0字节,然后就是随机的数据。 如果OllyDbg发现了一个寄存器是一个指向字符串的指针,那么它会显示这个字符串。

让我们按下F8(步过)多次,我们可以看到当前地址的游标将在循环体中回到开始的地方:图13.2。我们可以看到EAX现在包含有字符串的第二个字符。

我们继续按F8,然后执行完整个循环:图13.3。我们可以看到EAX现在包含空字符()的地址,也就是字符串的末尾。同时,EDX并没有改变,所以它还是指向字符串的最开始的地方。现在它就可以计算这两个寄存器的差值了。

然后SUB指令会被执行:图13.4。 差值保存在EAX中,为7。 但是,字符串“hello!”的长度是6,这儿7是因为包含了末尾的。但是strlen()函数必须返回非0部分字符串的长度,所以在最后还是要给EAX减去1,然后将它作为返回值返回,退出函数。

图13.1: 第一次循环迭代起始位置

图13.2:第二次循环迭代开始位置

图13.3: 现在要计算二者的差了

图13.4: EAX需要减一

13.1.5 优化过的GCC

让我们打开GCC 4.4.1的编译优化选项(-O3):

        public strlen
strlen  proc near
 
arg_0   = dword ptr 8
 
        push    ebp
        mov     ebp, esp
        mov     ecx, [ebp+arg_0]
        mov     eax, ecx
 
loc_8048418:
        movzx   edx, byte ptr [eax]
        add     eax, 1
        test    dl, dl
        jnz     short loc_8048418
        not     ecx
        add     eax, ecx
        pop     ebp
        retn
strlen endp

这儿GCC和MSVC的表现方式几乎一样,除了MOVZX的表达方式。

但是,这里的MOVZX可能被替换为mov dl, byte ptr [eax]。

可能是因为对GCC编译器来说,生成此种代码会让它更容易记住整个寄存器已经分配给char变量了,然后因此它就可以确认高位在任何时候都不会有任何干扰数据的存在了。

之后,我们可以看到新的操作符NOT。这个操作符把操作数的所有位全部取反。可以说,它和XOR ECX, 0fffffffh效果是一样的。NOT和接下来的ADD指令计算差值然后将结果减一。在最开始的ECX出存储了str的指针,翻转之后会将它的值减一。

请参考“有符号数的表达方式”。(第35章)

换句话说,在函数最后,也就是循环体后面其实是做了这样一个操作:

ecx=str;
eax=eos;
ecx=(-ecx)-1;
eax=eax+ecx
return eax

这样做其实几乎相等于:

ecx=str;
eax=eos;
eax=eax-ecx;
eax=eax-1;
return eax

为什么GCC会认为它更棒呢?我不能确定,但是我确定上下两种方式都应该有相同的效率。

13.2 ARM

13.2.1 无优化 Xcode (LLVM) + ARM模式

清单13.2: 无优化的Xcode(LLVM)+ ARM模式

_strlen
 
eos     = -8
str     = -4
        SUB     SP, SP, #8 ; allocate 8 bytes for local variables
        STR     R0, [SP,#8+str]
        LDR     R0, [SP,#8+str]
        STR     R0, [SP,#8+eos]
 
loc_2CB8                ; CODE XREF: _strlen+28
        LDR     R0, [SP,#8+eos]
        ADD     R1, R0, #1
        STR     R1, [SP,#8+eos]
        LDRSB   R0, [R0]
        CMP     R0, #0
        BEQ     loc_2CD4
        B       loc_2CB8
; ----------------------------------------------------------------
 
loc_2CD4                ; CODE XREF: _strlen+24
        LDR     R0, [SP,#8+eos]
        LDR     R1, [SP,#8+str]
        SUB     R0, R0, R1 ; R0=eos-str
        SUB     R0, R0, #1 ; R0=R0-1
        ADD     SP, SP, #8 ; deallocate 8 bytes for local variables
        BX      LR

无优化的LLVM生成了太多的代码,但是,这里我们可以看到函数是如何在栈上处理本地变量的。我们的函数里只有两个本地变量,eos和str。

在这个IDA生成的列表里,我把var_8和var_4命名为了eos和str。

所以,第一个指令只是把输入的值放到str和eos里。

循环体从loc_2CB8标签处开始。

循环体的前三个指令(LDR、ADD、STR)将eos的值载入R0,然后值会加一,然后存回栈上本地变量eos。

下一条指令“LDRSB R0, [R0]”(Load Register Signed Byte,读取寄存器有符号字)将从R0地址处读取一个字节,然后把它符号扩展到32位。这有点像是x86里的MOVSX函数(见13.1.1节)。因为char在C标准里面是有符号的,所以编译器也把这个字节当作有符号数。我已经在13.1.1节写了这个,虽然那里是相对x86来说的。 需要注意的是,在ARM里会单独分割使用8位或者16位或者32位的寄存器,就像x86一样。显然,这是因为x86有一个漫长的历史上的兼容性问题,它需要和他的前身:16位8086处理器甚至8位的8080处理器相兼容。但是ARM确是从32位的精简指令集处理器中发展而成的。因此,为了处理单独的字节,程序必须使用32位的寄存器。 所以LDRSB一个接一个的将符号从字符串内载入R0,下一个CMP和BEQ指令将检查是否读入的符号是0,如果不是0,控制流将重新回到循环体,如果是0,那么循环结束。 在函数最后,程序会计算eos和str的差,然后减一,返回值通过R0返回。

注意:这个函数并没有保存寄存器。这是因为由ARM调用时的转换,R0-R3寄存器是“临时寄存器”(scratch register),它们只是为了传递参数用的,它们的值并不会在函数退出后保存,因为这时候函数也不会再使用它们。因此,它们可以被我们用来做任何事情,而这里其他寄存器都没有使用到,这也就是为什么我们的栈上事实上什么都没有的原因。因此,控制流可以通过简单跳转(BX)来返回调用的函数,地址存在LR寄存器中。

13.2.2 优化后的 Xcode (LLVM) + thumb 模式

清单13.3: 优化后的 Xcode(LLVM) + thumb模式

_strlen
        MOV     R1, R0
 
loc_2DF6                ; CODE XREF: _strlen+8
        LDRB.W  R2, [R1],#1
        CMP     R2, #0
        BNE     loc_2DF6
        MVNS    R0, R0
        ADD     R0, R1
        BX      LR

在优化后的LLVM中,为eos和str准备的栈上空间可能并不会分配,因为这些变量可以永远正确的存储在寄存器中。在循环体开始之前,str将一直存储在R0中,eos在R1中。

"LDRB.W R2, [R1],#1"指令从R1内存中读取字节到R2里,按符号扩展成32位的值,但是不仅仅这样。 在指令最后的#1被称为“后变址”(Post-indexed address),这代表着在字节读取之后,R1将会加一。这个在读取数组时特别方便。

在x86中这里并没有这样的地址存取方式,但是在其他处理器中却是有的,甚至在PDP-11里也有。这是PDP-11中一个前增、后增、前减、后减的例子。这个很像是C语言(它是在PDP-11上开发的)中“罪恶的”语句形式ptr++、++ptr、ptr–、–ptr。顺带一提,C的这个语法真的很难让人记住。下为具体叙述:

C语言作者之一的Dennis Ritchie提到了这个可能是由于另一个作者Ken Thompson开发的功能,因此这个处理器特性在PDP-7中最早出现了(参考资料[28][29])。因此,C语言编译器将在处理器支持这种指令时使用它。

然后可以指出的是循环体的CMP和BNE,这两个指令将一直处理到字符串中的0出现为止。

MVNS(翻转所有位,也即x86的NOT)指令和ADD指令计算cos-str-1.事实上,这两个指令计算出R0=str+cos。这和源码里的指令效果一样,为什么他要这么做的原因我在13.1.5节已经说过了。

显然,LLVM,就像是GCC一样,会把代码变得更短或者更快。

13.2.3 优化后的 Keil + ARM 模式

清单13.4: 优化后的 Keil + ARM模式

_strlen
        MOV     R1, R0
loc_2C8                 ; CODE XREF: _strlen+14
        LDRB    R2, [R1],#1
        CMP     R2, #0
        SUBEQ   R0, R1, R0
        SUBEQ   R0, R0, #1
        BNE     loc_2C8
        BX      LR

这个和我们之前看到的几乎一样,除了str-cos-1这个表达式并不在函数末尾计算,而是被调到了循环体中间。 可以回忆一下-EQ后缀,这个代表指令仅仅会在CMP执行之前的语句互相相等时才会执行。因此,如果R0的值是0,两个SUBEQ指令都会执行,然后结果会保存在R0寄存器中。

除法

下面是一个非常简单的函数

int f(int a)
{
    return a/9;
};

14.1 x86

以一种十分容易预测的方式编译的

_a$ = 8             ; size = 4
_f   PROC
    push    ebp
    mov     ebp, esp
    mov     eax, DWORD PTR _a$[ebp]
    cdq             ; sign extend EAX to EDX:EAX
    mov     ecx, 9
    idiv    ecx
    pop     ebp
    ret     0
_f  ENDP

IDIV 有符号数除法指令 64位的被除数分存在两个寄存器EDX:EAX,除数放在单个寄存器ECX中。运算结束后,商放在EAX,余数放在EDX。f()函数的返回值将包含在eax寄存器中,也就是说,在进行除法运算之后,值不会再放到其他位置,它已经在合适的地方了。正因为IDIV指令要求被除数分存在EDX:EAX里,所以需要在做除法前用CDQ指令将EAX中的值扩展成64位有符号数,就像MOVSX指令(13.1.1)所做的一样。如果我们切换到优化模式(/0x),我们会得到

清单14.2:MSVC优化模式

_a$ = 8                         ; size = 4
_f   PROC

    mov     ecx, DWORD PTR _a$[esp-4]
    mov     eax, 954437177      ; 38e38e39H
    imul    ecx
    sar     edx, 1
    mov     eax, edx
    shr     eax, 31             ; 0000001fH
    add     eax, edx
    ret     0
_f   ENDP

这里将除法优化为乘法。乘法运算要快得多。使用这种技巧可以得到更高效的代码。

在编译器优化中,这也称为“strength reduction”

GCC4.4.1甚至在没有打开优化模式的情况下生成了和在MSVC下打开优化模式的生成的几乎一样的代码。

清单14.3 GCC 4.4.1 非优化模式

        public f
f       procnear
arg_0   = dword ptr 8
 
        push    ebp
        mov     ebp, esp
        mov     ecx, [ebp+arg_0]
        mov     edx, 954437177 ; 38E38E39h
        mov     eax, ecx
        imul    edx
        sar     edx, 1
        mov     eax, ecx
        sar     eax, 1Fh
        mov     ecx, edx
        sub     ecx, eax
        mov     eax, ecx
        pop     ebp
        retn
f       endp

14.2 ARM

ARM处理器,就像其他的“纯”RISC处理器一样,缺少除法指令,缺少32位常数乘法的单条指令。利用一个技巧,通过加法,减法,移位是可以实现除法的。 这里有一个32位数被10(20,3.3常量除法)除的例子,输出商和余数。

; takes argument in a1
; returns quotient in a1, remainder in a2
; cycles could be saved if only divide or remainder is required
    SUB     a2, a1, #10         ; keep (x-10) for later
    SUB     a1, a1, a1, lsr #2
    ADD     a1, a1, a1, lsr #4
    ADD     a1, a1, a1, lsr #8
    ADD     a1, a1, a1, lsr #16
    MOV     a1, a1, lsr #3
    ADD     a3, a1, a1, asl #2
    SUBS    a2, a2, a3, asl #1  ; calc (x-10) - (x/10)*10
    ADDPL   a1, a1, #1          ; fix-up quotient
    ADDMI   a2, a2, #10         ; fix-up remainder
    MOV     pc, lr

14.2.1 Xcode优化模式(LLVM)+ARM模式

__text:00002C58 39 1E 08 E3 E3 18 43 E3     MOV     R1, 0x38E38E39
__text:00002C60 10 F1 50 E7                 SMMUL   R0, R0, R1
__text:00002C64 C0 10 A0 E1                 MOV     R1, R0,ASR#1
__text:00002C68 A0 0F 81 E0                 ADD     R0, R1, R0,LSR#31
__text:00002C6C 1E FF 2F E1                 BX      LR

运行原理

这里的代码和优化模式的MSVC和GCC生成的基本相同。显然,LLVM在产生常量上使用相同的算法。

善于观察的读者可能会问,MOV指令是如何将32位数值写入寄存器中的,因为这在ARM模式下是不可能的。实际上是可能的,但是,就像我们看到的,与标准指令每条有四个字节不同的是,这里的每条指令有8个字节,其实这是两条指令。第一条指令将值0x8E39装入寄存器的低十六位,第二条指令是MOVT,它将0x383E装入寄存器的高16位。IDA知道这些顺序,并且为了精简紧凑,将它精简转换成一条伪代码。

SMMUL (Signed Most Significant Word Multiply)实现两个32位有符号数的乘法,并且将高32位的部分放在r0中,弃掉结果的低32位部分。

MOV R1,R0,ASR#1 指令算数右移一位。
ADD R0,R1,LSR#31 R0=R1+R0>>32

事实上,在ARM模式下,并没有单独的移位指令。相反,像(MOV,ADD,SUB,RSB)3 这样的数据处理指令,第二个操作数需要被移位。ASR表示算数右移,LSR表示逻辑右移。

14.2.2 优化 Xcode(LLVM)+thumb-2 模式

MOV         R1, 0x38E38E39
SMMUL.W     R0, R0, R1
ASRS        R1, R0, #1
ADD.W       R0, R1, R0,LSR#31
BX          LR

在thumb模式下有些单独的移位指令,这个例子中使用了ASRS(算数右移)

14.2.3 Xcode非优化模式(LLVM) keil模式

非优化模式 LLVM不生成我们之前看到的那样的代码,它插入了一个调用库函数的call __divsi3

关于keil:通常插入一个调用库函数的call __aeabi_idivmod

14.3 工作原理

下面展示的是怎样用乘法来优化除法,其中借助了2^n的阶乘

M是一个magic系数

M的计算过程

因此这些代码片段通常具有这样的形式

n可以是任意数,可能是32(那么这样运算结果的高位部分从EX或者RDX寄存器中获取),可能是31(这种情况下乘法结果的高位部分结果右移)

n的选取是为了减少错误。

当进行有符号数除法运算,乘法结果的符号也会被放到输出结果中。

下面来看看不同之处。

int f3_32_signed(int a)
{
    return a/3;
};
unsigned int f3_32_unsigned(unsigned int a)
{
    return a/3;
};

在无符号版本的函数中,magic系数是0xAAAAAAAB,乘法结果被2^3*3除。

在有符号版本的函数中,magic系数是0x55555556,乘法结果被2^32除。

符号来自于乘法结果:高32位的结果右移31位(将符号位放在EAX中最不重要的位置)。如果最后结果为负,则会设置为1。

清单14.4:MSVC 2012/OX

_f3_32_unsigned     PROC
        mov     eax, -1431655765        ; aaaaaaabH
        mul     DWORD PTR _a$[esp-4]    ; unsigned multiply
        shr     edx, 1
        mov     eax, edx
        ret     0
_f3_32_unsigned ENDP
 
_f3_32_signed PROC
        mov     eax, 1431655766         ; 55555556H
        imul    DWORD PTR _a$[esp-4]    ; signed multiply
        mov     eax, edx
        shr     eax, 31                 ; 0000001fH
        add     eax, edx                ; add 1 if sign is negative
        ret     0
_f3_32_signed ENDP

14.4 得到除数

14.4.1 变形#1

通常,代码具有这样一种形式

mov     eax, MAGICAL CONSTANT
imul    input value
sar     edx, SHIFTING COEFFICIENT ; signed division by 2^x using arithmetic shift right
mov     eax, edx
shr     eax, 31
add     eax, edx

我们将32位的magic系数表示为M,移位表示为C,除数表示为D

我们得到的除法是

举个例子

清单14.5:优化模式 MSVC2012

mov     eax, 2021161081     ; 78787879H
imul    DWORD PTR _a$[esp-4]
sar     edx, 3
mov     eax, edx
shr     eax, 31             ; 0000001fH
add     eax, edx

比32位的数字大,为了方便,于是我们使用用Wolfram Mathematica软件。

In[1]:=N[2^(32+3)/2021161081]
Out[1]:=17.

因此例子中的代码得到结果是17。

对于64位除法来说,原理是一样的,但是应该使用264来代替232。

uint64_t f1234(uint64_t a)
{
    return a/1234;
};

清单14.7:MSVC2012/Ox

f1234   PROC
        mov     rax, 7653754429286296943 ; 6a37991a23aead6fH
        mul     rcx
        shr     rdx, 9
        mov     rax, rdx
        ret     0
f1234   ENDP

清单14.8:Wolfram Mathematica

In[1]:=N[2^(64+9)/16^^6a37991a23aead6f]
Out[1]:=1234.

14.4.2 变形#2

忽略算数移位的变形也是存在的

mov     eax, 55555556h ; 1431655766
imul    ecx
mov     eax, edx
shr     eax, 1Fh

更加简洁

在这个例子中

再用一次Wolfram Mathematica

In[1]:=N[2^32/16^^55555556]
Out[1]:=3.

得到的除数是3

用FPU工作

FPU是一个主cpu被设计用来处理浮点数的设备。

过去它被称为协处理器,放在CPU旁边,看起来像可编程的计算器,在学习FPU之前学习堆栈机或forth语言是值得的。

有趣的是,在过去(80486cpu之前),协处理器是一个单独的芯片,并不总是安装在母版上,单独购买和安装也是可以的。

但从80486 DX CPU开始,FPU就被安装在里面了。

FWAIT指令可能提醒我们一个事实--它将CPU转换成等待模式,因此它可以一直等待直到FPU完成工作。另外一点是FPU指令操作码从所谓的escape操作码(D8..DF)开始,进入了FPU。

FPU有可以容纳8个80字节的寄存器栈容量,每一个寄存器可以存储一个IEEE 754格式的数字。

C/C++语言提供至少两种浮点数类型,float(单精度,32位),double类型(双精度,64位)。

GCC也支持多精度类型(扩展精度,80位),但是MSVC不支持。

在32位环境中,浮点数要求和int类型的位数相同,但是数值的表示法完全不同。

数值包括符号位,尾数(也叫做分数)和指数。

参数列表中有float和double类型的函数通过栈来获得值,如果函数返回float或者double类型的值,那么返回值将放在ST(0)寄存器中--在FPU的栈顶。

15.1 简单实例

下面我们来研究一个简单的例子

double f (double a, double b)
{
    return a/3.14 + b*4.1;
}

15.1.1 x86

在msvc2010中编译

CONST SEGMENT
__real@4010666666666666 DQ 04010666666666666r   ; 4.1
CONST ENDS
CONST SEGMENT
__real@40091eb851eb851f DQ 040091eb851eb851fr   ; 3.14
CONST ENDS
_TEXT SEGMENT
_a$ = 8         ; size = 8
_b$ = 16        ; size = 8
_f  PROC
    push    ebp
    mov     ebp, esp
    fld     QWORD PTR _a$[ebp]
 
; current stack state: ST(0) = _a
 
    fdiv    QWORD PTR __real@40091eb851eb851f
 
; current stack state: ST(0) = result of _a divided by 3.13
 
    fld     QWORD PTR _b$[ebp]
 
; current stack state: ST(0) = _b; ST(1) = result of _a divided by 3.13
 
    fmul    QWORD PTR __real@4010666666666666
 
; current stack state: ST(0) = result of _b * 4.1; ST(1) = result of _a divided by 3.13
 
    faddp   ST(1), ST(0)
 
; current stack state: ST(0) = result of addition
 
    pop     ebp
    ret     0
_f ENDP

FLD从栈中取8个字节并将这个数字放入ST(0)寄存器中,自动将它转换成内部80位格式的扩展操作数。

FDIV除存储在ST(0)中地址指向的数值 __real@40091eb851eb851f —3.14 就放在那里。

汇编语法丢失浮点数,因此,我们这里看到的是64位IEEE754编码的16进制表示的3.14。

执行FDIV执行后,ST(0)将保存除法的结果。

另外,这里也有FDIVP指令,用ST(0)除ST(1),从栈中将将这些值抛出来,然后将结果压栈。如果你懂forth语言,你会很快意识到这是堆栈机。

FLD指令将b的值压入栈中之后,商放入ST(1)寄存器中,ST(0)中保存b的值。

接下来FMUL指令将来自ST(0)的b值和在__real@4010666666666666 (4.1 的值在那里)相乘,然后将结果放入ST(0)中。

最后,FADDP指令将栈顶的两个值相加,将结果存储在ST(1)寄存器中,然后从ST(1)中弹出,再放入ST(0)中。

这个函数必须返回ST(0)寄存器中的值,因此,在执行FADDP命令后,没有其他额外的的指令了需要执行了。

GCC 4.4.1(选项03)生成基本同样的代码,有小小的不同之处。

不同之处在于,首先,3.14被压入栈中(进入ST(0)),然后arg_0的值除以ST(0)寄存器中的值

FDIVR 意味着逆向除法 被除数和除数交换。

因为乘法两个乘数可交换,所以没有这样的指令,我们只有FMUL而没有逆乘。

FADDP也是将两个值相加,其中一个来自栈。然后ST(0)保存它们的和。

这段反编译代码的碎片是由IDA产生的,ST(0)简称为ST。

15.1.2 ARM: Xcode优化模式(LLVM)+ARM 模式

直到ARM有标准化的浮点数支持后,几家处理器厂商才将其加入到他们自己指令扩展中。然后,VFP(向量浮点运算单元)标准化了。

与x86相比,一个重要的不同是,在x86中使用fpu栈工作,而在ARM中,这里没有栈,你只能使用寄存器。

f
        VLDR        D16, =3.14
        VMOV        D17, R0, R1 ; load a
        VMOV        D18, R2, R3 ; load b
        VDIV.F64    D16, D17, D16 ; a/3.14
        VLDR        D17, =4.1
        VMUL.F64    D17, D18, D17 ; b*4.1
        VADD.F64    D16, D17, D16 ; +
        VMOV        R0, R1, D16
        BX          LR
dbl_2C98    DCFD 3.14 ; DATA XREF: f
dbl_2CA0    DCFD 4.1 ; DATA XREF: f+10

可以看到,这里我们使用了新的寄存器,并以D开头。这些是64位寄存器,有32个,他们既可以用作浮点数(double)运算也可以用作SIMD(在ARM中称为NEON)。

它们同时也可以作为32个32位的S寄存器使用,它们被用于单精度操作浮点数(float)运算。

记住它们很容易:D系列寄存器用于双精度数字,S寄存器用于单精度数字,记住Double和Single的首字母就可以了。

两个常量(3.14和4.1)都是以IEEE 754的形式存储在内存中。

VLDR和VMOV指令,容易推断,类似LDR和MOV指令,但是它们使用D系列寄存器,需要注意的就是这些指令不就之后也会展现出,就像D系列寄存器一样,不仅可以进行浮点数运算而且也可以用于SIMD(NEON)运算,参数传递的方式仍旧是通过R系列寄存器传递,但是每个具有双精度的数值有64位,所以为了便于传递需要两个寄存器。

VMOV D17,R0,R1在最开始,将两个来自R0和R1的32位的值组成一个64位的值并且将它保存在D17中。
VMOV R0,R1,D16是一个逆操作,D16中的值放回R0,R1中。
VDIV,VMUL,VADD都是用于浮点数的处理计算的指令,分别为除法指令,乘法指令,加法指令。

thumb-2的代码也是相同的。

15.1.3 ARM:优化 keil+thumb 模式

f
            PUSH    {R3-R7,LR}
            MOVS    R7, R2
            MOVS    R4, R3
            MOVS    R5, R0
            MOVS    R6, R1
            LDR     R2, =0x66666666
            LDR     R3, =0x40106666
            MOVS    R0, R7
            MOVS    R1, R4
            BL      __aeabi_dmul
            MOVS    R7, R0
            MOVS    R4, R1
            LDR     R2, =0x51EB851F
            LDR     R3, =0x40091EB8
            MOVS    R0, R5
            MOVS    R1, R6
            BL      __aeabi_ddiv
            MOVS    R2, R7
            MOVS    R3, R4
            BL      __aeabi_dadd
            POP     {R3-R7,PC}
dword_364   DCD 0x66666666          ; DATA XREF: f+A
dword_368   DCD 0x40106666          ; DATA XREF: f+C
dword_36C   DCD 0x51EB851F          ; DATA XREF: f+1A
dword_370   DCD 0x40091EB8          ; DATA XREF: f+1C

keil为处理器生成的代码不支持FPU和NEON。因此,双精度浮点数通过通用R寄存器来传递双精度数字,与FPU指令不同的是,通过对库函数调用(如_aeabidmul, aeabi_ddiv, aeabi_dadd)用来实现乘法,除法,浮点数加法。当然,这比FPU协处理器慢,但总比没有强。

另外,在x86的世界中,当协处理器少而贵并且只安装昂贵的计算机上时,在FPU模拟库非常受欢迎。

在ARM的世界中,FPU处理器模拟称为soft float 或者armel,用协处理器的FPU指令的称为hard float和armhf。

举个例子,树莓派的linux内核用两种变量编译。如果是soft float,参数就会通过R系列寄存器编码,hard float则会通过D系列寄存器。

这就是不让你使用例子中来自armel编码的armhf库原因,反之亦然。那也是linux分区必须根据调用惯例编译的原因。

15.2 通过参数通过浮点数

#include <math.h>
#include <stdio.h>
int main ()
{
    printf ("32.01 ^ 1.54 = %lf", pow (32.01,1.54));
    return 0;
}

15.2.1 x86

让我们来看看在(msvc2010)中得到的东西

清单15.3 :MSVC 2010

CONST   SEGMENT
__real@40400147ae147ae1 DQ 040400147ae147ae1r   ; 32.01
__real@3ff8a3d70a3d70a4 DQ 03ff8a3d70a3d70a4r   ; 1.54
CONST ENDS
 
_main     PROC
        push    ebp
        mov     ebp, esp
        sub     esp, 8 ; allocate place for the first variable
        fld     QWORD PTR __real@3ff8a3d70a3d70a4
        fstp    QWORD PTR [esp]
        sub     esp, 8 ; allocate place for the second variable
        fld     QWORD PTR __real@40400147ae147ae1
        fstp    QWORD PTR [esp]
        call    _pow
        add     esp, 8 ; "return back" place of one variable.
 
; in local stack here 8 bytes still reserved for us.
; result now in ST(0)
 
        fstp    QWORD PTR [esp] ; move result from ST(0) to local stack for printf()
        push    OFFSET $SG2651
        call    _printf
        add     esp, 12
        xor     eax, eax
        pop     ebp
        ret     0
_main       ENDP

FLD和FSTP读取FPU的栈中的变量。pow()从FPU栈中拿出两个值然后将结果返回到ST(0)寄存器中。printf()函数从本地栈中取出8字节并且将他们翻译为双精度变量。

15.2.2 ARM+Non-optimizing Xcode(LLVM)+thumb-2模式

_main
var_C       = -0xC
            PUSH    {R7,LR}
            MOV     R7, SP
            SUB     SP, SP, #4
            VLDR    D16, =32.01
            VMOV    R0, R1, D16
            VLDR    D16, =1.54
            VMOV    R2, R3, D16
            BLX     _pow
            VMOV    D16, R0, R1
            MOV     R0, 0xFC1 ; "32.01 ^ 1.54 = %lf
"
            ADD     R0, PC
            VMOV    R1, R2, D16
            BLX     _printf
            MOVS    R1, 0
            STR     R0, [SP,#0xC+var_C]
            MOV     R0, R1
            ADD     SP, SP, #4
            POP     {R7,PC}
dbl_2F90    DCFD 32.01      ; DATA XREF: _main+6
dbl_2F98    DCFD 1.54       ; DATA XREF: _main+E

就像我以前写的一样,64位的浮点数是成对传递给R系列寄存器的。这样的代码是冗陈的(当然是因为优化选项关掉了),因为,事实上直接从R系列寄存器传递值,不借助D系列寄存器是可能的。

因此我们可以看到,pow 将第一个参数放入R0和R1中,第二个参数放入R2和R3中。函数结果放入R0和R1中。pwn的结果先放入了D16中,然后再放入R1和R2中,然后printf函数将取走这个值。

15.2.3 ARM+非优化模式keil+ARM模式

_main
                STMFD   SP!, {R4-R6,LR}
                LDR     R2, =0xA3D70A4 ; y
                LDR     R3, =0x3FF8A3D7
                LDR     R0, =0xAE147AE1 ; x
                LDR     R1, =0x40400147
                BL      pow
                MOV     R4, R0
                MOV     R2, R4
                MOV     R3, R1
                ADR     R0, a32_011_54Lf ; "32.01 ^ 1.54 = %lf
"
                BL      __2printf
                MOV     R0, #0
                LDMFD   SP!, {R4-R6,PC}
 
y               DCD 0xA3D70A4       ; DATA XREF: _main+4
dword_520       DCD 0x3FF8A3D7      ; DATA XREF: _main+8
; double x
x               DCD 0xAE147AE1      ; DATA XREF: _main+C
dword_528       DCD 0x40400147      ; DATA XREF: _main+10
a32_011_54Lf    DCB "32.01 ^ 1.54 = %lf",0xA,0
                                    ; DATA XREF: _main+24

D系列寄存器在这里不使用,只成对地使用R系列的寄存器

15.3 对比实例

试试这个

double d_max (double a, double b)
{
    if (a>b)
    return a;
    return b;
};

15.3.1 x86

尽管这个函数很简单,但是理解它的工作原理并不容易。

MSVC 2010生成

PUBLIC      _d_max
_TEXT   SEGMENT
_a$ = 8         ; size = 8
_b$ = 16        ; size = 8
_d_max      PROC
    push    ebp
    mov     ebp, esp
    fld     QWORD PTR _b$[ebp]
 
; current stack state: ST(0) = _b
; compare _b (ST(0)) and _a, and pop register
 
    fcomp   QWORD PTR _a$[ebp]
 
; stack is empty here
 
    fnstsw  ax
    test    ah, 5
    jp      SHORT $LN1@d_max
 
; we are here only if a>b
 
    fld     QWORD PTR _a$[ebp]
    jmp     SHORT $LN2@d_max
$LN1@d_max:
    fld     QWORD PTR _b$[ebp]
$LN2@d_max:
    pop     ebp
    ret     0
_d_max      ENDP

因此,FLD将_b中的值装入ST(0)寄存器中。

FCOMP对比ST(0)寄存器和_a值,设置FPU状态字寄存器中的C3/C2/C0位,这是一个反应FPU当前状态的16位寄存器。

C3/C2/C0位被设置后,不幸的是,IntelP6之前的CPU没有任何检查这些标志位的条件转移指令。可能是历史的原因(FPU曾经是单独的一块芯片)。从Intel P6开始,现在的CPU拥有FCOMI/FCOMIP/FUCOMI/FUCOMIP指令,这些指令功能相同,但会改变CPU的ZF/PF/CF标志位。

当标志位被设好后,FCOMP指令从栈中弹出一个变量。这就是和FCOM的不同之处,FCOM只对比值,让栈保持同样的状态。

FNSTSW讲FPU状态字寄存器的内容拷贝到AX中,C3/C2/C0放置在14/10/8位中,它们会在AX寄存器中相应的位置上,并且都放在AX的高位部分—AH。

如果 b>a 在我们的例子中,C3/C2/C0位会被设置为:0,0,0
如果 a>b 标志位被设为:0,0,1
如果 a=b 标识位被设为:1,0,0

执行了 test sh,5 之后,C3和C1的标志位被设为0,但是第0位和第2位(在AH寄存器中)C0和C2位会保留。

下面我们谈谈奇偶位标志。Another notable epoch rudiment:

一个常见的原因是测试奇偶位标志事实上与奇偶没有任何关系。FPU有4个条件标志(C0到C3),但是它们不能被直接测试,必须先拷贝到标志位寄存器中,在这个时候,C0放在进位标志中,C2放在奇偶位标志中,C3放在0标志位中。当例子中不可比较的浮点数(NaN或者其他不支持的格式)使用FUCOM指令进行比较的时候,会设置C2标志位。

如果一个数字是奇数这个标志就会被设置为1。如果是偶数就会被设置为0.

因此,PF标志会被设置为1如果C0和C2都被设置为0或者都被设置为1。然后jp跳转就会实现。如果我们recall valuesof C3/C2/C0,我们将会发现条件跳转jp可能会在两种情况下触发:b>a或者a==b(C3位这里不再考虑,因为在执行test sh,5指令之后已经被清零了)

之后就简单了。如果条件跳转被触发,FLD会将_b的值放入ST(0)寄存器中,如果没有被触发,_a变量的值会被加载 但是还没有结束。

15.3.2 下面我们用msvc2010优化模式来编译它/0x

_a$ = 8         ; size = 8
_b$ = 16        ; size = 8
_d_max  PROC
    fld     QWORD PTR _b$[esp-4]
    fld     QWORD PTR _a$[esp-4]
; current stack state: ST(0) = _a, ST(1) = _b
    fcom ST(1) ; compare _a and ST(1) = (_b)
    fnstsw ax
    test ah, 65 ; 00000041H
    jne SHORT $LN5@d_max
    fstp ST(1) ; copy ST(0) to ST(1) and pop register, leave (_a) on top
; current stack state: ST(0) = _a
    ret 0
$LN5@d_max:
    fstp ST(0) ; copy ST(0) to ST(0) and pop register, leave (_b) on top
; current stack state: ST(0) = _b
    ret 0
_d_max ENDP

FCOM区别于FCOMP在某种程度上是它只比较值然后并不改变FPU的状态。和之前的例子不同的是,操作数是逆序的。这也是C3/C2/C0中的比较结果是不同的原因。

如果 a>b  在我们的例子中,C3/C3/C0会被设为0,0,0
如果 b>a  标志位被设为:0,0,1
如果 a=b  标志位被设为:1,0,0

可以这么说,test ah,65指令只保留两位—C3和C0.如果a>b那么两者都被设为0:在那种情况下,JNE跳转不会被触发。 FSTP ST(1)接下来—这个指令会复制ST(0)中的值放入操作数中,然后从FPU栈中跑出一个值。 换句话说,这个这个指令将ST(0)中的值复制到ST(1)中。然后,a的两个值现在在栈定。之后,一个值被抛出。之后,ST(0)会包含a然后函数执行完毕。

条件跳转JNE在两种情况下触发:b>a或者a==b。ST(0)中的值拷贝到ST(0)中,就像nop指令一样,然后一个值从栈中抛出,然后栈顶(ST(0))会包含ST(1)之前的包含的内容(就是_b)。函数执行完毕。这条指令在这里使用的原因可能是FPU没有从栈中抛出值的指令并且没有地方存储。 但是,还没有结束。

15.3.3 GCC 4.4.1

d_max proc near
b               =qword ptr -10h
a               =qword ptr -8
a_first_half    = dword ptr 8
a_second_half   = dword ptr 0Ch
b_first_half    = dword ptr 10h
b_second_half   = dword ptr 14h
 
    push    ebp
    mov     ebp, esp
    sub     esp, 10h
 
; put a and b to local stack:
 
    mov     eax, [ebp+a_first_half]
    mov     dword ptr [ebp+a], eax
    mov     eax, [ebp+a_second_half]
    mov     dword ptr [ebp+a+4], eax
    mov     eax, [ebp+b_first_half]
    mov     dword ptr [ebp+b], eax
    mov     eax, [ebp+b_second_half]
    mov     dword ptr [ebp+b+4], eax
 
; load a and b to FPU stack:
 
    fld     [ebp+a]
    fld     [ebp+b]
; current stack state: ST(0) - b; ST(1) - a
 
    fxch    st(1) ; this instruction swapping ST(1) and ST(0)
 
; current stack state: ST(0) - a; ST(1) - b
 
    fucompp     ; compare a and b and pop two values from stack, i.e., a and b
    fnstsw  ax  ; store FPU status to AX
    sahf        ; load SF, ZF, AF, PF, and CF flags state from AH
    setnbe  al  ; store 1 to AL if CF=0 and ZF=0
    test    al, al               ; AL==0 ?
    jz      short loc_8048453    ; yes
    fld     [ebp+a]
    jmp     short locret_8048456
 
loc_8048453:
    fld     [ebp+b]
locret_8048456:
    leave
    retn
d_max endp

FUCOMMP 类似FCOM指令,但是两个值都从栈中取,并且处理NaN(非数)有一些不同之处。

更多关于”非数“的:

FPU能够处理特殊的值比如非数字或者NaNs。它们是无穷大的,除零的结果等等。NaN可以是“quiet”并且“signaling”的。但是如果进行任何有关“signaling”的操作将会产生异常。

FCOM会产生异常如果操作数中有NaN。FUCOM只在操作数有signaling NaN (SNaN)的情况下产生异常。

接下来的指令是SANF—这条指令很少用,它不使用FPU。AH的8位以这样的顺序放入CPU标志位的低8位中:SF:ZF:-:AF:-:PF:-:CF<-AH。

FNSTSW将C3/C2/C0位放入AH寄存器的第6,2,0位中。

换句话说,fnstsw ax/sahf指令对是将C3/C2/C0移入CPU标志位ZF,PF,CF中。

现在我们来回顾一下,C3/C2/C0位会被设置成什么。

在我们的例子中,如果a比b大,那么C3/C2/C0位会被设为0,0,0
如果a比b小,这些位会被设为0,0,1
如果a=b,这些位会被设为1,0,0

换句话说,在 FUCOMPP/FNSTSW/SAHF指令后,我们的CPU标志位的状态如下

如果a>b,CPU的标志位会被设为:ZF=0,PF=0,CF=0
如果a<b,CPU的标志位会被设为:ZF=0,PF=0,CF=1
如果a=b,CPU的标志位会被设为:ZF=1,PF=0,CF=0

SETNBE指令怎样给AL存储0或1:取决于CPU标志位。几乎是JNBE的计数器,利用设置cc码产生的异常,来给AL写入0或1,但是Jccbut Jcc do actual jump or not.SETNBE存储1只在CF=0并且ZF=0的情况下。如果为假,将会存储0。

cf和ZF都为0只存在于一种情况:a>b

然后one将会被存入AL中,接下来JZ不会被触发,函数将返回_a。在其他的情况下,返回的是_b。

15.3.4 GCC 4.4.1-03优化选项turned开关

            public d_max
d_max       proc near
arg_0       = qword ptr 8
arg_8       = qword ptr 10h
            push    ebp
            mov     ebp, esp
            fld     [ebp+arg_0] ; _a
            fld     [ebp+arg_8] ; _b
 
; stack state now: ST(0) = _b, ST(1) = _a
            fxch    st(1)
 
; stack state now: ST(0) = _a, ST(1) = _b
            fucom   st(1) ; compare _a and _b
 
            fnstsw  ax
            sahf
            ja      short loc_8048448
; store ST(0) to ST(0) (idle operation), pop value at top of stack, leave _b at top
            fstp    st
            jmp     short loc_804844A
 
loc_8048448:
; store _a to ST(0), pop value at top of stack, leave _a at top
            fstp    st(1)
loc_804844A:
            pop     ebp
            retn
d_max       endp

几乎相同除了一种情况:JA替代了SAHF。事实上,条件跳转指令(JA, JAE, JBE, JBE, JE/JZ, JNA, JNAE, JNB, JNBE, JNE/JNZ)检查通过检查CF和ZF标志来知晓两个无符号数字的比较结果。C3/C2/C0位在比较之后被放入这些标志位中然后条件跳转就会起效。JA会生效如果CF和ZF都为0。

因此,这里列出的条件跳转指令可以在FNSTSW/SAHF指令对之后使用。

看上去,FPU C3/C2/C0状态位故意放置在那里,传递给CPU而不需要额外的交换。

15.3.5 ARM+优化Xcode(LLVM)+ARM模式

VMOV        D16, R2, R3 ; b
VMOV        D17, R0, R1 ; a
VCMPE.F64   D17, D16
VMRS        APSR_nzcv, FPSCR
VMOVGT.F64  D16, D17 ; copy b to D16
VMOV        R0, R1, D16
BX          LR

一个简单例子。输入值放在D17到D16寄存器中,然后借助VCMPE指令进行比较。就像x86协处理器一样,ARM协处理器拥有自己的标志位寄存器(FPSCR),因为存储协处理器的特殊标志需要存储。

就像x86中一样,在ARM中没有条件跳转指令,在协处理器状态寄存器中检查位,因此这里有VMRS指令,从协处理器状态字复制4位(N,Z,C,V)放入通用状态位(APSR寄存器)

VMOVGT类似MOVGT指令,如果比较时一个操作数比其它的大,指令将会被执行。

如果被执行了,b值将会写入D16,暂时被存储在D17中。

如果没有被执行,a的值将会保留在D16寄存器中。

倒数第二个指令VMOV将会通过R0和R1寄存器对准备D16寄存去中的值来返回。

15.3.6 ARM+优化 Xcode(LLVM)+thumb-2 模式

VMOV        D16, R2, R3 ; b
VMOV        D17, R0, R1 ; a
VCMPE.F64   D17, D16
VMRS        APSR_nzcv, FPSCR
IT GT
VMOVGT.F64  D16, D17
VMOV        R0, R1, D16
BX          LR

几乎和前一个例子一样,有一些小小的不同。事实上,许多ARM中的指令在ARM模式下根据条件判定,当条件为真则执行。

但是在thumb代码中没有这样的事。在16位的指令中没有空闲的4位来编码条件。

但是,thumb-2为老的thumb指令进行扩展使得特殊判断成为可能。

这里是IDA-生成的表单,我们可以看到VMOVGT指令,和在前一个例子中是相同的。

但事实上,常见的VMOV就这样编码,但是IDA加上了—GT后缀,因为以前会放置“IT GT”指令。

IT指令定义所谓的if-then块。指令后面最多放置四条指令是可能的,判断后缀会被加上。在我们的例子中,“IT GT”意味着下一条指令会被执行,如果GT(Greater Than)条件为真。

下面是一段更加复杂的代码,来源于“愤怒的小鸟”(ios版)

ITE NE
VMOVNE    R2, R3, D16
VMOVEQ    R2, R3, D17

ITE意味着if-the-else并且它为接下来的两条指令加上后缀。第一条指令将会执行如果ITE(NE,不相等)这时为真,为假则执行第二条指令。(与NE对立的就是EQ(equal))

这段代码也来自“愤怒的小鸟”:

ITTTT EQ
MOVEQ       R0, R4
ADDEQ       SP, SP, #0x20
POPEQ.W     {R8,R10}
POPEQ       {R4-R7,PC}

4个“T”符号在助记符中意味着接下来的4条指令将会被执行如果条件为真。这也是IDA在每条指令后面加上-EQ后缀的原因。

如果出现上面例子中ITEEE EQ(if-then-else-else-else),那么这些后缀将会被这样设置。

-EQ
-NE
-NE
-NE

另一段来自“愤怒的小鸟”的代码。

CMP.W       R0, #0xFFFFFFFF
ITTE LE
SUBLE.W     R10, R0, #1
NEGLE       R0, R0
MOVGT       R10, R0

ITTE(if-then-then-else)意味着第一条第二条指令将会被执行,如果LE(Less or Equal)条件为真,反之第三条指令将会执行。

编译器通常不生成所有的组合。举个例子,在“愤怒的小鸟”中提到的(ios经典版)只有这些IT指令会被使用:IT,ITE,ITT,ITTE,ITTT,ITTTT.我们怎样去学习它呢?在IDA中,产生这些列举的文件是可能的,于是我这么做了,并且设置选项以4字节的格式现实操作码。因为IT操作码的高16位是0xBF,使用grep指令

cat AngryBirdsClassic.lst | grep " BF" | grep "IT" > results.lst

另外,对于thumb-2模式 ARM汇编语言的程序,通过附加的条件后缀,必要的时候汇编会自动加上IT指令和相应的标志。

15.3.7 ARM+非优化模式 Xcode(LLVM)+ARM模式

b               =-0x20
a               =-0x18
val_to_return   = -0x10
saved_R7        = -4
                STR         R7, [SP,#saved_R7]!
                MOV         R7, SP
                SUB         SP, SP, #0x1C
                BIC         SP, SP, #7
                VMOV        D16, R2, R3
                VMOV        D17, R0, R1
                VSTR        D17, [SP,#0x20+a]
                VSTR        D16, [SP,#0x20+b]
                VLDR        D16, [SP,#0x20+a]
                VLDR        D17, [SP,#0x20+b]
                VCMPE.F64   D16, D17
                VMRS        APSR_nzcv, FPSCR
                BLE         loc_2E08
                VLDR        D16, [SP,#0x20+a]
                VSTR        D16, [SP,#0x20+val_to_return]
                B           loc_2E10
loc_2E08
                VLDR        D16, [SP,#0x20+b]
                VSTR        D16, [SP,#0x20+val_to_return]
loc_2E10
                VLDR        D16, [SP,#0x20+val_to_return]
                VMOV        R0, R1, D16
                MOV         SP, R7
                LDR         R7, [SP+0x20+b],#4
                BX          LR

基本和我们看到的一样,但是太多冗陈代码,因为a和b的变量存储在本地栈中,还有返回值

15.3.8 ARM+优化模式keil+thumb模式

        PUSH    {R3-R7,LR}
        MOVS    R4, R2
        MOVS    R5, R3
        MOVS    R6, R0
        MOVS    R7, R1
        BL      __aeabi_cdrcmple
        BCS     loc_1C0
        MOVS    R0, R6
        MOVS    R1, R7
        POP     {R3-R7,PC}
loc_1C0
        MOVS    R0, R4
        MOVS    R1, R5
        POP     {R3-R7,PC}

keil 不为浮点数的比较生成特殊的指令,因为他不能依靠核心CPU的支持,它也不能直接按位比较。这里有一个外部函数用于比较:__aeabi_cdrcmple. N.B. 比较的结果用来设置标志,因此接下来的BCS(标志位设置 - 大于或等于)指令可能有效并且无需额外的代码。

15.4 x64

更多关于x86-64位浮点数的处理参考后续的翻译文章。 # 数组

数组是在内存中连续排列的一组变量,这些变量具有相同类型1。

16.1 小例子

#include <stdio.h>
int main()
{
    int a[20];
    int i;
    for (i=0; i<20; i++)
        a[i]=i*2;
    for (i=0; i<20; i++)
        printf ("a[%d]=%d", i, a[i]);
    return 0;
};

16.1.1 x86

编译后:

Listing 16.1: MSVC

_TEXT   SEGMENT
_i$ = -84                                   ; size = 4
_a$ = -80                                   ; size = 80
_main       PROC
    push    ebp
    mov     ebp, esp
    sub     esp, 84         ; 00000054H
    mov     DWORD PTR _i$[ebp], 0
    jmp     SHORT $LN6@main
$LN5@main:
    mov     eax, DWORD PTR _i$[ebp]
    add     eax, 1
    mov     DWORD PTR _i$[ebp], eax
$LN6@main:
    cmp     DWORD PTR _i$[ebp], 20 ; 00000014H
    jge     SHORT $LN4@main
    mov     ecx, DWORD PTR _i$[ebp]
    shl     ecx, 1
    mov     edx, DWORD PTR _i$[ebp]
    mov     DWORD PTR _a$[ebp+edx*4], ecx
    jmp     SHORT $LN5@main
$LN4@main:
    mov     DWORD PTR _i$[ebp], 0
    jmp     SHORT $LN3@main
$LN2@main:
    mov     eax, DWORD PTR _i$[ebp]
    add     eax, 1
    mov     DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp     DWORD PTR _i$[ebp], 20 ; 00000014H
    jge     SHORT $LN1@main
    mov     ecx, DWORD PTR _i$[ebp]
    mov     edx, DWORD PTR _a$[ebp+ecx*4]
    push    edx
    mov     eax, DWORD PTR _i$[ebp]
    push    eax
    push    OFFSET $SG2463
    call    _printf
    add     esp, 12 ; 0000000cH
    jmp     SHORT $LN2@main
$LN1@main:
    xor eax, eax
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP

这段代码主要有两个循环:第一个循环填充数组,第二个循环打印数组元素。shl ecx,1指令使ecx的值乘以2,更多关于左移请参考17.3.1。 在堆栈上为数组分配了80个字节的空间,包含20个元素,每个元素4字节大小。

GCC 4.4.1编译后为:

Listing 16.2: GCC 4.4.1

            public main
main        proc near                       ; DATA XREF: _start+17
 
var_70      = dword ptr -70h
var_6C      = dword ptr -6Ch
var_68      = dword ptr -68h
i_2         = dword ptr -54h
i           = dword ptr -4
 
            push    ebp
            mov     ebp, esp
            and     esp, 0FFFFFFF0h
            sub     esp, 70h
            mov     [esp+70h+i], 0 ; i=0
            jmp     short loc_804840A
loc_80483F7:
            mov     eax, [esp+70h+i]
            mov     edx, [esp+70h+i]
            add     edx, edx ; edx=i*2
            mov     [esp+eax*4+70h+i_2], edx
            add     [esp+70h+i], 1 ; i++
loc_804840A:
            cmp     [esp+70h+i], 13h
            jle     short loc_80483F7
            mov     [esp+70h+i], 0
            jmp     short loc_8048441
loc_804841B:
            mov     eax, [esp+70h+i]
            mov     edx, [esp+eax*4+70h+i_2]
            mov     eax, offset aADD ; "a[%d]=%d
"
            mov     [esp+70h+var_68], edx
            mov     edx, [esp+70h+i]
            mov     [esp+70h+var_6C], edx
            mov     [esp+70h+var_70], eax
            call    _printf
            add     [esp+70h+i], 1
loc_8048441:
            cmp     [esp+70h+i], 13h
            jle     short loc_804841B
            mov     eax, 0
            leave
            retn
main        endp

顺便提一下,一个int类型(指向int的指针)的变量—你可以使该变量指向数组并将该数组传递给另一个函数,更准确的说,传递的指针指向数组的第一个元素(该数组其它元素的地址需要显示计算)。比如a[idx],idx加上指向该数组的指针并返回该元素。 一个有趣的例子:类似”string”字符数组的类型是const char,索引可以应用与该指针。比如可能写作”string”[i]—正确的C/C++表达式。

16.1.2 ARM + Non-optimizing Keil + ARM mode

        EXPORT _main
_main
        STMFD   SP!, {R4,LR}
        SUB     SP, SP, #0x50           ; allocate place for 20 int variables
; first loop
        MOV     R4, #0                  ; i
        B       loc_4A0
loc_494
        MOV     R0, R4,LSL#1            ; R0=R4*2
        STR     R0, [SP,R4,LSL#2]       ; store R0 to SP+R4<<2 (same as SP+R4*4)
        ADD     R4, R4, #1              ; i=i+1
loc_4A0
        CMP     R4, #20                 ; i<20?
        BLT     loc_494                 ; yes, run loop body again
; second loop
        MOV     R4, #0                  ; i
        B       loc_4C4
loc_4B0
        LDR     R2, [SP,R4,LSL#2]       ; (second printf argument) R2=*(SP+R4<<4) (same as *(SP+R4*4))
        MOV     R1, R4                  ; (first printf argument) R1=i
        ADR     R0, aADD                ; "a[%d]=%d
"
        BL      __2printf
        ADD     R4, R4, #1              ; i=i+1
loc_4C4
        CMP     R4, #20                 ; i<20?
        BLT     loc_4B0                 ; yes, run loop body again
        MOV     R0, #0                  ; value to return
        ADD     SP, SP, #0x50           ; deallocate place, allocated for 20 int variables
        LDMFD   SP!, {R4,PC}

int类型长度为32bits即4字节,20个int变量需要80(0x50)字节,因此“sub sp,sp,#0x50”指令为在栈上分配存储空间。 两个循环迭代器i被存储在R4寄存器中。 值i2被写入数组,通过将i值左移1位实现乘以2的效果,整个过程通过”MOV R0,R4,LSL#1指令来实现。 “STR R0, [SP,R4,LSL#2]”把R0内容写入数组。过程为:SP指向数组开始,R4是i,i左移2位相当于乘以4,即(SP+R44)=R0。 第二个loop的“LDR R2, [SP,R4,LSL#2]”从数组读取数值到寄存器,R2=(SP+R4*4)。

16.1.3 ARM + Keil + thumb 模式优化后

_main
        PUSH    {R4,R5,LR}
; allocate place for 20 int variables + one more variable
        SUB     SP, SP, #0x54
; first loop
        MOVS    R0, #0                  ; i
        MOV     R5, SP                  ; pointer to first array element
loc_1CE
        LSLS    R1, R0, #1              ; R1=i<<1 (same as i*2)
        LSLS    R2, R0, #2              ; R2=i<<2 (same as i*4)
        ADDS    R0, R0, #1              ; i=i+1
        CMP     R0, #20                 ; i<20?
        STR     R1, [R5,R2]             ; store R1 to *(R5+R2) (same R5+i*4)
        BLT     loc_1CE                 ; yes, i<20, run loop body again
; second loop
        MOVS    R4, #0                  ; i=0
loc_1DC
        LSLS    R0, R4, #2              ; R0=i<<2 (same as i*4)
        LDR     R2, [R5,R0]             ; load from *(R5+R0) (same as R5+i*4)
        MOVS    R1, R4
        ADR     R0, aADD                ; "a[%d]=%d
"
        BL      __2printf
        ADDS    R4, R4, #1              ; i=i+1
        CMP     R4, #20                 ; i<20?
        BLT     loc_1DC                 ; yes, i<20, run loop body again
        MOVS    R0, #0                  ; value to return
; deallocate place, allocated for 20 int variables + one more variable
        ADD     SP, SP, #0x54
        POP     {R4,R5,PC}

Thumb代码也是非常类似的。Thumb模式计算数组偏移的移位操作使用特定的指令LSLS。 编译器在堆栈中申请的数组空间更大,但是最后4个字节的空间未使用。

16.2 缓冲区溢出

Array[index]中index指代数组索引,仔细观察下面的代码,你可能注意到代码没有index是否小于20。如果index大于20?这是C/C++经常被批评的特征。 以下代码可以成功编译可以工作:

#include <stdio.h>
int main()
{
    int a[20];
    int i;
    for (i=0; i<20; i++)
        a[i]=i*2;
    printf ("a[100]=%d", a[100]);
    return 0;
};

编译后 (MSVC 2010):

_TEXT   SEGMENT
_i$ = -84                               ; size = 4
_a$ = -80                               ; size = 80
_main       PROC
    push    ebp
    mov     ebp, esp
    sub     esp, 84                 ; 00000054H
    mov     DWORD PTR _i$[ebp], 0
    jmp     SHORT $LN3@main
$LN2@main:
    mov     eax, DWORD PTR _i$[ebp]
    add     eax, 1
    mov     DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp     DWORD PTR _i$[ebp], 20  ; 00000014H
    jge     SHORT $LN1@main
    mov     ecx, DWORD PTR _i$[ebp]
    shl     ecx, 1
    mov     edx, DWORD PTR _i$[ebp]
    mov     DWORD PTR _a$[ebp+edx*4], ecx
    jmp     SHORT $LN2@main
$LN1@main:
    mov     eax, DWORD PTR _a$[ebp+400]
    push    eax
    push    OFFSET $SG2460
    call    _printf
    add     esp, 8
    xor     eax, eax
    mov     esp, ebp
    pop     ebp
    ret     0
_main       ENDP

运行,我们得到: a[100]=760826203

打印的数字仅仅是距离数组第一个元素400个字节处的堆栈上的数值。 编译器可能会自动添加一些判断数组边界的检测代码(更高级语言3),但是这可能影响运行速度。 我们可以从栈上非法读取数值,是否可以写入数值呢? 下面我们将写入数值:

#include <stdio.h>
int main()
{
    int a[20];
    int i;
 
    for (i=0; i<30; i++)
        a[i]=i;
 
    return 0;
};

我们得到:

_TEXT   SEGMENT
_i$ = -84                                   ; size = 4
_a$ = -80                                   ; size = 80
_main       PROC
    push    ebp
    mov     ebp, esp
    sub     esp, 84 ; 00000054H
    mov     DWORD PTR _i$[ebp], 0
    jmp     SHORT $LN3@main
$LN2@main:
    mov     eax, DWORD PTR _i$[ebp]
    add     eax, 1
    mov     DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp     DWORD PTR _i$[ebp], 30 ; 0000001eH
    jge     SHORT $LN1@main
    mov     ecx, DWORD PTR _i$[ebp]
    mov     edx, DWORD PTR _i$[ebp] ; that instruction is obviously redundant
    mov     DWORD PTR _a$[ebp+ecx*4], edx ; ECX could be used as second operand here instead
    jmp     SHORT $LN2@main
$LN1@main:
    xor     eax, eax
    mov     esp, ebp
    pop     ebp
    ret     0
_main       ENDP

编译后运行,程序崩溃。我们找出导致崩溃的地方。 没有使用调试器,而是使用我自己写的小工具tracer足以完成任务。 我们用它看被调试进程崩溃的地方:

generic tracer 0.4 (WIN32), http://conus.info/gt

New process: C:PRJ...1.exe, PID=7988
EXCEPTION_ACCESS_VIOLATION: 0x15 (<symbol (0x15) is in unknown module>), ExceptionInformation
[0]=8
EAX=0x00000000 EBX=0x7EFDE000 ECX=0x0000001D EDX=0x0000001D
ESI=0x00000000 EDI=0x00000000 EBP=0x00000014 ESP=0x0018FF48
EIP=0x00000015
FLAGS=PF ZF IF RF
PID=7988|Process exit, return code -1073740791

我们来看各个寄存器的状态,异常发生在地址0x15。这是个非法地址—至少对win32代码来说是!这种情况并不是我们期望的,我们还可以看到EBP值为0x14,ECX和EDX都为0x1D。 让我们来研究堆栈布局。 代码进入main()后,EBP寄存器的值被保存在栈上。为数组和变量i一共分配84字节的栈空间,即(20+1)*sizeof(int)。此时ESP指向_i变量,之后执行push something,something将紧挨着_i。 此时main()函数内栈布局为:

ESP
ESP+4
ESP+84
ESP+88
4 bytes for i
80 bytes for a[20] array
saved EBP value
returning address

指令a[19]=something写入最后的int到数组边界(这里是数组边界!)。 指令a[20]=something,something将覆盖栈上保存的EBP值。 请注意崩溃时寄存器的状态。在此例中,数字20被写入第20个元素,即原来存放EBP值得地方被写入了20(20的16进制表示是0x14)。然后RET指令被执行,相当于执行POP EIP指令。 RET指令从堆栈中取出返回地址(该地址为CRT内部调用main()的地址),返回地址处被存储了21(0x15)。CPU执行地址0x15的代码,异常被抛出。 Welcome!这被称为缓冲区溢出4。 使用字符数组代替int数组,创建一个较长的字符串,把字符串传递给程序,函数没有检测字符串长度,把字符复制到较短的缓冲区,你能够找到找到程序必须跳转的地址。事实上,找出它们并不是很简单。 我们来看GCC 4.4.1编译后的同类代码:

            public main
main        proc near
 
a           = dword ptr -54h
i           = dword ptr -4
 
            push    ebp
            mov     ebp, esp
            sub     esp, 60h
            mov     [ebp+i], 0
            jmp     short loc_80483D1
loc_80483C3:
            mov     eax, [ebp+i]
            mov     edx, [ebp+i]
            mov     [ebp+eax*4+a], edx
            add     [ebp+i], 1
loc_80483D1:
            cmp     [ebp+i], 1Dh
            jle     short loc_80483C3
            mov     eax, 0
            leave
            retn
main        endp

在linux下运行将产生:段错误。使用GDB调试:

(gdb) r
Starting program: /home/dennis/RE/1
 
Program received signal SIGSEGV, Segmentation fault.
0x00000016 in ?? ()
(gdb) info registers
eax         0x0                 0
ecx         0xd2f96388      -755407992
edx         0x1d            29
ebx         0x26eff4        2551796
esp         0xbffff4b0      0xbffff4b0
ebp         0x15            0x15
esi         0x0                 0
edi         0x0                 0
eip         0x16            0x16
eflags      0x10202         [ IF RF ]
cs          0x73            115
ss          0x7b            123
ds          0x7b            123
es          0x7b            123
fs          0x0                 0
gs          0x33            51
(gdb)

寄存器的值与win32例子略微不同,因为堆栈布局也不太一样。

16.3 防止缓冲区溢出的方法

下面一些方法防止缓冲区溢出。MSVC使用以下编译选项:

/RTCs Stack Frame runtime checking
/GZ Enable stack checks (/RTCs)

一种方法是在函数局部变量和序言之间写入随机值。在函数退出之前检查该值。如果该值不一致则挂起而不执行RET。进程将被挂起。 该随机值有时被称为“探测值”。 如果使用MSVC编译简单的例子(16.1),使用RTC1和RTCs选项,将能看到函数调用@_RTC_CheckStackVars@8函数来检测“探测值“。

我们来看GCC如何处理这些。我们使用alloca()(4.2.4)例子:

#include <malloc.h>
#include <stdio.h>
void f()
{
    char *buf=(char*)alloca (600);
    _snprintf (buf, 600, "hi! %d, %d, %d", 1, 2, 3);
 
    puts (buf);
};

我们不使用任何附加编译选项,只使用默认选项,GCC 4.7.3将插入“探测“检测代码:

Listing 16.3: GCC 4.7.3

.LC0:
    .string "hi! %d, %d, %d
"
f:
    push    ebp
    mov     ebp, esp
    push    ebx
    sub     esp, 676
    lea     ebx, [esp+39]
    and     ebx, -16
    mov     DWORD PTR [esp+20], 3
    mov     DWORD PTR [esp+16], 2
    mov     DWORD PTR [esp+12], 1
    mov     DWORD PTR [esp+8], OFFSET FLAT:.LC0     ; "hi! %d, %d, %d
"
    mov     DWORD PTR [esp+4], 600
    mov     DWORD PTR [esp], ebx
    mov     eax, DWORD PTR gs:20                    ; canary
    mov     DWORD PTR [ebp-12], eax
    xor     eax, eax
    call    _snprintf
    mov     DWORD PTR [esp], ebx
    call    puts
    mov     eax, DWORD PTR [ebp-12]
    xor     eax, DWORD PTR gs:20                    ; canary
    jne     .L5
    mov     ebx, DWORD PTR [ebp-4]
    leave
    ret
.L5:
call __stack_chk_fail

随机值存在于gs:20。它被写入到堆栈,在函数的结尾与gs:20的探测值对比,如果不一致,__stack_chk_fail函数将被调用,控制台(Ubuntu 13.04 x86)将输出以下信息:

*** buffer overflow detected ***: ./2_1 terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x63)[0xb7699bc3]
/lib/i386-linux-gnu/libc.so.6(+0x10593a)[0xb769893a]
/lib/i386-linux-gnu/libc.so.6(+0x105008)[0xb7698008]
/lib/i386-linux-gnu/libc.so.6(_IO_default_xsputn+0x8c)[0xb7606e5c]
/lib/i386-linux-gnu/libc.so.6(_IO_vfprintf+0x165)[0xb75d7a45]
/lib/i386-linux-gnu/libc.so.6(__vsprintf_chk+0xc9)[0xb76980d9]
/lib/i386-linux-gnu/libc.so.6(__sprintf_chk+0x2f)[0xb7697fef]
./2_1[0x8048404]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0xb75ac935]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:01 2097586 /home/dennis/2_1
08049000-0804a000 r--p 00000000 08:01 2097586 /home/dennis/2_1
0804a000-0804b000 rw-p 00001000 08:01 2097586 /home/dennis/2_1
094d1000-094f2000 rw-p 00000000 00:00 0 [heap]
b7560000-b757b000 r-xp 00000000 08:01 1048602 /lib/i386-linux-gnu/libgcc_s.so.1
b757b000-b757c000 r--p 0001a000 08:01 1048602 /lib/i386-linux-gnu/libgcc_s.so.1
b757c000-b757d000 rw-p 0001b000 08:01 1048602 /lib/i386-linux-gnu/libgcc_s.so.1
b7592000-b7593000 rw-p 00000000 00:00 0
b7593000-b7740000 r-xp 00000000 08:01 1050781 /lib/i386-linux-gnu/libc-2.17.so
b7740000-b7742000 r--p 001ad000 08:01 1050781 /lib/i386-linux-gnu/libc-2.17.so
b7742000-b7743000 rw-p 001af000 08:01 1050781 /lib/i386-linux-gnu/libc-2.17.so
b7743000-b7746000 rw-p 00000000 00:00 0
b775a000-b775d000 rw-p 00000000 00:00 0
b775d000-b775e000 r-xp 00000000 00:00 0 [vdso]
b775e000-b777e000 r-xp 00000000 08:01 1050794 /lib/i386-linux-gnu/ld-2.17.so
b777e000-b777f000 r--p 0001f000 08:01 1050794 /lib/i386-linux-gnu/ld-2.17.so
b777f000-b7780000 rw-p 00020000 08:01 1050794 /lib/i386-linux-gnu/ld-2.17.so
bff35000-bff56000 rw-p 00000000 00:00 0 [stack]
Aborted (core dumped)

gs被叫做段寄存器,这些寄存器被广泛用在MS-DOS和扩展DOS时代。现在的作用和以前不同。简要的说,gs寄存器在linux下一直指向TLS(48)–存储线程的各种信息(win32环境下,fs寄存器同样的作用,指向TIB8 9)。 更多信息请参考linux源码arch/x86/include/asm/stackprotector.h(至少3.11版本)。

16.3.1 Optimizing Xcode (LLVM) + thumb-2 mode

我们回头看简单的数组例子(16.1)。我们来看LLVM如何检查“探测值“。

_main
var_64      = -0x64
var_60      = -0x60
var_5C      = -0x5C
var_58      = -0x58
var_54      = -0x54
var_50      = -0x50
var_4C      = -0x4C
var_48      = -0x48
var_44      = -0x44
var_40      = -0x40
var_3C      = -0x3C
var_38      = -0x38
var_34      = -0x34
var_30      = -0x30
var_2C      = -0x2C
var_28      = -0x28
var_24      = -0x24
var_20      = -0x20
var_1C      = -0x1C
var_18      = -0x18
canary      = -0x14
var_10      = -0x10
 
            PUSH    {R4-R7,LR}
            ADD     R7, SP, #0xC
            STR.W   R8, [SP,#0xC+var_10]!
            SUB     SP, SP, #0x54
            MOVW    R0, #aObjc_methtype ; "objc_methtype"
            MOVS    R2, #0
            MOVT.W  R0, #0
            MOVS    R5, #0
            ADD     R0, PC
            LDR.W   R8, [R0]
            LDR.W   R0, [R8]
            STR     R0, [SP,#0x64+canary]
            MOVS    R0, #2
            STR     R2, [SP,#0x64+var_64]
            STR     R0, [SP,#0x64+var_60]
            MOVS    R0, #4
            STR     R0, [SP,#0x64+var_5C]
            MOVS    R0, #6
            STR     R0, [SP,#0x64+var_58]
            MOVS    R0, #8
            STR     R0, [SP,#0x64+var_54]
            MOVS    R0, #0xA
            STR     R0, [SP,#0x64+var_50]
            MOVS    R0, #0xC
            STR     R0, [SP,#0x64+var_4C]
            MOVS    R0, #0xE
            STR     R0, [SP,#0x64+var_48]
            MOVS    R0, #0x10
            STR     R0, [SP,#0x64+var_44]
            MOVS    R0, #0x12
            STR     R0, [SP,#0x64+var_40]
            MOVS    R0, #0x14
            STR     R0, [SP,#0x64+var_3C]
            MOVS    R0, #0x16
            STR     R0, [SP,#0x64+var_38]
            MOVS    R0, #0x18
            STR     R0, [SP,#0x64+var_34]
            MOVS    R0, #0x1A
            STR     R0, [SP,#0x64+var_30]
            MOVS    R0, #0x1C
            STR     R0, [SP,#0x64+var_2C]
            MOVS    R0, #0x1E
            STR     R0, [SP,#0x64+var_28]
            MOVS    R0, #0x20
            STR     R0, [SP,#0x64+var_24]
            MOVS    R0, #0x22
            STR     R0, [SP,#0x64+var_20]
            MOVS    R0, #0x24
            STR     R0, [SP,#0x64+var_1C]
            MOVS    R0, #0x26
            STR     R0, [SP,#0x64+var_18]
            MOV     R4, 0xFDA ; "a[%d]=%d
"
            MOV     R0, SP
            ADDS    R6, R0, #4
            ADD     R4, PC
            B loc_2F1C
; second loop begin
 
loc_2F14
            ADDS    R0, R5, #1
            LDR.W   R2, [R6,R5,LSL#2]
            MOV     R5, R0
loc_2F1C
            MOV     R0, R4
            MOV     R1, R5
            BLX     _printf
            CMP     R5, #0x13
            BNE     loc_2F14
            LDR.W   R0, [R8]
            LDR     R1, [SP,#0x64+canary]
            CMP     R0, R1
            ITTTT   EQ              ; canary still correct?
            MOVEQ   R0, #0
            ADDEQ   SP, SP, #0x54
            LDREQ.W R8, [SP+0x64+var_64],#4
            POPEQ   {R4-R7,PC}
            BLX     ___stack_chk_fail

首先可以看到,LLVM循环展开写入数组,LLVM认为先计算出数组元素的值速度更快。 在函数的结尾我们能看到“探测值“的检测—局部存储的值与R8指向的标准值对比。如果相等4指令块将通过”ITTTT EQ“触发,R0写入0,函数退出。如果不相等,指令块将不会被触发,跳向___stack_chk_fail函数,结束进程。

16.4 One more word about arrays

现在我们来理解下面的C/C++代码为什么不能正常使用10:

void f(int size)
{
    int a[size];
    ...
};

这是因为在编译阶段编译器不知道数组的具体大小无论是在堆栈或者数据段,无法分配具体空间。 如果你需要任意大小的数组,应该通过malloc()分配空间,然后访问内存块来访问你需要的类型数组。或者使用C99标准[15,6.7.5/2],但它内部看起来更像alloca()(4.2.4)。

16.5 Multidimensional arrays

多维数组和线性数组在本质上是一样的。 因为计算机内存是线性的,它是一维数组。但是一维数组可以很容易用来表现多维的。 比如数组a[3][4]元素可以放置在一维数组的12个单元中:

[0][0]
[0][1]
[0][2]
[0][3]
[1][0]
[1][4]
[1][5]
[1][6]
[2][0]
[2][7]
[2][8]
[2][9]

该二维数组在内存中用一维数组索引表示为:

1 2 3
4 5 6 7
8 9 10 11

为了计算我们需要的元素地址,首先,第一个索引乘以4(矩阵宽度),然后加上第二个索引。这种被称为行优先,C/C++和Python常用这种方法。行优先的意思是:先写入第一行,接着是第二行,…,最后是最后一行。 另一种方法就是列优先,主要用在FORTRAN,MATLAB,R等。列优先的意思是:先写入第一列,然后是第二列,…,最后是最后一列。 多维数组与此类似。 我们看个例子:

Listing 16.4: simple example

#include <stdio.h>
 
int a[10][20][30];
 
void insert(int x, int y, int z, int value)
{
    a[x][y][z]=value;
};

16.5.1 x86

MSVC 2010:

Listing 16.5: MSVC 2010

_DATA   SEGMENT
COMM    _a:DWORD:01770H
_DATA   ENDS
PUBLIC  _insert
_TEXT   SEGMENT
_x$ = 8         ; size = 4
_y$ = 12        ; size = 4
_z$ = 16        ; size = 4
_value$ = 20    ; size = 4
_insert     PROC
    push    ebp
    mov     ebp, esp
    mov     eax, DWORD PTR _x$[ebp]
    imul    eax, 2400                   ; eax=600*4*x
    mov     ecx, DWORD PTR _y$[ebp]
    imul    ecx, 120                    ; ecx=30*4*y
    lea     edx, DWORD PTR _a[eax+ecx]  ; edx=a + 600*4*x + 30*4*y
    mov     eax, DWORD PTR _z$[ebp]
    mov     ecx, DWORD PTR _value$[ebp]
    mov     DWORD PTR [edx+eax*4], ecx  ; *(edx+z*4)=value
    pop     ebp
    ret     0
_insert ENDP
_TEXT ENDS

多维数组计算索引公式:address=6004x+304y+4z。因为int类型为32-bits(4字节),所以要乘以4。

Listing 16.6: GCC 4.4.1

        public insert
insert  proc near
x       = dword ptr 8
y       = dword ptr 0Ch
z       = dword ptr 10h
value   = dword ptr 14h
        push    ebp
        mov     ebp, esp
        push    ebx
        mov     ebx, [ebp+x]
        mov     eax, [ebp+y]
        mov     ecx, [ebp+z]
        lea     edx, [eax+eax] ; edx=y*2
        mov     eax, edx ; eax=y*2
        shl     eax, 4 ; eax=(y*2)<<4 = y*2*16 = y*32
        sub     eax, edx ; eax=y*32 - y*2=y*30
        imul    edx, ebx, 600 ; edx=x*600
        add     eax, edx ; eax=eax+edx=y*30 + x*600
        lea     edx, [eax+ecx] ; edx=y*30 + x*600 + z
        mov     eax, [ebp+value]
        mov     dword ptr ds:a[edx*4], eax ; *(a+edx*4)=value
        pop     ebx
        pop     ebp
        retn
insert  endp

GCC使用的不同的计算方法。为计算第一个操作值30y,GCC没有使用乘法指令。GCC的做法是:(???? + ????) ≪ 4 − (???? + ????) = (2????) ≪ 4 − 2???? = 2 ・ 16 ・ ???? − 2???? = 32???? − 2???? = 30????。因此30y的计算仅使用加法和移位操作,这样速度更快。

16.5.2 ARM + Non-optimizing Xcode (LLVM) + thumb mode

Listing 16.7: Non-optimizing Xcode (LLVM) + thumb mode

_insert
 
value       = -0x10
z           = -0xC
y           = -8
x           = -4
 
; allocate place in local stack for 4 values of int type
SUB         SP, SP, #0x10
MOV         R9, 0xFC2 ; a
ADD         R9, PC
LDR.W       R9, [R9]
STR         R0, [SP,#0x10+x]
STR         R1, [SP,#0x10+y]
STR         R2, [SP,#0x10+z]
STR         R3, [SP,#0x10+value]
LDR         R0, [SP,#0x10+value]
LDR         R1, [SP,#0x10+z]
LDR         R2, [SP,#0x10+y]
LDR         R3, [SP,#0x10+x]
MOV         R12, 2400
MUL.W       R3, R3, R12
ADD         R3, R9
MOV         R9, 120
MUL.W       R2, R2, R9
ADD         R2, R3
LSLS        R1, R1, #2 ; R1=R1<<2
ADD         R1, R2
STR         R0, [R1] ; R1 - address of array element
; deallocate place in local stack, allocated for 4 values of int type
ADD         SP, SP, #0x10
BX          LR

非优化的LLVM代码在栈中保存了所有变量,这是冗余的。元素地址的计算我们通过公式已经找到了。

16.5.3 ARM + Optimizing Xcode (LLVM) + thumb mode

Listing 16.8: Optimizing Xcode (LLVM) + thumb mode

_insert
MOVW        R9, #0x10FC
MOV.W       R12, #2400
MOVT.W      R9, #0
RSB.W       R1, R1, R1,LSL#4    ; R1 - y. R1=y<<4 - y = y*16 - y = y*15
ADD         R9, PC ; R9 = pointer to a array
LDR.W       R9, [R9]
MLA.W       R0, R0, R12, R9     ; R0 - x, R12 - 2400, R9 - pointer to a. R0=x*2400 + ptr to a
ADD.W       R0, R0, R1,LSL#3    ; R0 = R0+R1<<3 = R0+R1*8 = x*2400 + ptr to a + y*15*8 =
                                ; ptr to a + y*30*4 + x*600*4
STR.W       R3, [R0,R2,LSL#2]   ; R2 - z, R3 - value. address=R0+z*4 =
                                ; ptr to a + y*30*4 + x*600*4 + z*4
BX          LR

这里的小技巧没有使用乘法,使用移位、加减法等。 这里有个新指令RSB(逆向减法),该指令的意义是让第一个操作数像SUB第二个操作数一样可以应用LSL#4附加操作。 “LDR.W R9, [R9]”类似于x86下的LEA指令(B.6.2),这里什么都没有做,是冗余的。显然,编译器没有优化它。 # 位域

很多函数参数的输入标志使用了位域。当然,可以使用bool类型来替代,只是有点浪费。

17.1 Specific bit checking

17.1.1 x86

Win32 API 例子:

    HANDLE fh;

    fh=CreateFile("file", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);

MSVC 2010: Listing 17.1: MSVC 2010

push    0
push    128            ; 00000080H
push    4
push    0
push    1
push    -1073741824    ; c0000000H
push    OFFSET $SG78813
call    DWORD PTR __imp__CreateFileA@28
mov     DWORD PTR _fh$[ebp], eax

我们再查看WinNT.h:

Listing 17.2: WinNT.h

#define GENERIC_READ (0x80000000L)
#define GENERIC_WRITE (0x40000000L)
#define GENERIC_EXECUTE (0x20000000L)
#define GENERIC_ALL (0x10000000L)

容易看出GENERIC_READ | GENERIC_WRITE = 0x80000000 | 0x40000000 = 0xC0000000,该值作为CreateFile()1函数的第二个参数。 CreateFile()如何检查该标志呢? 以Windows XP SP3 x86为例,在kernel32.dll中查看CreateFileW检查该标志的代码片段: Listing 17.3: KERNEL32.DLL (Windows XP SP3 x86)

.text:7C83D429 test byte ptr [ebp+dwDesiredAccess+3], 40h
.text:7C83D42D mov [ebp+var_8], 1
.text:7C83D434 jz short loc_7C83D417
.text:7C83D436 jmp loc_7C810817

我们来看TEST指令,该指令并未检测整个第二个参数,仅检测关键的一个字节(ebp+dwDesiredAccess+3),检测0x40标志(这里代表GENERIC_WRITE标志)。 Test对两个参数(目标,源)执行AND逻辑操作,并根据结果设置标志寄存器,结果本身不会保存(CMP和SUB与此类似(6.6.1))。 该代码片段逻辑如下:

if ((dwDesiredAccess&0x40000000) == 0) goto loc_7C83D417

如果AND指令没有设置ZF位,JZ将不触发跳转。如果dwDesiredAccess不等于0x40000000,AND结果将是0,ZF位将会被设置,条件跳转将被触发。

我们在linux GCC 4.4.1下查看:

#include <stdio.h>
#include <fcntl.h>
void main()
{
    int handle;

    handle=open ("file", O_RDWR | O_CREAT);
};

我们得到: Listing 17.4: GCC 4.4.1

    public main
main proc near
 
 
var_20 = dword ptr -20h
var_1C = dword ptr -1Ch
var_4 = dword ptr -4
 
 
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 20h
    mov [esp+20h+var_1C], 42h
    mov [esp+20h+var_20], offset aFile ; "file"
    call _open
    mov [esp+20h+var_4], eax
    leave
    retn
main endp

我们在libc.so.6库中查看open()函数,看到syscall: Listing 17.5: open() (libc.so.6)

.text:000BE69B mov edx, [esp+4+mode] ; mode
.text:000BE69F mov ecx, [esp+4+flags] ; flags
.text:000BE6A3 mov ebx, [esp+4+filename] ; filename
.text:000BE6A7 mov eax, 5
.text:000BE6AC int 80h ; LINUX - sys_open

因此open()对于标志位的检测在内核中。 对于linux2.6,当sys_open被调用时,最终传递到do_sys_open内核函数,然后进入do_filp_open()函数(该函数位于源码fs/namei.c中)。 除了通过堆栈传递参数,还可以通过寄存器传递方式,这种调用方式成为fastcall(47.3)。这种调用方式CPU不需要访问堆栈就可以直接读取参数的值,所以速度更快。GCC有编译选项regram2,可以设置通过寄存器传递的参数的个数。 Linux2.6内核编译附加选项为-mregram=33 4。 这意味着前3个参数通过EAX、EDX、ECX寄存器传递,剩余的参数通过堆栈传递。如果参数小于3,仅部分寄存器被使用。 我们下载linux内核2.6.31源码,在Ubuntu中编译:make vmlinux,在IDA中打开,找到do_filp_open()函数。在开始部分我们可以看到(注释个人添加): Listing 17.6:do_filp_open() (linux kernel 2.6.31)

do_filp_open proc near
...
    push ebp
    mov ebp, esp
    push edi
    push esi
    push ebx
    mov ebx, ecx
    add ebx, 1
    sub esp, 98h
    mov esi, [ebp+arg_4] ; acc_mode (5th arg)
    test bl, 3
    mov [ebp+var_80], eax ; dfd (1th arg)
    mov [ebp+var_7C], edx ; pathname (2th arg)
    mov [ebp+var_78], ecx ; open_flag (3th arg)
    jnz short loc_C01EF684
    mov ebx, ecx ; ebx <- open_flag

GCC保存3个参数的值到堆栈。否则,可能会造成寄存器浪费。 我们来看代码片段: Listing 17.7: do_filp_open() (linux kernel 2.6.31)

loc_C01EF6B4:            ; CODE XREF: do_filp_open+4F
    test bl, 40h         ; O_CREAT
    jnz loc_C01EF810
    mov edi, ebx
    shr edi, 11h
    xor edi, 1
    and edi, 1
    test ebx, 10000h
    jz short loc_C01EF6D3
    or edi, 2

O_CREAT宏等于0x40,如果open_flag为0x40,标志位被置1,接下来的JNZ指令将被触发。

17.1.2 ARM

Linux kernel3.8.0检测O_CREAT过程有点不同。 Listing 17.8: linux kernel 3.8.0

struct file *do_filp_open(int dfd, struct filename *pathname, const struct open_flags *op)
{
... filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU); ... 
}

static struct file *path_openat(int dfd, struct filename *pathname, struct nameidata *nd, const struct open_flags *op, int flags)

{
... error = do_last(nd, &path, file, op, &opened, pathname); ... 
}

static int do_last(struct nameidata *nd, struct path *path, struct file *file, const struct open_flags *op, int *opened, struct filename *name) 
{
    ...
    if (!(open_flag & O_CREAT)) {
        ...
        error = lookup_fast(nd, path, &inode);
        ...
    } else {
        ... error = complete_walk(nd);
    }
    ...
}

在IDA中查看ARM模式内核: Listing 17.9: do_last() (vmlinux)

...
.text:C0169EA8 MOV           R9, R3 ; R3 - (4th argument) open_flag
...
.text:C0169ED4 LDR           R6, [R9] ; R6 - open_flag
...
.text:C0169F68 TST           R6, #0x40 ; jumptable C0169F00 default case
.text:C0169F6C BNE           loc_C016A128
.text:C0169F70 LDR           R2, [R4,#0x10]
.text:C0169F74 ADD           R12, R4, #8
.text:C0169F78 LDR           R3, [R4,#0xC]
.text:C0169F7C MOV           R0, R4
.text:C0169F80 STR           R12, [R11,#var_50]
.text:C0169F84 LDRB          R3, [R2,R3]
.text:C0169F88 MOV           R2, R8
.text:C0169F8C CMP           R3, #0
.text:C0169F90 ORRNE         R1, R1, #3
.text:C0169F94 STRNE         R1, [R4,#0x24]
.text:C0169F98 ANDS          R3, R6, #0x200000
.text:C0169F9C MOV           R1, R12
.text:C0169FA0 LDRNE         R3, [R4,#0x24]
.text:C0169FA4 ANDNE         R3, R3, #1
.text:C0169FA8 EORNE         R3, R3, #1
.text:C0169FAC STR           R3, [R11,#var_54]
.text:C0169FB0 SUB           R3, R11, #-var_38
.text:C0169FB4 BL            lookup_fast
...
.text:C016A128 loc_C016A128          ; CODE XREF: do_last.isra.14+DC
.text:C016A128 MOV           R0, R4
.text:C016A12C BL            complete_walk
...

TST指令类似于x86下的TEST指令。 这段代码来自do_last()函数源码,有两个分支lookup_fast()和complete_walk()。这里O_CREAT宏也等于0x40。

17.2 Specific bit setting/clearing

例如:

#define IS_SET(flag, bit) ((flag) & (bit))
#define SET_BIT(var, bit) ((var) |= (bit))
#define REMOVE_BIT(var, bit) ((var) &= ~(bit))
int f(int a)
{
    int rt=a;
    SET_BIT (rt, 0x4000);
    REMOVE_BIT (rt, 0x200);

    return rt;
};

17.2.1 x86

MSVC 2010: Listing 17.10: MSVC 2010

_rt$ = -4 ; size = 4
_a$ = 8 ; size = 4
_f PROC
push ebp
mov ebp, esp
push ecx
mov eax, DWORD PTR _a$[ebp]
mov DWORD PTR _rt$[ebp], eax
mov ecx, DWORD PTR _rt$[ebp]
or ecx, 16384            ; 00004000H
mov DWORD PTR _rt$[ebp], ecx
mov edx, DWORD PTR _rt$[ebp]
and edx, -513            ; fffffdffH
mov DWORD PTR _rt$[ebp], edx
mov eax, DWORD PTR _rt$[ebp]
mov esp, ebp
pop ebp
ret 0
_f ENDP

OR指令添加一个或多个bit位而忽略了其余位。 AND用来重置一个bit位。 如果我们使用msvc编译,并且打开优化选项(/Ox),代码将会更短: Listing 17.11: Optimizing MSVC

_a$ = 8          ; size = 4
_f PROC
    mov eax, DWORD PTR _a$[esp-4]
    and eax, -513    ; fffffdffH
    or eax, 16384    ; 00004000H
    ret 0
_f ENDP

我们来看GCC 4.4.1无优化的代码:

            public f
f           proc near
var_4       = dword ptr -4
arg_0       = dword ptr 8
        push ebp
        mov ebp, esp
        sub esp, 10h
        mov eax, [ebp+arg_0]
        mov [ebp+var_4], eax
        or [ebp+var_4], 4000h
        and [ebp+var_4], 0FFFFFDFFh
        mov eax, [ebp+var_4]
        leave
        retn
f           endp

MSVC未优化的代码有些冗余。 现在我们来看GCC打开优化选项-O3:

Listing 17.13: Optimizing GCC

    public f
f       proc near
arg_0 = dword ptr 8
    push ebp
    mov ebp, esp
    mov eax, [ebp+arg_0]
    pop ebp
    or ah, 40h
    and ah, 0FDh
    retn
f       endp

代码更短。值得注意的是编译器使用了AH寄存器-EAX寄存器8bit-15bit部分。

8086 16位CPU累加器被称为AX,包含两个8位部分-AL(低字节)和AH(高字节)。在80386下所有寄存器被扩展为32位,累加器被命名为EAX,为了保持兼容性,它的老的部分仍可以作为AX/AH/AL寄存器来访问。 因为所有的x86 CPU都兼容于16位CPU,所以老的16位操作码比32位操作码更短。”or ah,40h”指令仅复制3个字节比“or eax,04000h”需要复制5个字节甚至6个字节(如果第一个操作码不是EAX)更合理。 编译时候开启-O3并且设置regram=3生成的代码会更短。

Listing 17.14: Optimizing GCC

        public f
f       proc near
    push ebp
    or ah, 40h
    mov ebp, esp
    and ah, 0FDh
    pop ebp
    retn
f       endp

事实上,第一个参数已经被加载到EAX了,所以可以直接使用了。值得注意的是,函数序言(push ebp/mov ebp,esp)和结语(pop ebp)很容易被忽略。GCC并没有优化掉这些代码。更短的代码可以使用内联函数(27)。

17.2.2 ARM + Optimizing Keil + ARM mode

Listing 17.15: Optimizing Keil + ARM mode

02 0C C0 E3     BIC     R0, R0, #0x200
01 09 80 E3     ORR     R0, R0, #0x4000
1E FF 2F E1     BX  LR

BIC是“逻辑and“类似于x86下的AND。ORR是”逻辑or“类似于x86下的OR。

17.2.3 ARM + Optimizing Keil + thumb mode

Listing 17.16: Optimizing Keil + thumb mode

01 21 89 03     MOVS     R1, 0x4000
08 43       ORRS     R0, R1
49 11       ASRS     R1, R1, #5 ; generate 0x200 and place to R1
88 43       BICS     R0, R1
70 47       BX   LR5

从0x4000右移生成0x200,采用移位使代码更简洁。

17.2.4 ARM + Optimizing Xcode (LLVM) + ARM mode

Listing 17.17: Optimizing Xcode (LLVM) + ARM mode

42 0C C0 E3     BIC     R0, R0, #0x4200
01 09 80 E3     ORR     R0, R0, #0x4000
1E FF 2F E1     BX  LR

该代码由LLVM生成,从源码形式上看,看起来更像是:

REMOVE_BIT (rt, 0x4200);
SET_BIT (rt, 0x4000);

为什么是0x4200?可能是编译器构造的5,可能是编译器编译错误,但生成的代码是可用的。 更多编译器异常请参考相关资料(67)。 对于thumb模式,优化Xcode(LLVM)生成的代码相似。

17.3 Shifts

C/C++的移位操作通过<<和>>实现。 这里有一个例子函数,计算输入变量有多少个位被置为1.

#define IS_SET(flag, bit) ((flag) & (bit))
int f(unsigned int a)
{
    int i;
    int rt=0;
    for (i=0; i<32; i++)
        if (IS_SET (a, 1<<i))
            rt++;
        return rt;
    };

在循环中,迭代计数从0到31,1<&lti语句将计数从1到0x80000000。1<<i即1左移n位,将包含32位数字所有可能的bit位。每次移位仅有1位被置1,其它位均为0,IS_SET宏将判断a对应的位是否置1。

IS_SET宏就是逻辑与(AND)操作,如果对应的位不为1,则返回0。if条件表达式如果不为0,if()将被触发。

17.3.1 x86

MSVC 2010:

Listing 17.18: MSVC 2010

_rt$ = -8       ; size = 4
_i$ = -4        ; size = 4
_a$ = 8         ; size = 4
_f PROC
    push ebp
    mov ebp, esp
    sub esp, 8
    mov DWORD PTR _rt$[ebp], 0
    mov DWORD PTR _i$[ebp], 0
    jmp SHORT $LN4@f
$LN3@f:
    mov eax, DWORD PTR _i$[ebp]     ; increment of 1
    add eax, 1
    mov DWORD PTR _i$[ebp], eax
$LN4@f:
    cmp DWORD PTR _i$[ebp], 32      ; 00000020H
    jge SHORT $LN2@f            ; loop finished?
    mov edx, 1
    mov ecx, DWORD PTR _i$[ebp]
    shl edx, cl             ; EDX=EDX<<CL
    and edx, DWORD PTR _a$[ebp]
    je SHORT $LN1@f             ; result of AND instruction was 0?
    ; then skip next instructions
    mov eax, DWORD PTR _rt$[ebp]    ; no, not zero
    add eax, 1 ; increment rt
    mov DWORD PTR _rt$[ebp], eax
$LN1@f:
    jmp SHORT $LN3@f
$LN2@f:
    mov eax, DWORD PTR _rt$[ebp]
    mov esp, ebp
    pop ebp
    ret 0
_f ENDP

下面是GCC 4.4.1编译的代码: Listing 17.19: GCC 4.4.1

        public f
f           proc near
rt      = dword ptr -0Ch
i       = dword ptr -8
arg_0   = dword ptr 8
 
        push ebp
        mov ebp, esp
        push ebx
        sub esp, 10h
        mov [ebp+rt], 0
        mov [ebp+i], 0
        jmp short loc_80483EF
loc_80483D0:
        mov eax, [ebp+i]
        mov edx, 1
        mov ebx, edx
        mov ecx, eax
        shl ebx, cl
        mov eax, ebx
        and eax, [ebp+arg_0]
        test eax, eax
        jz short loc_80483EB
        add [ebp+rt], 1
loc_80483EB:
        add [ebp+i], 1
loc_80483EF:
        cmp [ebp+i], 1Fh
            jle short loc_80483D0
            mov eax, [ebp+rt]
        add esp, 10h
        pop ebx
        pop ebp
        retn
f           endp

在乘以或者除以2的指数值(1,2,4,8等)时经常使用移位操作。 例如:

unsigned int f(unsigned int a)
{
    return a/4;
};

MSVC 2010: Listing 17.20: MSVC 2010

_a$ = 8                     ; size = 4
_f      PROC
        mov eax, DWORD PTR _a$[esp-4]
        shr eax, 2
        ret 0
_f ENDP

例子中的SHR(逻辑右移)指令将a值右移2位,最高两位被置0,最低2位被丢弃。实施上丢弃的两位是除法的余数。 SHR作用类似SHL只是移位方向不同。

使用十进制23很好来理解。23除以10,丢弃最后的数字(3是余数),商为2。 与此类似的是乘法。比如乘以4,仅需将数字左移2位,最低两位被置0。就像3乘以100—仅仅在最后补两个0就行了。

17.3.2 ARM + Optimizing Xcode (LLVM) + ARM mode

Listing 17.21: Optimizing Xcode (LLVM) + ARM mode

        MOV R1, R0
        MOV R0, #0
        MOV R2, #1
        MOV R3, R0
loc_2E54
        TST R1, R2,LSL R3       ; set flags according to R1 & (R2<<R3)
        ADD R3, R3, #1      ; R3++
        ADDNE R0, R0, #1        ; if ZF flag is cleared by TST, R0++
        CMP R3, #32
        BNE loc_2E54
        BX LR

TST类似于x86下的TEST指令。 正如我前面提到的(14.2.1),ARM模式下没有单独的移位指令。对于用作修饰的LSL(逻辑左移)、LSR(逻辑右移)、ASR(算术右移)、ROR(循环右移)和RRX(带扩展的循环右移指令),需要与MOV,TST,CMP,ADD,SUB,RSB结合来使用6。 这些修饰指令被定义,第二个操作数指定移动的位数。 因此“TST R1, R2,LSL R3”指令所做的工作为????1 ∧ (????2 ≪ ????3).

17.3.3 ARM + Optimizing Xcode (LLVM) + thumb-2 mode

几乎一样,只是这里使用LSL.W/TST指令而不是只有TST。因为Thumb模式下TST没有定义修饰符LSL。

    MOV     R1, R0
    MOVS    R0, #0
    MOV.W   R9, #1
    MOVS    R3, #0
loc_2F7A
    LSL.W   R2, R9, R3
    TST     R2, R1
    ADD.W   R3, R3, #1
    IT NE
    ADDNE   R0, #1
    CMP     R3, #32
    BNE     loc_2F7A
    BX      LR

 17.4 CRC32哈希散列计算例子

这是非常流行的CRC32哈希散列计算。

/* By Bob Jenkins, (c) 2006, Public Domain */
#include <stdio.h>
#include <stddef.h>
#include <string.h>
typedef unsigned long ub4;
typedef unsigned char ub1;
static const ub4 crctab[256] = {
    0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
    0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
    0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
    0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
    0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
    0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
    0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
    0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
    0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
    0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
    0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
    0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
    0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
    0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
    0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
    0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
    0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
    0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
    0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
    0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
    0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
    0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
    0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
    0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
    0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
    0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
    0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
    0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
    0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
    0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
    0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
    0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
    0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
    0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
    0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
    0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
    0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
    0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
    0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
    0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
    0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
    0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
    0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
};
/* how to derive the values in crctab[] from polynomial 0xedb88320 */
void build_table()
{
    ub4 i, j;
    for (i=0; i<256; ++i) {
        j = i;
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
        printf("0x%.8lx, ", j);
        if (i%6 == 5) printf("");
     }
}
/* the hash function */
ub4 crc(const void *key, ub4 len, ub4 hash)
{
    ub4 i;
    const ub1 *k = key;
    for (hash=len, i=0; i<len; ++i)
        hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ k[i]];
    return hash;
}

/* To use, try "gcc -O crc.c -o crc; crc < crc.c" */
int main()
{
    char s[1000];
    while (gets(s)) printf("%.8lx", crc(s, strlen(s), 0));
    return 0;
}

我们只关心crc()函数。注意for()语句两个循环初始化:hash=len,i=0。标准C/C++允许这样做。循环体内通常需要使用两个初始化部分。 让我们用MSVC优化(/Ox)。为了简洁,仅列出crc()函数的代码,包括我做的注释。

key$ = 8               ; size = 4
_len$ = 12              ; size = 4
_hash$ = 16             ; size = 4
_crc PROC
    mov     edx, DWORD PTR _len$[esp-4]
    xor     ecx, ecx    ; i will be stored in ECX
    mov     eax, edx
    test    edx, edx
    jbe     SHORT $LN1@crc
    push    ebx
    push    esi
    mov     esi, DWORD PTR _key$[esp+4] ; ESI = key
    push    edi
$LL3@crc:
 
; work with bytes using only 32-bit registers. byte from address key+i we store into EDI
 
    movzx   edi, BYTE PTR [ecx+esi]
    mov     ebx, eax ; EBX = (hash = len)
    and     ebx, 255 ; EBX = hash & 0xff
; XOR EDI, EBX (EDI=EDI^EBX) - this operation uses all 32 bits of each register
; but other bits (8-31) are cleared all time, so it’s OK
; these are cleared because, as for EDI, it was done by MOVZX instruction above
; high bits of EBX was cleared by AND EBX, 255 instruction above (255 = 0xff)
 
    xor     edi, ebx
 
; EAX=EAX>>8; bits 24-31 taken "from nowhere" will be cleared
 
        shr     eax, 8
 
; EAX=EAX^crctab[EDI*4] - choose EDI-th element from crctab[] table
    xor     eax, DWORD PTR _crctab[edi*4]
    inc     ecx         ; i++
    cmp     ecx, edx ; i<len ?
    jb      SHORT $LL3@crc ; yes
    pop     edi
    pop     esi
    pop     ebx
$LN1@crc:
    ret 0
_crc ENDP

我们来看GCC 4.4.1优化后的代码:

    public crc
crc     proc near
key     = dword ptr 8
hash    = dword ptr 0Ch
push    ebp
xor     edx, edx
mov     ebp, esp
push    esi
mov     esi, [ebp+key]
push    ebx
mov     ebx, [ebp+hash]
test    ebx, ebx
mov     eax, ebx
jz      short loc_80484D3
nop                 ; padding
lea     esi, [esi+0]        ; padding; ESI doesn’t changing here
loc_80484B8:
mov     ecx, eax        ; save previous state of hash to ECX
xor     al, [esi+edx]       ; AL=*(key+i)
add     edx, 1          ; i++
shr     ecx, 8          ; ECX=hash>>8
movzx   eax, al         ; EAX=*(key+i)
mov     eax, dword ptr ds:crctab[eax*4]     ; EAX=crctab[EAX]
xor     eax, ecx        ; hash=EAX^ECX
cmp     ebx, edx
ja      short loc_80484B8
loc_80484D3:
    pop ebx
    pop esi
    pop ebp
    retn
crc endp

GCC在循环开始的时候通过填入NOP和lea esi,esi+0来按8字节对齐。更多信息请阅读npad小结(64)。

结构体

C/C++的结构体可以这么定义:它是一组存储在内存中的变量的集合,成员变量类型不要求相同。

18.1 SYSTEMTIME 的例子

让我们看看Win32结构体SYSTEMTIME的定义:

清单18.1: WinBase.h

typedef struct _SYSTEMTIME {
    WORD wYear;
    WORD wMonth;
    WORD wDayOfWeek;
    WORD wDay;
    WORD wHour;
    WORD wMinute;
    WORD wSecond;
    WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;

让我们写一个获取当前时间的C程序:

#include <windows.h>
#include <stdio.h>
void main()
{
    SYSTEMTIME t;
    GetSystemTime(&t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
        t.wYear, t.wMonth, t.wDay,
        t.wHour, t.wMinute, t.wSecond);
    return;
};

反汇编结果如下(MSVC 2010):

清单18.2: MSVC 2010

_t$ = -16 ; size = 16
_main PROC
    push ebp
    mov ebp, esp
    sub esp, 16 ; 00000010H
    lea eax, DWORD PTR _t$[ebp]
    push eax
    call DWORD PTR __imp__GetSystemTime@4
    movzx ecx, WORD PTR _t$[ebp+12] ; wSecond
    push ecx
    movzx edx, WORD PTR _t$[ebp+10] ; wMinute
    push edx
    movzx eax, WORD PTR _t$[ebp+8] ; wHour
    push eax
    movzx ecx, WORD PTR _t$[ebp+6] ; wDay
    push ecx
    movzx edx, WORD PTR _t$[ebp+2] ; wMonth
    push edx
    movzx eax, WORD PTR _t$[ebp] ; wYear
    push eax
    push OFFSET $SG78811 ; ’%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H
    call _printf
    add esp, 28 ; 0000001cH
    xor eax, eax
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP

在本地栈上程序为这个结构体分配了16个字节:这正是sizeof(WORD)*8的大小(因为结构体里有8个WORD)。 请注意结构体是由wYear开始的,因此,我们既可以说这是“传给GetSystemTime()函数的,一个指向SYSTEMTIME结构体的指针”,也可以说是它“传递了wYear的指针”。这两种说法是一样的!GetSystemTime()函数会把当前的年份写入指向的WORD指针中,然后把指针向后移动2个字节(译注:WORD大小为2字节),再写入月份,以此类推。 事实上,结构体的成员其实就是一个个紧贴在一起的变量。我可以用下面的方法来访问SYSTEMTIME结构体,代码如下:

#include <windows.h>
#include <stdio.h>
void main()
{
    WORD array[8];
    GetSystemTime (array);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
        array[0] /* wYear */, array[1] /* wMonth */, array[3] /* wDay */,
        array[4] /* wHour */, array[5] /* wMinute */, array[6] /* wSecond */);
    return;
};

编译器会稍稍给出一点警告:

systemtime2.c(7) : warning C4133: ’function’ : incompatible types - from ’WORD [8]’ to ’LPSYSTEMTIME’

不过至少,它会产生如下代码:

清单18.3: MSVC 2010

$SG78573 DB ’%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H
_array$ = -16 ; size = 16
_main PROC
    push ebp
    mov ebp, esp
    sub esp, 16 ; 00000010H
    lea eax, DWORD PTR _array$[ebp]
    push eax
    call DWORD PTR __imp__GetSystemTime@4
    movzx ecx, WORD PTR _array$[ebp+12] ; wSecond
    push ecx
    movzx edx, WORD PTR _array$[ebp+10] ; wMinute
    push edx
    movzx eax, WORD PTR _array$[ebp+8] ; wHoure
    push eax
    movzx ecx, WORD PTR _array$[ebp+6] ; wDay
    push ecx
    movzx edx, WORD PTR _array$[ebp+2] ; wMonth
    push edx
    movzx eax, WORD PTR _array$[ebp] ; wYear
    push eax
    push OFFSET $SG78573
    call _printf
    add esp, 28 ; 0000001cH
    xor eax, eax
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP

当然,它也能一样正常工作! 一个很有趣的情况是这两次编译结果居然一样,所以光看编译结果,我们还看不出来到底别人用的结构体还是单单用的变量数组。 不过,没几个人会这么无聊的用这种方法写代码,因为这太麻烦了。还有,结构体也有可能会被开发者更改,交换,等等,所以还是用结构体方便。

18.2 让我们用malloc()为结构体分配空间

但是,有时候把结构体放在堆中而不是栈上却更简单:

#include <windows.h>
#include <stdio.h>
void main()
{
    SYSTEMTIME *t;
    t=(SYSTEMTIME *)malloc (sizeof (SYSTEMTIME));
    GetSystemTime (t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
        t->wYear, t->wMonth, t->wDay,
        t->wHour, t->wMinute, t->wSecond);
    free (t);
    return;
};

让我们用优化/Ox编译一下它,看看我们得到什么东西

清单18.4: 优化的MSVC

_main PROC
    push esi
    push 16 ; 00000010H
    call _malloc
    add esp, 4
    mov esi, eax
    push esi
    call DWORD PTR __imp__GetSystemTime@4
    movzx eax, WORD PTR [esi+12] ; wSecond
    movzx ecx, WORD PTR [esi+10] ; wMinute
    movzx edx, WORD PTR [esi+8] ; wHour
    push eax
    movzx eax, WORD PTR [esi+6] ; wDay
    push ecx
    movzx ecx, WORD PTR [esi+2] ; wMonth
    push edx
    movzx edx, WORD PTR [esi] ; wYear
    push eax
    push ecx
    push edx
    push OFFSET $SG78833
    call _printf
    push esi
    call _free
    add esp, 32 ; 00000020H
    xor eax, eax
    pop esi
    ret 0
_main ENDP

所以,sizeof(SYSTEMTIME) = 16, 这正是malloc所分配的字节数。它返回了刚刚分配的地址空间,这个指针存在EAX寄存器里。然后,这个指针会被移动到ESI结存器中, GetSystemTime()会用它来存储返回值,这也就是为什么这里分配完之后并没有把EAX放到某个地方保存起来,而是直接使用它的原因。

新指令:MOVZX(Move with Zero eXtent, 0扩展移动)。它可以说是和MOVSX基本一样(13.1.1节),但是,它把其他位都设置为0。这是因为printf()需要一个32位的整数,但是我们的结构体里面是WORD,这只有16位厂。这也就是为什么从WORD复制到INT时第16~31位必须清零的原因了。因为,如果不清除的话,剩余位可能有之前操作留下来的干扰数据。

在下面这个例子里面,我可以用WORD数组来重现这个结构:

#include <windows.h>
#include <stdio.h>
void main()
{
    WORD *t;
    t=(WORD *)malloc (16);
    GetSystemTime (t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d",
        t[0] /* wYear */, t[1] /* wMonth */, t[3] /* wDay */,
        t[4] /* wHour */, t[5] /* wMinute */, t[6] /* wSecond */);
    free (t);
    return;
};

我们得到:

$SG78594 DB ’%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H
_main PROC
    push esi
    push 16 ; 00000010H
    call _malloc
    add esp, 4
    mov esi, eax
    push esi
    call DWORD PTR __imp__GetSystemTime@4
    movzx eax, WORD PTR [esi+12]
    movzx ecx, WORD PTR [esi+10]
    movzx edx, WORD PTR [esi+8]
    push eax
    movzx eax, WORD PTR [esi+6]
    push ecx
    movzx ecx, WORD PTR [esi+2]
    push edx
    movzx edx, WORD PTR [esi]
    push eax
    push ecx
    push edx
    push OFFSET $SG78594
    call _printf
    push esi
    call _free
    add esp, 32 ; 00000020H
    xor eax, eax
    pop esi
    ret 0
_main ENDP

同样,我们可以看到编译结果和之前一样。个人重申一次,你不应该在写代码的时候用这么晦涩的方法来表达它。

18.3 结构体tm

18.3.1 linux

在Linux下,我们看看time.h中的tm结构体是什么样子的:

#include <stdio.h>
#include <time.h>
void main()
{
    struct tm t;
    time_t unix_time;
    unix_time=time(NULL);
    localtime_r (&unix_time, &t);
    printf ("Year: %d", t.tm_year+1900);
    printf ("Month: %d", t.tm_mon);
    printf ("Day: %d", t.tm_mday);
    printf ("Hour: %d", t.tm_hour);
    printf ("Minutes: %d", t.tm_min);
    printf ("Seconds: %d", t.tm_sec);
};

在GCC 4.4.1下编译得到:

清单18.6:GCC 4.4.1

main proc near
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 40h
    mov dword ptr [esp], 0 ; first argument for time()
    call time
    mov [esp+3Ch], eax
    lea eax, [esp+3Ch] ; take pointer to what time() returned
    lea edx, [esp+10h] ; at ESP+10h struct tm will begin
    mov [esp+4], edx ; pass pointer to the structure begin
    mov [esp], eax ; pass pointer to result of time()
    call localtime_r
    mov eax, [esp+24h] ; tm_year
    lea edx, [eax+76Ch] ; edx=eax+1900
    mov eax, offset format ; "Year: %d"
    mov [esp+4], edx
    mov [esp], eax
    call printf
    mov edx, [esp+20h] ; tm_mon
    mov eax, offset aMonthD ; "Month: %d"
    mov [esp+4], edx
    mov [esp], eax
    call printf
    mov edx, [esp+1Ch] ; tm_mday
    mov eax, offset aDayD ; "Day: %d"
    mov [esp+4], edx
    mov [esp], eax
    call printf
    mov edx, [esp+18h] ; tm_hour
    mov eax, offset aHourD ; "Hour: %d"
    mov [esp+4], edx
    mov [esp], eax
    call printf
    mov edx, [esp+14h] ; tm_min
    mov eax, offset aMinutesD ; "Minutes: %d"
    mov [esp+4], edx
    mov [esp], eax
    call printf
    mov edx, [esp+10h]
    mov eax, offset aSecondsD ; "Seconds: %d"
    mov [esp+4], edx ; tm_sec
    mov [esp], eax
    call printf
    leave
    retn
main endp

可是,IDA并没有为本地栈上变量建立本地变量名。但是因为我们已经学了汇编了,我们也不需要在这么简单的例子里面如此依赖它。

请也注意一下lea edx, [eax+76ch],这个指令把eax的值加上0x76c,但是并不修改任何标记位。请也参考LEA的相关章节(B.6.2节)

为了表现出结构体只是一个个的变量连续排列的东西,让我们重新测试一下这个例子,我们看看time.h: 清单18.7 time.h

struct tm
{
    int tm_sec;
    int tm_min;
    int tm_hour;
    int tm_mday;
    int tm_mon;
    int tm_year;
    int tm_wday;
    int tm_yday;
    int tm_isdst;
};
#include <stdio.h>
#include <time.h>
void main()
{
    int tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, tm_wday, tm_yday, tm_isdst;
    time_t unix_time;
    unix_time=time(NULL);
    localtime_r (&unix_time, &tm_sec);
    printf ("Year: %d", tm_year+1900);
    printf ("Month: %d", tm_mon);
    printf ("Day: %d", tm_mday);
    printf ("Hour: %d", tm_hour);
    printf ("Minutes: %d", tm_min);
    printf ("Seconds: %d", tm_sec);
};

注:指向tm_sec的指针会传递给localtime_r,或者说第一个“结构体”元素。 编译器会这么警告我们

清单18.8 GCC4.7.3

GCC_tm2.c: In function ’main’:
GCC_tm2.c:11:5: warning: passing argument 2 of ’localtime_r’ from incompatible pointer type [
enabled by default]
In file included from GCC_tm2.c:2:0:
/usr/include/time.h:59:12: note: expected ’struct tm *’ but argument is of type ’int *’

但是至少,它会生成这段代码:

清单18.9 GCC 4.7.3

main proc near
    var_30 = dword ptr -30h
    var_2C = dword ptr -2Ch
    unix_time = dword ptr -1Ch
    tm_sec = dword ptr -18h
    tm_min = dword ptr -14h
    tm_hour = dword ptr -10h
    tm_mday = dword ptr -0Ch
    tm_mon = dword ptr -8
    tm_year = dword ptr -4
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 30h
    call __main
    mov [esp+30h+var_30], 0 ; arg 0
    mov [esp+30h+unix_time], eax
    lea eax, [esp+30h+tm_sec]
    mov [esp+30h+var_2C], eax
    lea eax, [esp+30h+unix_time]
    mov [esp+30h+var_30], eax
    call localtime_r
    mov eax, [esp+30h+tm_year]
    add eax, 1900
    mov [esp+30h+var_2C], eax
    mov [esp+30h+var_30], offset aYearD ; "Year: %d"
    call printf
    mov eax, [esp+30h+tm_mon]
    mov [esp+30h+var_2C], eax
    mov [esp+30h+var_30], offset aMonthD ; "Month: %d"
    call printf
    mov eax, [esp+30h+tm_mday]
    mov [esp+30h+var_2C], eax
    mov [esp+30h+var_30], offset aDayD ; "Day: %d"
    call printf
    mov eax, [esp+30h+tm_hour]
    mov [esp+30h+var_2C], eax
    mov [esp+30h+var_30], offset aHourD ; "Hour: %d"
    call printf
    mov eax, [esp+30h+tm_min]
    mov [esp+30h+var_2C], eax
    mov [esp+30h+var_30], offset aMinutesD ; "Minutes: %d"
    call printf
    mov eax, [esp+30h+tm_sec]
    mov [esp+30h+var_2C], eax
    mov [esp+30h+var_30], offset aSecondsD ; "Seconds: %d"
    call printf
    leave
    retn
main endp

这个代码和我们之前看到的一样,依然无法分辨出源代码是用了结构体还是只是数组而已。

当然这样也是可以运行的,但是实际操作中还是不建议用这种晦涩的方法。因为通常,编译器会在栈上按照声明顺序分配变量空间,但是并不能保证每次都是这样。

还有,其他编译器可能会警告tm_year,tm_mon, tm_mday, tm_hour, tm_min变量而不是tm_sec使用时未初始化。事实上,计算机并不知道调用localtime_r()的时候他们会被自动填充上。

我选择了这个例子来解释是因为他们都是int类型的,而SYSTEMTIME的所有成员是16位的WORD,如果把它们作为本地变量来声明的话,他们会按照32位的边界值来对齐,因此什么都用不了了(因为由于数据对齐,此时GetSystemTime()会把它们错误的填充起来)。请继续读下一节的内容:“结构体的成员封装”。

所以,结构体只是把一组变量封装到一个位置上,数据是一个接一个的。我可以说结构体是一个语法糖,因为它只是用来让编译器把一组变量保存在一个地方。但是,我不是编程方面的专家,所以更有可能的是,我可能会误读这个术语。还有,在早期(1972年以前)的时候,C是不支持结构体的。

18.3.2 ARM+优化Keil+thumb模式

同样的例子: 清单18.10: 优化Keil+thumb模式

var_38 = -0x38
var_34 = -0x34
var_30 = -0x30
var_2C = -0x2C
var_28 = -0x28
var_24 = -0x24
timer = -0xC
    PUSH {LR}
    MOVS R0, #0 ; timer
    SUB SP, SP, #0x34
    BL time
    STR R0, [SP,#0x38+timer]
    MOV R1, SP ; tp
    ADD R0, SP, #0x38+timer ; timer
    BL localtime_r
    LDR R1, =0x76C
    LDR R0, [SP,#0x38+var_24]
    ADDS R1, R0, R1
    ADR R0, aYearD ; "Year: %d"
    BL __2printf
    LDR R1, [SP,#0x38+var_28]
    ADR R0, aMonthD ; "Month: %d"
    BL __2printf
    LDR R1, [SP,#0x38+var_2C]
    ADR R0, aDayD ; "Day: %d"
    BL __2printf
    LDR R1, [SP,#0x38+var_30]
    ADR R0, aHourD ; "Hour: %d"
    BL __2printf
    LDR R1, [SP,#0x38+var_34]
    ADR R0, aMinutesD ; "Minutes: %d"
    BL __2printf
    LDR R1, [SP,#0x38+var_38]
    ADR R0, aSecondsD ; "Seconds: %d"
    BL __2printf
    ADD SP, SP, #0x34
    POP {PC}

18.3.3 ARM+优化Xcode(LLVM)+thumb-2模式

IDA“碰巧知道”tm结构体(因为IDA“知道”例如localtime_r()这些库函数的参数类型),所以他把这里的结构变量的名字也显示出来了。

var_38 = -0x38
var_34 = -0x34
    PUSH {R7,LR}
    MOV R7, SP
    SUB SP, SP, #0x30
    MOVS R0, #0 ; time_t *
    BLX _time
    ADD R1, SP, #0x38+var_34 ; struct tm *
    STR R0, [SP,#0x38+var_38]
    MOV R0, SP ; time_t *
    BLX _localtime_r
    LDR R1, [SP,#0x38+var_34.tm_year]
    MOV R0, 0xF44 ; "Year: %d"
    ADD R0, PC ; char *
    ADDW R1, R1, #0x76C
    BLX _printf
    LDR R1, [SP,#0x38+var_34.tm_mon]
    MOV R0, 0xF3A ; "Month: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1, [SP,#0x38+var_34.tm_mday]
    MOV R0, 0xF35 ; "Day: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1, [SP,#0x38+var_34.tm_hour]
    MOV R0, 0xF2E ; "Hour: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1, [SP,#0x38+var_34.tm_min]
    MOV R0, 0xF28 ; "Minutes: %d"
    ADD R0, PC ; char *
    BLX _printf
    LDR R1, [SP,#0x38+var_34]
    MOV R0, 0xF25 ; "Seconds: %d"
    ADD R0, PC ; char *
    BLX _printf
    ADD SP, SP, #0x30
    POP {R7,PC}
...
00000000 tm struc ; (sizeof=0x2C, standard type)
00000000 tm_sec DCD ?
00000004 tm_min DCD ?
00000008 tm_hour DCD ?
0000000C tm_mday DCD ?
00000010 tm_mon DCD ?
00000014 tm_year DCD ?
00000018 tm_wday DCD ?
0000001C tm_yday DCD ?
00000020 tm_isdst DCD ?
00000024 tm_gmtoff DCD ?
00000028 tm_zone DCD ? ; offset
0000002C tm ends

清单18.11: ARM+优化Xcode(LLVM)+thumb-2模式

18.4 结构体的成员封装

结构体做的一个重要的事情就是封装了成员,让我们看看简单的例子:

#include <stdio.h>
struct s
{
    char a;
    int b;
    char c;
    int d;
};
void f(struct s s)
{
    printf ("a=%d; b=%d; c=%d; d=%d", s.a, s.b, s.c, s.d);
};

如我们所看到的,我们有2个char成员(每个1字节),和两个int类型的数据(每个4字节)。

18.4.1 x86

编译后得到:

_s$ = 8 ; size = 16
?f@@YAXUs@@@Z PROC ; f
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _s$[ebp+12]
    push eax
    movsx ecx, BYTE PTR _s$[ebp+8]
    push ecx
    mov edx, DWORD PTR _s$[ebp+4]
    push edx
    movsx eax, BYTE PTR _s$[ebp]
    push eax
    push OFFSET $SG3842
    call _printf
    add esp, 20 ; 00000014H
    pop ebp
    ret 0
?f@@YAXUs@@@Z ENDP ; f
_TEXT ENDS

如我们所见,每个成员的地址都按4字节对齐了,这也就是为什么char也会像int一样占用4字节。为什么?因为对齐后对CPU来说更容易读取数据。

但是,这么看明显浪费了一些空间。 让我们能用/Zp1(/Zp[n]代表结构体边界值为n字节)来编译它:

清单18.12: MSVC /Zp1

_TEXT SEGMENT
_s$ = 8 ; size = 10
?f@@YAXUs@@@Z PROC ; f
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _s$[ebp+6]
    push eax
    movsx ecx, BYTE PTR _s$[ebp+5]
    push ecx
    mov edx, DWORD PTR _s$[ebp+1]
    push edx
    movsx eax, BYTE PTR _s$[ebp]
    push eax
    push OFFSET $SG3842
    call _printf
    add esp, 20 ; 00000014H
    pop ebp
    ret 0
?f@@YAXUs@@@Z ENDP ; f

现在,结构体只用了10字节,而且每个char都占用1字节。我们得到了最小的空间,但是反过来看,CPU却无法用最优化的方式存取这些数据。 可以容易猜到的是,如果这个结构体在很多源代码和对象中被使用的话,他们都需要用同一种方式来编译起来。 除了MSVC /Zp选项,还有一个是#pragma pack编译器选项可以在源码中定义边界值。这个语句在MSVC和GCC中均被支持。 回到SYSTEMTIME结构体中的16位成员,我们的编译器怎么才能把它们按1字节边界来打包? WinNT.h有这么个代码:

清单18.13:WINNT.H

#include "pshpack1.h"

和这个:

清单18.14:WINNT.H

#include "pshpack4.h" // 4 byte packing is the default

文件PshPack1.h看起来像

清单18.15: PSHPACK1.H

#if ! (defined(lint) || defined(RC_INVOKED))
#if ( _MSC_VER >= 800 && !defined(_M_I86)) || defined(_PUSHPOP_SUPPORTED)
#pragma warning(disable:4103)
#if !(defined( MIDL_PASS )) || defined( __midl )
#pragma pack(push,1)
#else
#pragma pack(1)
#endif
#else
#pragma pack(1)
#endif
#endif /* ! (defined(lint) || defined(RC_INVOKED)) */

这就是#pragma pack处理结构体大小的方法。

18.4.2 ARM+优化Keil+thumb模式

清单18.16

.text:0000003E exit ; CODE XREF: f+16
.text:0000003E 05 B0 ADD SP, SP, #0x14
.text:00000040 00 BD POP {PC}
 
 
.text:00000280 f
.text:00000280
.text:00000280 var_18 = -0x18
.text:00000280 a = -0x14
.text:00000280 b = -0x10
.text:00000280 c = -0xC
.text:00000280 d = -8
.text:00000280
.text:00000280 0F B5 PUSH {R0-R3,LR}
.text:00000282 81 B0 SUB SP, SP, #4
.text:00000284 04 98 LDR R0, [SP,#16] ; d
.text:00000286 02 9A LDR R2, [SP,#8] ; b
.text:00000288 00 90 STR R0, [SP]
.text:0000028A 68 46 MOV R0, SP
.text:0000028C 03 7B LDRB R3, [R0,#12] ; c
.text:0000028E 01 79 LDRB R1, [R0,#4] ; a
.text:00000290 59 A0 ADR R0, aADBDCDDD ; "a=%d; b=%d; c=%d; d=%d
"
.text:00000292 05 F0 AD FF BL __2printf
.text:00000296 D2 E6 B exit

我们可以回忆到的是,这里它直接用了结构体而不是指向结构体的指针,而且因为ARM里函数的前4个参数是通过寄存器传递的,所以结构体其实是通过R0-R3寄存器传递的。

LDRB指令将内存中的一个字节载入,然后把它扩展到32位,同时也考虑它的符号。这和x86架构的MOVSX(参考13.1.1节)基本一样。这里它被用来传递结构体的a、c两个成员。

还有一个我们可以容易指出来的是,在函数的末尾处,这里它没有使用正常的函数尾该有的指令,而是直接跳转到了另一个函数的末尾! 的确,这是一个相当不同的函数,而且跟我们的函数没有任何关联。但是,他却有着相同的函数结尾(也许是因为他也有5个本地变量(5 x 4 = 0x14))。而且他就在我们的函数附近(看看地址就知道了)。事实上,函数结尾并不重要,只要函数好好执行就行了嘛。显然,Keil决定要重用另一个函数的一部分,原因就是为了优化代码大小。普通函数结尾需要4字节,而跳转指令只要2个字节。

18.4.3 ARM+优化XCode(LLVM)+thumb-2模式

清单18.17: 优化的Xcode (LLVM)+thumb-2模式

var_C = -0xC
    PUSH {R7,LR}
    MOV R7, SP
    SUB SP, SP, #4
    MOV R9, R1 ; b
    MOV R1, R0 ; a
    MOVW R0, #0xF10 ; "a=%d; b=%d; c=%d; d=%d
"
    SXTB R1, R1 ; prepare a
    MOVT.W R0, #0
    STR R3, [SP,#0xC+var_C] ; place d to stack for printf()
    ADD R0, PC ; format-string
    SXTB R3, R2 ; prepare c
    MOV R2, R9 ; b
    BLX _printf
    ADD SP, SP, #4
    POP {R7,PC}

SXTB(Singned Extend Byte,有符号扩展字节)和x86的MOVSX(见13.1.1节)差不多,但是它不是对内存操作的,而是对一个寄存器操作的,至于剩余的——都一样。

18.5 嵌套结构

如果一个结构体里定义了另一个结构体会怎么样?

#include <stdio.h>
struct inner_struct
{
    int a;
    int b;
};
struct outer_struct
{
    char a;
    int b;
    struct inner_struct c;
    char d;
    int e;
};
void f(struct outer_struct s)
{
    printf ("a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d", s.a, s.b, s.c.a, s.c.b, s.d, s.e);
};

在这个例子里,我们把inner_struct放到了outer_struct的abde中间。 让我们在MSVC 2010中编译:

清单18.18: MSVC 2010

_s$ = 8 ; size = 24
_f PROC
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _s$[ebp+20] ; e
    push eax
    movsx ecx, BYTE PTR _s$[ebp+16] ; d
    push ecx
    mov edx, DWORD PTR _s$[ebp+12] ; c.b
    push edx
    mov eax, DWORD PTR _s$[ebp+8] ; c.a
    push eax
    mov ecx, DWORD PTR _s$[ebp+4] ; b
    push ecx
    movsx edx, BYTE PTR _s$[ebp] ;a
    push edx
    push OFFSET $SG2466
    call _printf
    add esp, 28 ; 0000001cH
    pop ebp
    ret 0
_f ENDP

一个令我们好奇的事情是,看看这个反汇编代码,我们甚至不知道它的体内有另一个结构体!因此,我们可以说,嵌套的结构体,最终都会转化为线性的或者一维的结构。 当然,如果我们把struct inner_struct c;换成struct inner_struct *c(因此这里其实是定义个了一个指针),这个情况下状况则会大为不同。

18.6 结构体中的位

18.6.1 CPUID 的例子

C/C++中允许给结构体的每一个成员都定义一个准确的位域。如果我们想要节省空间的话,这个对我们来说将是非常有用的。比如,对BOOL来说,1位就足矣了。但是当然,如果我们想要速度的话,必然会浪费点空间。 让我们以CPUID指令为例,这个指令返回当前CPU的信息和特性。 如果EAX在指令执行之前就设置为了1,CPUID将会返回这些内容到EAX中。

MSVC 2010有CPUID的宏,但是GCC 4.4.1没有,所以,我们就手动的利用它的内联汇编器为GCC写一个吧。

#include <stdio.h>
#ifdef __GNUC__
static inline void cpuid(int code, int *a, int *b, int *c, int *d) {
    asm volatile("cpuid":"=a"(*a),"=b"(*b),"=c"(*c),"=d"(*d):"a"(code));
}
#endif
#ifdef _MSC_VER
#include <intrin.h>
#endif
struct CPUID_1_EAX
{
    unsigned int stepping:4;
    unsigned int model:4;
    unsigned int family_id:4;
    unsigned int processor_type:2;
    unsigned int reserved1:2;
    unsigned int extended_model_id:4;
    unsigned int extended_family_id:8;
    unsigned int reserved2:4;
};
int main()
{
    struct CPUID_1_EAX *tmp;
    int b[4];
    #ifdef _MSC_VER
    __cpuid(b,1);
    #endif
    #ifdef __GNUC__
    cpuid (1, &b[0], &b[1], &b[2], &b[3]);
    #endif
    tmp=(struct CPUID_1_EAX *)&b[0];
    printf ("stepping=%d", tmp->stepping);
    printf ("model=%d", tmp->model);
    printf ("family_id=%d", tmp->family_id);
    printf ("processor_type=%d", tmp->processor_type);
    printf ("extended_model_id=%d", tmp->extended_model_id);
    printf ("extended_family_id=%d", tmp->extended_family_id);
    return 0;
};

之后CPU会填充EAX,EBX,ECX,EDX,这些寄存器的值会通过b[]数组显现出来。接着我们用一个指向CPUID_1_EAX结构体的指针,把它指向b[]数组的EAX值。 换句话说,我们将把32位的INT类型的值当作一个结构体来看。 然后我们就能从结构体中读取数据。 让我们在MSVC 2008用/Ox编译一下:

清单18.19: MSVC 2008

_b$ = -16 ; size = 16
_main PROC
    sub esp, 16 ; 00000010H
    push ebx
    xor ecx, ecx
    mov eax, 1
    cpuid
    push esi
    lea esi, DWORD PTR _b$[esp+24]
    mov DWORD PTR [esi], eax
    mov DWORD PTR [esi+4], ebx
    mov DWORD PTR [esi+8], ecx
    mov DWORD PTR [esi+12], edx
    mov esi, DWORD PTR _b$[esp+24]
    mov eax, esi
    and eax, 15 ; 0000000fH
    push eax
    push OFFSET $SG15435 ; ’stepping=%d’, 0aH, 00H
    call _printf
    mov ecx, esi
    shr ecx, 4
    and ecx, 15 ; 0000000fH
    push ecx
    push OFFSET $SG15436 ; ’model=%d’, 0aH, 00H
    call _printf
    mov edx, esi
    shr edx, 8
    and edx, 15 ; 0000000fH
    push edx
    push OFFSET $SG15437 ; ’family_id=%d’, 0aH, 00H
    call _printf
    mov eax, esi
    shr eax, 12 ; 0000000cH
    and eax, 3
    push eax
    push OFFSET $SG15438 ; ’processor_type=%d’, 0aH, 00H
    call _printf
    mov ecx, esi
    shr ecx, 16 ; 00000010H
    and ecx, 15 ; 0000000fH
    push ecx
    push OFFSET $SG15439 ; ’extended_model_id=%d’, 0aH, 00H
    call _printf
    shr esi, 20 ; 00000014H
    and esi, 255 ; 000000ffH
    push esi
    push OFFSET $SG15440 ; ’extended_family_id=%d’, 0aH, 00H
    call _printf
    add esp, 48 ; 00000030H
    pop esi
    xor eax, eax
    pop ebx
    add esp, 16 ; 00000010H
    ret 0
_main ENDP

SHR指令将EAX寄存器的值右移位,移出去的值必须被忽略,例如我们会忽略右边的位。 AND指令将清除左边不需要的位,换句话说,它处理过后EAX将只留下我们需要的值。 让我们在GCC4.4.1下用-O3编译。

清单18.20: GCC 4.4.1

main proc near ; DATA XREF: _start+17
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    push esi
    mov esi, 1
    push ebx
    mov eax, esi
    sub esp, 18h
    cpuid
    mov esi, eax
    and eax, 0Fh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aSteppingD ; "stepping=%d"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 4
    and eax, 0Fh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aModelD ; "model=%d"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 8
    and eax, 0Fh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aFamily_idD ; "family_id=%d"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 0Ch
    and eax, 3
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aProcessor_type ; "processor_type=%d"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov eax, esi
    shr eax, 10h
    shr esi, 14h
    and eax, 0Fh
    and esi, 0FFh
    mov [esp+8], eax
    mov dword ptr [esp+4], offset aExtended_model ; "extended_model_id=%d"
    mov dword ptr [esp], 1
    call ___printf_chk
    mov [esp+8], esi
    mov dword ptr [esp+4], offset unk_80486D0
    mov dword ptr [esp], 1
    call ___printf_chk
    add esp, 18h
    xor eax, eax
    pop ebx
    pop esi
    mov esp, ebp
    pop ebp
    retn
main endp

几乎一样。只有一个需要注意的地方就是GCC在调用每个printf()之前会把extended_model_id和extended_family_id的计算联合到一块去,而不是把它们分开计算。

18.6.2 将浮点数当作结构体看待

我们已经在FPU(15章)中注意到了float和double两个类型都是有符号的,他们分为符号、有效数字和指数部分。但是我们能直接用上这些位嘛?让我们试一试float。

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <memory.h>
struct float_as_struct
{
    unsigned int fraction : 23; // fractional part
    unsigned int exponent : 8; // exponent + 0x3FF
    unsigned int sign : 1; // sign bit
};
float f(float _in)
{
    float f=_in;
    struct float_as_struct t;
    assert (sizeof (struct float_as_struct) == sizeof (float));
    memcpy (&t, &f, sizeof (float));
    t.sign=1; // set negative sign
    t.exponent=t.exponent+2; // multiple d by 2^n (n here is 2)
    memcpy (&f, &t, sizeof (float));
    return f;
};
int main()
{
    printf ("%f", f(1.234));
};

float_as_struct结构占用了和float一样多的内存空间,也就是4字节,或者说,32位。 现在我们给输入值设置一个负值,然后指数加2,这样我们就能把整个数按照22的值来倍乘,也就是乘以4。 让我们在MSVC2008无优化模式下编译它。

清单18.21: MSVC 2008

_t$ = -8 ; size = 4
_f$ = -4 ; size = 4
__in$ = 8 ; size = 4
?f@@YAMM@Z PROC ; f
    push ebp
    mov ebp, esp
    sub esp, 8
    fld DWORD PTR __in$[ebp]
    fstp DWORD PTR _f$[ebp]
    push 4
    lea eax, DWORD PTR _f$[ebp]
    push eax
    lea ecx, DWORD PTR _t$[ebp]
    push ecx
    call _memcpy
    add esp, 12 ; 0000000cH
    mov edx, DWORD PTR _t$[ebp]
    or edx, -2147483648 ; 80000000H - set minus sign
    mov DWORD PTR _t$[ebp], edx
    mov eax, DWORD PTR _t$[ebp]
    shr eax, 23 ; 00000017H - drop significand
    and eax, 255 ; 000000ffH - leave here only exponent
    add eax, 2 ; add 2 to it
    and eax, 255 ; 000000ffH
    shl eax, 23 ; 00000017H - shift result to place of bits 30:23
    mov ecx, DWORD PTR _t$[ebp]
    and ecx, -2139095041 ; 807fffffH - drop exponent
    or ecx, eax ; add original value without exponent with new calculated exponent
    mov DWORD PTR _t$[ebp], ecx
    push 4
    lea edx, DWORD PTR _t$[ebp]
    push edx
    lea eax, DWORD PTR _f$[ebp]
    push eax
    call _memcpy
    add esp, 12 ; 0000000cH
    fld DWORD PTR _f$[ebp]
    mov esp, ebp
    pop ebp
    ret 0
?f@@YAMM@Z ENDP ; f

有点多余。如果用/Ox编译的话,这里就没有memcpy调用了。f变量会被直接使用,但是没有优化的版本看起来会更容易理解一点。 GCC 4.4.1的-O3选项会怎么做?

清单18.22: Gcc 4.4.1

; f(float)
public _Z1ff
_Z1ff proc near
var_4 = dword ptr -4
arg_0 = dword ptr 8
    push ebp
    mov ebp, esp
    sub esp, 4
    mov eax, [ebp+arg_0]
    or eax, 80000000h ; set minus sign
    mov edx, eax
    and eax, 807FFFFFh ; leave only significand and exponent in EAX
    shr edx, 23 ; prepare exponent
    add edx, 2 ; add 2
    movzx edx, dl ; clear all bits except 7:0 in EAX
    shl edx, 23 ; shift new calculated exponent to its place
    or eax, edx ; add new exponent and original value without exponent
    mov [ebp+var_4], eax
    fld [ebp+var_4]
    leave
    retn
    _Z1ff endp
    public main
    main proc near
    push ebp
    mov ebp, esp
    and esp, 0FFFFFFF0h
    sub esp, 10h
    fld ds:dword_8048614 ; -4.936
    fstp qword ptr [esp+8]
    mov dword ptr [esp+4], offset asc_8048610 ; "%f
"
    mov dword ptr [esp], 1
    call ___printf_chk
    xor eax, eax
    leave
    retn
main endp

f()函数基本可以理解,但是有趣的是,GCC可以在编译阶段就通过我们这堆大杂烩一样的代码计算出f(1.234)的值,从而会把他当作参数直接给printf()。

联合体

19.1 伪随机数生成器的例子

如果我们需要0~1的随机浮点数,最简单的方法就是用PRNG(伪随机数发生器),比如马特赛特旋转演算法可以生成一个随机的32位的DWORD。然后我们可以把这个值转为FLOAT类型,然后除以RAND_MAX(我们的例子是0xFFFFFFFF),这样,我们得到的将是0..1区间的数。 但是如我们所知道的是,除法很慢。我们是否能摆脱它呢?就像我们用乘法做除法一样(14章)。 让我们想想浮点数由什么构成:符号位、有效数字位、指数位。我们只需要在这里面存储一些随机的位就好了。 指数不能变成0(在本例里面数字会不正常),所以我们存储0111111到指数里面,这意味着指数位将是1。然后,我们用随机位填充有效数字位,然后把符号位设置为0(正数)。生成的数字将在1-2的间隔中生成,所以我们必须从里面再减去1。 我例子里面是最简单的线性同余随机数生成器,生成32位(译注:32-bit比特位,非数字位)的数字。PRNG将会用UNIX时间戳来初始化。 然后,我们会把float类型当作联合体(union)来处理,这是一个C/C++的结构。它允许我们把一片内存里面各种不同类型的数据联合覆盖到一起用。在我们的例子里,我们可以创建一个union,然后通过float或者uint32_t来访问它。因此,这只是一个小技巧,而且是很脏的技巧。

#include <stdio.h>
#include <stdint.h>
#include <time.h>
union uint32_t_float
{
    uint32_t i;
    float f;
};
// from the Numerical Recipes book
const uint32_t RNG_a=1664525;
const uint32_t RNG_c=1013904223;
int main()
{
    uint32_t_float tmp;
    uint32_t RNG_state=time(NULL); // initial seed
    for (int i=0; i<100; i++)
    {
        RNG_state=RNG_state*RNG_a+RNG_c;
        tmp.i=RNG_state & 0x007fffff | 0x3F800000;
        float x=tmp.f-1;
        printf ("%f", x);
    };
    return 0;
};

清单19.1: MSVC 2010 (/Ox)

$SG4232 DB ’%f’, 0aH, 00H
__real@3ff0000000000000 DQ 03ff0000000000000r ; 1
tv140= -4 ; size = 4
_tmp$= -4 ; size = 4
_main PROC
    push ebp
    mov ebp, esp
    and esp, -64 ; ffffffc0H
    sub esp, 56 ; 00000038H
    push esi
    push edi
    push 0
    call __time64
    add esp, 4
    mov esi, eax
    mov edi, 100 ; 00000064H
$LN3@main:
    ; let’s generate random 32-bit number
    imul esi, 1664525 ; 0019660dH
    add esi, 1013904223 ; 3c6ef35fH
    mov eax, esi
    ; leave bits for significand only
    and eax, 8388607 ; 007fffffH
    ; set exponent to 1
    or eax, 1065353216 ; 3f800000H
    ; store this value as int
    mov DWORD PTR _tmp$[esp+64], eax
    sub esp, 8
    ; load this value as float
    fld DWORD PTR _tmp$[esp+72]
    ; subtract one from it
    fsub QWORD PTR __real@3ff0000000000000
    fstp DWORD PTR tv140[esp+72]
    fld DWORD PTR tv140[esp+72]
    fstp QWORD PTR [esp]
    push OFFSET $SG4232
    call _printf
    add esp, 12 ; 0000000cH
    dec edi
    jne SHORT $LN3@main
    pop edi
    xor eax, eax
    pop esi
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP
_TEXT ENDS
END

GCC也生成了非常相似的代码。 # 函数指针

函数指针是指向函数的指针,和其他指针一样,只是该指针指向函数代码段的开始地址。函数指针经常用作回调1。

典型的例子如下:

C标准库的 qsort()2, aexit()3;
*NIX OS的信号机制;
线程启动:CreateThread()(Win32),pthread_create()(POSIX);
其他更多的Win32函数,比如EnumChildWindows()5。
qsort()函数是C/C++标准库快速排序函数。该函数能够排序任意类型的数据。qsort()调用比较函数。

比较函数被定义为如下形式:

int (*compare)(const void *, const void *)

我们稍作修改:

/* ex3 Sorting ints with qsort */
#include <stdio.h>
#include <stdlib.h>

int comp(const void * _a, const void * _b)
{
    const int *a=(const int *)_a;
    const int *b=(const int *)_b;

    if (*a==*b)
        return 0;
    else
        if (*a < *b)
            return -1;
        else
            return 1;
}

int main(int argc, char* argv[])
{
    int numbers[10]={1892,45,200,-98,4087,5,-12345,1087,88,-100000};
    int i;
    /* Sort the array */
    qsort(numbers,10,sizeof(int),comp) ;
    for (i=0;i<9;i++)
        printf("Number = %d",numbers[ i ]) ;
    return 0;
}

20.1 MSVC

MSVC2010 /Ox选项编译:

Listing 20.1: Optimizing MSVC 2010: /Ox /GS- /MD

__a$ = 8                                            ; size = 4
__b$ = 12                                           ; size = 4
_comp   PROC
        mov     eax, DWORD PTR __a$[esp-4]
        mov     ecx, DWORD PTR __b$[esp-4]
        mov     eax, DWORD PTR [eax]
        mov     ecx, DWORD PTR [ecx]
        cmp     eax, ecx
        jne     SHORT $LN4@comp
        xor     eax, eax
        ret     0
$LN4@comp:
        xor     edx, edx
        cmp     eax, ecx
        setge   dl
        lea     eax, DWORD PTR [edx+edx-1]
        ret     0
_comp ENDP
 
_numbers$ = -40                                     ; size = 40
_argc$ = 8                                          ; size = 4
_argv$ = 12                                         ; size = 4
_main   PROC
        sub     esp, 40                             ; 00000028H
        push    esi
        push    OFFSET _comp
        push    4
        lea     eax, DWORD PTR _numbers$[esp+52]
        push    10 ; 0000000aH
        push    eax
        mov     DWORD PTR _numbers$[esp+60], 1892   ; 00000764H
        mov     DWORD PTR _numbers$[esp+64], 45     ; 0000002dH
        mov     DWORD PTR _numbers$[esp+68], 200    ; 000000c8H
        mov     DWORD PTR _numbers$[esp+72], -98    ; ffffff9eH
        mov     DWORD PTR _numbers$[esp+76], 4087   ; 00000ff7H
        mov     DWORD PTR _numbers$[esp+80], 5
        mov     DWORD PTR _numbers$[esp+84], -12345 ; ffffcfc7H
        mov     DWORD PTR _numbers$[esp+88], 1087   ; 0000043fH
        mov     DWORD PTR _numbers$[esp+92], 88     ; 00000058H
        mov     DWORD PTR _numbers$[esp+96], -100000 ; fffe7960H
        call    _qsort
        add     esp, 16 ; 00000010H
...

第四个参数传递了一个地址标签_comp,指向了comp()函数。

我们来看MSVCR80.DLL(包含C标准库函数的MSVC DLL模块)里该函数的内部调用:

Listing 20.2: MSVCR80.DLL

.text:7816CBF0 ; void __cdecl qsort(void *, unsigned int, unsigned int, int (__cdecl *)(const void *, const void *))
.text:7816CBF0          public _qsort
.text:7816CBF0 _qsort   proc near
.text:7816CBF0
.text:7816CBF0 lo       = dword ptr -104h
.text:7816CBF0 hi       = dword ptr -100h
.text:7816CBF0 var_FC   = dword ptr -0FCh
.text:7816CBF0 stkptr   = dword ptr -0F8h
.text:7816CBF0 lostk    = dword ptr -0F4h
.text:7816CBF0 histk    = dword ptr -7Ch
.text:7816CBF0 base     = dword ptr 4
.text:7816CBF0 num      = dword ptr 8
.text:7816CBF0 width    = dword ptr 0Ch
.text:7816CBF0 comp     = dword ptr 10h
.text:7816CBF0
.text:7816CBF0 sub      esp, 100h
....
.text:7816CCE0 loc_7816CCE0:                ; CODE XREF: _qsort+B1
.text:7816CCE0          shr     eax, 1
.text:7816CCE2          imul    eax, ebp
.text:7816CCE5          add     eax, ebx
.text:7816CCE7          mov     edi, eax
.text:7816CCE9          push    edi
.text:7816CCEA          push    ebx
.text:7816CCEB          call    [esp+118h+comp]
.text:7816CCF2          add     esp, 8
.text:7816CCF5          test    eax, eax
.text:7816CCF7          jle     short loc_7816CD04

第四个参数comp传递函数指针,comp()有两个参数,参数被检测后才执行。

这种使用函数指针的方式有一定的风险。第一种原因是如果你用qsort()调用了错误的函数指针,可能造成程序崩溃,并且这个错误很难被发现。

第二个原因是即使回调函数类型完全正确,使用错误的参数调用函数可能会导致更严重的问题。进程崩溃不是最大的问题,最大的问题是崩溃的原因—编译器很难发现这种潜在的问题。

20.1.1 MSVC + OllyDbg

我们在OD中加载我们的例子,并在comp()函数下断点。

我们可以看到第一次comp()调用时是如何比较的:fig.20.1.OD代码窗口显示了比较的值。我们还可以看到SP指向的RA地址在qsort()函数空间里(实际上位于MSVCR100.DLL)。

按F8直到函数返回到qsort()函数:fig20.2.这里比较函数被调用。

第二次调用comp()—当前比较的值不相同:fig203。

Figure 20.1: OllyDbg: first call of comp()

Figure 20.2: OllyDbg: the code in qsort() right a_er comp() call

Figure 20.3: OllyDbg: second call of comp()

20.1.2 MSVC + tracer

我们来看成对比较,来对10个数字进行排序:1892, 45, 200, -98, 4087, 5, -12345, 1087, 88,-100000.

我们找到comp()函数中的CMP指令地址,并在其地址0x0040100C上设置断点。

tracer.exe -l:17_1.exe bpx=17_1.exe!0x0040100C

断点中断是的寄存器地址:

PID=4336|New process 17_1.exe
(0) 17_1.exe!0x40100c
EAX=0x00000764 EBX=0x0051f7c8 ECX=0x00000005 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=IF
(0) 17_1.exe!0x40100c
EAX=0x00000005 EBX=0x0051f7c8 ECX=0xfffe7960 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=PF ZF IF
(0) 17_1.exe!0x40100c
EAX=0x00000764 EBX=0x0051f7c8 ECX=0x00000005 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=CF PF ZF IF
...

过滤EAX和ECX得到:

EAX=0x00000764 ECX=0x00000005
EAX=0x00000005 ECX=0xfffe7960
EAX=0x00000764 ECX=0x00000005
EAX=0x0000002d ECX=0x00000005
EAX=0x00000058 ECX=0x00000005
EAX=0x0000043f ECX=0x00000005
EAX=0xffffcfc7 ECX=0x00000005
EAX=0x000000c8 ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0x00000ff7 ECX=0x00000005
EAX=0x00000ff7 ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0x00000005 ECX=0xffffcfc7
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0xffffff9e ECX=0xffffcfc7
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0x000000c8 ECX=0x00000ff7
EAX=0x0000002d ECX=0x00000ff7
EAX=0x0000043f ECX=0x00000ff7
EAX=0x00000058 ECX=0x00000ff7
EAX=0x00000764 ECX=0x00000ff7
EAX=0x000000c8 ECX=0x00000764
EAX=0x0000002d ECX=0x00000764
EAX=0x0000043f ECX=0x00000764
EAX=0x00000058 ECX=0x00000764
EAX=0x000000c8 ECX=0x00000058
EAX=0x0000002d ECX=0x000000c8
EAX=0x0000043f ECX=0x000000c8
EAX=0x000000c8 ECX=0x00000058
EAX=0x0000002d ECX=0x000000c8
EAX=0x0000002d ECX=0x00000058

有34对。因此快速排序算法对10个数字排序需要34此对比操作。

20.1.3 MSVC + tracer (code coverage)

我们使用跟踪特性收集寄存器的值并在IDA中查看。

跟踪comp()函数所有指令:

tracer.exe -l:17_1.exe bpf=17_1.exe!0x00401000,trace:cc

IDA加载.idc脚本:fig20.4。

IDA给出了函数名字(PtFuncCompare)—IDA认为该函数指针被传递给qsort()。

可以看到a和b指向数组不同的位置,并且相差4-32bit的字节数。

0x401010 和 0x401012之间的指令从没有被执行:事实上comp()从来不返回0,因为没有相等的元素。

Figure 20.4: tracer and IDA. N.B.: some values are cutted at right

20.2 GCC

没有太大的不同:

Listing 20.3: GCC

lea eax, [esp+40h+var_28]
mov [esp+40h+var_40], eax
mov [esp+40h+var_28], 764h
mov [esp+40h+var_24], 2Dh
mov [esp+40h+var_20], 0C8h
mov [esp+40h+var_1C], 0FFFFFF9Eh
mov [esp+40h+var_18], 0FF7h
mov [esp+40h+var_14], 5
mov [esp+40h+var_10], 0FFFFCFC7h
mov [esp+40h+var_C], 43Fh
mov [esp+40h+var_8], 58h
mov [esp+40h+var_4], 0FFFE7960h
mov [esp+40h+var_34], offset comp
mov [esp+40h+var_38], 4
mov [esp+40h+var_3C], 0Ah
call _qsort

comp() 函数:

        public comp
comp    proc near
arg_0   = dword ptr 8
arg_4   = dword ptr 0Ch
        push ebp
        mov ebp, esp
        mov eax, [ebp+arg_4]
        mov ecx, [ebp+arg_0]
        mov edx, [eax]
        xor eax, eax
        cmp [ecx], edx
        jnz short loc_8048458
        pop ebp
        retn
loc_8048458:
        setnl al
        movzx eax, al
        lea eax, [eax+eax-1]
        pop ebp
        retn
comp    endp

qsort()的实现在libc.so里,它实际上是qsort_r()的封装。

我们通过传递函数指针然后调用快速排序:

Listing 20.4: (file libc.so.6, glibc version—2.10.1)

.text:0002DDF6 mov edx, [ebp+arg_10]
.text:0002DDF9 mov [esp+4], esi
.text:0002DDFD mov [esp], edi
.text:0002DE00 mov [esp+8], edx
.text:0002DE04 call [ebp+arg_C]
...

20.2.1 GCC + GDB (with source code)

因为我们有例子的C源代码,我们能在行数(11—第一次比较的地方)设置断点(b)。编译例子的时候使用了带有调试信息的选项(-g),当前可以查看地址及行号,也可以打印变量(p):调试信息包含寄存器和变量值信息。

我们查看堆栈(bt),看到glibc使用的中间函数msort_with_tmp()。

Listing 20.5: GDB session

dennis@ubuntuvm:~/polygon$ gcc 17_1.c -g
dennis@ubuntuvm:~/polygon$ gdb ./a.out
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/a.out...done.
(gdb) b 17_1.c:11
Breakpoint 1 at 0x804845f: file 17_1.c, line 11.
(gdb) run
Starting program: /home/dennis/polygon/./a.out
Breakpoint 1, comp (_a=0xbffff0f8, _b=_b@entry=0xbffff0fc) at 17_1.c:11
11 if (*a==*b)
(gdb) p *a
$1 = 1892
(gdb) p *b
$2 = 45
(gdb) c
Continuing.
Breakpoint 1, comp (_a=0xbffff104, _b=_b@entry=0xbffff108) at 17_1.c:11
11 if (*a==*b)
(gdb) p *a
$3 = -98
(gdb) p *b
$4 = 4087
(gdb) bt
#0 comp (_a=0xbffff0f8, _b=_b@entry=0xbffff0fc) at 17_1.c:11
#1 0xb7e42872 in msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=2)
at msort.c:65
#2 0xb7e4273e in msort_with_tmp (n=2, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#3 msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=5) at msort.c:53
#4 0xb7e4273e in msort_with_tmp (n=5, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#5 msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=10) at msort.c:53
#6 0xb7e42cef in msort_with_tmp (n=10, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#7 __GI_qsort_r (b=b@entry=0xbffff0f8, n=n@entry=10, s=s@entry=4, cmp=cmp@entry=0x804844d <comp
>,
arg=arg@entry=0x0) at msort.c:297
#8 0xb7e42dcf in __GI_qsort (b=0xbffff0f8, n=10, s=4, cmp=0x804844d <comp>) at msort.c:307
#9 0x0804850d in main (argc=1, argv=0xbffff1c4) at 17_1.c:26
(gdb)

20.2.2 GCC + GDB (no source code)

更多时候我们没有源码,我们可以反汇编comp()函数(disas),找到CMP指令地址并设置断点(b)。每次中断后,dump所有寄存器的值(info registers),堆栈信息(bt),但是没有comp()函数对应的行号信息。

Listing 20.6: GDB session

dennis@ubuntuvm:~/polygon$ gcc 17_1.c
dennis@ubuntuvm:~/polygon$ gdb ./a.out
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas comp
Dump of assembler code for function comp:
0x0804844d <+0>: push ebp
0x0804844e <+1>: mov ebp,esp
0x08048450 <+3>: sub esp,0x10
0x08048453 <+6>: mov eax,DWORD PTR [ebp+0x8]
0x08048456 <+9>: mov DWORD PTR [ebp-0x8],eax
0x08048459 <+12>: mov eax,DWORD PTR [ebp+0xc]
0x0804845c <+15>: mov DWORD PTR [ebp-0x4],eax
0x0804845f <+18>: mov eax,DWORD PTR [ebp-0x8]
0x08048462 <+21>: mov edx,DWORD PTR [eax]
0x08048464 <+23>: mov eax,DWORD PTR [ebp-0x4]
0x08048467 <+26>: mov eax,DWORD PTR [eax]
0x08048469 <+28>: cmp edx,eax
0x0804846b <+30>: jne 0x8048474 <comp+39>
0x0804846d <+32>: mov eax,0x0
0x08048472 <+37>: jmp 0x804848e <comp+65>
0x08048474 <+39>: mov eax,DWORD PTR [ebp-0x8]
0x08048477 <+42>: mov edx,DWORD PTR [eax]
0x08048479 <+44>: mov eax,DWORD PTR [ebp-0x4]
0x0804847c <+47>: mov eax,DWORD PTR [eax]
0x0804847e <+49>: cmp edx,eax
0x08048480 <+51>: jge 0x8048489 <comp+60>
0x08048482 <+53>: mov eax,0xffffffff
0x08048487 <+58>: jmp 0x804848e <comp+65>
0x08048489 <+60>: mov eax,0x1
0x0804848e <+65>: leave
0x0804848f <+66>: ret
End of assembler dump.
(gdb) b *0x08048469
Breakpoint 1 at 0x8048469
(gdb) run
Starting program: /home/dennis/polygon/./a.out
 
Breakpoint 1, 0x08048469 in comp ()
(gdb) info registers
eax 0x2d 45
ecx 0xbffff0f8 -1073745672
edx 0x764 1892
ebx 0xb7fc0000 -1208221696
esp 0xbfffeeb8 0xbfffeeb8
ebp 0xbfffeec8 0xbfffeec8
esi 0xbffff0fc -1073745668
edi 0xbffff010 -1073745904
eip 0x8048469 0x8048469 <comp+28>
eflags 0x286 [ PF SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) c
Continuing.
 
Breakpoint 1, 0x08048469 in comp ()
(gdb) info registers
eax 0xff7 4087
ecx 0xbffff104 -1073745660
edx 0xffffff9e -98
ebx 0xb7fc0000 -1208221696
esp 0xbfffee58 0xbfffee58
ebp 0xbfffee68 0xbfffee68
esi 0xbffff108 -1073745656
edi 0xbffff010 -1073745904
eip 0x8048469 0x8048469 <comp+28>
eflags 0x282 [ SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) c
Continuing.
Breakpoint 1, 0x08048469 in comp ()
(gdb) info registers
eax 0xffffff9e -98
ecx 0xbffff100 -1073745664
edx 0xc8 200
ebx 0xb7fc0000 -1208221696
esp 0xbfffeeb8 0xbfffeeb8
ebp 0xbfffeec8 0xbfffeec8
esi 0xbffff104 -1073745660
edi 0xbffff010 -1073745904
eip 0x8048469 0x8048469 <comp+28>
eflags 0x286 [ PF SF IF ]
cs 0x73 115
ss 0x7b 123
ds 0x7b 123
es 0x7b 123
fs 0x0 0
gs 0x33 51
(gdb) bt
#0 0x08048469 in comp ()
#1 0xb7e42872 in msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=2)
at msort.c:65
#2 0xb7e4273e in msort_with_tmp (n=2, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#3 msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=5) at msort.c:53
#4 0xb7e4273e in msort_with_tmp (n=5, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#5 msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=10) at msort.c:53
#6 0xb7e42cef in msort_with_tmp (n=10, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#7 __GI_qsort_r (b=b@entry=0xbffff0f8, n=n@entry=10, s=s@entry=4, cmp=cmp@entry=0x804844d <comp
>,
arg=arg@entry=0x0) at msort.c:297
#8 0xb7e42dcf in __GI_qsort (b=0xbffff0f8, n=10, s=4, cmp=0x804844d <comp>) at msort.c:307
#9 0x0804850d in main ()

在32位环境中的64位值

在32位环境中的通用寄存器是32位的,所以64位值转化为一对32位值。

21.1参数的传递,加法,减法

#include <stdint.h>
uint64_t f1 (uint64_t a, uint64_t b)
{
        return a+b;
};
void f1_test ()
{
#ifdef __GNUC__
        printf ("%lld", f1(12345678901234, 23456789012345));
#else
        printf ("%I64d", f1(12345678901234, 23456789012345));
#endif
};
uint64_t f2 (uint64_t a, uint64_t b)
{
        return a-b;
};

代码 21.1: MSVC 2012 /Ox /Ob1

_a$ = 8                                             ; size = 8
_b$ = 16                                            ; size = 8
_f1     PROC
        mov     eax, DWORD PTR _a$[esp-4]
        add     eax, DWORD PTR _b$[esp-4]
        mov     edx, DWORD PTR _a$[esp]
        adc     edx, DWORD PTR _b$[esp]
        ret     0
_f1     ENDP
 
_f1_test    PROC
        push        5461                            ; 00001555H
        push        1972608889                      ; 75939f79H
        push        2874                            ; 00000b3aH
        push        1942892530                      ; 73ce2ff2H
        call        _f1
        push        edx
        push        eax
        push        OFFSET $SG1436 ; ’%I64d’, 0aH, 00H
        call        _printf
        add         esp, 28                         ; 0000001cH
        ret     0
_f1_test    ENDP
_f2     PROC
        mov     eax, DWORD PTR _a$[esp-4]
        sub     eax, DWORD PTR _b$[esp-4]
        mov     edx, DWORD PTR _a$[esp]
        sbb     edx, DWORD PTR _b$[esp]
        ret     0
_f2     ENDP

我们可以看到在函数f1_test()中每个64位值转化为2个32位值,高位先转,然后是低位。加法和减法也是如此。

当进行加法操作时,低32位部分先做加法。如果相加过程中产生进位,则设置CF标志。下一步通过ADC指令加上高位部分,如果CF置1了就增加1。

减法操作也是如此。第一个SUB操作也会导致CF标志的改变,并在随后的SBB操作中检查:如果CF置1了,那么最终结果也会减去1。

在32位环境中,64位的值是从EDX:EAX这一对寄存器的函数中返回的。可以很容易看出f1()函数是如何转化为printf()函数的。

代码 21.2: GCC 4.8.1 -O1 -fno-inline

_f1:
        mov     eax, DWORD PTR [esp+12]
        mov     edx, DWORD PTR [esp+16]
        add     eax, DWORD PTR [esp+4]
        adc     edx, DWORD PTR [esp+8]
        ret

_f1_test:
        sub     esp, 28
        mov     DWORD PTR [esp+8], 1972608889           ; 75939f79H
        mov     DWORD PTR [esp+12], 5461                ; 00001555H
        mov     DWORD PTR [esp], 1942892530             ; 73ce2ff2H
        mov     DWORD PTR [esp+4], 2874                 ; 00000b3aH
        call    _f1
        mov     DWORD PTR [esp+4], eax
        mov     DWORD PTR [esp+8], edx
        mov     DWORD PTR [esp], OFFSET FLAT:LC0        ; "%lld12"
        call    _printf
        add     esp, 28
        ret

_f2:
        mov     eax, DWORD PTR [esp+4]
        mov     edx, DWORD PTR [esp+8]
        sub     eax, DWORD PTR [esp+12]
        sbb     edx, DWORD PTR [esp+16]
        ret

GCC代码也是如此。

21.2 乘法,除法

#include <stdint.h>
uint64_t f3 (uint64_t a, uint64_t b)
{
        return a*b;
};
uint64_t f4 (uint64_t a, uint64_t b)
{
        return a/b;
};
uint64_t f5 (uint64_t a, uint64_t b)
{
        return a % b;
};

代码 21.3: MSVC 2012 /Ox /Ob1

_a$ = 8                                     ; size = 8
_b$ = 16                                    ; size = 8
_f3     PROC
        push        DWORD PTR _b$[esp]
        push        DWORD PTR _b$[esp]
        push        DWORD PTR _a$[esp+8]
        push        DWORD PTR _a$[esp+8]
        call        __allmul ; long long multiplication
        ret         0
_f3     ENDP
_a$ = 8                                     ; size = 8
_b$ = 16                                    ; size = 8
_f4     PROC
        push        DWORD PTR _b$[esp]
        push        DWORD PTR _b$[esp]
        push        DWORD PTR _a$[esp+8]
        push        DWORD PTR _a$[esp+8]
        call        __aulldiv ; unsigned long long division
        ret         0
_f4     ENDP
_a$ = 8                                     ; size = 8
_b$ = 16                                    ; size = 8
_f5     PROC
        push        DWORD PTR _b$[esp]
        push        DWORD PTR _b$[esp]
        push        DWORD PTR _a$[esp+8]
        push        DWORD PTR _a$[esp+8]
        call        __aullrem ; unsigned long long remainder
        ret         0
_f5     ENDP

乘法和除法是更为复杂的操作,一般来说,编译器会嵌入库函数的calls来使用。

部分函数的意义:可参见附录E。

Listing 21.4: GCC 4.8.1 -O3 -fno-inline

_f3:
        push        ebx
        mov         edx, DWORD PTR [esp+8]
        mov         eax, DWORD PTR [esp+16]
        mov         ebx, DWORD PTR [esp+12]
        mov         ecx, DWORD PTR [esp+20]
        imul        ebx, eax
        imul        ecx, edx
        mul         edx
        add         ecx, ebx
        add         edx, ecx
        pop         ebx
        ret
_f4:
        sub         esp, 28
        mov         eax, DWORD PTR [esp+40]
        mov         edx, DWORD PTR [esp+44]
        mov         DWORD PTR [esp+8], eax
        mov         eax, DWORD PTR [esp+32]
        mov         DWORD PTR [esp+12], edx
        mov         edx, DWORD PTR [esp+36]
        mov         DWORD PTR [esp], eax
        mov         DWORD PTR [esp+4], edx
        call        ___udivdi3 ; unsigned division
        add         esp, 28
        ret
_f5:
        sub         esp, 28
        mov         eax, DWORD PTR [esp+40]
        mov         edx, DWORD PTR [esp+44]
        mov         DWORD PTR [esp+8], eax
        mov         eax, DWORD PTR [esp+32]
        mov         DWORD PTR [esp+12], edx
        mov         edx, DWORD PTR [esp+36]
        mov         DWORD PTR [esp], eax
        mov         DWORD PTR [esp+4], edx
        call        ___umoddi3 ; unsigned modulo
        add         esp, 28
        ret

GCC的做法几乎一样,但是乘法代码内联在函数中,可认为这样更有效。

GCC有一些不同的库函数:参见附录D

21.3 右移

#include <stdint.h>
uint64_t f6 (uint64_t a)
{
        return a>>7;
};

代码 21.5: MSVC 2012 /Ox /Ob1

_a$ = 8                                     ; size = 8
_f6     PROC
        mov     eax, DWORD PTR _a$[esp-4]
        mov     edx, DWORD PTR _a$[esp]
        shrd    eax, edx, 7
        shr     edx, 7
        ret     0
_f6     ENDP

代码 21.6: GCC 4.8.1 -O3 -fno-inline

_f6:
        mov     edx, DWORD PTR [esp+8]
        mov     eax, DWORD PTR [esp+4]
        shrd        eax, edx, 7
        shr     edx, 7
        ret

右移也是分成两步完成:先移低位,然后移高位。但是低位部分通过指令SHRD移动,它将EDX的值移动7位,并从EAX借来1位,也就是从高位部分。而高位部分通过更受欢迎的指令SHR移动:的确,高位释放出来的位置用0填充。

21.4从32位值转化为64位值

#include <stdint.h>
int64_t f7 (int64_t a, int64_t b, int32_t c)
{
        return a*b+c;
};

int64_t f7_main ()
{
        return f7(12345678901234, 23456789012345, 12345);
};

代码 21.7: MSVC 2012 /Ox /Ob1

_a$ = 8                                 ; size = 8
_b$ = 16                                ; size = 8
_c$ = 24                                ; size = 4
_f7     PROC
        push        esi
        push        DWORD PTR _b$[esp+4]
        push        DWORD PTR _b$[esp+4]
        push        DWORD PTR _a$[esp+12]
        push        DWORD PTR _a$[esp+12]
        call        __allmul ; long long multiplication
        mov         ecx, eax
        mov         eax, DWORD PTR _c$[esp]
        mov         esi, edx
        cdq                 ; input: 32-bit value in EAX; output: 64-bit value in EDX:EAX
        add         eax, ecx
        adc         edx, esi
        pop         esi
        ret         0
_f7     ENDP
 
_f7_main PROC
        push        12345               ; 00003039H
        push        5461                ; 00001555H
        push        1972608889          ; 75939f79H
        push        2874                ; 00000b3aH
        push        1942892530          ; 73ce2ff2H
        call        _f7
        add     esp, 20                 ; 00000014H
        ret     0
_f7_main ENDP

这里我们有必要将有符号的32位值从c转化为有符号的64位值。无符号值的转化简单了当:所有的高位部分全部置0。但是这样不适合有符号的数据类型:符号标志应复制到结果中的高位部分。这里用到的指令是CDQ,它从EAX中取出数值,将其变为64位并存放到EDX:EAX这一对寄存器中。换句话说,指令CDQ从EAX中获取符号(通过EAX中最重要的位),并根据它来设置EDX中所有位为0还是为1。它的操作类似于指令MOVSX(13.1.1)。

代码 21.8: GCC 4.8.1 -O3 -fno-inline

_f7:
        push        edi
        push        esi
        push        ebx
        mov         esi, DWORD PTR [esp+16]
        mov         edi, DWORD PTR [esp+24]
        mov         ebx, DWORD PTR [esp+20]
        mov         ecx, DWORD PTR [esp+28]
        mov         eax, esi
        mul         edi
        imul        ebx, edi
        imul        ecx, esi
        mov         esi, edx
        add         ecx, ebx
        mov         ebx, eax
        mov         eax, DWORD PTR [esp+32]
        add         esi, ecx
        cdq             ; input: 32-bit value in EAX; output: 64-bit value in EDX:EAX
        add         eax, ebx
        adc         edx, esi
        pop         ebx
        pop         esi
        pop         edi
        ret
_f7_main:
        sub         esp, 28
        mov         DWORD PTR [esp+16], 12345               ; 00003039H
        mov         DWORD PTR [esp+8], 1972608889           ; 75939f79H
        mov         DWORD PTR [esp+12], 5461                ; 00001555H
        mov         DWORD PTR [esp], 1942892530             ; 73ce2ff2H
        mov         DWORD PTR [esp+4], 2874                 ; 00000b3aH
        call
_f7
        add         esp, 28
        ret

GCC生成的汇编代码跟MSVC一样,但是在函数中内联乘法代码。 更多:32位值在16位环境中(30.4) # SIMD

SIMD是Single Instruction, Multiple Data的首字母。简单说就是单指令多数据流。

就像FPU,FPU看起来更像独立于x86处理器。

SIMD开始于MMX x86。8个新的64位寄存器MM0-MM7被添加。

每个MMX寄存器包含2个32-bit值/4个16-bit值/8字节。比如可以通过一次添加两个值到MMX寄存器来添加8个8-bit(字节)。

一个简单的例子就是图形编辑器,将图像表示为一个二维数组,当用户改变图像的亮度,编辑器必须添加每个像素的差值。为了简单起见,将每个像素定义为一个8位字节,就可以同时改变8个像素的亮度。

当使用MMX的时候,这些寄存器实际上位于FPU寄存器。所以可以同时使用FPU和MMX寄存器。有人可能会认为,intel基于晶体管保存,事实上,这种共生关系的原因是:老的操作系统不知道额外的CPU寄存器,上下文切换是不会保存这些寄存器,可以节省FPU寄存器。这样激活MMX的CPU+旧的操作系统+利用MMX特性的处理器=所有一起工作。

SSE-SIMD寄存器扩展至128bits,独立于FPU。

AVX-另一种256bits扩展。

实际应用还包括内存复制(memcpy)和内存比较(memcmp)等等。

一个例子是:DES加密算法需要64-bits block,56-bits key,加密块生成64位结果。DES算法可以认为是一个非常大的电子电路,带有网格和AND/OR/NOT门。

Bitslice DES2—可以同时处理块和密钥。比如说unsigned int类型变量在X86下可以容纳32位,因此,使用64+56 unsigned int类型的变量,可以同时存储32个blocks-keys对。

我写了一个爆破Oracle RDBMS密码/哈希(基于DES)的工具。稍微修改了DES算法(SSE2和AVX)现在可以同时加密128或256block-keys对。

http://conus.info/utils/ops_SIMD/

22.1 Vectorization

向量化3,例如循环用两个数组生成一个数组。循环体从输入数组中取值,处理后存储到另一个数组。重要的一点是操作了每一个元素。向量化—同时处理多个元素。

向量化并不是新的技术:本书的作者在1998年使用Cray Y-MP EL“lite”时从Cray Y-MP supercomputer line看到过。

例子:

for (i = 0; i < 1024; i++)
{
    C[i] = A[i]*B[i];
}

这段代码从A和B中取出元素,相乘,并把结果保存到C。

如果每个元素为32位int型,那么可以从A中加载4个元素到128bits XMM寄存器,B加载到另一个XMM寄存器,通过执行PMULID(Multiply Packed Signed Dword Integers and Store Low Result)和PMULHW(Multiply Packed Signed Integers and Store High Result),一次可以得到4个64位结果。

循环次数从1024变成1024/4,当然更快。

一些简单的情况下某些编译器可以自动向量化,Intel C++5.

函数如下:

int f (int sz, int *ar1, int *ar2, int *ar3)
{
        for (int i=0; i<sz; i++)
                ar3[i]=ar1[i]+ar2[i];
        return 0;
};

22.1.1 Intel C++

Intel C++ 11.1.051 win32下编译:

icl intel.cpp /QaxSSE2 /Faintel.asm /Ox

可以得到(IDA中):

; int __cdecl f(int, int *, int *, int *)
                public ?f@@YAHHPAH00@Z
?f@@YAHHPAH00@Z proc near

var_10          = dword ptr -10h
sz              = dword ptr 4
ar1             = dword ptr 8
ar2             = dword ptr 0Ch
ar3             = dword ptr 10h
                push    edi
                push    esi
                push    ebx
                push    esi
                mov     edx, [esp+10h+sz]
                test    edx, edx
                jle     loc_15B
                mov     eax, [esp+10h+ar3]
                cmp     edx, 6
                jle     loc_143
                cmp     eax, [esp+10h+ar2]
                jbe     short loc_36
                mov     esi, [esp+10h+ar2]
                sub     esi, eax
                lea     ecx, ds:0[edx*4]
                neg     esi
                cmp     ecx, esi
                jbe     short loc_55
 
loc_36:                                         ; CODE XREF: f(int,int *,int *,int *)+21
                cmp     eax, [esp+10h+ar2]
                jnb     loc_143
                mov     esi, [esp+10h+ar2]
                sub     esi, eax
                lea     ecx, ds:0[edx*4]
                cmp     esi, ecx
                jb      loc_143
loc_55: ; CODE XREF: f(int,int *,int *,int *)+34
                cmp     eax, [esp+10h+ar1]
                jbe     short loc_67
                mov     esi, [esp+10h+ar1]
                sub     esi, eax
                neg     esi
                cmp     ecx, esi
                jbe     short loc_7F
 
loc_67:                                          ; CODE XREF: f(int,int *,int *,int *)+59
                cmp     eax, [esp+10h+ar1]
                jnb     loc_143
                mov     esi, [esp+10h+ar1]
                sub     esi, eax
                cmp     esi, ecx
                jb      loc_143
 
loc_7F:                                          ; CODE XREF: f(int,int *,int *,int *)+65
                mov     edi, eax ; edi = ar1
                and     edi, 0Fh ; is ar1 16-byte aligned?
                jz      short loc_9A ; yes
                test    edi, 3
                jnz     loc_162
                neg     edi
                add     edi, 10h
                shr     edi, 2
 
loc_9A:                                          ; CODE XREF: f(int,int *,int *,int *)+84
                lea     ecx, [edi+4]
                cmp     edx, ecx
                jl      loc_162
                mov     ecx, edx
                sub     ecx, edi
                and     ecx, 3
                neg     ecx
                add     ecx, edx
                test    edi, edi
                jbe     short loc_D6
                mov     ebx, [esp+10h+ar2]
                mov     [esp+10h+var_10], ecx
                mov     ecx, [esp+10h+ar1]
                xor     esi, esi
 
loc_C1:                                          ; CODE XREF: f(int,int *,int *,int *)+CD
                mov     edx, [ecx+esi*4]
                add     edx, [ebx+esi*4]
                mov     [eax+esi*4], edx
                inc     esi
                cmp     esi, edi
                jb      short loc_C1
                mov     ecx, [esp+10h+var_10]
                mov     edx, [esp+10h+sz]
 
loc_D6:                                           ; CODE XREF: f(int,int *,int *,int *)+B2
                mov     esi, [esp+10h+ar2]
                lea     esi, [esi+edi*4] ; is ar2+i*4 16-byte aligned?
                test    esi, 0Fh
                jz      short loc_109 ; yes!
                mov     ebx, [esp+10h+ar1]
                mov     esi, [esp+10h+ar2]
 
loc_ED:                                           ; CODE XREF: f(int,int *,int *,int *)+105
                movdqu  xmm1, xmmword ptr [ebx+edi*4]
                movdqu  xmm0, xmmword ptr [esi+edi*4] ; ar2+i*4 is not 16-byte aligned, so load
                it to   xmm0
                paddd   xmm1, xmm0
                movdqa  xmmword ptr [eax+edi*4], xmm1
                add     edi, 4
                cmp     edi, ecx
                jb      short loc_ED
                jmp     short loc_127
; ---------------------------------------------------------------------------
loc_109:                                          ; CODE XREF: f(int,int *,int *,int *)+E3
                mov     ebx, [esp+10h+ar1]
                mov     esi, [esp+10h+ar2]
loc_111:                                          ; CODE XREF: f(int,int *,int *,int *)+125
                movdqu  xmm0, xmmword ptr [ebx+edi*4]
                paddd   xmm0, xmmword ptr [esi+edi*4]
                movdqa  xmmword ptr [eax+edi*4], xmm0
                add     edi, 4
                cmp     edi, ecx
                jb      short loc_111
 
loc_127:                                          ; CODE XREF: f(int,int *,int *,int *)+107
                                                  ; f(int,int *,int *,int *)+164
                cmp     ecx, edx
                jnb     short loc_15B
                mov     esi, [esp+10h+ar1]
                mov     edi, [esp+10h+ar2]
 
loc_133: ; CODE XREF: f(int,int *,int *,int *)+13F
                mov     ebx, [esi+ecx*4]
                add     ebx, [edi+ecx*4]
                mov     [eax+ecx*4], ebx
                inc     ecx
                cmp     ecx, edx
                jb      short loc_133
                jmp     short loc_15B
; ---------------------------------------------------------------------------
loc_143:                                          ; CODE XREF: f(int,int *,int *,int *)+17
                                                  ; f(int,int *,int *,int *)+3A ...
                mov     esi, [esp+10h+ar1]
                mov     edi, [esp+10h+ar2]
                xor     ecx, ecx
 
loc_14D:                                          ; CODE XREF: f(int,int *,int *,int *)+159
                mov     ebx, [esi+ecx*4]
                add     ebx, [edi+ecx*4]
                mov     [eax+ecx*4], ebx
                inc     ecx
                cmp     ecx, edx
                jb      short loc_14D
 
loc_15B:                                          ; CODE XREF: f(int,int *,int *,int *)+A
                                                  ; f(int,int *,int *,int *)+129 ...
                xor     eax, eax
                pop     ecx
                pop     ebx
                pop     esi
                pop     edi
                retn
; ---------------------------------------------------------------------------
loc_162:                                           ; CODE XREF: f(int,int *,int *,int *)+8C
                                                   ; f(int,int *,int *,int *)+9F
                xor     ecx, ecx
                jmp     short loc_127
?f@@YAHHPAH00@Z endp

SSE2相关指令是:

MOVDQU (Move Unaligned Double Quadword)—仅仅从内存加载16个字节到XMM寄存器。
PADDD (Add Packed Integers)—把源存储器与目的寄存器按双字对齐无符号整数普通相加,结果送入目的寄存器,内存变量必须对齐内存16字节.
MOVDQA (Move Aligned Double Quadword)—把源存储器内容值送入目的寄存器,当有m128时,必须对齐内存16字节.

如果工作元素超过4对,并且指针ar3按照16字节对齐,SSE2指令将被执行: 如果ar2按照16字节对齐,则代码如下:

movdqu  xmm0, xmmword ptr [ebx+edi*4] ; ar1+i*4
paddd   xmm0, xmmword ptr [esi+edi*4] ; ar2+i*4
movdqa  xmmword ptr [eax+edi*4], xmm0 ; ar3+i*4

否则,ar2处的值将用MOVDQU加载到XMM0,它不需要对齐指针,代码如下:

movdqu  xmm1, xmmword ptr [ebx+edi*4] ; ar1+i*4
movdqu  xmm0, xmmword ptr [esi+edi*4] ; ar2+i*4 is not 16-byte aligned, so load it to xmm0
paddd   xmm1, xmm0
movdqa  xmmword ptr [eax+edi*4], xmm1 ; ar3+i*4

其他情况,将没有SSE2代码被执行。

22.1.2 GCC

gcc用-O3 选项同时打开SSE2支持: -msse2.

可以得到(GCC 4.4.1):

; f(int, int *, int *, int *)
                public _Z1fiPiS_S_
_Z1fiPiS_S_     proc near
 
var_18          = dword ptr -18h
var_14          = dword ptr -14h
var_10          = dword ptr -10h
arg_0           = dword ptr 8
arg_4           = dword ptr 0Ch
arg_8           = dword ptr 10h
arg_C           = dword ptr 14h
                push    ebp
                mov     ebp, esp
                push    edi
                push    esi
                push    ebx
                sub     esp, 0Ch
                mov     ecx, [ebp+arg_0]
                mov     esi, [ebp+arg_4]
                mov     edi, [ebp+arg_8]
                mov     ebx, [ebp+arg_C]
                test    ecx, ecx
                jle     short loc_80484D8
                cmp     ecx, 6
                lea     eax, [ebx+10h]
                ja      short loc_80484E8
 
loc_80484C1:                    ; CODE XREF: f(int,int *,int *,int *)+4B
                                ; f(int,int *,int *,int *)+61 ...
                xor     eax, eax
                nop
                lea     esi, [esi+0]
loc_80484C8:                    ; CODE XREF: f(int,int *,int *,int *)+36
                mov     edx, [edi+eax*4]
                add     edx, [esi+eax*4]
                mov     [ebx+eax*4], edx
                add     eax, 1
                cmp     eax, ecx
                jnz     short loc_80484C8
 
loc_80484D8:                    ; CODE XREF: f(int,int *,int *,int *)+17
                                ; f(int,int *,int *,int *)+A5
                add     esp, 0Ch
                xor     eax, eax
                pop     ebx
                pop     esi
                pop     edi
                pop     ebp
                retn
; ---------------------------------------------------------------------------
                align 8
loc_80484E8:                    ; CODE XREF: f(int,int *,int *,int *)+1F
                test    bl, 0Fh
                jnz     short loc_80484C1
                lea     edx, [esi+10h]
                cmp     ebx, edx
                jbe     loc_8048578
 
loc_80484F8:                    ; CODE XREF: f(int,int *,int *,int *)+E0
                lea     edx, [edi+10h]
                cmp     ebx, edx
                ja      short loc_8048503
                cmp     edi, eax
                jbe     short loc_80484C1
 
loc_8048503:                    ; CODE XREF: f(int,int *,int *,int *)+5D
                mov     eax, ecx
                shr     eax, 2
                mov     [ebp+var_14], eax
                shl     eax, 2
                test    eax, eax
                mov     [ebp+var_10], eax
                jz      short loc_8048547
                mov     [ebp+var_18], ecx
                mov     ecx, [ebp+var_14]
                xor     eax, eax
                xor     edx, edx
                nop
 
loc_8048520:                    ; CODE XREF: f(int,int *,int *,int *)+9B
                movdqu  xmm1, xmmword ptr [edi+eax]
                movdqu  xmm0, xmmword ptr [esi+eax]
                add     edx, 1
                paddd   xmm0, xmm1
                movdqa  xmmword ptr [ebx+eax], xmm0
                add     eax, 10h
                cmp     edx, ecx
                jb      short loc_8048520
                mov     ecx, [ebp+var_18]
                mov     eax, [ebp+var_10]
                cmp     ecx, eax
                jz      short loc_80484D8
 
loc_8048547:                    ; CODE XREF: f(int,int *,int *,int *)+73
                lea     edx, ds:0[eax*4]
                add     esi, edx
                add     edi, edx
                add     ebx, edx
                lea     esi, [esi+0]
 
loc_8048558:                    ; CODE XREF: f(int,int *,int *,int *)+CC
                mov     edx, [edi]
                add     eax, 1
                add     edi, 4
                add     edx, [esi]
                add     esi, 4
                mov     [ebx], edx
                add     ebx, 4
                cmp     ecx, eax
                jg      short loc_8048558
                add     esp, 0Ch
                xor     eax, eax
                pop     ebx
                pop     esi
                pop     edi
                pop     ebp
                retn
; ---------------------------------------------------------------------------
loc_8048578:                    ; CODE XREF: f(int,int *,int *,int *)+52
                cmp     eax, esi
                jnb     loc_80484C1
                jmp     loc_80484F8
_Z1fiPiS_S_     endp

几乎一样,但没有Intel的细致。

22.2 SIMD strlen() implementation

SIMD指令可能通过特殊的宏8插入到C/C++代码中。MSVC中他们被保存在intrin.h中。

Strlen()函数9的实现使用了SIMD指令,比常规的实现快了2-2.5倍。该函数将16个字符加载到一个XMM寄存器并检查是否为零

size_t strlen_sse2(const char *str)
{
        register size_t len = 0;
        const char *s=str;
        bool str_is_aligned=(((unsigned int)str)&0xFFFFFFF0) == (unsigned int)str;

        if (str_is_aligned==false)
                return strlen (str);

        __m128i xmm0 = _mm_setzero_si128();
        __m128i xmm1;
        int mask = 0;

        for (;;)
        {
                xmm1 = _mm_load_si128((__m128i *)s);
                xmm1 = _mm_cmpeq_epi8(xmm1, xmm0);
                if ((mask = _mm_movemask_epi8(xmm1)) != 0)
                {
                        unsigned long pos;
                        _BitScanForward(&pos, mask);
                        len += (size_t)pos;
                        break;
                }
                s += sizeof(__m128i);
                len += sizeof(__m128i);
        };

        return len;
}

(这里的例子基于源代码).

MSVC 2010 /Ox 编译选项:

_pos$75552 = -4                 ; size = 4
_str$ = 8                       ; size = 4
?strlen_sse2@@YAIPBD@Z PROC     ; strlen_sse2
 
    push    ebp
    mov     ebp, esp
    and     esp, -16 ; fffffff0H
    mov     eax, DWORD PTR _str$[ebp]
    sub     esp, 12 ; 0000000cH
    push    esi
    mov     esi, eax
    and     esi, -16 ; fffffff0H
    xor     edx, edx
    mov     ecx, eax
    cmp     esi, eax
    je      SHORT $LN4@strlen_sse
    lea     edx, DWORD PTR [eax+1]
    npad    3
$LL11@strlen_sse:
    mov     cl, BYTE PTR [eax]
    inc     eax
    test    cl, cl
    jne     SHORT $LL11@strlen_sse
    sub     eax, edx
    pop     esi
    mov     esp, ebp
    pop     ebp
    ret     0
$LN4@strlen_sse:
    movdqa  xmm1, XMMWORD PTR [eax]
    pxor    xmm0, xmm0
    pcmpeqb         xmm1, xmm0
    pmovmskb        eax, xmm1
    test    eax, eax
    jne     SHORT $LN9@strlen_sse
$LL3@strlen_sse:
    movdqa  xmm1, XMMWORD PTR [ecx+16]
    add     ecx, 16 ; 00000010H
    pcmpeqb         xmm1, xmm0
    add     edx, 16 ; 00000010H
    pmovmskb        eax, xmm1
    test    eax, eax
    je      SHORT $LL3@strlen_sse
$LN9@strlen_sse:
    bsf     eax, eax
    mov     ecx, eax
    mov     DWORD PTR _pos$75552[esp+16], eax
    lea     eax, DWORD PTR [ecx+edx]
    pop     esi
    mov     esp, ebp
    pop     ebp
    ret     0
?strlen_sse2@@YAIPBD@Z ENDP ; strlen_sse2

首先,检查str指针,如果不是按照16字节对齐则调用常规实现。

然后使用movdqa指令加载16个字节到xmm1.这里不使用movdqu的原因是如果指针不一致则从内存中加载的数据可能会不一致。

是的,它可能会以这种方式做,如果指针对齐,使用MOVDQA加载数据,否则使用比较慢的MOVDQU。

但是我们应该注意到这样的警告:

在windowsNT操作系统但不限于该操作系统,内存页按4kb对齐。每个win32进程独占4GB虚拟内存。事实上,只有部分地址空间与真实物理内存对应,如果进程访问的内存没有对应物理内存,将触发异常。这是虚拟内存的工作方式10.

一个函数一次加载16个字节,可能会跨内存分块访问。我们考虑这样一种情况,操作系统在x008c0000分配8192(0x2000)字节,因此块字节从地质0x008c0000到0x008c1fff。 内存块之后从0x008c2000什么都没有,操作系统没有分配任何内存。访问该地址将触发异常。

假如内存块包含的最后5个字符如下:

0x008c1ff8      ’h’
0x008c1ff9      ’e’
0x008c1ffa      ’l’
0x008c1ffb      ’l’
0x008c1ffc      ’o’
0x008c1ffd      ’x00’
0x008c1ffe      random noise
0x008c1fff      random noise

正常情况下,strlen()只会读取到”hello”。

如果我们使用MOVDQU读取16个字节,将会触发异常,应该避免这种情况。

因为我们要确保16字节对齐,保证我们不会读取未分配的内存。

让我们回到函数:

_mm_setzero_si128()—宏pxor xmm0, xmm0—清空XMM0寄存器。
_mm_load_si128()—宏 MOVDQA, 从内存加载16个字节到XMM寄存器。
_mm_cmpeq_epi8()—宏PCMPEQB,比较XMM寄存器的字节位,如果相等则为0xff否则为0。

比如:

XMM1: 11223344556677880000000000000000
XMM0: 11ab3444007877881111111111111111

执行pcmpeqb xmm1, xmm0之后,XMM1寄存器的值为:

XMM1: ff0000ff0000ffff0000000000000000

在本例中该指令比较每一个16字节块与16字节0字节块对比,XMM0通过pxor xmm0 xmm0置零。

接下来宏_mm_movemask_epi8() —这是PMOVMSKB指令。

pmovmskb eax, xmm1

pmovmskb创建源操作数每一个字节的自高位掩码,并保存结果到目的操作数的低byte。源操作数必须为MMX寄存器,目的操作数必须为32位通用寄存器。

比如:

XMM1: 0000ff00000000000000ff0000000000

对应的EAX:

EAX=0010000000100000b

之后bsf eax,eax被执行,eax值为5,意味着第一个是1的位置是5(从0开始)。

MSVC关于这个指令的宏是:_BitScanForward.

至此,找到结尾0的位置,然后程序返回长度计数。

整个过程大致就是这样。

顺便提一下,MSVC为了优化,使用了两个并排的循环。

SSE 4.2(英特尔core i7)提供了更多的指令,这些可能更容易字符串操作。

http://www.strchr.com/strcmp_and_strlen_using_sse_4.2 # 64位化

23.1 x86-64

对x86架构来说这是一个64位的扩展。 从反编译工程师的角度来看,最重要的区别是: 几乎所有的寄存器(除了FPU和SIMD)都扩展到了64位,而且都有一个r-前缀,而且还额外添加了8个寄存器。 现在所有的通用寄存器是:RAX、RBX、RCX、RDX、RBP、RSP、RSI、RDI、R8、R9、R10、R11、R12、R13、R14、R15。 当然,还是可以像以前一样加载旧的寄存器的。比如,使用EAX就可以访问RAX的低32位部分。 新的R8-R15寄存器也有对应的低位:R8D-R15D(低32位)、R8W-R15W(低16位)、R8B-R15B(低8位)。 SIMD寄存器的数量从8个扩展到了16个:XMM0-XMM15。

在Win64下,函数调用转换有一些轻微的变化。例如fastcall(见47.3节)。最开始的4个参数将存储在RCX、RDX、R8和R9寄存器里,其他的保存在栈上。调用者函数必须分配32个字节,因此被调用者可以保存前4个参数,然后再去按照他自己的需要去利用这些寄存器。一些较短的函数可以直接从寄存器里使用参数,但是大点的函数就需要把参数保存到栈上了。 系统V AMD64 ABI(LINUX, *BSD, MAC OS X)也改变了fastcall的方式。它为前6个参数使用了6个寄存器RDI、RSI、RDX、RCX、R8、R9。剩余的参数将传入栈中。 请看调用转换(47)一节。

为了保证兼容性,C int类型依然是32位。 •现在所有的指针都是64位的了。 当然这个有时候很麻烦:因为现在我们需要2倍的空间来存储指针,包括缓存,而不管事实上64位CPU只会使用48位的扩展内存这个情况。

由于现在寄存器数量翻倍了,编译器也将有更多的空间来处理寄存器分配的策略。对我们来说,也就是现在提交的代码将会有更少的本地变量。 例如,DES加密算法中计算第一个S-Box时,使用位切割DES方法(见22章)他将每次处理32/64/128/256个变量(依据DES_type类型(uint32、uint64、SSE2或者AVX))。

/*
* Generated S-box files.
*
* This software may be modified, redistributed, and used for any purpose,
* so long as its origin is acknowledged.
*
* Produced by Matthew Kwan - March 1998
*/
#ifdef _WIN64
#define DES_type unsigned __int64
#else
#define DES_type unsigned int
#endif
void
s1 (
    DES_type a1,
    DES_type a2,
    DES_type a3,
    DES_type a4,
    DES_type a5,
    DES_type a6,
    DES_type *out1,
    DES_type *out2,
    DES_type *out3,
    DES_type *out4
) {
    DES_type x1, x2, x3, x4, x5, x6, x7, x8;
    DES_type x9, x10, x11, x12, x13, x14, x15, x16;
    DES_type x17, x18, x19, x20, x21, x22, x23, x24;
    DES_type x25, x26, x27, x28, x29, x30, x31, x32;
    DES_type x33, x34, x35, x36, x37, x38, x39, x40;
    DES_type x41, x42, x43, x44, x45, x46, x47, x48;
    DES_type x49, x50, x51, x52, x53, x54, x55, x56;
    x1 = a3 & ~a5;
    x2 = x1 ^ a4;
    x3 = a3 & ~a4;
    x4 = x3 | a5;
    x5 = a6 & x4;
    x6 = x2 ^ x5;
    x7 = a4 & ~a5;
    x8 = a3 ^ a4;
    x9 = a6 & ~x8;
    x10 = x7 ^ x9;
    x11 = a2 | x10;
    x12 = x6 ^ x11;
    x13 = a5 ^ x5;
    x14 = x13 & x8;
    x15 = a5 & ~a4;
    x16 = x3 ^ x14;
    x17 = a6 | x16;
    x18 = x15 ^ x17;
    x19 = a2 | x18;
    x20 = x14 ^ x19;
    x21 = a1 & x20;
    x22 = x12 ^ ~x21;
    *out2 ^= x22;
    x23 = x1 | x5;
    x24 = x23 ^ x8;
    x25 = x18 & ~x2;
    x26 = a2 & ~x25;
    x27 = x24 ^ x26;
    x28 = x6 | x7;
    x29 = x28 ^ x25;
    x30 = x9 ^ x24;
    x31 = x18 & ~x30;
    x32 = a2 & x31;
    x33 = x29 ^ x32;
    x34 = a1 & x33;
    x35 = x27 ^ x34;
    *out4 ^= x35;
    x36 = a3 & x28;
    x37 = x18 & ~x36;
    x38 = a2 | x3;
    x39 = x37 ^ x38;
    x40 = a3 | x31;
    x41 = x24 & ~x37;
    x42 = x41 | x3;
    x43 = x42 & ~a2;
    x44 = x40 ^ x43;
    x45 = a1 & ~x44;
    x46 = x39 ^ ~x45;
    *out1 ^= x46;
    x47 = x33 & ~x9;
    x48 = x47 ^ x39;
    x49 = x4 ^ x36;
    x50 = x49 & ~x5;
    x51 = x42 | x18;
    x52 = x51 ^ a5;
    x53 = a2 & ~x52;
    x54 = x50 ^ x53;
    x55 = a1 | x54;
    x56 = x48 ^ ~x55;
    *out3 ^= x56;
}

这儿也有许多本地变量。当然,并不是所有的这些都存在本地栈上。让我们用MSVC2008的/Ox选项来编译一下:

清单23.1 使用MSVC 2008编译

PUBLIC _s1
; Function compile flags: /Ogtpy
_TEXT SEGMENT
_x6$ = -20 ; size = 4
_x3$ = -16 ; size = 4
_x1$ = -12 ; size = 4
_x8$ = -8 ; size = 4
_x4$ = -4 ; size = 4
_a1$ = 8 ; size = 4
_a2$ = 12 ; size = 4
_a3$ = 16 ; size = 4
_x33$ = 20 ; size = 4
_x7$ = 20 ; size = 4
_a4$ = 20 ; size = 4
_a5$ = 24 ; size = 4
tv326 = 28 ; size = 4
_x36$ = 28 ; size = 4
_x28$ = 28 ; size = 4
_a6$ = 28 ; size = 4
_out1$ = 32 ; size = 4
_x24$ = 36 ; size = 4
_out2$ = 36 ; size = 4
_out3$ = 40 ; size = 4
_out4$ = 44 ; size = 4
_s1 PROC
    sub esp, 20 ; 00000014H
    mov edx, DWORD PTR _a5$[esp+16]
    push ebx
    mov ebx, DWORD PTR _a4$[esp+20]
    push ebp
    push esi
    mov esi, DWORD PTR _a3$[esp+28]
    push edi
    mov edi, ebx
    not edi
    mov ebp, edi
    and edi, DWORD PTR _a5$[esp+32]
    mov ecx, edx
    not ecx
    and ebp, esi
    mov eax, ecx
    and eax, esi
    and ecx, ebx
    mov DWORD PTR _x1$[esp+36], eax
    xor eax, ebx
    mov esi, ebp
    or esi, edx
    mov DWORD PTR _x4$[esp+36], esi
    and esi, DWORD PTR _a6$[esp+32]
    mov DWORD PTR _x7$[esp+32], ecx
    mov edx, esi
    xor edx, eax
    mov DWORD PTR _x6$[esp+36], edx
    mov edx, DWORD PTR _a3$[esp+32]
    xor edx, ebx
    mov ebx, esi
    xor ebx, DWORD PTR _a5$[esp+32]
    mov DWORD PTR _x8$[esp+36], edx
    and ebx, edx
    mov ecx, edx
    mov edx, ebx
    xor edx, ebp
    or edx, DWORD PTR _a6$[esp+32]
    not ecx
    and ecx, DWORD PTR _a6$[esp+32]
    xor edx, edi
    mov edi, edx
    or edi, DWORD PTR _a2$[esp+32]
    mov DWORD PTR _x3$[esp+36], ebp
    mov ebp, DWORD PTR _a2$[esp+32]
    xor edi, ebx
    and edi, DWORD PTR _a1$[esp+32]
    mov ebx, ecx
    xor ebx, DWORD PTR _x7$[esp+32]
    not edi
    or ebx, ebp
    xor edi, ebx
    mov ebx, edi
    mov edi, DWORD PTR _out2$[esp+32]
    xor ebx, DWORD PTR [edi]
    not eax
    xor ebx, DWORD PTR _x6$[esp+36]
    and eax, edx
    mov DWORD PTR [edi], ebx
    mov ebx, DWORD PTR _x7$[esp+32]
    or ebx, DWORD PTR _x6$[esp+36]
    mov edi, esi
    or edi, DWORD PTR _x1$[esp+36]
    mov DWORD PTR _x28$[esp+32], ebx
    xor edi, DWORD PTR _x8$[esp+36]
    mov DWORD PTR _x24$[esp+32], edi
    xor edi, ecx
    not edi
    and edi, edx
    mov ebx, edi
    and ebx, ebp
    xor ebx, DWORD PTR _x28$[esp+32]
    xor ebx, eax
    not eax
    mov DWORD PTR _x33$[esp+32], ebx
    and ebx, DWORD PTR _a1$[esp+32]
    and eax, ebp
    xor eax, ebx
    mov ebx, DWORD PTR _out4$[esp+32]
    xor eax, DWORD PTR [ebx]
    xor eax, DWORD PTR _x24$[esp+32]
    mov DWORD PTR [ebx], eax
    mov eax, DWORD PTR _x28$[esp+32]
    and eax, DWORD PTR _a3$[esp+32]
    mov ebx, DWORD PTR _x3$[esp+36]
    or edi, DWORD PTR _a3$[esp+32]
    mov DWORD PTR _x36$[esp+32], eax
    not eax
    and eax, edx
    or ebx, ebp
    xor ebx, eax
    not eax
    and eax, DWORD PTR _x24$[esp+32]
    not ebp
    or eax, DWORD PTR _x3$[esp+36]
    not esi
    and ebp, eax
    or eax, edx
    xor eax, DWORD PTR _a5$[esp+32]
    mov edx, DWORD PTR _x36$[esp+32]
    xor edx, DWORD PTR _x4$[esp+36]
    xor ebp, edi
    mov edi, DWORD PTR _out1$[esp+32]
    not eax
    and eax, DWORD PTR _a2$[esp+32]
    not ebp
    and ebp, DWORD PTR _a1$[esp+32]
    and edx, esi
    xor eax, edx
    or eax, DWORD PTR _a1$[esp+32]
    not ebp
    xor ebp, DWORD PTR [edi]
    not ecx
    and ecx, DWORD PTR _x33$[esp+32]
    xor ebp, ebx
    not eax
    mov DWORD PTR [edi], ebp
    xor eax, ecx
    mov ecx, DWORD PTR _out3$[esp+32]
    xor eax, DWORD PTR [ecx]
    pop edi
    pop esi
    xor eax, ebx
    pop ebp
    mov DWORD PTR [ecx], eax
    pop ebx
    add esp, 20 ; 00000014H
    ret 0
_s1 ENDP

编译器在本地栈上分配了5个变量。 现在再让我们在MSVC 2008的64位环境中试一试:

清单23.2 使用MSVC 2008编译

a1$ = 56
a2$ = 64
a3$ = 72
a4$ = 80
x36$1$ = 88
a5$ = 88
a6$ = 96
out1$ = 104
out2$ = 112
out3$ = 120
out4$ = 128
s1 PROC
    $LN3:
    mov QWORD PTR [rsp+24], rbx
    mov QWORD PTR [rsp+32], rbp
    mov QWORD PTR [rsp+16], rdx
    mov QWORD PTR [rsp+8], rcx
    push rsi
    push rdi
    push r12
    push r13
    push r14
    push r15
    mov r15, QWORD PTR a5$[rsp]
    mov rcx, QWORD PTR a6$[rsp]
    mov rbp, r8
    mov r10, r9
    mov rax, r15
    mov rdx, rbp
    not rax
    xor rdx, r9
    not r10
    mov r11, rax
    and rax, r9
    mov rsi, r10
    mov QWORD PTR x36$1$[rsp], rax
    and r11, r8
    and rsi, r8
    and r10, r15
    mov r13, rdx
    mov rbx, r11
    xor rbx, r9
    mov r9, QWORD PTR a2$[rsp]
    mov r12, rsi
    or r12, r15
    not r13
    and r13, rcx
    mov r14, r12
    and r14, rcx
    mov rax, r14
    mov r8, r14
    xor r8, rbx
    xor rax, r15
    not rbx
    and rax, rdx
    mov rdi, rax
    xor rdi, rsi
    or rdi, rcx
    xor rdi, r10
    and rbx, rdi
    mov rcx, rdi
    or rcx, r9
    xor rcx, rax
    mov rax, r13
    xor rax, QWORD PTR x36$1$[rsp]
    and rcx, QWORD PTR a1$[rsp]
    or rax, r9
    not rcx
    xor rcx, rax
    mov rax, QWORD PTR out2$[rsp]
    xor rcx, QWORD PTR [rax]
    xor rcx, r8
    mov QWORD PTR [rax], rcx
    mov rax, QWORD PTR x36$1$[rsp]
    mov rcx, r14
    or rax, r8
    or rcx, r11
    mov r11, r9
    xor rcx, rdx
    mov QWORD PTR x36$1$[rsp], rax
    mov r8, rsi
    mov rdx, rcx
    xor rdx, r13
    not rdx
    and rdx, rdi
    mov r10, rdx
    and r10, r9
    xor r10, rax
    xor r10, rbx
    not rbx
    and rbx, r9
    mov rax, r10
    and rax, QWORD PTR a1$[rsp]
    xor rbx, rax
    mov rax, QWORD PTR out4$[rsp]
    xor rbx, QWORD PTR [rax]
    xor rbx, rcx
    mov QWORD PTR [rax], rbx
    mov rbx, QWORD PTR x36$1$[rsp]
    and rbx, rbp
    mov r9, rbx
    not r9
    and r9, rdi
    or r8, r11
    mov rax, QWORD PTR out1$[rsp]
    xor r8, r9
    not r9
    and r9, rcx
    or rdx, rbp
    mov rbp, QWORD PTR [rsp+80]
    or r9, rsi
    xor rbx, r12
    mov rcx, r11
    not rcx
    not r14
    not r13
    and rcx, r9
    or r9, rdi
    and rbx, r14
    xor r9, r15
    xor rcx, rdx
    mov rdx, QWORD PTR a1$[rsp]
    not r9
    not rcx
    and r13, r10
    and r9, r11
    and rcx, rdx
    xor r9, rbx
    mov rbx, QWORD PTR [rsp+72]
    not rcx
    xor rcx, QWORD PTR [rax]
    or r9, rdx
    not r9
    xor rcx, r8
    mov QWORD PTR [rax], rcx
    mov rax, QWORD PTR out3$[rsp]
    xor r9, r13
    xor r9, QWORD PTR [rax]
    xor r9, r8
    mov QWORD PTR [rax], r9
    pop r15
    pop r14
    pop r13
    pop r12
    pop rdi
    pop rsi
    ret 0
s1 ENDP

编译器在栈上并没有分配任何内存空间,x36是a5的同义词。 顺带一提,我们可以在这儿看到的是,函数在调用者空间中保存了RCX和RDX,但是R8和R9虽然在一开始就使用了,但是却并没有保存。 还有,还有拥有更多GPR的CPU,比如Itanium(有128个寄存器)。

23.2 ARM

在ARM中,64位指令在ARMv8中才开始出现。

23.3 浮点数字

见24章以了解更多的x86-64处理器中是如何处理浮点数的。 # 使用x64下的SIMD来处理浮点数

当然,在增加了x64扩展这个特性之后,FPU在x86兼容处理器中还是存在的。但是同事,SIMD扩展(SSE, SSE2等)已经有了,他们也可以处理浮点数。数字格式依然相同(使用IEEE754标准)。

所以,x86-64编译器通常都使用SIMD指令。可以说这是一个好消息,因为这让我们可以更容易的使用他们。 24.1 简单的例子

double f (double a, double b)
{
    return a/3.14 + b*4.1;
};

清单24.1: MSFC 2012 x64 /Ox

__real@4010666666666666 DQ 04010666666666666r ; 4.1
__real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
a$ = 8
b$ = 16
f PROC
    divsd xmm0, QWORD PTR __real@40091eb851eb851f
    mulsd xmm1, QWORD PTR __real@4010666666666666
    addsd xmm0, xmm1
    ret 0
f ENDP

输入的浮点数被传入了XMM0-XMM3寄存器,其他的通过栈来传递。 a被传入了XMM0,b则是通过XMM1。 XMM寄存器是128位的(可以参考SIMD22一节),但是我们的类型是double型的,也就意味着只有一半的寄存器会被使用。

DIVSD是一个SSE指令,意思是“Divide Scalar Double-Precision Floating-Point Values”(除以标量双精度浮点数值),它只是把一个double除以另一个double,然后把结果存在操作符的低一半位中。 常量会被编译器以IEEE754格式提前编码。 MULSD和ADDSD也是类似的,只不过一个是乘法,一个是加法。 函数处理double的结果将保存在XMM0寄存器中。

这是无优化的MSVC编译器的结果:

清单24.2: MSVC 2012 x64

__real@4010666666666666 DQ 04010666666666666r ; 4.1
__real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
a$ = 8
b$ = 16
f PROC
    movsdx QWORD PTR [rsp+16], xmm1
    movsdx QWORD PTR [rsp+8], xmm0
    movsdx xmm0, QWORD PTR a$[rsp]
    divsd xmm0, QWORD PTR __real@40091eb851eb851f
    movsdx xmm1, QWORD PTR b$[rsp]
    mulsd xmm1, QWORD PTR __real@4010666666666666
    addsd xmm0, xmm1
    ret 0
f ENDP

有一些繁杂,输入参数保存在“shadow space”(影子空间,7.2.1节),但是只有低一半的寄存器,也即只有64位存了这个double的值。

GCC编译器生成了几乎一样的代码。

24.2 通过参数传递浮点型变量

#include <math.h>
#include <stdio.h>
int main ()
{
    printf ("32.01 ^ 1.54 = %lf\n", pow (32.01,1.54));
    return 0;
}

他们通过XMM0-XMM3的低一半寄存器传递。

清单24.3: MSVC 2012 x64 /Ox

$SG1354 DB ’32.01 ^ 1.54 = %lf’, 0aH, 00H
__real@40400147ae147ae1 DQ 040400147ae147ae1r ; 32.01
__real@3ff8a3d70a3d70a4 DQ 03ff8a3d70a3d70a4r ; 1.54
main PROC
    sub rsp, 40 ; 00000028H
    movsdx xmm1, QWORD PTR __real@3ff8a3d70a3d70a4
    movsdx xmm0, QWORD PTR __real@40400147ae147ae1
    call pow
    lea rcx, OFFSET FLAT:$SG1354
    movaps xmm1, xmm0
    movd rdx, xmm1
    call printf
    xor eax, eax
    add rsp, 40 ; 00000028H
    ret 0
main ENDP

在Intel和AMD的手册中(见14章和1章)并没有MOVSDX这个指令,而只有MOVSD一个。所以在x86中有两个指令共享了同一个名字(另一个见B.6.2)。显然,微软的开发者想要避免弄得一团糟,所以他们把它重命名为MOVSDX,它只是会多把一个值载入XMM寄存器的低一半中。 pow()函数从XMM0和XMM1中加载参数,然后返回结果到XMM0中。 然后把值移动到RDX中,因为接下来printf()需要调用这个函数。为什么?老实说我也不知道,也许是因为printf()是一个参数不定的函数?

清单24.4:GCC 4.4.6 x64 -O3

.LC2:
.string "32.01 ^ 1.54 = %lf\n"
main:
    sub rsp, 8
    movsd xmm1, QWORD PTR .LC0[rip]
    movsd xmm0, QWORD PTR .LC1[rip]
    call pow
    ; result is now in XMM0
    mov edi, OFFSET FLAT:.LC2
    mov eax, 1 ; number of vector registers passed
    call printf
    xor eax, eax
    add rsp, 8
    ret
.LC0:
    .long 171798692
    .long 1073259479
.LC1:
    .long 2920577761
    .long 1077936455

GCC让结果更清晰,printf()的值传入到了XMM0中。顺带一提,这是一个因为printf()才把1写入EAX中的例子。这意味着参数会被传递到向量寄存器中,就像标准需求一样(见21章)。

24.3 比较式的例子

double d_max (double a, double b)
{
    if (a>b)
    return a;
    return b;
};

清单 24.5: MSVC 2012 x64 /Ox

a$ = 8
b$ = 16
d_max PROC
    comisd xmm0, xmm1
    ja SHORT $LN2@d_max
    movaps xmm0, xmm1
$LN2@d_max:
    fatret 0
d_max ENDP

优化过的MSVC产生了很容易理解的代码。 COMISD是“Compare Scalar Ordered Double-Precision Floating-Point Values and Set EFLAGS”(比较标量双精度浮点数的值然后设置EFLAG)的缩写,显然,看着名字就知道他要干啥了。 非优化的MSVC代码产生了更加丰富的代码,但是仍然不难理解:

清单 24.6: MSVC 2012 x64

a$ = 8
b$ = 16
d_max PROC
    comisd xmm0, xmm1
    ja SHORT $LN2@d_max
    movaps xmm0, xmm1
    $LN2@d_max:
    fatret 0
d_max ENDP

但是,GCC 4.4.6生成了更多的优化代码,并且使用了MAXSD(“Return Maximum Scalar Double-Precision Floating-Point Value”,返回最大的双精度浮点数的值)指令,它将选中其中一个最大数。

清单24.7: GCC 4.4.6 x64 -O3

a$ = 8
b$ = 16
d_max PROC
    movsdx QWORD PTR [rsp+16], xmm1
    movsdx QWORD PTR [rsp+8], xmm0
    movsdx xmm0, QWORD PTR a$[rsp]
    comisd xmm0, QWORD PTR b$[rsp]
    jbe SHORT $LN1@d_max
    movsdx xmm0, QWORD PTR a$[rsp]
    jmp SHORT $LN2@d_max
    $LN1@d_max:
    movsdx xmm0, QWORD PTR b$[rsp]
    $LN2@d_max:
    fatret 0
d_max ENDP

24.4 总结

只有低一半的XMM寄存器会被使用,一组IEEE754格式的数字也会被存在这里。 显然,所有的指令都有SD后缀(标量双精度数),这些操作数是可以用于IEEE754浮点数的,他们存在XMM寄存器的低64位中。 比FPU更简单的是,显然SIMD扩展并不像FPU以前那么混乱,栈寄存器模型也没使用。 如果你像试着将例子中的double替换成float的话,它们还是会使用同样的指令,但是后缀是SS(标量单精度数),例如MOVSS,COMISS,ADDSS等等。 标量(Scalar)代表着SIMD寄存器会包含仅仅一个值,而不是所有的。可以在所有类型的值中生效的指令都被“封装”成同一个名字。 # 温度转换

另一个在初学者的编程书中常见的例子是温度转换程序,例如将华氏度转为摄氏度,或者反过来。

我也添加了一个简单的错误处理: 1)我们应该检查用户是否输入了正确的数字 2)我们应该检查摄氏度是否低于-273゜C,因为这比绝对零度还低,学校物理课上的东西应该都还记得。 exit()函数将立即终止程序,而不会回到调用者函数。

25.1 整数值

#include <stdio.h>
#include <stdlib.h>
int main()
{
    int celsius, fahr;
    printf ("Enter temperature in Fahrenheit:\n");
    if (scanf ("%d", &fahr)!=1)
    {
        printf ("Error while parsing your input\n");
        exit(0);
    };
    celsius = 5 * (fahr-32) / 9;
    if (celsius<-273)
    {
        printf ("Error: incorrect temperature!\n");
        exit(0);
    };
    printf ("Celsius: %d\n", celsius);
};

25.1.1 MSVC 2012 x86 /Ox

清单25.1: MSVC 2012 x86 /Ox

$SG4228 DB ’Enter temperature in Fahrenheit:’, 0aH, 00H
$SG4230 DB ’%d’, 00H
$SG4231 DB ’Error while parsing your input’, 0aH, 00H
$SG4233 DB ’Error: incorrect temperature!’, 0aH, 00H
$SG4234 DB ’Celsius: %d’, 0aH, 00H
_fahr$ = -4 ; size = 4
_main PROC
    push ecx
    push esi
    mov esi, DWORD PTR __imp__printf
    push OFFSET $SG4228 ; ’Enter temperature in Fahrenheit:’
    call esi ; call printf()
    lea eax, DWORD PTR _fahr$[esp+12]
    push eax
    push OFFSET $SG4230 ; ’%d’
    call DWORD PTR __imp__scanf
    add esp, 12 ; 0000000cH
    cmp eax, 1
    je SHORT $LN2@main
    push OFFSET $SG4231 ; ’Error while parsing your input’
    call esi ; call printf()
    add esp, 4
    push 0
    call DWORD PTR __imp__exit
    $LN9@main:
    $LN2@main:
    mov eax, DWORD PTR _fahr$[esp+8]
    add eax, -32 ; ffffffe0H
    lea ecx, DWORD PTR [eax+eax*4]
    mov eax, 954437177 ; 38e38e39H
    imul ecx
    sar edx, 1
    mov eax, edx
    shr eax, 31 ; 0000001fH
    add eax, edx
    cmp eax, -273 ; fffffeefH
    jge SHORT $LN1@main
    push OFFSET $SG4233 ; ’Error: incorrect temperature!’
    call esi ; call printf()
    add esp, 4
    push 0
    call DWORD PTR __imp__exit
    $LN10@main:
    $LN1@main:
    push eax
    push OFFSET $SG4234 ; ’Celsius: %d’
    call esi ; call printf()
    add esp, 8
    ; return 0 - at least by C99 standard
    xor eax, eax
    pop esi
    pop ecx
    ret 0
$LN8@main:
_main ENDP

关于这个我们可以说的是:

25.1.2 MSVC 2012 x64 /Ox

生成的代码几乎一样,但是我发现每个exit()调用之后都有INT 3。

xor ecx, ecx
call QWORD PTR __imp_exit
int 3

INT 3是一个调试器断点。 可以知道的是exit()是永远不会return的函数之一。所以如果他“返回”了,那么估计发生了什么奇怪的事情,也是时候启动调试器了。

25.2 浮点数值

清单11.1: MSVC 2010

#include <stdio.h>
#include <stdlib.h>
int main()
{
    double celsius, fahr;
    printf ("Enter temperature in Fahrenheit:\n");
    if (scanf ("%lf", &fahr)!=1)
    {
        printf ("Error while parsing your input\n");
        exit(0);
    };
    celsius = 5 * (fahr-32) / 9;
    if (celsius<-273)
    {
        printf ("Error: incorrect temperature!\n");
        exit(0);
    };
    printf ("Celsius: %lf\n", celsius);
};

MSVC 2010 x86使用FPU指令…

清单25.2: MSVC 2010 x86 /Ox

$SG4038 DB ’Enter temperature in Fahrenheit:’, 0aH, 00H
$SG4040 DB ’%lf’, 00H
$SG4041 DB ’Error while parsing your input’, 0aH, 00H
$SG4043 DB ’Error: incorrect temperature!’, 0aH, 00H
$SG4044 DB ’Celsius: %lf’, 0aH, 00H
__real@c071100000000000 DQ 0c071100000000000r ; -273
__real@4022000000000000 DQ 04022000000000000r ; 9
__real@4014000000000000 DQ 04014000000000000r ; 5
__real@4040000000000000 DQ 04040000000000000r ; 32
_fahr$ = -8 ; size = 8
_main PROC
    sub esp, 8
    push esi
    mov esi, DWORD PTR __imp__printf
    push OFFSET $SG4038 ; ’Enter temperature in Fahrenheit:’
    call esi ; call printf
    lea eax, DWORD PTR _fahr$[esp+16]
    push eax
    push OFFSET $SG4040 ; ’%lf’
    call DWORD PTR __imp__scanf
    add esp, 12 ; 0000000cH
    cmp eax, 1
    je SHORT $LN2@main
    push OFFSET $SG4041 ; ’Error while parsing your input’
    call esi ; call printf
    add esp, 4
    push 0
    call DWORD PTR __imp__exit
    $LN2@main:
    fld QWORD PTR _fahr$[esp+12]
    fsub QWORD PTR __real@4040000000000000 ; 32
    fmul QWORD PTR __real@4014000000000000 ; 5
    fdiv QWORD PTR __real@4022000000000000 ; 9
    fld QWORD PTR __real@c071100000000000 ; -273
    fcomp ST(1)
    fnstsw ax
    test ah, 65 ; 00000041H
    jne SHORT $LN1@main
    push OFFSET $SG4043 ; ’Error: incorrect temperature!’
    fstp ST(0)
    call esi ; call printf
    add esp, 4
    push 0
    call DWORD PTR __imp__exit
    $LN1@main:
    sub esp, 8
    fstp QWORD PTR [esp]
    push OFFSET $SG4044 ; ’Celsius: %lf’
    call esi
    add esp, 12 ; 0000000cH
    ; return 0
    xor eax, eax
    pop esi
    add esp, 8
    ret 0
$LN10@main:
_main ENDP

但是MSVC从2012年开始又改成了使用SIMD指令:

清单25.3: MSVC 2010 x86 /Ox

$SG4228 DB ’Enter temperature in Fahrenheit:’, 0aH, 00H
$SG4230 DB ’%lf’, 00H
$SG4231 DB ’Error while parsing your input’, 0aH, 00H
$SG4233 DB ’Error: incorrect temperature!’, 0aH, 00H
$SG4234 DB ’Celsius: %lf’, 0aH, 00H
__real@c071100000000000 DQ 0c071100000000000r ; -273
__real@4040000000000000 DQ 04040000000000000r ; 32
__real@4022000000000000 DQ 04022000000000000r ; 9
__real@4014000000000000 DQ 04014000000000000r ; 5
_fahr$ = -8 ; size = 8
_main PROC
    sub esp, 8
    push esi
    mov esi, DWORD PTR __imp__printf
    push OFFSET $SG4228 ; ’Enter temperature in Fahrenheit:’
    call esi ; call printf
    lea eax, DWORD PTR _fahr$[esp+16]
    push eax
    push OFFSET $SG4230 ; ’%lf’
    call DWORD PTR __imp__scanf
    add esp, 12 ; 0000000cH
    cmp eax, 1
    je SHORT $LN2@main
    push OFFSET $SG4231 ; ’Error while parsing your input’
    call esi ; call printf
    add esp, 4
    push 0
    call DWORD PTR __imp__exit
    $LN9@main:
    $LN2@main:
    movsd xmm1, QWORD PTR _fahr$[esp+12]
    subsd xmm1, QWORD PTR __real@4040000000000000 ; 32
    movsd xmm0, QWORD PTR __real@c071100000000000 ; -273
    mulsd xmm1, QWORD PTR __real@4014000000000000 ; 5
    divsd xmm1, QWORD PTR __real@4022000000000000 ; 9
    comisd xmm0, xmm1
    jbe SHORT $LN1@main
    push OFFSET $SG4233 ; ’Error: incorrect temperature!’
    call esi ; call printf
    add esp, 4
    push 0
    call DWORD PTR __imp__exit
    $LN10@main:
    $LN1@main:
    sub esp, 8
    movsd QWORD PTR [esp], xmm1
    push OFFSET $SG4234 ; ’Celsius: %lf’
    call esi ; call printf
    add esp, 12 ; 0000000cH
    ; return 0
    xor eax, eax
    pop esi
    add esp, 8
    ret 0
$LN8@main:
_main ENDP

当然,SIMD在x86下也是可用的,包括这些浮点数的运算。使用他们计算起来也确实方便点,所以微软编译器使用了他们。 我们也可以注意到 -273 这个值会很早的被载入XMM0。这个没问题,因为编译器并不一定会按照源代码里面的顺序产生代码。 # C99的限制

这个例子说明了为什么某些情况下FORTRAN的速度比C/C++要快

void f1 (int* x, int* y, int* sum, int* product, int* sum_product, int* update_me, size_t s)
{
    for (int i=0; i<s; i++)
        {
        sum[i]=x[i]+y[i];
        product[i]=x[i]*y[i];
        update_me[i]=i*123; // some dummy value
        sum_product[i]=sum[i]+product[i];
    };
};

这是一个十分简单的例子,但是有一点需要注意:指向update_me数组的指针也可以指向sum数组,甚至是sum_product数组。但是这不是严重的错误,对吗? 编译器很清楚这一点,所以他在循环体中产生了四个阶段: 1.计算下一个sum[i] 2.计算下一个product[i] 3.计算下一个unpdate_me[i] 4.计算下一个sum_product[i],在这个阶段,我们需要从已经计算过sum[i]和product[i]的内存中载入数据

最后一个阶段可以优化吗?既然已经计算过的sum[i]和product[i]是不需要再次从内存装载的(因为我们已经计算过他们了)。但是编译器不能保证在第三个阶段没有东西被覆盖掉!这就叫“指针别名”,在这种情况下编译器无法确定指针指向区域的内存是否已经被改变。

C99标准中的限制给解决这一问题带来了一线曙光。由设计器传送给编译器的函数单元在标记这种关键字(restrict)后,它会指向不同的内存区域,并且不 会被混用。 如果要更加准确地描述这种情况,restrict表明了只有指针是可以访问对象的。这样的话我们可以通过特定的指针进行工作,并且不会用到其他指针。也就是说一个对象如果被标记为restrict,那么它只能通过一个指针访问。 我们把每个指向变量的指针标记为restrict关键字:

void f2 (int* restrict x, int* restrict y, int* restrict sum, int* restrict product, int*
restrict sum_product,
int* restrict update_me, size_t s)
{
    for (int i=0; i<s; i++)
    {
        sum[i]=x[i]+y[i];
        product[i]=x[i]*y[i];
        update_me[i]=i*123; // some dummy value
        sum_product[i]=sum[i]+product[i];
    };
};

来看下结果:

清单26.1: GCC x64: f1()

f1:
    push r15 r14 r13 r12 rbp rdi rsi rbx
    mov r13, QWORD PTR 120[rsp]
    mov rbp, QWORD PTR 104[rsp]
    mov r12, QWORD PTR 112[rsp]
    test r13, r13
    je .L1
    add r13, 1
    xor ebx, ebx
    mov edi, 1
    xor r11d, r11d
    jmp .L4
    .L6:
    mov r11, rdi
    mov rdi, rax
    .L4:
    lea rax, 0[0+r11*4]
    lea r10, [rcx+rax]
    lea r14, [rdx+rax]
    lea rsi, [r8+rax]
    add rax, r9
    mov r15d, DWORD PTR [r10]
    add r15d, DWORD PTR [r14]
    mov DWORD PTR [rsi], r15d ; store to sum[]
    mov r10d, DWORD PTR [r10]
    imul r10d, DWORD PTR [r14]
    mov DWORD PTR [rax], r10d ; store to product[]
    mov DWORD PTR [r12+r11*4], ebx ; store to update_me[]
    add ebx, 123
    mov r10d, DWORD PTR [rsi] ; reload sum[i]
    add r10d, DWORD PTR [rax] ; reload product[i]
    lea rax, 1[rdi]
    cmp rax, r13
    mov DWORD PTR 0[rbp+r11*4], r10d ; store to sum_product[]
    jne .L6
    .L1:
    pop rbx rsi rdi rbp r12 r13 r14 r15
    ret

清单26.2: GCC x64: f2()

f2:
    push r13 r12 rbp rdi rsi rbx
    mov r13, QWORD PTR 104[rsp]
    mov rbp, QWORD PTR 88[rsp]
    mov r12, QWORD PTR 96[rsp]
    test r13, r13
    je .L7
    add r13, 1
    xor r10d, r10d
    mov edi, 1
    xor eax, eax
    jmp .L10
    .L11:
    mov rax, rdi
    mov rdi, r11
    .L10:
    mov esi, DWORD PTR [rcx+rax*4]
    mov r11d, DWORD PTR [rdx+rax*4]
    mov DWORD PTR [r12+rax*4], r10d ; store to update_me[]
    add r10d, 123
    lea ebx, [rsi+r11]
    imul r11d, esi
    mov DWORD PTR [r8+rax*4], ebx ; store to sum[]
    mov DWORD PTR [r9+rax*4], r11d ; store to product[]
    add r11d, ebx
    mov DWORD PTR 0[rbp+rax*4], r11d ; store to sum_product[]
    lea r11, 1[rdi]
    cmp r11, r13
    jne .L11
    .L7:
    pop rbx rsi rdi rbp r12 r13
    ret

被编译过的f1()和f2()的不同点是:在f1()中,sum[i]和product[i]在循环中途被装入,但是在f2()中没有这样的特性。已经计算过的变量将被使用,既然我们已经向编译器“保证”在循环执行期间,sum[i]和product[i]不会发生改变,所以编译器“确信”变量的值不用从内存被再装入。很明显,第二个例子的程序更快。 但是如果函数变量中的指针发生混淆的情况又能如何呢?这与一个程序员的认知有关,并且结果是不正确的。 回到FORTRAN。FORTRAN语言编译器按照指针的本身含义对待他,所以当FORTRAN程序在这种情况下不可能使用restrict的时候,它可以生成生成执行更快的代码。

这有什么实用价值?当函数处理内存中很多大“块”的时候,比如说用超级计算机解决线性代数问题。或许这就是为什么FORTRAN语言还在这个领域被使用。 但是当迭代步骤不是很多的时候,速度的增加并不是显著的。 # 内联函数

内联代码是指当编译的时候,将函数体直接嵌入正确位置,而不是在这个位置放上函数声明。

#include <stdio.h>
int celsius_to_fahrenheit (int celsius)
{
    return celsius * 9 / 5 + 32;
};
int main(int argc, char *argv[])
{
    int celsius=atol(argv[1]);
    printf ("%d\n", celsius_to_fahrenheit (celsius));
};

这个编译是意料之中的,但是如果换成GCC的优化方案,我们会看到:

清单27.2: GCC 4.8.1 -O3

_main:
    push ebp
    mov ebp, esp
    and esp, -16
    sub esp, 16
    call ___main
    mov eax, DWORD PTR [ebp+12]
    mov eax, DWORD PTR [eax+4]
    mov DWORD PTR [esp], eax
    call _atol
    mov edx, 1717986919
    mov DWORD PTR [esp], OFFSET FLAT:LC2 ; "%d\12\0"
    lea ecx, [eax+eax*8]
    mov eax, ecx
    imul edx
    sar ecx, 31
    sar edx
    sub edx, ecx
    add edx, 32
    mov DWORD PTR [esp+4], edx
    call _printf
    leave
    ret

这里的除法由乘法完成。 是的,我们的小函数被放到了printf()调用之前。为什么?因为这比直接执行函数之前的“调用/返回”过程速度更快。 在过去,这样的函数在函数声明的时候必须被标记为“内联”。在现代,这样的函数会自动被编译器识别。 另外一个普通的自动优化的例子是内联字符串函数,比如strcpy(),strcmp()等

清单27.3 : 另一个简单的例子

bool is_bool (char *s)
{
    if (strcmp (s, "true")==0)
    return true;
    if (strcmp (s, "false")==0)
    return false;
    assert(0);
};

清单27.4: GCC 4.8.1 -O3

_is_bool:
    push edi
    mov ecx, 5
    push esi
    mov edi, OFFSET FLAT:LC0 ; "true\0"
    sub esp, 20
    mov esi, DWORD PTR [esp+32]
    repz cmpsb
    je L3
    mov esi, DWORD PTR [esp+32]
    mov ecx, 6
    mov edi, OFFSET FLAT:LC1 ; "false\0"
    repz cmpsb
    seta cl
    setb dl
    xor eax, eax
    cmp cl, dl
    jne L8
    add esp, 20
    pop esi
    pop edi
    ret

这是一个经常可以见到的关于MSVC生成的strcmp()的例子。

清单27.5: MSVC

    mov dl, [eax]
    cmp dl, [ecx]
    jnz short loc_10027FA0
    test dl, dl
    jz short loc_10027F9C
    mov dl, [eax+1]
    cmp dl, [ecx+1]
    jnz short loc_10027FA0
    add eax, 2
    add ecx, 2
    test dl, dl
    jnz short loc_10027F80
    loc_10027F9C: ; CODE XREF: f1+448
    xor eax, eax
    jmp short loc_10027FA5
; ---------------------------------------------------------------------------
    loc_10027FA0: ; CODE XREF: f1+444
; f1+450
    sbb eax, eax
    sbb eax, 0FFFFFFFFh

我写了一个小的用于搜索和归纳的IDA脚本,这样的脚本经常能在内联代码中看到:IDA_scripts. # 处理不当的反汇编代码

逆向工程师经常需要处理不当的反汇编代码

 28.1反汇编于不正确起始位置(x86)

不同于ARM和MIPS架构(任何指令长度只有2个字节长度或者4个字节长度), x86架构的指令长度是不定长的,因此,任何反汇编器从x86指令中间开始反汇编,可能会长生不正确的结果。

举个例子:

add [ebp-31F7Bh], cl
dec dword ptr [ecx-3277Bh]
dec dword ptr [ebp-2CF7Bh]
inc dword ptr [ebx-7A76F33Ch]
fdiv st(4), st
;-------------------------------------------------------------
db 0FFh
;-------------------------------------------------------------
dec dword ptr [ecx-21F7Bh]
dec dword ptr [ecx-22373h]
dec dword ptr [ecx-2276Bh]
dec dword ptr [ecx-22B63h]
dec dword ptr [ecx-22F4Bh]
dec dword ptr [ecx-23343h]
jmp dword ptr [esi-74h]
;-------------------------------------------------------------
xchg eax, ebp
clc
std
;-------------------------------------------------------------
db 0FFh
db 0FFh
;-------------------------------------------------------------
mov word ptr [ebp-214h], cs
mov word ptr [ebp-238h], ds
mov word ptr [ebp-23Ch], es
mov word ptr [ebp-240h], fs
mov word ptr [ebp-244h], gs
pushf
pop dword ptr [ebp-210h]
mov eax, [ebp+4]
mov [ebp-218h], eax
lea eax, [ebp+4]
mov [ebp-20Ch], eax
mov dword ptr [ebp-2D0h], 10001h
mov eax, [eax-4]
mov [ebp-21Ch], eax
mov eax, [ebp+0Ch]
mov [ebp-320h], eax
mov eax, [ebp+10h]
mov [ebp-31Ch], eax
mov eax, [ebp+4]
mov [ebp-314h], eax
call ds:IsDebuggerPresent
mov edi, eax
lea eax, [ebp-328h]
push eax
call sub_407663
pop ecx
test eax, eax
jnz short loc_402D7B

虽然上面的代码片段一开始是从错误的起始位置反汇编的,但最终,反汇编器能够自己调整到正确的轨道上。

28.2不正确的反汇编代码的特点

可以很容易发现它们的共同特点是:

很少出现大尺寸的指令,最常见的有x86指令的push,mov,call。 但是我们可以看到这些指令来自各个不同的指令组,有FPU指令,IN/OUT指令,少数的系统指令,一切都是因为反汇编器从一个错误的位置上开始反汇编机器码给搞砸了。 偏移量和立即数都是一些随机值,而且数值较大。 跳转到不正确的偏移地址常常会跳转到另一个指令的中间。 代码清单28.1:x86架构不正确的反汇编代码示例

    mov     bl, 0Ch
    mov     ecx, 0D38558Dh
    mov     eax, ds:2C869A86h
    db      67h
    mov     dl, 0CCh
    insb
    movsb
    push    eax
    xor     [edx-53h], ah
    fcom    qword ptr [edi-45A0EF72h]
    pop     esp
    pop     ss
    in      eax, dx
    dec     ebx
    push    esp
    lds     esp, [esi-41h]
    retf
    rcl     dword ptr [eax], cl
    mov     cl, 9Ch
    mov     ch, 0DFh
    push    cs
    insb
    mov     esi, 0D9C65E4Dh
    imul    ebp, [ecx], 66h
    pushf
    sal     dword ptr [ebp-64h], cl
    sub     eax, 0AC433D64h
    out     8Ch, eax
    pop     ss
    sbb     [eax], ebx
    aas
    xchg    cl, [ebx+ebx*4+14B31Eh]
    jecxz   short near ptr loc_58+1
    xor     al, 0C6h
    inc     edx
    db      36h
    pusha
    stosb
    test    [ebx], ebx
    sub     al, 0D3h ; 'L'
    pop     eax
    stosb

loc_58: ; CODE XREF: seg000:0000004A
    test    [esi], eax
    inc     ebp
    das
    db      64h
    pop     ecx
    das
    hlt

    pop     edx
    out     0B0h, al
    lodsb
    push    ebx
    cdq
    out     dx, al
    sub     al, 0Ah
    sti
    outsd
    add     dword ptr [edx], 96FCBE4Bh
    and     eax, 0E537EE4Fh
    inc     esp
    stosd
    cdq
    push    ecx
    in      al, 0CBh
    mov     ds:0D114C45Ch, al
    mov     esi, 659D1985h
    enter   6FE8h, 0D9h
    enter   6FE6h, 0D9h
    xchg    eax, esi
    sub     eax, 0A599866Eh
    retn

    pop     eax
    dec     eax
    adc     al, 21h ; '!'
    lahf
    inc     edi
    sub     eax, 9062EE5Bh
    bound   eax, [ebx]

loc_A2: ; CODE XREF: seg000:00000120
    wait
    iret

    jnb     short loc_D7
    cmpsd
    iret

    jnb     short loc_D7
    sub     ebx, [ecx]
    in      al, 0Ch
    add     esp, esp
    mov     bl, 8Fh
    xchg    eax, ecx
    int     67h
    pop     ds
    pop     ebx
    db      36h
    xor     esi, [ebp-4Ah]
    mov     ebx, 0EB4F980Ch
    repne add bl, dh
    imul    ebx, [ebp+5616E7A5h], 67A4D1EEh
    xchg    eax, ebp
    scasb
    push    esp
    wait
    mov     dl, 11h
    mov     ah, 29h ; ')'
    fist    dword ptr [edx]

loc_D7: ; CODE XREF: seg000:000000A4
        ; seg000:000000A8 ...
    dec     dword ptr [ebp-5D0E0BA4h]
    call    near ptr 622FEE3Eh
    sbb     ax, 5A2Fh
    jmp     dword ptr cs:[ebx]

    xor     ch, [edx-5]
    inc     esp
    push    edi
    xor     esp, [ebx-6779D3B8h]
    pop     eax
    int     3               ; Trap to Debugger
    rcl     byte ptr [ebx-3Eh], cl
    xor     [edi], bl
    sbb     al, [edx+ecx*4]
    xor     ah, [ecx-1DA4E05Dh]
    push    edi
    xor     ah, cl
    popa
    cmp     dword ptr [edx-62h], 46h ; 'F'
    dec     eax
    in      al, 69h
    dec     ebx
    iret

    or      al, 6
    jns     short near ptr loc_D7+3
    shl     byte ptr [esi], 42h
    repne adc [ebx+2Ch], eax
    icebp
    cmpsd
    leave
    push    esi
    jmp     short loc_A2

    and     eax, 0F2E41FE9h
    push    esi
    loop    loc_14F
    add     ah, fs:[edx]

loc_12D: ; CODE XREF: seg000:00000169
    mov     dh, 0F7h
    add     [ebx+7B61D47Eh], esp
    mov     edi, 79F19525h
    rcl     byte ptr [eax+22015F55h], cl
    cli
    sub     al, 0D2h ; 'T'
    dec     eax
    mov     ds:0A81406F5h, eax
    sbb     eax, 0A7AA179Ah
    in      eax, dx

loc_14F: ; CODE XREF: seg000:00000128
    and     [ebx-4CDFAC74h], ah
    pop     ecx
    push    esi
    mov     bl, 2Dh ; '-'
    in      eax, 2Ch
    stosd
    inc     edi
    push    esp

locret_15E: ; CODE XREF: seg000:loc_1A0
    retn    0C432h

    and     al, 86h
    cwde
    and     al, 8Fh
    cmp     ebp, [ebp+7]
    jz      short loc_12D
    sub     bh, ch
    or      dword ptr [edi-7Bh], 8A16C0F7h
    db      65h
    insd
    mov     al, ds:0A3A5173Dh
    dec     ecx
    push    ds
    xor     al, cl
    jg      short loc_195
    push    6Eh ; 'n'
    out     0DDh, al
    inc     edi
    sub     eax, 6899BBF1h
    leave
    rcr     dword ptr [ecx-69h], cl
    sbb     ch, [edi+5EDDCB54h]

loc_195: ; CODE XREF: seg000:0000017F
    push    es
    repne sub ah, [eax-105FF22Dh]
    cmc
    and     ch, al

loc_1A0: ; CODE XREF: seg000:00000217
    jnp     short near ptr locret_15E+1
    or      ch, [eax-66h]
    add     [edi+edx-35h], esi
    out     dx, al
    db      2Eh
    call    far ptr 1AAh:6832F5DDh
    jz      short near ptr loc_1DA+1
    sbb     esp, [edi+2CB02CEFh]
    xchg    eax, edi
    xor     [ebx-766342ABh], edx

loc_1C1: ; CODE XREF: seg000:00000212
    cmp     eax, 1BE9080h
    add     [ecx], edi
    aad     0
    imul    esp, [edx-70h], 0A8990126h
    or      dword ptr [edx+10C33693h], 4Bh
    popf

loc_1DA: ; CODE XREF: seg000:000001B2
    mov     ecx, cs
    aaa
    mov     al, 39h ; '9'
    adc     byte ptr [eax-77F7F1C5h], 0C7h
    add     [ecx], bl
    retn    0DD42h

    db      3Eh
    mov     fs:[edi], edi
    and     [ebx-24h], esp
    db      64h
    xchg    eax, ebp
    push    cs
    adc     eax, [edi+36h]
    mov     bh, 0C7h
    sub     eax, 0A710CBE7h
    xchg    eax, ecx
    or      eax, 51836E42h
    xchg    eax, ebx
    inc     ecx
    jb      short near ptr loc_21E+3
    db      64h
    xchg    eax, esp
    and     dh, [eax-31h]
    mov     ch, 13h
    add     ebx, edx
    jnb     short loc_1C1
    db      65h
    adc     al, 0C5h
    js      short loc_1A0
    sbb     eax, 887F5BEEh

loc_21E: ; CODE XREF: seg000:00000207
    mov     eax, 888E1FD6h
    mov     bl, 90h
    cmp     [eax], ecx
    rep int 61h             ; reserved for user interrupt
    and     edx, [esi-7EB5C9EAh]
    fisttp  qword ptr [eax+esi*4+38F9BA6h]
    jmp     short loc_27C

    fadd    st, st(2)
    db      3Eh
    mov     edx, 54C03172h
    retn

    db      64h
    pop     ds
    xchg    eax, esi
    rcr     ebx, cl
    cmp     [di+2Eh], ebx
    repne xor [di-19h], dh
    insd
    adc     dl, [eax-0C4579F7h]
    push    ss
    xor     [ecx+edx*4+65h], ecx
    mov     cl, [ecx+ebx-32E8AC51h]
    or      [ebx], ebp
    cmpsb
    lodsb
    iret

代码清单28.2:x86_64架构不正确的反汇编代码示例

    lea     esi, [rax+rdx*4+43558D29h]

loc_AF3: ; CODE XREF: seg000:0000000000000B46
    rcl     byte ptr [rsi+rax*8+29BB423Ah], 1
    lea     ecx, cs:0FFFFFFFFB2A6780Fh
    mov     al, 96h
    mov     ah, 0CEh
    push    rsp
    lods    byte ptr [esi]

    db  2Fh ; /

    pop     rsp
    db      64h
    retf    0E993h

    cmp     ah, [rax+4Ah]
    movzx   rsi, dword ptr [rbp-25h]
    push    4Ah
    movzx   rdi, dword ptr [rdi+rdx*8]

    db  9Ah

    rcr     byte ptr [rax+1Dh], cl
    lodsd
    xor     [rbp+6CF20173h], edx
    xor     [rbp+66F8B593h], edx
    push    rbx
    sbb     ch, [rbx-0Fh]
    stosd
    int     87h
    db      46h, 4Ch
    out     33h, rax
    xchg    eax, ebp
    test    ecx, ebp
    movsd
    leave
    push    rsp

    db  16h

    xchg    eax, esi
    pop     rdi

loc_B3D: ; CODE XREF: seg000:0000000000000B5F
    mov     ds:93CA685DF98A90F9h, eax
    jnz     short near ptr loc_AF3+6
    out     dx, eax
    cwde
    mov     bh, 5Dh ; ']'
    movsb
    pop     rbp

    db  60h ; `

    movsxd  rbp, dword ptr [rbp-17h]
    pop     rbx
    out     7Dh, al
    add     eax, 0D79BE769h

    db  1Fh

    retf    0CAB9h

    jl      short near ptr loc_B3D+4
    sal     dword ptr [rbx+rbp+4Dh], 0D3h
    mov     cl, 41h ; 'A'
    imul    eax, [rbp-5B77E717h], 1DDE6E5h
    imul    ecx, ebx, 66359BCCh
    xlat

    db  60h ; `

    cmp     bl, [rax]
    and     ebp, [rcx-57h]
    stc
    sub     [rcx+1A533AB4h], al
    jmp     short loc_C05

    db  4Bh ; K

    int     3               ; Trap to Debugger
    xchg    ebx, [rsp+rdx-5Bh]

    db 0D6h

    mov     esp, 0C5BA61F7h
    out     0A3h, al        ; Interrupt Controller #2, 8259A
    add     al, 0A6h
    pop     rbx
    cmp     bh, fs:[rsi]
    and     ch, cl
    cmp     al, 0F3h

    db  0Eh

    xchg    dh, [rbp+rax*4-4CE9621Ah]
    stosd
    xor     [rdi], ebx
    stosb
    xchg    eax, ecx
    push    rsi
    insd
    fidiv   word ptr [rcx]
    xchg    eax, ecx
    mov     dh, 0C0h ; 'L'
    xchg    eax, esp
    push    rsi
    mov     dh, [rdx+rbp+6918F1F3h]
    xchg    eax, ebp
    out     9Dh, al

loc_BC0: ; CODE XREF: seg000:0000000000000C26
    or      [rcx-0Dh], ch
    int     67h             ;  - LIM EMS
    push    rdx
    sub     al, 43h ; 'C'
    test    ecx, ebp
    test    [rdi+71F372A4h], cl

    db    7

    imul    ebx, [rsi-0Dh], 2BB30231h
    xor     ebx, [rbp-718B6E64h]
    jns     short near ptr loc_C56+1
    ficomp  dword ptr [rcx-1Ah]
    and     eax, 69BEECC7h
    mov     esi, 37DA40F6h
    imul    r13, [rbp+rdi*8+529F33CDh], 0FFFFFFFFF35CDD30h
    or      [rbx], edx
    imul    esi, [rbx-34h], 0CDA42B87h

    db  36h ; 6
    db  1Fh


loc_C05: ; CODE XREF: seg000:0000000000000B86
    add     dh, [rcx]
    mov     edi, 0DD3E659h
    ror     byte ptr [rdx-33h], cl
    xlat
    db      48h
    sub     rsi, [rcx]

    db  1Fh
    db    6

    xor     [rdi+13F5F362h], bh
    cmpsb
    sub     esi, [rdx]
    pop     rbp
    sbb     al, 62h ; 'b'
    mov     dl, 33h ; '3'

    db  4Dh ; M
    db  17h

    jns     short loc_BC0
    push    0FFFFFFFFFFFFFF86h

loc_C2A: ; CODE XREF: seg000:0000000000000C8F
    sub     [rdi-2Ah], eax

    db 0FEh

    cmpsb
    wait
    rcr     byte ptr [rax+5Fh], cl
    cmp     bl, al
    pushfq
    xchg    ch, cl

    db  4Eh ; N
    db  37h ; 7

    mov     ds:0E43F3CCD3D9AB295h, eax
    cmp     ebp, ecx
    jl      short loc_C87
    retn    8574h

    out     3, al           ; DMA controller, 8237A-5.
                            ; channel 1 base address and word count

loc_C4C: ; CODE XREF: seg000:0000000000000C7F
    cmp     al, 0A6h
    wait
    push    0FFFFFFFFFFFFFFBEh

    db  82h

    ficom   dword ptr [rbx+r10*8]

loc_C56: ; CODE XREF: seg000:0000000000000BDE
    jnz     short loc_C76
    xchg    eax, edx
    db      26h
    wait
    iret

    push    rcx

    db  48h ; H
    db  9Bh
    db  64h ; d
    db  3Eh ; >
    db  2Fh ; /

    mov     al, ds:8A7490CA2E9AA728h
    stc

    db  60h ; `

    test    [rbx+rcx], ebp
    int     3               ; Trap to Debugger
    xlat

loc_C72: ; CODE XREF: seg000:0000000000000CC6
    mov     bh, 98h

    db  2Eh ; .
    db 0DFh


loc_C76: ; CODE XREF: seg000:loc_C56
    jl      short loc_C91
    sub     ecx, 13A7CCF2h
    movsb
    jns     short near ptr loc_C4C+1
    cmpsd
    sub     ah, ah
    cdq

    db  6Bh ; k
    db  5Ah ; Z


loc_C87: ; CODE XREF: seg000:0000000000000C45
    or      ecx, [rbx+6Eh]
    rep in eax, 0Eh         ; DMA controller, 8237A-5.
                            ; Clear mask registers.
                            ; Any OUT enables all 4 channels.
    cmpsb
    jnb     short loc_C2A

loc_C91: ; CODE XREF: seg000:loc_C76
    scasd
    add     dl, [rcx+5FEF30E6h]
    enter   0FFFFFFFFFFFFC733h, 7Ch
    insd
    mov     ecx, gs
    in      al, dx
    out     2Dh, al
    mov     ds:6599E434E6D96814h, al
    cmpsb
    push    0FFFFFFFFFFFFFFD6h
    popfq
    xor     ecx, ebp
    db      48h
    insb
    test    al, cl
    xor     [rbp-7Bh], cl
    and     al, 9Bh

    db  9Ah

    push    rsp
    xor     al, 8Fh
    cmp     eax, 924E81B9h
    clc
    mov     bh, 0DEh
    jbe     short near ptr loc_C72+1

    db  1Eh

    retn    8FCAh

    db 0C4h ; -


loc_CCD: ; CODE XREF: seg000:0000000000000D22
    adc     eax, 7CABFBF8h

    db  38h ; 8

    mov     ebp, 9C3E66FCh
    push    rbp
    dec     byte ptr [rcx]
    sahf
    fidivr  word ptr [rdi+2Ch]

    db  1Fh

    db      3Eh
    xchg    eax, esi

loc_CE2: ; CODE XREF: seg000:0000000000000D5E
    mov     ebx, 0C7AFE30Bh
    clc
    in      eax, dx
    sbb     bh, bl
    xchg    eax, ebp

    db  3Fh ; ?

    cmp     edx, 3EC3E4D7h
    push    51h
    db      3Eh
    pushfq
    jl      short loc_D17
    test    [rax-4CFF0D49h], ebx

    db  2Fh ; /

    rdtsc
    jns     short near ptr loc_D40+4
    mov     ebp, 0B2BB03D8h
    in      eax, dx

    db  1Eh

    fsubr   dword ptr [rbx-0Bh]
    jns     short loc_D70
    scasd
    mov     ch, 0C1h ; '+'
    add     edi, [rbx-53h]

    db 0E7h


loc_D17: ; CODE XREF: seg000:0000000000000CF7
    jp      short near ptr unk_D79
    scasd
    cmc
    sbb     ebx, [rsi]
    fsubr   dword ptr [rbx+3Dh]
    retn

    db    3

    jnp     short near ptr loc_CCD+4
    db      36h
    adc     r14b, r13b

    db  1Fh

    retf

    test    [rdi+rdi*2], ebx
    cdq
    or      ebx, edi
    test    eax, 310B94BCh
    ffreep  st(7)
    cwde
    sbb     esi, [rdx+53h]
    push    5372CBAAh

loc_D40: ; CODE XREF: seg000:0000000000000D02
    push    53728BAAh
    push    0FFFFFFFFF85CF2FCh

    db  0Eh

    retn    9B9Bh

    movzx   r9, dword ptr [rdx]
    adc     [rcx+43h], ebp
    in      al, 31h

    db  37h ; 7

    jl      short loc_DC5
    icebp
    sub     esi, [rdi]
    clc
    pop     rdi
    jb      short near ptr loc_CE2+1
    or      al, 8Fh
    mov     ecx, 770EFF81h
    sub     al, ch
    sub     al, 73h ; 's'
    cmpsd
    adc     bl, al
    out     87h, eax        ; DMA page register 74LS612:
                            ; Channel 0 (address bits 16-23)

loc_D70: ; CODE XREF: seg000:0000000000000D0E
    adc     edi, ebx
    db      49h
    outsb
    enter   33E5h, 97h
    xchg    eax, ebx

unk_D79   db 0FEh ; CODE XREF: seg000:loc_D17
          db 0BEh
          db 0E1h
          db  82h


loc_D7D: ; CODE XREF: seg000:0000000000000DB3
    cwde

    db    7
    db  5Ch ; \
    db  10h
    db  73h ; s
    db 0A9h
    db  2Bh ; +
    db  9Fh


loc_D85: ; CODE XREF: seg000:0000000000000DD1
    dec     dh
    jnz     short near ptr loc_DD3+3
    mov     ds:7C1758CB282EF9BFh, al
    sal     ch, 91h
    rol     dword ptr [rbx+7Fh], cl
    fbstp   tbyte ptr [rcx+2]
    repne mov al, ds:4BFAB3C3ECF2BE13h
    pushfq
    imul    edx, [rbx+rsi*8+3B484EE9h], 8EDC09C6h
    cmp     [rax], al
    jg      short loc_D7D
    xor     [rcx-638C1102h], edx
    test    eax, 14E3AD7h
    insd

    db  38h ; 8
    db  80h
    db 0C3h


loc_DC5: ; CODE XREF: seg000:0000000000000D57
         ; seg000:0000000000000DD8
    cmp     ah, [rsi+rdi*2+527C01D3h]
    sbb     eax, 5FC631F0h
    jnb     short loc_D85

loc_DD3: ; CODE XREF: seg000:0000000000000D87
    call    near ptr 0FFFFFFFFC03919C7h
    loope   near ptr loc_DC5+3
    sbb     al, 0C8h
    std

代码清单28.2:ARM架构(ARM 模式)不正确的反汇编代码示例

BLNE    0xFE16A9D8
BGE     0x1634D0C
SVCCS   0x450685
STRNVT  R5, [PC],#-0x964
LDCGE   p6, c14, [R0],#0x168
STCCSL  p9, c9, [LR],#0x14C
CMNHIP  PC, R10,LSL#22
FLDMIADNV LR!, {D4}
MCR     p5, 2, R2,c15,c6, 4
BLGE    0x1139558
BLGT    0xFF9146E4
STRNEB  R5, [R4],#0xCA2
STMNEIB R5, {R0,R4,R6,R7,R9-SP,PC}
STMIA   R8, {R0,R2-R4,R7,R8,R10,SP,LR}^
STRB    SP, [R8],PC,ROR#18
LDCCS   p9, c13, [R6,#0x1BC]
LDRGE   R8, [R9,#0x66E]
STRNEB  R5, [R8],#-0x8C3
STCCSL  p15, c9, [R7,#-0x84]
RSBLS   LR, R2, R11,ASR LR
SVCGT   0x9B0362
SVCGT   0xA73173
STMNEDB R11!, {R0,R1,R4-R6,R8,R10,R11,SP}
STR     R0, [R3],#-0xCE4
LDCGT   p15, c8, [R1,#0x2CC]
LDRCCB  R1, [R11],-R7,ROR#30
BLLT    0xFED9D58C
BL      0x13E60F4
LDMVSIB R3!, {R1,R4-R7}^
USATNE  R10, #7, SP,LSL#11
LDRGEB  LR, [R1],#0xE56
STRPLT  R9, [LR],#0x567
LDRLT   R11, [R1],#-0x29B
SVCNV   0x12DB29
MVNNVS  R5, SP,LSL#25
LDCL    p8, c14, [R12,#-0x288]
STCNEL  p2, c6, [R6,#-0xBC]!
SVCNV   0x2E5A2F
BLX     0x1A8C97E
TEQGE   R3, #0x1100000
STMLSIA R6, {R3,R6,R10,R11,SP}
BICPLS  R12, R2, #0x5800
BNE     0x7CC408
TEQGE   R2, R4,LSL#20
SUBS    R1, R11, #0x28C
BICVS   R3, R12, R7,ASR R0
LDRMI   R7, [LR],R3,LSL#21
BLMI    0x1A79234
STMVCDB R6, {R0-R3,R6,R7,R10,R11}
EORMI   R12, R6, #0xC5
MCRRCS  p1, 0xF, R1,R3,c2

代码清单28.2:ARM架构(Thumb 模式)不正确的反汇编代码示例

    LSRS    R3, R6, #0x12
    LDRH    R1, [R7,#0x2C]
    SUBS    R0, #0x55 ; 'U'
    ADR     R1, loc_3C
    LDR     R2, [SP,#0x218]
    CMP     R4, #0x86
    SXTB    R7, R4
    LDR     R4, [R1,#0x4C]
    STR     R4, [R4,R2]
    STR     R0, [R6,#0x20]
    BGT     0xFFFFFF72
    LDRH    R7, [R2,#0x34]
    LDRSH   R0, [R2,R4]
    LDRB    R2, [R7,R2]

    DCB 0x17
    DCB 0xED

    STRB    R3, [R1,R1]
    STR     R5, [R0,#0x6C]
    LDMIA   R3, {R0-R5,R7}
    ASRS    R3, R2, #3
    LDR     R4, [SP,#0x2C4]
    SVC     0xB5
    LDR     R6, [R1,#0x40]
    LDR     R5, =0xB2C5CA32
    STMIA   R6, {R1-R4,R6}
    LDR     R1, [R3,#0x3C]
    STR     R1, [R5,#0x60]
    BCC     0xFFFFFF70
    LDR     R4, [SP,#0x1D4]
    STR     R5, [R5,#0x40]
    ORRS    R5, R7

loc_3C ; DATA XREF: ROM:00000006
    B       0xFFFFFF98

    ASRS    R4, R1, #0x1E
    ADDS    R1, R3, R0
    STRH    R7, [R7,#0x30]
    LDR     R3, [SP,#0x230]
    CBZ     R6, loc_90
    MOVS    R4, R2
    LSRS    R3, R4, #0x17
    STMIA   R6!, {R2,R4,R5}
    ADDS    R6, #0x42 ; 'B'
    ADD     R2, SP, #0x180
    SUBS    R5, R0, R6
    BCC     loc_B0
    ADD     R2, SP, #0x160
    LSLS    R5, R0, #0x1A
    CMP     R7, #0x45
    LDR     R4, [R4,R5]

    DCB 0x2F ; /
    DCB 0xF4

    B       0xFFFFFD18

    ADD     R4, SP, #0x2C0
    LDR     R1, [SP,#0x14C]
    CMP     R4, #0xEE

    DCB  0xA
    DCB 0xFB

    STRH    R7, [R5,#0xA]
    LDR     R3, loc_78

    DCB 0xBE ; -
    DCB 0xFC

    MOVS    R5, #0x96

    DCB 0x4F ; O
    DCB 0xEE

    B       0xFFFFFAE6

    ADD     R3, SP, #0x110

loc_78 ; DATA XREF: ROM:0000006C
    STR     R1, [R3,R6]
    LDMIA   R3!, {R2,R5-R7}
    LDRB    R2, [R4,R2]
    ASRS    R4, R0, #0x13
    BKPT    0xD1
    ADDS    R5, R0, R6
    STR     R5, [R3,#0x58]

代码清单28.2:MIPS架构(小端序)不正确的反汇编代码示例

lw      $t9, 0xCB3($t5)
sb      $t5, 0x3855($t0)
sltiu   $a2, $a0, -0x657A
ldr     $t4, -0x4D99($a2)
daddi   $s0, $s1, 0x50A4
lw      $s7, -0x2353($s4)
bgtzl   $a1, 0x17C5C

.byte 0x17
.byte 0xED
.byte 0x4B  # K
.byte 0x54  # T

lwc2    $31, 0x66C5($sp)
lwu     $s1, 0x10D3($a1)
ldr     $t6, -0x204B($zero)
lwc1    $f30, 0x4DBE($s2)
daddiu  $t1, $s1, 0x6BD9
lwu     $s5, -0x2C64($v1)
cop0    0x13D642D
bne     $gp, $t4, 0xFFFF9EF0
lh      $ra, 0x1819($s1)
sdl     $fp, -0x6474($t8)
jal     0x78C0050
ori     $v0, $s2, 0xC634
blez    $gp, 0xFFFEA9D4
swl     $t8, -0x2CD4($s2)
sltiu   $a1, $k0, 0x685
sdc1    $f15, 0x5964($at)
sw      $s0, -0x19A6($a1)
sltiu   $t6, $a3, -0x66AD
lb      $t7, -0x4F6($t3)
sd      $fp, 0x4B02($a1)

.byte 0x96
.byte 0x25  # %
.byte 0x4F  # O
.byte 0xEE

swl     $a0, -0x1AC9($k0)
lwc2    $4, 0x5199($ra)
bne     $a2, $a0, 0x17308

.byte 0xD1
.byte 0xBE
.byte 0x85
.byte 0x19

swc2    $8, 0x659D($a2)
swc1    $f8, -0x2691($s6)
sltiu   $s6, $t4, -0x2691
sh      $t9, -0x7992($t4)
bne     $v0, $t0, 0x163A4
sltiu   $a3, $t2, -0x60DF
lbu     $v0, -0x11A5($v1)
pref    0x1B, 0x362($gp)
pref    7, 0x3173($sp)
blez    $t1, 0xB678
swc1    $f3, flt_CE4($zero)
pref    0x11, -0x704D($t4)
ori     $k1, $s2, 0x1F67
swr     $s6, 0x7533($sp)
swc2    $15, -0x67F4($k0)
ldl     $s3, 0xF2($t7)
bne     $s7, $a3, 0xFFFE973C
sh      $s1, -0x11AA($a2)
bnel    $a1, $t6, 0xFFFE566C
sdr     $s1, -0x4D65($zero)
sd      $s2, -0x24D7($t8)
scd     $s4, 0x5C8D($t7)

.byte 0xA2
.byte 0xE8
.byte 0x5C  # \
.byte 0xED

bgtz    $t3, 0x189A0
sd      $t6, 0x5A2F($t9)
sdc2    $10, 0x3223($k1)
sb      $s3, 0x5744($t9)
lwr     $a2, 0x2C48($a0)
beql    $fp, $s2, 0xFFFF3258

同样重要的是要记住,巧妙地运用解压缩和解密技术(包括自修改),可能看起来像是一段不正确的反汇编代码,但是,它是能够正确运行的(注1)。

注1: 一段代码在经过压缩或者加密之后,他的机器码全都变乱了,因此,反汇编结果得到的是一段错误的反汇编代码。但是经过一段解压缩程序或者解密程序处理之后,它就能够还原出原来的机器码,因此反汇编出来的代码和运行结果都是正确的。

28.3 代码的熵值

(熵在理想的情况是,压缩(或加密)文件是每字节8位;如果每字节的熵为0,它可能是任意大小的空白文件) 在这里我们可以看到当CPU(ARM模式的ARM架构或者MIPS架构)采用4个字节的指令代码的时候,在这个意义上是最有效率的。

28.3.1 x86

Windows 2003系统下ntoskrnl.exe文件中的.text节表:熵 = 每字节6.662739位。 最佳的压缩方法将这个593920个字节大小的文件的大小减少16%。Windows 7 x64系统下ntoskrnl.exe文件中的.text节表:熵 = 每字节6.549586位。 最佳的压缩方法将这个1685504个字节大小的文件的大小减少18%。

28.3.2 ARM(Thumb模式)

AngryBirds Classic 熵 = 每字节7.058766位。 最佳的压缩方法将这个3336888个字节大小的文件的大小减少11%。

28.3.3 ARM(ARM模式)

Linux Kernel 3.8.8: 熵 = 每字节6.036160位。 最佳的压缩方法将这个6946037个字节大小的文件的大小减少24%。

28.3.4 MIPS(小端序)

Windows NT 4系统下user32.dll文件中的.text节表: 熵 = 每字节6.098227位。 最佳的压缩方法将这个433152个字节大小的文件的大小减少23%。 # 花指令

花指令是企图隐藏掉不想被逆向工程的代码块(或其它功能)的一种方法。

文本字符串

我发现在文本字符串使用可能会很有用,程序员意识某字符串不想被逆向工程的时候,可能会试图隐藏掉该字符串,让IDA或者其他十六进制编辑器无法找到。 这里说明一个简单的方法,那就是怎么去构造这样的字符串的实现方式:

mov byte ptr [ebx], ’h’
mov byte ptr [ebx+1], ’e’
mov byte ptr [ebx+2], ’l’
mov byte ptr [ebx+3], ’l’
mov byte ptr [ebx+4], ’o’
mov byte ptr [ebx+5], ’ ’
mov byte ptr [ebx+6], ’w’
mov byte ptr [ebx+7], ’o’
mov byte ptr [ebx+8], ’r’
mov byte ptr [ebx+9], ’l’
mov byte ptr [ebx+10], ’d’

当两个字符串进行比较的时候看起来是这样:

mov ebx, offset username
cmp byte ptr [ebx], ’j’
jnz fail
cmp byte ptr [ebx+1], ’o’
jnz fail
cmp byte ptr [ebx+2], ’h’
jnz fail
cmp byte ptr [ebx+3], ’n’
jnz fail
jz it_is_john

在这两种情况下,是不可能通过十六进制编辑器中找到这些字符串的。

顺便提一下,这种方法使得字符串不可能被分配到程序的代码段中。在某些场合可能会用到,比如,在PIC或者在shellcode中。

另一种方法是,我曾经看到用sprintf()构造字符串。

sprintf(buf, "%s%c%s%c%s", "hel",’l’,"o w",’o’,"rld");

代码看起来比较怪异,但是做为一个简单的防止逆向工程确实一个有用的方法。 文本字符串也可能存在于加密的形式,那么所有字符串在使用前比较闲将字符串解密了。

29.2 可执行代码

29.2.1

可执行代码花指令的意思是在真实的代码中插入一些垃圾代码,但是保证原有程序的执行正确。

举个简单的例子:

add eax, ebx
mul ecx

代码清单29.1: 花指令

xor esi, 011223344h ; garbage
add esi, eax ; garbage
add eax, ebx
mov edx, eax ; garbage
shl edx, 4 ; garbage
mul ecx
xor esi, ecx ; garbage

这里的花指令使用原程序代码中没有使用的寄存器(ESI和EDX)。无论如何,增加花指令之后,原有的汇编代码变得更为枯涩难懂,从而达到不轻易被逆向工程的效果。

29.2.2 替换与原有指令等价的指令

mov op1, op2可以替换为 push op2/pop op1这两条指令。
jmp label可以替换为 push label/ret这两条指令,IDA将不会显示被引用的label。
call label可以替换为push label_after_call_instruction/push label/ref这三条指令。
push op可以替换为 sub esp, 4(或者8)/mov [esp], op这两条指令。

29.2.3 绝对被执行的代码与绝对不被执行的代码

如果开发人员肯定ESI寄存器始终为0:

    mov esi, 1
    ... ; some code not touching ESI
    dec esi
    ... ; some code not touching ESI
    cmp esi, 0
    jz real_code
    ;fakeluggage
real_code:

逆向工程需要一段时间才能够执行到real_code。这也被称为opaque predicate。 另一个例子(同上,假设可以肯定ESI寄存器始终为0):

add eax, ebx ; real code
mul ecx ; real code
add eax, esi ; opaque predicate. XOR, AND or SHL, etc, can be here instead of ADD.

29.2.4打乱执行流程

举个例子,比如执行下面这三条指令:

instruction 1
instruction 2
instruction 3

可以被替换为:

begin: 
    jmp ins1_label
ins2_label: 
    instruction 2
    jmp ins3_label
ins3_label: 
    instruction 3
    jmp exit
ins1_label: 
    instruction 1
    jmp ins2_label
exit:

29.2.4使用间接指针

dummy_data1 db 100h dup (0)
message1 db ’hello world’,0

dummy_data2 db 200h dup (0)
message2 db ’another message’,0

func proc
    ...
    mov eax, offset dummy_data1 ; PE or ELF reloc here
    add eax, 100h
    push eax
    call dump_string
    ...
    mov eax, offset dummy_data2 ; PE or ELF reloc here
    add eax, 200h
    push eax
    call dump_string
    ...
func endp

IDA仅会显示dummy_data1和dummy_data2的引用,但无法引导到文本字符串,全局变量甚至是函数的访问方式都可能使用这种方法以达到混淆代码的目地。

29.3 虚拟机/伪代码

程序员可能写一个PL或者ISA来解释程序(例如Visual Basic 5.0与之前的版本, .NET, Java machine)。这使得逆向工程不得不花费更多的时间去了解这些语言它们的所有ISP指令详细信息。更有甚者,他们可能需要编写其中某些语言的反汇编器。

29.4 其它

我为TCC(Tiny C compiler)添加一个产生花指令功能的补丁:http://blog.yurichev.com/node/58。 # 16位Windows

16位windows程序现在很少见了,但是在旧式计算机或者入侵软件狗的时候(58章),我有时候还会遇到这个问题。 16位的windows版本最高到3.11,95(*注:作者笔误写成了Win96)/98/ME也支持16位代码,他们同时也是一个Windows NT家族的32位版本。64位版本的Windows NT家族完全不支持16位程序。 代码类似于MS-DOS代码。 执行文件并不是MZ式或者PE文件,而是NE式(所谓的“New Executable”,新执行程序)。 所有的例子都由OpenWatcom 1.9编译器编译,使用这些参数:

Wcl.exe -i=C:/WATCOM/h/win/ -s -os -bt=windows example.c

30.1 例子#1

#include <windows.h>
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    MessageBeep(MB_ICONEXCLAMATION);
    return 0;
};
WinMain proc near
    push bp
    mov bp, sp
    mov ax, 30h ; ’0’ ; MB_ICONEXCLAMATION constant
    push ax
    call MESSAGEBEEP
    xor ax, ax ; return 0
    pop bp
    retn 0Ah
WinMain endp

到现在为止,看起来都很简单。

30.2 例子#2

#include <windows.h>
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    MessageBox (NULL, "hello, world", "caption", MB_YESNOCANCEL);
    return 0;
};
WinMain proc near
        push bp
        mov bp, sp
        xor ax, ax ; NULL
        push ax
        push ds
        mov ax, offset aHelloWorld ; 0x18. "hello, world"
        push ax
        push ds
        mov ax, offset aCaption ; 0x10. "caption"
        push ax
        mov ax, 3 ; MB_YESNOCANCEL
        push ax
        call MESSAGEBOX
        xor ax, ax ; return 0
        pop bp
        retn 0Ah
WinMain endp
dseg02:0010 aCaption db ’caption’,0
dseg02:0018 aHelloWorld db ’hello, world’,0

有两个重要的信息:PASCAL调用转换表明先传递最后的参数(MB_YESNOCANCEL),然后才是第一个参数NULL。这个调用也表明了调用者恢复栈指针:因为RETN有一个0Ah的参数,这个意味着栈指针将在函数退出时上移10个字节。 指针按对传递:一组数据先传递,指针就在这组数据里面。例子这里只有一组数据,所以DS永远指向可执行文件的data段。

30.3 例子#3

#include <windows.h>
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    int result=MessageBox (NULL, "hello, world", "caption", MB_YESNOCANCEL);
    if (result==IDCANCEL)
        MessageBox (NULL, "you pressed cancel", "caption", MB_OK);
    else if (result==IDYES)
        MessageBox (NULL, "you pressed yes", "caption", MB_OK);
    else if (result==IDNO)
        MessageBox (NULL, "you pressed no", "caption", MB_OK);
    return 0;
};
WinMain proc near
        push bp
        mov bp, sp
        xor ax, ax ; NULL
        push ax
        push ds
        mov ax, offset aHelloWorld ; "hello, world"
        push ax
        push ds
        mov ax, offset aCaption ; "caption"
        push ax
        mov ax, 3 ; MB_YESNOCANCEL
        push ax
        call MESSAGEBOX
        cmp ax, 2 ; IDCANCEL
        jnz short loc_2F
        xor ax, ax
        push ax
        push ds
        mov ax, offset aYouPressedCanc ; "you pressed cancel"
        jmp short loc_49
        ; ---------------------------------------------------------------------------
        loc_2F:
        cmp ax, 6 ; IDYES
        jnz short loc_3D
        xor ax, ax
        push ax
        push ds
        mov ax, offset aYouPressedYes ; "you pressed yes"
        jmp short loc_49
        ; ---------------------------------------------------------------------------
        loc_3D:
        cmp ax, 7 ; IDNO
        jnz short loc_57
        xor ax, ax
        push ax
        push ds
        mov ax, offset aYouPressedNo ; "you pressed no"
        loc_49:
        push ax
        push ds
        mov ax, offset aCaption ; "caption"
        push ax
        xor ax, ax
        push ax
        call MESSAGEBOX
        loc_57:
        xor ax, ax
        pop bp
        retn 0Ah
WinMain endp

就是前一节的扩展而已。

30.4 例子#4

#include <windows.h>
int PASCAL func1 (int a, int b, int c)
{
    return a*b+c;
};
long PASCAL func2 (long a, long b, long c)
{
    return a*b+c;
};
long PASCAL func3 (long a, long b, long c, int d)
{
    return a*b+c-d;
};

int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    func1 (123, 456, 789);
    func2 (600000, 700000, 800000);
    func3 (600000, 700000, 800000, 123);
    return 0;
};
func1 proc near
    c = word ptr 4
    b = word ptr 6
    a = word ptr 8
    push bp
    mov bp, sp
    mov ax, [bp+a]
    imul [bp+b]
    add ax, [bp+c]
    pop bp
    retn 6
func1 endp
func2 proc near
    arg_0 = word ptr 4
    arg_2 = word ptr 6
    arg_4 = word ptr 8
    arg_6 = word ptr 0Ah
    arg_8 = word ptr 0Ch
    arg_A = word ptr 0Eh
    push bp
    mov bp, sp
    mov ax, [bp+arg_8]
    mov dx, [bp+arg_A]
    mov bx, [bp+arg_4]
    mov cx, [bp+arg_6]
    call sub_B2 ; long 32-bit multiplication
    add ax, [bp+arg_0]
    adc dx, [bp+arg_2]
    pop bp
    retn 12
func2 endp
func3 proc near
    arg_0 = word ptr 4
    arg_2 = word ptr 6
    arg_4 = word ptr 8
    arg_6 = word ptr 0Ah
    arg_8 = word ptr 0Ch
    arg_A = word ptr 0Eh
    arg_C = word ptr 10h
    push bp
    mov bp, sp
    mov ax, [bp+arg_A]
    mov dx, [bp+arg_C]
    mov bx, [bp+arg_6]
    mov cx, [bp+arg_8]
    call sub_B2 ; long 32-bit multiplication
    mov cx, [bp+arg_2]
    add cx, ax
    mov bx, [bp+arg_4]
    adc bx, dx ; BX=high part, CX=low part
    mov ax, [bp+arg_0]
    cwd ; AX=low part d, DX=high part d
    sub cx, ax
    mov ax, cx
    sbb bx, dx
    mov dx, bx
    pop bp
    retn 14
    func3 endp
    WinMain proc near
    push bp
    mov bp, sp
    mov ax, 123
    push ax
    mov ax, 456
    push ax
    mov ax, 789
    push ax
    call func1
    mov ax, 9 ; high part of 600000
    push ax
    mov ax, 27C0h ; low part of 600000
    push ax
    mov ax, 0Ah ; high part of 700000
    push ax
    mov ax, 0AE60h ; low part of 700000
    push ax
    mov ax, 0Ch ; high part of 800000
    push ax
    mov ax, 3500h ; low part of 800000
    push ax
    call func2
    mov ax, 9 ; high part of 600000
    push ax
    mov ax, 27C0h ; low part of 600000
    push ax
    mov ax, 0Ah ; high part of 700000
    push ax
    mov ax, 0AE60h ; low part of 700000
    push ax
    mov ax, 0Ch ; high part of 800000
    push ax
    mov ax, 3500h ; low part of 800000
    push ax
    mov ax, 7Bh ; 123
    push ax
    call func3
    xor ax, ax ; return 0
    pop bp
    retn 0Ah
WinMain endp

32位的值(long数据类型代表32位,int代表16位数据)在16位模式下(MSDOS和win16)都会按对传递,就像64位数据在32位环境下使用的方式一样(21章)。

Sub_B2在这里是一个编译器生成的库函数,他的作用是“long乘法”,例如两个32位类型想成,其他的编译器函数列在了附录E, D.中。 ADD/ADC指令对用来相加两个值:ADD将设置/清空CF进位标识,ADC将会使用它。 SUB/SBB将会做减法,SUB会设置/清空CF标识位,SBB将会使用它。 32位值按照DX:AX寄存器对返回。 常数同样在WinMain()中按照值对的方式传递。 Int类型的123常量首先被转为32位的值,使用的是CWD指令。

30.5 例子#5

#include <windows.h>
int PASCAL string_compare (char *s1, char *s2)
{
    while (1)
    {
        if (*s1!=*s2)
            return 0;
        if (*s1==0 || *s2==0)
            return 1; // end of string
        s1++;
        s2++;
    };
};

int PASCAL string_compare_far (char far *s1, char far *s2)
{
    while (1)
    {
        if (*s1!=*s2)
            return 0;
        if (*s1==0 || *s2==0)
            return 1; // end of string
        s1++;
        s2++;
    };
};
void PASCAL remove_digits (char *s)
{
    while (*s)
    {
        if (*s>=’0’ && *s<=’9’)
            *s=’-’;
        s++;
    };
};

char str[]="hello 1234 world";
int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    string_compare ("asd", "def");
    string_compare_far ("asd", "def");
    remove_digits (str);
    MessageBox (NULL, str, "caption", MB_YESNOCANCEL);
    return 0;
};
string_compare proc near
arg_0 = word ptr 4
arg_2 = word ptr 6
push bp
mov bp, sp
push si
mov si, [bp+arg_0]
mov bx, [bp+arg_2]
loc_12: ; CODE XREF: string_compare+21j
mov al, [bx]
cmp al, [si]
jz short loc_1C
xor ax, ax
jmp short loc_2B
; ---------------------------------------------------------------------------
loc_1C: ; CODE XREF: string_compare+Ej
test al, al
jz short loc_22
jnz short loc_27
loc_22: ; CODE XREF: string_compare+16j
mov ax, 1
jmp short loc_2B
; ---------------------------------------------------------------------------
loc_27: ; CODE XREF: string_compare+18j
inc bx
inc si
jmp short loc_12
; ---------------------------------------------------------------------------
loc_2B: ; CODE XREF: string_compare+12j
; string_compare+1Dj
pop si
pop bp
retn 4
string_compare endp
string_compare_far proc near ; CODE XREF: WinMain+18p
arg_0 = word ptr 4
arg_2 = word ptr 6
arg_4 = word ptr 8
arg_6 = word ptr 0Ah
push bp
mov bp, sp
push si
mov si, [bp+arg_0]
mov bx, [bp+arg_4]
loc_3A: ; CODE XREF: string_compare_far+35j
mov es, [bp+arg_6]
mov al, es:[bx]
mov es, [bp+arg_2]
cmp al, es:[si]
jz short loc_4C
xor ax, ax
jmp short loc_67
; ---------------------------------------------------------------------------
loc_4C: ; CODE XREF: string_compare_far+16j
mov es, [bp+arg_6]
cmp byte ptr es:[bx], 0
jz short loc_5E
mov es, [bp+arg_2]
cmp byte ptr es:[si], 0
jnz short loc_63
loc_5E: ; CODE XREF: string_compare_far+23j
mov ax, 1
jmp short loc_67
; ---------------------------------------------------------------------------
loc_63: ; CODE XREF: string_compare_far+2Cj
inc bx
inc si
jmp short loc_3A
; ---------------------------------------------------------------------------
loc_67: ; CODE XREF: string_compare_far+1Aj
; string_compare_far+31j
pop si
pop bp
retn 8
string_compare_far endp
remove_digits proc near ; CODE XREF: WinMain+1Fp
arg_0 = word ptr 4
push bp
mov bp, sp
mov bx, [bp+arg_0]
loc_72: ; CODE XREF: remove_digits+18j
mov al, [bx]
test al, al
jz short loc_86
cmp al, 30h ; ’0’
jb short loc_83
cmp al, 39h ; ’9’
ja short loc_83
mov byte ptr [bx], 2Dh ; ’-’
loc_83: ; CODE XREF: remove_digits+Ej
; remove_digits+12j
inc bx
jmp short loc_72
; ---------------------------------------------------------------------------
loc_86: ; CODE XREF: remove_digits+Aj
pop bp
retn 2
remove_digits endp
WinMain proc near ; CODE XREF: start+EDp
push bp
mov bp, sp
mov ax, offset aAsd ; "asd"
push ax
mov ax, offset aDef ; "def"
push ax
call string_compare
push ds
mov ax, offset aAsd ; "asd"
push ax
push ds
mov ax, offset aDef ; "def"
push ax
call string_compare_far
mov ax, offset aHello1234World ; "hello 1234 world"
push ax
call remove_digits
xor ax, ax
push ax
push ds
mov ax, offset aHello1234World ; "hello 1234 world"
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
mov ax, 3 ; MB_YESNOCANCEL
push ax
call MESSAGEBOX
xor ax, ax
pop bp
retn 0Ah
WinMain endp

我们可以看到所谓的“near”指针和“far”指针:另一个奇怪的16位8086现象。 可以在70章继续读到相关内容。 近指针就是那些指向当前数据段内的指针。因为,string_compare()函数仅仅用到2个16位指针,而且访问数据通过DS指向了它(mov al, es:[bx])。远指针也同样在我的16位MessageBox()例子里面:见30.2节。 因此,在访问文本时,Windows内核并不关心使用那个数据段,所以它需要更完整的信息。 使用这种区别的原因可能是因为紧凑的程序可能使用仅仅一个64kb的数据段。所以他并不需要传递地址的高位数据,因为它们永远是不变的。大一点的程序可能会使用多个64kb数据段,所以它们每次操作都需要需要区分它们是在哪个数据段里面。 对代码段来说也是相同的故事,比较短小的程序可能在64k的数据段里面包含有所有的可执行代码,然后所有的函数都会由CALL NEAR来调用,代码使用RETN返回。但是,如果有多个代码段的话,函数地址就会按对区分,然后使用CALL FAR来调用,代码会使用RETF返回。 这就是在编译器中指定“内存模型”会发生的事情。 MS-DOS和Win16编译器针对每个内存模型都有有特别的库:它们会因为数据和代码的不同的指针模型而不同。

30.6 例子#6

#include <windows.h>
#include <time.h>
#include <stdio.h>
char strbuf[256];

int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    struct tm *t;
    time_t unix_time;
    unix_time=time(NULL);
    t=localtime (&unix_time);
    sprintf (strbuf, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year+1900, t->tm_mon, t->tm_mday,
    t->tm_hour, t->tm_min, t->tm_sec);
    MessageBox (NULL, strbuf, "caption", MB_OK);
    return 0;
};
WinMain proc near
var_4 = word ptr -4
var_2 = word ptr -2
push bp
mov bp, sp
push ax
push ax
xor ax, ax
call time_
mov [bp+var_4], ax ; low part of UNIX time
mov [bp+var_2], dx ; high part of UNIX time
lea ax, [bp+var_4] ; take a pointer of high part
call localtime_
mov bx, ax ; t
push word ptr [bx] ; second
push word ptr [bx+2] ; minute
push word ptr [bx+4] ; hour
push word ptr [bx+6] ; day
push word ptr [bx+8] ; month
mov ax, [bx+0Ah] ; year
add ax, 1900
push ax
mov ax, offset a04d02d02d02d02 ; "%04d-%02d-%02d %02d:%02d:%02d"
push ax
mov ax, offset strbuf
push ax
call sprintf_
add sp, 10h
xor ax, ax ; NULL
push ax
push ds
mov ax, offset strbuf
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax ; MB_OK
push ax
call MESSAGEBOX
xor ax, ax
mov sp, bp
pop bp
retn 0Ah
WinMain endp

UNIX时间是32位的,所以它返回在DX:AX寄存器对中,而且将他们存储到两个本地16位变量中。然后一个指向值对的指针会被当作参数传给localtime()函数。Localtime()函数有一个struct tm,它将通过C库分配内存,所以只有指向它的指针返回了。顺便一提,这也意味着在它的结果被使用之前,函数不能被再次调用。 对time()和localtime()两个函数来说,Watcom调用转换将会在这里:前四个参数使用AX、DX、BX、CX传递,剩余的通过栈来传递。使用这个转换的函数也会在名字最后使用下划线来标记。 Sprintf()并不使用PASCAL调用转换,也不会使用watcom转换,所以参数将使用寻常的cdecl方式传递(47.1节)。

30.6.1 全局变量

这里用同样的例子,但是变量是全局变量:

#include <windows.h>
#include <time.h>
#include <stdio.h>
char strbuf[256];
struct tm *t;
time_t unix_time;

int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    unix_time=time(NULL);
    t=localtime (&unix_time);
    sprintf (strbuf, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year+1900, t->tm_mon, t->tm_mday,
    t->tm_hour, t->tm_min, t->tm_sec);
    MessageBox (NULL, strbuf, "caption", MB_OK);
    return 0;
};
unix_time_low dw 0
unix_time_high dw 0
t dw 0
WinMain proc near
push bp
mov bp, sp
xor ax, ax
call time_
mov unix_time_low, ax
mov unix_time_high, dx
mov ax, offset unix_time_low
call localtime_
mov bx, ax
mov t, ax ; will not be used in future...
push word ptr [bx] ; seconds
push word ptr [bx+2] ; minutes
push word ptr [bx+4] ; hour
push word ptr [bx+6] ; day
push word ptr [bx+8] ; month
mov ax, [bx+0Ah] ; year
add ax, 1900
push ax
mov ax, offset a04d02d02d02d02 ; "%04d-%02d-%02d %02d:%02d:%02d"
push ax
mov ax, offset strbuf
push ax
call sprintf_
add sp, 10h
xor ax, ax ; NULL
push ax
push ds
mov ax, offset strbuf
push ax
push ds
mov ax, offset aCaption ; "caption"
push ax
xor ax, ax ; MB_OK
push ax
call MESSAGEBOX
xor ax, ax ; return 0
pop bp
retn 0Ah
WinMain endp

T不会被使用,但是编译器还是用代码存储了这个值。因为他并不确定,也许这个值会在某个地方被用到。 # 类

31.1

举个简单的例子。

在程序内部,C++类的表示基本和结构体一样。让我们试试这个有2个变量,2个构造函数和1个方法的类。

#include <stdio.h>
class c
{
private:
    int v1;
    int v2;
public:
    c() // default ctor
    {
        v1=667;
        v2=999;
    };
    c(int a, int b) // ctor
    {
        v1=a;
        v2=b;
    };
    void dump()
    {
        printf ("%d; %d", v1, v2);
    };
};

int main()
{
    class c c1;
    class c c2(5,6);
    c1.dump();
    c2.dump();
    return 0;
};

31.1.1 MSVC-X86

这里可以看到main()函数是如何被翻译成汇编代码的:

_c2$ = -16 ; size = 8
_c1$ = -8 ; size = 8
_main PROC
push ebp
mov ebp, esp
sub esp, 16 ; 00000010H
lea ecx, DWORD PTR _c1$[ebp]
call ??0c@@QAE@XZ ; c::c
push 6
push 5
lea ecx, DWORD PTR _c2$[ebp]
call ??0c@@QAE@HH@Z ; c::c
lea ecx, DWORD PTR _c1$[ebp]
call ?dump@c@@QAEXXZ ; c::dump
lea ecx, DWORD PTR _c2$[ebp]
call ?dump@c@@QAEXXZ ; c::dump
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP

所以,发生什么了。对每个对象来说(而不是类c),会分配8个字节。这正好是2个变量存储所需的大小。 对c1来说一个默认的无参数构造函数??0c@@QAE@XZ会被调用。对c2来说另一个??0c@@QAE@HH@Z会被调用,有两个数字会被作为参数传递。 指向对象的指针(C++术语的“this”)会被通过ECX寄存器传递。这被叫做thiscall(31.1.1)–这是一个指向对象的指针传递方式。 MSVC使用ECX来传递它。无需说明的是,它并不是一个标准化的方法,其他编译器可能用其他方法,例如通过第一个函数参数,比如GCC就是这么做的。 为什么函数的名字这么奇怪?这是因为名字打碎方式的缘故。 C++类可能有多个同名的重载函数,因此,不同的类也可能有相同的函数名。 名字打碎可以把类的类名+函数名+参数类型编码到一个字符串里面,然后它就会被用作内部名称。这完全是因为编译器和DLL OS加载器都 不知道C++或者面向对象的缘故。 Dump()函数在之后被调用了2次。 让我们看看构造函数的代码。

_this$ = -4 ; size = 4
??0c@@QAE@XZ PROC ; c::c, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov DWORD PTR [eax], 667 ; 0000029bH
mov ecx, DWORD PTR _this$[ebp]
mov DWORD PTR [ecx+4], 999 ; 000003e7H
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 0


??0c@@QAE@XZ ENDP ; c::c
_this$ = -4 ; size = 4
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
??0c@@QAE@HH@Z PROC ; c::c, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR _a$[ebp]
mov DWORD PTR [eax], ecx
mov edx, DWORD PTR _this$[ebp]
mov eax, DWORD PTR _b$[ebp]
mov DWORD PTR [edx+4], eax
mov eax, DWORD PTR _this$[ebp]
mov esp, ebp
pop ebp
ret 8
??0c@@QAE@HH@Z ENDP ; c::c

构造函数只是函数,它们会使用ECX中存储的指向结构体的指针,然后把指针指向自己的本地变量,但是,这个操作并不是必须的。 对C++标准来说我们知道构造函数不应该返回任何值。事实上,构造函数会返回指向新创建对象的指针,比如“this”。 现在看看dump()函数:

_this$ = -4 ; size = 4
?dump@c@@QAEXXZ PROC ; c::dump, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR [eax+4]
push ecx
mov edx, DWORD PTR _this$[ebp]
mov eax, DWORD PTR [edx]
push eax
push OFFSET ??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@
call _printf
add esp, 12 ; 0000000cH
mov esp, ebp
pop ebp
ret 0
?dump@c@@QAEXXZ ENDP ; c::dump

简单的可以:dump()会把带有2个int的结构体传给ecx,然后从他里面取出2个值,然后传给printf()。 如果使用/Ox优化,代码会更短。

??0c@@QAE@XZ PROC ; c::c, COMDAT
; _this$ = ecx
mov eax, ecx
mov DWORD PTR [eax], 667 ; 0000029bH
mov DWORD PTR [eax+4], 999 ; 000003e7H
ret 0
??0c@@QAE@XZ ENDP ; c::c
_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
??0c@@QAE@HH@Z PROC ; c::c, COMDAT
; _this$ = ecx
mov edx, DWORD PTR _b$[esp-4]
mov eax, ecx
mov ecx, DWORD PTR _a$[esp-4]
mov DWORD PTR [eax], ecx
mov DWORD PTR [eax+4], edx
ret 8
??0c@@QAE@HH@Z ENDP ; c::c
?dump@c@@QAEXXZ PROC ; c::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push eax
push ecx
push OFFSET ??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@
call _printf
add esp, 12 ; 0000000cH
ret 0
?dump@c@@QAEXXZ ENDP ; c::dump

还要说的就是栈指针在调用add esp ,x之后并不正确。所以构造函数还需要ret 8来返回,而不是ret。 这是因为这儿调用方式是thiscall(31.1.1),这个方法会使用栈来传递参数,和stdcall对比(47.2)来看,他将为被调用者维护正确的栈,而不是调用者。Ret x指令会额外的给esp加上x,然后会把控制流交还给调用者函数。 调用转换见47章。 还有需要注意的是,编译器会决定什么时候调用构造函数什么时候调用析构函数,但是我们从c++语言基础里面已经知道调用时机了。

31.1.2 MSVC-x86-64

像我们已经知道的那样,x86-64中前4个函数参数是通过RCX/RDX/R8/R9寄存器传递的,剩余的通过栈传递。但是this是用RCX传递的 ,而第一个函数参数是从RDX开始传递的。我们可以通过c(int a, int b)这个函数看出来。

; void dump()
?dump@c@@QEAAXXZ PROC ; c::dump
mov r8d, DWORD PTR [rcx+4]
mov edx, DWORD PTR [rcx]
lea rcx, OFFSET FLAT:??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@ ; ’%d; %d’
jmp printf
?dump@c@@QEAAXXZ ENDP ; c::dump
; c(int a, int b)
??0c@@QEAA@HH@Z PROC ; c::c
mov DWORD PTR [rcx], edx ; 1st argument: a
mov DWORD PTR [rcx+4], r8d ; 2nd argument: b
mov rax, rcx
ret 0
??0c@@QEAA@HH@Z ENDP ; c::c
; default ctor
??0c@@QEAA@XZ PROC ; c::c
mov DWORD PTR [rcx], 667 ; 0000029bH
mov DWORD PTR [rcx+4], 999 ; 000003e7H
mov rax, rcx
ret 0
??0c@@QEAA@XZ ENDP ; c::c

X64中,Int数据类型依然是32位的。所以这里也使用了32位寄存器部分。 我们还可以看到dump()里的JMP printf,而不是RET,这个技巧我们已经在11.1.1里面见过了。

31.1.3 GCC-x86

几乎和GCC4.4.1一样的结果,除了几个例外。

public main
main proc near ; DATA XREF: _start+17
var_20 = dword ptr -20h
var_1C = dword ptr -1Ch
var_18 = dword ptr -18h
var_10 = dword ptr -10h
var_8 = dword ptr -8
push ebp
mov ebp, esp
and esp, 0FFFFFFF0h
sub esp, 20h
lea eax, [esp+20h+var_8]
mov [esp+20h+var_20], eax
call _ZN1cC1Ev
mov [esp+20h+var_18], 6
mov [esp+20h+var_1C], 5
lea eax, [esp+20h+var_10]
mov [esp+20h+var_20], eax
call _ZN1cC1Eii
lea eax, [esp+20h+var_8]
mov [esp+20h+var_20], eax
call _ZN1c4dumpEv
lea eax, [esp+20h+var_10]
mov [esp+20h+var_20], eax
call _ZN1c4dumpEv
mov eax, 0
leave
retn
main endp

我们可以看到另一个命名破碎模式,这个GNU特殊的模式可以看到指向对象的this时针其实是作为函数的第一个参数被传入的,当然,这个对程序员来说是透明的。 第一个构造函数:

public _ZN1cC1Ev ; weak
_ZN1cC1Ev proc near ; CODE XREF: main+10
arg_0 = dword ptr 8
push ebp
mov ebp, esp
mov eax, [ebp+arg_0]
mov dword ptr [eax], 667
mov eax, [ebp+arg_0]
mov dword ptr [eax+4], 999
pop ebp
retn
_ZN1cC1Ev endp

他所做的无非就是使用第一个传来的参数写入两个数字。 第二个构造函数:

public _ZN1cC1Eii
_ZN1cC1Eii proc near
arg_0 = dword ptr 8
arg_4 = dword ptr 0Ch
arg_8 = dword ptr 10h
push ebp
mov ebp, esp
mov eax, [ebp+arg_0]
mov edx, [ebp+arg_4]
mov [eax], edx
mov eax, [ebp+arg_0]
mov edx, [ebp+arg_8]
mov [eax+4], edx
pop ebp
retn
_ZN1cC1Eii endp

这是个函数,原型类似于:

void ZN1cC1Eii (int *obj, int a, int b)
{
    *obj=a;
    *(obj+1)=b;
};

这是完全可以预测到的,现在看看dump():

public _ZN1c4dumpEv
_ZN1c4dumpEv proc near
var_18 = dword ptr -18h
var_14 = dword ptr -14h
var_10 = dword ptr -10h
arg_0 = dword ptr 8
push ebp
mov ebp, esp
sub esp, 18h
mov eax, [ebp+arg_0]
mov edx, [eax+4]
mov eax, [ebp+arg_0]
mov eax, [eax]
mov [esp+18h+var_10], edx
mov [esp+18h+var_14], eax
mov [esp+18h+var_18], offset aDD ; "%d; %d
"
call _printf
leave
retn
_ZN1c4dumpEv endp

在这个函数的内部表达中有一个单独的参数,被用作指向当前对象,也即this。 因此,如果从这些简单的例子来看,MSVC和GCC的区别也就只有函数名编码的区别和传入this指针的区别(ECX寄存器或通过第一个参数)。

31.1.14 GCC-X86-64

前6个参数,会通过RDI/RSI/RDX/RCX/R8/R9[21章]的顺序传递,this指针会通过第一个RDI来传递,我们可以接着看到。 Int数据类型也是一个32位的数据,JMP替换RET的技巧这里也用到了。

; default ctor
_ZN1cC2Ev:
mov DWORD PTR [rdi], 667
mov DWORD PTR [rdi+4], 999
ret
; c(int a, int b)
_ZN1cC2Eii:
mov DWORD PTR [rdi], esi
mov DWORD PTR [rdi+4], edx
ret
; dump()
_ZN1c4dumpEv:
mov edx, DWORD PTR [rdi+4]
mov esi, DWORD PTR [rdi]
xor eax, eax
mov edi, OFFSET FLAT:.LC0 ; "%d; %d
"
jmp printf

31.2 类继承

可以说关于类继承就是我们已经研究了的这个结构体,但是它现在扩展成类了。 让我们看个简单的例子:

#include <stdio.h>
class object
{
public:
int color;
object() { };
object (int color) { this->color=color; };
void print_color() { printf ("color=%d
", color); };
};
class box : public object
{
private:
int width, height, depth;
public:
box(int color, int width, int height, int depth)
{
this->color=color;
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. color=%d, width=%d, height=%d, depth=%d
", color, width,
height, depth);
};
};
class sphere : public object
{
private:
    int radius;
public:
    sphere(int color, int radius)
    {
        this->color=color;
        this->radius=radius;
    };
void dump()
{
    printf ("this is sphere. color=%d, radius=%d", color, radius);};
};
int main()
{
    box b(1, 10, 20, 30);
    sphere s(2, 40);
    b.print_color();
    s.print_color();
    b.dump();
    s.dump();
    return 0;
};

让我们观察一下生成的dump()的代码和object::print_color(),让我们看看结构体对象的内存输出(作为32位代码) 所以,dump()方法其实是对应了好几个类,下面代码由MSVC 2008生成(/Ox+/Ob0) 优化的MSVC 2008 /Ob0

??_C@_09GCEDOLPA@color?$DN?$CFd?6?$AA@ DB ’color=%d’, 0aH, 00H ; ‘string’
?print_color@object@@QAEXXZ PROC ; object::print_color, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx]
push eax
; ’color=%d’, 0aH, 00H
push OFFSET ??_C@_09GCEDOLPA@color?$DN?$CFd?6?$AA@
call _printf
add esp, 8
ret 0
?print_color@object@@QAEXXZ ENDP ; object::print_color

优化的MSVC2008 /Ob0

?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
; ’this is box. color=%d, width=%d, height=%d, depth=%d’, 0aH, 00H ; ‘string’
push OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
call _printf
add esp, 20 ; 00000014H
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump

?dump@sphere@@QAEXXZ PROC ; sphere::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push eax
push ecx
; ’this is sphere. color=%d, radius=%d’, 0aH, 00H
push OFFSET ??_C@_0CF@EFEDJLDC@this?5is?5sphere?4?5color?$DN?$CFd?0?5radius@
call _printf
add esp, 12 ; 0000000cH
ret 0
?dump@sphere@@QAEXXZ ENDP ; sphere::dump

所以,这就是他的内存暑促后:(基类对象)

继承的对象 Box:

Sphere:

让我们看看main()函数体:

PUBLIC _main
_TEXT SEGMENT
_s$ = -24 ; size = 8
_b$ = -16 ; size = 16
_main PROC
sub esp, 24 ; 00000018H
push 30 ; 0000001eH
push 20 ; 00000014H
push 10 ; 0000000aH
push 1
lea ecx, DWORD PTR _b$[esp+40]
call ??0box@@QAE@HHHH@Z ; box::box
push 40 ; 00000028H
push 2
lea ecx, DWORD PTR _s$[esp+32]
call ??0sphere@@QAE@HH@Z ; sphere::sphere
lea ecx, DWORD PTR _b$[esp+24]
call ?print_color@object@@QAEXXZ ; object::print_color
lea ecx, DWORD PTR _s$[esp+24]
call ?print_color@object@@QAEXXZ ; object::print_color
lea ecx, DWORD PTR _b$[esp+24]
call ?dump@box@@QAEXXZ ; box::dump
lea ecx, DWORD PTR _s$[esp+24]
call ?dump@sphere@@QAEXXZ ; sphere::dump
xor eax, eax
add esp, 24 ; 00000018H
ret 0
_main ENDP

继承的类必须永远将它们的范围添加到基类的范围中,所以这样可以让基类的方法对其范围生效。 当object::print_color()方法被调用时,会有一个指针指向box对象和sphere对象会被传递进去,它就是“this”。它可以和这些对象简单的互动,因为color域指向的永远是固定的地址(+0x00偏移)。 可以说,object::print_color()方法对于输入对象类型来说是不可知的,如果你创建一个继承类,例如继承了box类编译器会自动在depth域之后加上新域,而把box的类域固定在一个固定的位置。 因此,box::dump()方法会在访问color/width/height/depths的时候顺利工作,因为地址的固定,它会很容易的知道偏移。 GCC生成的代码基本一样,只有一个不一样的就是this的传递,就像之前说的一样,它是作为第一个参数传递的,而不是通过ECX传递的。

31.3 封装

封装是一个把数据装在类的private域里面的动作,这样会让它们只能从类的内部被访问到,而从外面访问不到。 但是,生成的代码里面是否有什么东西指示一个变量是private呢? 没有,让我们看看简单的例子:

#include <stdio.h>
class box
{
private:
int color, width, height, depth;
public:
box(int color, int width, int height, int depth)
{
this->color=color;
this->width=width;
this->height=height;
this->depth=depth;
};
void dump()
{
printf ("this is box. color=%d, width=%d, height=%d, depth=%d
", color, width,
height, depth);
};
};

在MSVC 2008+/Ox和/Ob0选项,然后看看box::dump()代码:

?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
; ’this is box. color=%d, width=%d, height=%d, depth=%d’, 0aH, 00H
push OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
call _printf
add esp, 20 ; 00000014H
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump

这就是类的内存分布:

所有域都不允许其他类的访问,但是,我们知道这个存放方式之后是否可以修改这些域? 所以我加了hack_oop_encapsulation()函数,假设他有这个代码,当然我们没有编译:

void hack_oop_encapsulation(class box * o)
{
    o->width=1; // that code can’t be compiled: "error C2248: ’box::width’ : cannot access
    private member declared in class ’box’"
};

还有,如果要转换box的类型,把它从指针转为int数组,然后如果我们能修改这些数字,那么我们就成功了。

void hack_oop_encapsulation(class box * o)
{
    unsigned int *ptr_to_object=reinterpret_cast<unsigned int*>(o);
    ptr_to_object[1]=123;
};

这个函数的代码非常简单,剋说函数指示把指针指向这些int,然后把123写入第二个int:

?hack_oop_encapsulation@@YAXPAVbox@@@Z PROC ; hack_oop_encapsulation
mov eax, DWORD PTR _o$[esp-4]
mov DWORD PTR [eax+4], 123 ; 0000007bH
ret 0
?hack_oop_encapsulation@@YAXPAVbox@@@Z ENDP ; hack_oop_encapsulation

看看它是怎么工作的:

int main()
{
    box b(1, 10, 20, 30);
    b.dump();
    hack_oop_encapsulation(&b);
    b.dump();
    return 0;
};

运行后:

this is box. color=1, width=10, height=20, depth=30
this is box. color=1, width=123, height=20, depth=30

可以看到,private只是在编译阶段被保护了,c++编译器不会允许其他代码修改private域下的内容,但是如果用一些技巧,就可以修改private的值。

31.4 多重继承

多重继承是一个类的创建,这个类会从2个或多个类里面继承函数和成员。 看一个简单的例子:

#include <stdio.h>
class box
{
public:
    int width, height, depth;
    box() { };
    box(int width, int height, int depth)
    {
        this->width=width;
        this->height=height;
        this->depth=depth;
    };
    void dump()
    {
        printf ("this is box. width=%d, height=%d, depth=%d", width, height, depth);
    };
    int get_volume()
    {
        return width * height * depth;
    };
};
class solid_object
{
public:
    int density;
    solid_object() { };
    solid_object(int density)
    {
        this->density=density;
    };
    int get_density()
    {
        return density;
    };
    void dump()
    {
        printf ("this is solid_object. density=%d", density);
    };
};
class solid_box: box, solid_object
{
public:
    solid_box (int width, int height, int depth, int density)
    {
        this->width=width;
        this->height=height;
        this->depth=depth;
        this->density=density;
    };
    void dump()
    {
        printf ("this is solid_box. width=%d, height=%d, depth=%d, density=%d", width, height, depth, density);
    };
    int get_weight() { return get_volume() * get_density(); };
};
int main()
{
    box b(10, 20, 30);
    solid_object so(100);
    solid_box sb(10, 20, 30, 3);
    b.dump();
    so.dump();
    sb.dump();
    printf ("%d", sb.get_weight());
    return 0;
};

让我们在MSVC 2008中用/Ox和/Ob0选项来编译,然后看看box::dump()、solid_object::dump()和solid_box::dump()的函数代码:

?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+8]
mov edx, DWORD PTR [ecx+4]
push eax
mov eax, DWORD PTR [ecx]
push edx
push eax
; ’this is box. width=%d, height=%d, depth=%d’, 0aH, 00H
push OFFSET ??_C@_0CM@DIKPHDFI@this?5is?5box?4?5width?$DN?$CFd?0?5height?$DN?$CFd@
call _printf
add esp, 16 ; 00000010H
ret 0
?dump@box@@QAEXXZ ENDP ; box::dump

?dump@solid_object@@QAEXXZ PROC ; solid_object::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx]
push eax
; ’this is solid_object. density=%d’, 0aH
push OFFSET ??_C@_0CC@KICFJINL@this?5is?5solid_object?4?5density?$DN?$CFd@
call _printf
add esp, 8
ret 0
?dump@solid_object@@QAEXXZ ENDP ; solid_object::dump

?dump@solid_box@@QAEXXZ PROC ; solid_box::dump, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+12]
mov edx, DWORD PTR [ecx+8]
push eax
mov eax, DWORD PTR [ecx+4]
mov ecx, DWORD PTR [ecx]
push edx
push eax
push ecx
; ’this is solid_box. width=%d, height=%d, depth=%d, density=%d’, 0aH
push OFFSET ??_C@_0DO@HNCNIHNN@this?5is?5solid_box?4?5width?$DN?$CFd?0?5hei@
call _printf
add esp, 20 ; 00000014H
ret 0
?dump@solid_box@@QAEXXZ ENDP ; solid_box::dump

所以,这三个类的内存分布是:

Box:

Solid_object:

可以说,solid_box的类内存空间就是它们的组合:

Box::get_volume()和solid_object::get_density()函数的代码如下:

?get_volume@box@@QAEHXZ PROC ; box::get_volume, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx+8]
imul eax, DWORD PTR [ecx+4]
imul eax, DWORD PTR [ecx]
ret 0
?get_volume@box@@QAEHXZ ENDP ; box::get_volume

?get_density@solid_object@@QAEHXZ PROC ; solid_object::get_density, COMDAT
; _this$ = ecx
mov eax, DWORD PTR [ecx]
ret 0
?get_density@solid_object@@QAEHXZ ENDP ; solid_object::get_density

但是solid_box::get_weight()的代码更有趣:

?get_weight@solid_box@@QAEHXZ PROC ; solid_box::get_weight, COMDAT
; _this$ = ecx
push esi
mov esi, ecx
push edi
lea ecx, DWORD PTR [esi+12]
call ?get_density@solid_object@@QAEHXZ ; solid_object::get_density
mov ecx, esi
mov edi, eax
call ?get_volume@box@@QAEHXZ ; box::get_volume
imul eax, edi
pop edi
pop esi
ret 0
?get_weight@solid_box@@QAEHXZ ENDP ; solid_box::get_weight

Get_weight()函数只会调用2个函数,但是对于get_volume()来说,他只是传递指针给this,对get_density()来说,他指示传递指针给this,同时移位12(0xC)字节,然后在solid_box类的内存空间理,solid_object类开始了。 因此,solid_object::get_density()方法相信它正在处理普通的solid_object类,而且box::get_volume类将对它的3个域生效,而且相信这是普通的box类对象。 因此,我们可以说,类的一个对象,是从多个其他类继承阿日来,在内存中代表着组合起来的类,因为它有所有继承来的域。每个继承的方法都会又一个指向对应结构部分的指针来处理。

31.5 虚函数

还有一个简单的例子:

#include <stdio.h>
class object
{
public:
    int color;
    object() { };
    object (int color) { this->color=color; };
    virtual void dump()
    {
        printf ("color=%d", color);
    };
};
class box : public object
{
private:
    int width, height, depth;
public:
    box(int color, int width, int height, int depth)
    {
        this->color=color;
        this->width=width;
        this->height=height;
        this->depth=depth;
    };
    void dump()
    {
        printf ("this is box. color=%d, width=%d, height=%d, depth=%d", color, width,height, depth);
    };
};
class sphere : public object
{
private:
    int radius;
public:
    sphere(int color, int radius)
    {
        this->color=color;
        this->radius=radius;
    };
void dump()
{
    printf ("this is sphere. color=%d, radius=%d", color, radius);};
};
int main()
{
    box b(1, 10, 20, 30);
    sphere s(2, 40);
    object *o1=&b;
    object *o2=&s;
    o1->dump();
    o2->dump();
    return 0;
};

类object有一个虚函数dump(),被box和sphere类继承者替换。 如果在一个并不知道什么类型是什么对象的环境下,就像在main()这个函数里面一样,当一个虚函数dump()被调用的时候,我们还是需要知道它的返回类型的。 让我们在MSVC2008用/Ox 、 /Ob0编译看看main()的函数代码:

_s$ = -32 ; size = 12
_b$ = -20 ; size = 20
_main PROC
sub esp, 32 ; 00000020H
push 30 ; 0000001eH
push 20 ; 00000014H
push 10 ; 0000000aH
push 1
lea ecx, DWORD PTR _b$[esp+48]
call ??0box@@QAE@HHHH@Z ; box::box
push 40 ; 00000028H
push 2
lea ecx, DWORD PTR _s$[esp+40]
call ??0sphere@@QAE@HH@Z ; sphere::sphere
mov eax, DWORD PTR _b$[esp+32]
mov edx, DWORD PTR [eax]
lea ecx, DWORD PTR _b$[esp+32]
call edx
mov eax, DWORD PTR _s$[esp+32]
mov edx, DWORD PTR [eax]
lea ecx, DWORD PTR _s$[esp+32]
call edx
xor eax, eax
add esp, 32 ; 00000020H
ret 0
_main ENDP

指向dump()函数的指针在这个对象的某处被使用了,那么新函数的地址写到了哪里呢?只有在构造函数中有可能:其他地方都不会被main()调用。 看看类构造函数的代码:

??_R0?AVbox@@@8 DD FLAT:??_7type_info@@6B@ ; box ‘RTTI Type Descriptor’
DD 00H
DB ’.?AVbox@@’, 00H
??_R1A@?0A@EA@box@@8 DD FLAT:??_R0?AVbox@@@8 ; box::‘RTTI Base Class Descriptor at
(0,-1,0,64)’
DD 01H
DD 00H
DD 0ffffffffH
DD 00H
DD 040H
DD FLAT:??_R3box@@8
??_R2box@@8 DD FLAT:??_R1A@?0A@EA@box@@8 ; box::‘RTTI Base Class Array’
DD FLAT:??_R1A@?0A@EA@object@@8
??_R3box@@8 DD 00H ; box::‘RTTI Class Hierarchy Descriptor’
DD 00H
DD 02H
DD FLAT:??_R2box@@8
??_R4box@@6B@ DD 00H ; box::‘RTTI Complete Object Locator’
DD 00H
DD 00H
DD FLAT:??_R0?AVbox@@@8
DD FLAT:??_R3box@@8
??_7box@@6B@ DD FLAT:??_R4box@@6B@ ; box::‘vftable’
DD FLAT:?dump@box@@UAEXXZ
_color$ = 8 ; size = 4
_width$ = 12 ; size = 4
_height$ = 16 ; size = 4
_depth$ = 20 ; size = 4
??0box@@QAE@HHHH@Z PROC ; box::box, COMDAT
; _this$ = ecx
push esi
mov esi, ecx
call ??0object@@QAE@XZ ; object::object
mov eax, DWORD PTR _color$[esp]
mov ecx, DWORD PTR _width$[esp]
mov edx, DWORD PTR _height$[esp]
mov DWORD PTR [esi+4], eax
mov eax, DWORD PTR _depth$[esp]
mov DWORD PTR [esi+16], eax
mov DWORD PTR [esi], OFFSET ??_7box@@6B@
mov DWORD PTR [esi+8], ecx
mov DWORD PTR [esi+12], edx
mov eax, esi
pop esi
ret 16 ; 00000010H
??0box@@QAE@HHHH@Z ENDP ; box::box

我们可以看到一些轻微的内存布局的变化:第一个域是一个指向box::vftable(这个名字由MSVC编译器生成)的指针。 在这个函数表里我们看到了一个指向box::RTTI Complete Object Locator的连接,而且还有一个指向box::dump()函数的。所以这就是被命名的虚函数表和RTTI。虚函数表可以包含所有虚函数体的地址,RTTI表包含类型的信息。另外一提,RTTI表是c++调用dynamic_cast和typeid的结果的枚举表。你可以看到这里函数名是用明文表记的。因此,一个基对象可以调用虚函数object::dump(),然后,会从这个对象的结构里调用这个继承类的函数。 枚举这些函数表需要消耗额外的CPU时间,所以可以认为虚函数比普通调用要慢一些。 在GCC生成的代码里,RTTI表的构造有些轻微的不同。

ostream

继续以一个hello world程序为例,但是这次使用ostream

#include <iostream>
int main()
{
    std::cout << "Hello, world!\n";
}

几乎所有关于c++的书籍都会提到<<操作支持很多数据类型。这些支持是在ostream中完成的。通过反汇编之后的代码,可以看到ostream中的<<操作被调用:

$SG37112 DB ’Hello, world!’, 0aH, 00H
_main PROC
push OFFSET $SG37112
push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?
$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
add esp, 8
xor eax, eax
ret 0
_main ENDP

对示例程序做如下修改:

#include <iostream>
int main()
{
    std::cout << "Hello, " << "world!\n";
}

同样的,从许多C++书籍中可以知道,ostream的输出操作的运算结果可以用作下一次输出(即ostream的输出操作返回ostream对象)。

$SG37112 DB ’world!’, 0aH, 00H
$SG37113 DB ’Hello, ’, 00H
_main PROC
push OFFSET $SG37113 ; ’Hello, ’
push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?
$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
add esp, 8
push OFFSET $SG37112 ; ’world!’
push eax ; result of previous function
call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU?
$char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
add esp, 8
xor eax, eax
ret 0
_main ENDP

如果用函数f()替换<<运算符,示例代码等价于:

f(f(std::cout, "Hello, "), "world!")

通过GCC生成的代码和MSVC的代码基本相同。

引用: 在c++中,引用和指针一样,但是使用的时候更安全,因为在使用引用的时候几乎不会发生错误。例如,引用必须始终指向一个相应类型的对象,而不能为NULL。甚至于,引用不能被改变,不能将一个对象的引用重新赋值以指向另一个对象。 如果我们尝试修改指针的例子(9),将指针替换为引用:

void f2 (int x, int y, int & sum, int & product)
{
    sum=x+y;
    product=x*y;
};

可以想到,编译后的代码和使用指针生成的代码一致。

_x$ = 8 ; size = 4
_y$ = 12 ; size = 4
_sum$ = 16 ; size = 4
_product$ = 20 ; size = 4
?f2@@YAXHHAAH0@Z PROC ; f2
mov ecx, DWORD PTR _y$[esp-4]
mov eax, DWORD PTR _x$[esp-4]
lea edx, DWORD PTR [eax+ecx]
imul eax, ecx
mov ecx, DWORD PTR _product$[esp-4]
push esi
mov esi, DWORD PTR _sum$[esp]
mov DWORD PTR [esi], edx
mov DWORD PTR [ecx], eax
pop esi
ret 0
?f2@@YAXHHAAH0@Z ENDP ; f2

STL

注意:本章所有这些例子只在32位环境下进行了验证,没有在x64环境下验证

33.1 std::string

内部实现 许多string库的实现结构包含一个指向字符串缓冲区的指针,一个包含当前字符串长度的变量以及一个表示当前字符串缓冲区大小的变量。为了能够将缓冲区指针传递给使用ASCII字符串的函数,通常string缓冲区中的字符串以0结尾。 C++标准中没有规定std::string应该如何实现,因此通常是按照上述方式实现的。 按照规定,std::string应该是一个模板而不是类,以便能够支持不同的字符类型,如char、wchar_t等。

对于std::string,MSVC和GCC中的内部实现存在差异,下面依次进行说明

MSVC

MSVC的实现中,字符串存储在适当的位置,不一定位于指针指向的缓冲区(如果字符串的长度小于16个字符)。 这意味着短的字符串在32位环境下至少占据16+4+4=24字节的空间,在64位环境下至少占据16+8+8=32字节,当字符串长度大于16字符时,相应的需要增加字符串自身的长度。

#include <string>
#include <stdio.h>
struct std_string
{
    union
    {
        char buf[16];
        char* ptr;
    } u;
    size_t size; // AKA ’Mysize’ in MSVC
    size_t capacity; // AKA ’Myres’ in MSVC
};
void dump_std_string(std::string s)
{
    struct std_string *p=(struct std_string*)&s;
    printf ("[%s] size:%d capacity:%d\n", p->size>16 ? p->u.ptr : p->u.buf, p->size, p->
    capacity);
};
int main()
{
    std::string s1="short string";
    std::string s2="string longer that 16 bytes";
    dump_std_string(s1);
    dump_std_string(s2);
    // that works without using c_str()
    printf ("%s\n", &s1);
    printf ("%s\n", s2);
};

通过源代码可以清晰的看到这些。 如果字符串长度小于16个符号,存储字符的缓冲区不需要在堆上分配。实际上非常适宜这样做,因为大量的字符串确实都较短。显然,微软的开发人员认为16个字符是好的临界点。 在main函数尾部,虽然没有使用c_str()方法,但是如果编译运行上面的代码,所有字符串都将打印在控制台上。 当字符串的长度小于16个字符时,存储字符串的缓冲区位于std::string对象的开始位置,printf函数将指针当做指向以0结尾的字符数组,因此上述代码可以正常运行。 第二个超过16字符的字符串的打印方式比较危险,通常程序员犯的错误是忘记写c_str()。这在很长的一段时间不会引起人的注意,直到一个很长的字符串出现,然后程序崩溃。而上述代码可以工作,因为指向字符串缓冲区的指针位于结构体的开始。

GCC

GCC的实现中,增加了一个引用计数, 一个有趣的事实是一个指向std::string类实例的指针并不是指向结构体的起始位置,而是指向缓冲区的指针,在libstdc++-v3_string.h,中我们可以看到这主要是为了方便调试。

The reason you want Mdata pointing to the character %array and not the Rep is so that the debugger can see the string contents. (Probably we should add a non-inline member to get the Rep for the debugger to use, so users can check the actual string length.)

在我的例子中将考虑这一点:

#include <string>
#include <stdio.h>
struct std_string
{
    size_t length;
    size_t capacity;
    size_t refcount;
};
void dump_std_string(std::string s)
{
    char *p1=*(char**)&s; // GCC type checking workaround
    struct std_string *p2=(struct std_string*)(p1-sizeof(struct std_string));
    printf ("[%s] size:%d capacity:%d\n", p1, p2->length, p2->capacity);
};
int main()
{
    std::string s1="short string";
    std::string s2="string longer that 16 bytes";
    dump_std_string(s1);
    dump_std_string(s2);
    // GCC type checking workaround:
    printf ("%s\n", *(char**)&s1);
    printf ("%s\n", *(char**)&s2);
};

由于GCC有较强的类型检查,因此需要技巧来隐藏类似之前的错误,即使不使用c_str(),printf也能够正常工作。

更复杂的例子

#include <string>
#include <stdio.h>
int main()
{
    std::string s1="Hello, ";
    std::string s2="world!\n";
    std::string s3=s1+s2;
    printf ("%s\n", s3.c_str());
}
$SG39512 DB ’Hello, ’, 00H
$SG39514 DB ’world!’, 0aH, 00H
$SG39581 DB ’%s’, 0aH, 00H
_s2$ = -72 ; size = 24
_s3$ = -48 ; size = 24
_s1$ = -24 ; size = 24
_main PROC
sub esp, 72 ; 00000048H
push 7
push OFFSET $SG39512
lea ecx, DWORD PTR _s1$[esp+80]
mov DWORD PTR _s1$[esp+100], 15 ; 0000000fH
mov DWORD PTR _s1$[esp+96], 0
mov BYTE PTR _s1$[esp+80], 0
call ?assign@?$basic_string@DU?$char_traits@D@std@@V?
$allocator@D@2@@std@@QAEAAV12@PBDI@Z ; std::basic_string<char,std::char_traits<char>,std::
allocator<char> >::assign
push 7
push OFFSET $SG39514
lea ecx, DWORD PTR _s2$[esp+80]
mov DWORD PTR _s2$[esp+100], 15 ; 0000000fH
mov DWORD PTR _s2$[esp+96], 0
mov BYTE PTR _s2$[esp+80], 0
call ?assign@?$basic_string@DU?$char_traits@D@std@@V?
$allocator@D@2@@std@@QAEAAV12@PBDI@Z ; std::basic_string<char,std::char_traits<char>,std::
allocator<char> >::assign
lea eax, DWORD PTR _s2$[esp+72]
push eax
lea eax, DWORD PTR _s1$[esp+76]
push eax
lea eax, DWORD PTR _s3$[esp+80]
push eax
call ??$?HDU?$char_traits@D@std@@V?$allocator@D@1@@std@@YA?AV?$basic_string@DU?
$char_traits@D@std@@V?$allocator@D@2@@0@ABV10@0@Z ; std::operator+<char,std::char_traits<char
>,std::allocator<char> >
; inlined c_str() method:
cmp DWORD PTR _s3$[esp+104], 16 ; 00000010H
lea eax, DWORD PTR _s3$[esp+84]
cmovae eax, DWORD PTR _s3$[esp+84]
push eax
push OFFSET $SG39581
call _printf
add esp, 20 ; 00000014H
cmp DWORD PTR _s3$[esp+92], 16 ; 00000010H
jb SHORT $LN119@main
push DWORD PTR _s3$[esp+72]
call ??3@YAXPAX@Z ; operator delete
add esp, 4
$LN119@main:
cmp DWORD PTR _s2$[esp+92], 16 ; 00000010H
mov DWORD PTR _s3$[esp+92], 15 ; 0000000fH
mov DWORD PTR _s3$[esp+88], 0
mov BYTE PTR _s3$[esp+72], 0
jb SHORT $LN151@main
push DWORD PTR _s2$[esp+72]
call ??3@YAXPAX@Z ; operator delete
add esp, 4
$LN151@main:
cmp DWORD PTR _s1$[esp+92], 16 ; 00000010H
mov DWORD PTR _s2$[esp+92], 15 ; 0000000fH
mov DWORD PTR _s2$[esp+88], 0
mov BYTE PTR _s2$[esp+72], 0
jb SHORT $LN195@main
push DWORD PTR _s1$[esp+72]
call ??3@YAXPAX@Z ; operator delete
add esp, 4
$LN195@main:
xor eax, eax
add esp, 72 ; 00000048H
ret 0
_main ENDP

编译器并不是静态构造string对象,存储数据的缓冲区是否一定要在堆中呢?通常以0结尾的ASCII字符串存储在数据节中,然后运行时,通过赋值方法完成s1和s2两个string对象的构造。通过+操作符,s3完成string对象的构造。 可以注意到上述代码中并没有c_str()方法的调用,这是因为由于函数太小,编译器将其内联了,如果一个字符串小于16个字符,eax寄存器中存放指向缓冲区的指针,否则,存放指向堆中字符串缓冲区的指针。 然后,我们看到了三个析构函数的调用,当字符串长度超过16字符时,析构函数将被调用,在堆中的缓冲区会被释放。此外,由于三个std::string对象都存储在栈中,当函数结束时,他们将被自动释放。 可以得到一个结论,短的字符串对象处理起来更快,因为堆访问操作较少。 GCC生成的代码甚至更简单(正如我之前提到的,GCC并不将短的字符串存储在结构体中)

.LC0:
.string "Hello, "
.LC1:
.string "world!\n"
main:
push ebp
mov ebp, esp
push edi
push esi
push ebx
and esp, -16
sub esp, 32
lea ebx, [esp+28]
lea edi, [esp+20]
mov DWORD PTR [esp+8], ebx
lea esi, [esp+24]
mov DWORD PTR [esp+4], OFFSET FLAT:.LC0
mov DWORD PTR [esp], edi
call _ZNSsC1EPKcRKSaIcE
mov DWORD PTR [esp+8], ebx
mov DWORD PTR [esp+4], OFFSET FLAT:.LC1
mov DWORD PTR [esp], esi
call _ZNSsC1EPKcRKSaIcE
mov DWORD PTR [esp+4], edi
mov DWORD PTR [esp], ebx
call _ZNSsC1ERKSs
mov DWORD PTR [esp+4], esi
mov DWORD PTR [esp], ebx
call _ZNSs6appendERKSs
; inlined c_str():
mov eax, DWORD PTR [esp+28]
mov DWORD PTR [esp], eax
call puts
mov eax, DWORD PTR [esp+28]
lea ebx, [esp+19]
mov DWORD PTR [esp+4], ebx
sub eax, 12
mov DWORD PTR [esp], eax
call _ZNSs4_Rep10_M_disposeERKSaIcE
mov eax, DWORD PTR [esp+24]
mov DWORD PTR [esp+4], ebx
sub eax, 12
mov DWORD PTR [esp], eax
call _ZNSs4_Rep10_M_disposeERKSaIcE
mov eax, DWORD PTR [esp+20]
mov DWORD PTR [esp+4], ebx
sub eax, 12
mov DWORD PTR [esp], eax
call _ZNSs4_Rep10_M_disposeERKSaIcE
lea esp, [ebp-12]
xor eax, eax
pop ebx
pop esi
pop edi
pop ebp
ret

可以看到传递给析构函数的并不是一个对象的指针,而是在对象所在位置的前12个字节的位置,也就是结构体的真正起始位置。

33.1.2 std::string 作为全局变量使用

有经验的c++程序员会说,可以定义一个STL类型的全局变量。 是的,确实如此

#include <stdio.h>
#include <string>
std::string s="a string";
int main()
{
    printf ("%s\n", s.c_str());
};
MSVC:
$SG39512 DB ’a string’, 00H
$SG39519 DB ’%s’, 0aH, 00H
_main PROC
cmp DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
+20, 16 ; 00000010H
mov eax, OFFSET ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
; s
cmovae eax, DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?
$allocator@D@2@@std@@A
push eax
push OFFSET $SG39519
call _printf
add esp, 8
xor eax, eax
ret 0
_main ENDP
??__Es@@YAXXZ PROC ; ‘dynamic initializer for ’s’’, COMDAT
push 8
push OFFSET $SG39512
mov ecx, OFFSET ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
; s
call ?assign@?$basic_string@DU?$char_traits@D@std@@V?
$allocator@D@2@@std@@QAEAAV12@PBDI@Z ; std::basic_string<char,std::char_traits<char>,std::
allocator<char> >::assign
push OFFSET ??__Fs@@YAXXZ ; ‘dynamic atexit destructor for ’s’’
call _atexit
pop ecx
ret 0
??__Es@@YAXXZ ENDP ; ‘dynamic initializer for ’s’’
??__Fs@@YAXXZ PROC ; ‘dynamic atexit destructor for ’s’’,
COMDAT
push ecx
cmp DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
+20, 16 ; 00000010H
jb SHORT $LN23@dynamic
push esi
mov esi, DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?
$allocator@D@2@@std@@A
lea ecx, DWORD PTR $T2[esp+8]
call ??0?$_Wrap_alloc@V?$allocator@D@std@@@std@@QAE@XZ ; std::_Wrap_alloc<std::
allocator<char> >::_Wrap_alloc<std::allocator<char> >
push OFFSET ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A ; s
lea ecx, DWORD PTR $T2[esp+12]
call ??$destroy@PAD@?$_Wrap_alloc@V?$allocator@D@std@@@std@@QAEXPAPAD@Z ; std::
_Wrap_alloc<std::allocator<char> >::destroy<char *>
lea ecx, DWORD PTR $T1[esp+8]
call ??0?$_Wrap_alloc@V?$allocator@D@std@@@std@@QAE@XZ ; std::_Wrap_alloc<std::
allocator<char> >::_Wrap_alloc<std::allocator<char> >
push esi
call ??3@YAXPAX@Z ; operator delete
add esp, 4
pop esi
$LN23@dynamic:
mov DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
+20, 15 ; 0000000fH
mov DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
+16, 0
mov BYTE PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A, 0
pop ecx
ret 0
??__Fs@@YAXXZ ENDP ; ‘dynamic atexit destructor for ’s’’

实际上,main函数之前,CRT中将调用一个特殊的函数调用所有全局变量的构造函数。除此之外,CRT中还将通过atexit()注册另一个函数,该函数中将调用全局变量的析构函数。 GCC生成的代码如下:

main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 16
mov eax, DWORD PTR s
mov DWORD PTR [esp], eax
call puts
xor eax, eax
leave
ret
.LC0:
.string "a string"
_GLOBAL__sub_I_s:
sub esp, 44
lea eax, [esp+31]
mov DWORD PTR [esp+8], eax
mov DWORD PTR [esp+4], OFFSET FLAT:.LC0
mov DWORD PTR [esp], OFFSET FLAT:s
call _ZNSsC1EPKcRKSaIcE
mov DWORD PTR [esp+8], OFFSET FLAT:__dso_handle
mov DWORD PTR [esp+4], OFFSET FLAT:s
mov DWORD PTR [esp], OFFSET FLAT:_ZNSsD1Ev
call __cxa_atexit
add esp, 44
ret
.LFE645:
.size _GLOBAL__sub_I_s, .-_GLOBAL__sub_I_s
.section .init_array,"aw"
.align 4
.long _GLOBAL__sub_I_s
.globl s
.bss
.align 4
.type s, @object
.size s, 4
s:
.zero 4
.hidden __dso_handle

GCC并没有创建单独的函数来玩全局变量的析构,而是依次将全局变量的析构函数传递给atexit()函数

33.2 std::list

std::list是标准的双向链表,每一个元素包含两个指针,分别前一个和后一个元素。 这意味着每个元素的的内存空间大小要相应的扩大2个字。(在32位环境下为8字节,64位环境下为16字节) std::list也是一个循环链表,也就是说最后一个元素包含一个指向第一个元素的指针,反之亦然。 C++ STL只是为你希望做为链表使用的结构体增加前向和后向指针。 下面我们构造一个包含两个简单变量的结构体,将其存储在链表中。 虽然C++标准中并没有规定如何实现list,但是MSVC和GCC的实现比较相似,因此下面仅通过一段源代码来展示。

#include <stdio.h>
#include <list>
#include <iostream>
struct a
{
    int x;
    int y;
};
struct List_node
{
    struct List_node* _Next;
    struct List_node* _Prev;
    int x;
    int y;
};
void dump_List_node (struct List_node *n)
{
    printf ("ptr=0x%p _Next=0x%p _Prev=0x%p x=%d y=%d\n", n, n->_Next, n->_Prev, n->x, n->y);
};
void dump_List_vals (struct List_node* n)
{
    struct List_node* current=n;
    for (;;)
    {
        dump_List_node (current);
        current=current->_Next;
        if (current==n) // end
            break;
    };
};
void dump_List_val (unsigned int *a)
{
#ifdef _MSC_VER
// GCC implementation doesn’t have "size" field
printf ("_Myhead=0x%p, _Mysize=%d\n", a[0], a[1]);
#endif
dump_List_vals ((struct List_node*)a[0]);
};
int main()
{
    std::list<struct a> l;
    printf ("* empty list:\n");
    dump_List_val((unsigned int*)(void*)&l);
    struct a t1;
    t1.x=1;
    t1.y=2;
    l.push_front (t1);
    t1.x=3;
    t1.y=4;
    l.push_front (t1);
    t1.x=5;
    t1.y=6;
    l.push_back (t1);
    printf ("* 3-elements list:\n");
    dump_List_val((unsigned int*)(void*)&l);
    std::list<struct a>::iterator tmp;
    printf ("node at .begin:\n");
    tmp=l.begin();
    dump_List_node ((struct List_node *)*(void**)&tmp);
    printf ("node at .end:\n");
    tmp=l.end();
    dump_List_node ((struct List_node *)*(void**)&tmp);
    printf ("* let’s count from the begin:\n");
    std::list<struct a>::iterator it=l.begin();
    printf ("1st element: %d %d\n", (*it).x, (*it).y);
    it++;
    printf ("2nd element: %d %d\n", (*it).x, (*it).y);
    it++;
    printf ("3rd element: %d %d\n", (*it).x, (*it).y);
    it++;
    printf ("element at .end(): %d %d\n", (*it).x, (*it).y);
    printf ("* let’s count from the end:\n");
    std::list<struct a>::iterator it2=l.end();
    printf ("element at .end(): %d %d\n", (*it2).x, (*it2).y);
    it2--;
    printf ("3rd element: %d %d\n", (*it2).x, (*it2).y);
    it2--;
    printf ("2nd element: %d %d\n", (*it2).x, (*it2).y);
    it2--;
    printf ("1st element: %d %d\n", (*it2).x, (*it2).y);
    printf ("removing last element...\n");
    l.pop_back();
    dump_List_val((unsigned int*)(void*)&l);
};

GCC

我们以GCC为例开始,当我们运行这个例子,可以看到屏幕上打印出了大量输出,我们依次对其进行分析。

* empty list:
ptr=0x0028fe90 _Next=0x0028fe90 _Prev=0x0028fe90 x=3 y=0

我们可以看到一个空的链表,它由一个元素组成,其中x和y中包含有垃圾数据。前向和后向指针均指向自身。

这一时刻的.begin和.end迭代器两者相等。 当我们压入3个元素之后,链表的内部状态将变为:

* 3-elements list:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
ptr=0x00034988 _Next=0x00034b40 _Prev=0x000349a0 x=1 y=2
ptr=0x00034b40 _Next=0x0028fe90 _Prev=0x00034988 x=5 y=6
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6

最后一个元素依然位于0x28fe90,只有当链表被销毁的时候它才会移动。x和y中依然包含有垃圾数据。虽然和链表中的最后一个元素中的x和y拥有相同的值,但这并不能说明这些值有意义。 下面这幅图说明了链表中的三个元素如何在内存中存储

变量l始终指向第一个链表节点 .begin()和.end()迭代器不指向任何东西,也并不出现在内存中,但是当适当的方法被调用的时候将返回指向这些节点的指针。(这句话是说.begin()和.end()并不是变量,而是函数,该函数返回迭代器。) 链表中包含一个垃圾元素在链表的实现中是非常流行的方式,如果不包含它,很多操作都将变得复杂和低效。 迭代器实质上是一个指向链表节点的指针。list.begin()和list.end()仅仅返回迭代器。

node at .begin:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
node at .end:
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6

实际上链表是循环的非常有用:包含一个指向第一个链表元素的指针,类似l变量,这将有利于快速获取指向最后一个元素的指针,而不必遍历整个链表。这样也有利于快速的在链表尾部插入元素。 –和++操作符只是将当前的迭代器的值设置为current_node->prev、current_node->next。反向迭代器只是以相反的方向做相似的工作。

迭代器的*操作符返回指向节点结构指针,也就是用户结构的起始位置,如,指向第一个结构体元素x的指针。 List的插入和删除比较简单,只需要分配新节点同时给指针设置有效值。 这就是元素被删除后,迭代器会失效的原因,它依然指向已经被释放的节点。当然,迭代器指向的被释放的节点的数据是不能再使用的。

GCC的实现中没有存储链表的大小,由于没有其他方式获取这些信息,.size()方法需要遍历整个链表统计元素个数,因此执行速度较慢。这一操作的时间复杂度为O(n),即消耗时间多少和链表中元素个数成正比。

Listing34.7: GCC 4.8.1 -O3 -fno-inline-small-functions

main proc near
push ebp
mov ebp, esp
push esi
push ebx
and esp, 0FFFFFFF0h
sub esp, 20h
lea ebx, [esp+10h]
mov dword ptr [esp], offset s ; "* empty list:"
mov [esp+10h], ebx
mov [esp+14h], ebx
call puts
mov [esp], ebx
call _Z13dump_List_valPj ; dump_List_val(uint *)
lea esi, [esp+18h]
mov [esp+4], esi
mov [esp], ebx
mov dword ptr [esp+18h], 1 ; X for new element
mov dword ptr [esp+1Ch], 2 ; Y for new element
call _ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ; std::list<a,std::allocator<a
>>::push_front(a const&)
mov [esp+4], esi
mov [esp], ebx
mov dword ptr [esp+18h], 3 ; X for new element
mov dword ptr [esp+1Ch], 4 ; Y for new element
call _ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ; std::list<a,std::allocator<a
>>::push_front(a const&)
mov dword ptr [esp], 10h
mov dword ptr [esp+18h], 5 ; X for new element
mov dword ptr [esp+1Ch], 6 ; Y for new element
call _Znwj ; operator new(uint)
cmp eax, 0FFFFFFF8h
jz short loc_80002A6
mov ecx, [esp+1Ch]
mov edx, [esp+18h]
mov [eax+0Ch], ecx
mov [eax+8], edx
loc_80002A6: ; CODE XREF: main+86
mov [esp+4], ebx
mov [esp], eax
call _ZNSt8__detail15_List_node_base7_M_hookEPS0_ ; std::__detail::
_List_node_base::_M_hook(std::__detail::_List_node_base*)
mov dword ptr [esp], offset a3ElementsList ; "* 3-elements list:"
call puts
mov [esp], ebx
call _Z13dump_List_valPj ; dump_List_val(uint *)
mov dword ptr [esp], offset aNodeAt_begin ; "node at .begin:"
call puts
mov eax, [esp+10h]
mov [esp], eax
call _Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)
mov dword ptr [esp], offset aNodeAt_end ; "node at .end:"
call puts
mov [esp], ebx
call _Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)
mov dword ptr [esp], offset aLetSCountFromT ; "* let’s count from the begin:"
call puts
mov esi, [esp+10h]
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov esi, [esi] ; operator++: get ->next pointer
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov esi, [esi] ; operator++: get ->next pointer
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov eax, [esi] ; operator++: get ->next pointer
mov edx, [eax+0Ch]
mov [esp+0Ch], edx
mov eax, [eax+8]
mov dword ptr [esp+4], offset aElementAt_endD ; "element at .end(): %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov dword ptr [esp], offset aLetSCountFro_0 ; "* let’s count from the end:"
call puts
mov eax, [esp+1Ch]
mov dword ptr [esp+4], offset aElementAt_endD ; "element at .end(): %d %d\n"
mov dword ptr [esp], 1
mov [esp+0Ch], eax
mov eax, [esp+18h]
mov [esp+8], eax
call __printf_chk
mov esi, [esp+14h]
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov esi, [esi+4] ; operator--: get ->prev pointer
mov eax, [esi+0Ch]
mov [esp+0Ch], eax
mov eax, [esi+8]
mov dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov eax, [esi+4] ; operator--: get ->prev pointer
mov edx, [eax+0Ch]
mov [esp+0Ch], edx
mov eax, [eax+8]
mov dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
mov dword ptr [esp], 1
mov [esp+8], eax
call __printf_chk
mov dword ptr [esp], offset aRemovingLastEl ; "removing last element..."
call puts
mov esi, [esp+14h]
mov [esp], esi
call _ZNSt8__detail15_List_node_base9_M_unhookEv ; std::__detail::
_List_node_base::_M_unhook(void)
mov [esp], esi ; void *
call _ZdlPv ; operator delete(void *)
mov [esp], ebx
call _Z13dump_List_valPj ; dump_List_val(uint *)
mov [esp], ebx
call _ZNSt10_List_baseI1aSaIS0_EE8_M_clearEv ; std::_List_base<a,std::
allocator<a>>::_M_clear(void)
lea esp, [ebp-8]
xor eax, eax
pop ebx
pop esi
pop ebp
retn
main endp

Listing 34.8: 所有输出

* empty list:
ptr=0x0028fe90 _Next=0x0028fe90 _Prev=0x0028fe90 x=3 y=0
* 3-elements list:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
ptr=0x00034988 _Next=0x00034b40 _Prev=0x000349a0 x=1 y=2
ptr=0x00034b40 _Next=0x0028fe90 _Prev=0x00034988 x=5 y=6
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6
node at .begin:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
node at .end:
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6
* let’s count from the begin:
1st element: 3 4
2nd element: 1 2
3rd element: 5 6
element at .end(): 5 6
* let’s count from the end:
element at .end(): 5 6
3rd element: 5 6
2nd element: 1 2
1st element: 3 4
removing last element...
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
ptr=0x00034988 _Next=0x0028fe90 _Prev=0x000349a0 x=1 y=2
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034988 x=5 y=6

MSVC

MSVC实现形式基本相同,但是它存储了当前list的大小。这意味着.size()方法非常迅速,只需要从内存中读取一个值即可。从另一个方面来说,必须在插入和删除操作后更新size变量的值。 MSVC组织节点的方式也稍有不同。

GCC将垃圾元素放在链表的尾部,而MSVC放在链表起始位置。

Listing 34.9: MSVC 2012 /Fa2.asm /Ox /GS- /Ob1

_l$ = -16 ; size = 8
_t1$ = -8 ; size = 8
_main PROC
sub esp, 16 ; 00000010H
push ebx
push esi
push edi
push 0
push 0
lea ecx, DWORD PTR _l$[esp+36]
mov DWORD PTR _l$[esp+40], 0
; allocate first "garbage" element
call ?_Buynode0@?$_List_alloc@$0A@U?$_List_base_types@Ua@@V?
$allocator@Ua@@@std@@@std@@@std@@QAEPAU?$_List_node@Ua@@PAX@2@PAU32@0@Z ; std::_List_alloc<0,
std::_List_base_types<a,std::allocator<a> > >::_Buynode0
mov edi, DWORD PTR __imp__printf
mov ebx, eax
push OFFSET $SG40685 ; ’* empty list:’
mov DWORD PTR _l$[esp+32], ebx
call edi ; printf
lea eax, DWORD PTR _l$[esp+32]
push eax
call ?dump_List_val@@YAXPAI@Z ; dump_List_val
mov esi, DWORD PTR [ebx]
add esp, 8
lea eax, DWORD PTR _t1$[esp+28]
push eax
push DWORD PTR [esi+4]
lea ecx, DWORD PTR _l$[esp+36]
push esi
mov DWORD PTR _t1$[esp+40], 1 ; data for a new node
mov DWORD PTR _t1$[esp+44], 2 ; data for a new node
; allocate new node
call ??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU?
$_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy<a,std::allocator<a> >::_Buynode<a
const &>
mov DWORD PTR [esi+4], eax
mov ecx, DWORD PTR [eax+4]
mov DWORD PTR _t1$[esp+28], 3 ; data for a new node
mov DWORD PTR [ecx], eax
mov esi, DWORD PTR [ebx]
lea eax, DWORD PTR _t1$[esp+28]
push eax
push DWORD PTR [esi+4]
lea ecx, DWORD PTR _l$[esp+36]
push esi
mov DWORD PTR _t1$[esp+44], 4 ; data for a new node
; allocate new node
call ??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU?
$_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy<a,std::allocator<a> >::_Buynode<a
const &>
mov DWORD PTR [esi+4], eax
mov ecx, DWORD PTR [eax+4]
mov DWORD PTR _t1$[esp+28], 5 ; data for a new node
mov DWORD PTR [ecx], eax
lea eax, DWORD PTR _t1$[esp+28]
push eax
push DWORD PTR [ebx+4]
lea ecx, DWORD PTR _l$[esp+36]
push ebx
mov DWORD PTR _t1$[esp+44], 6 ; data for a new node
; allocate new node
call ??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU?
$_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy<a,std::allocator<a> >::_Buynode<a
const &>
mov DWORD PTR [ebx+4], eax
mov ecx, DWORD PTR [eax+4]
push OFFSET $SG40689 ; ’* 3-elements list:’
mov DWORD PTR _l$[esp+36], 3
mov DWORD PTR [ecx], eax
call edi ; printf
lea eax, DWORD PTR _l$[esp+32]
push eax
call ?dump_List_val@@YAXPAI@Z ; dump_List_val
push OFFSET $SG40831 ; ’node at .begin:’
call edi ; printf
push DWORD PTR [ebx] ; get next field of node $l$ variable points to
call ?dump_List_node@@YAXPAUList_node@@@Z ; dump_List_node
push OFFSET $SG40835 ; ’node at .end:’
call edi ; printf
push ebx ; pointer to the node $l$ variable points to!
call ?dump_List_node@@YAXPAUList_node@@@Z ; dump_List_node
push OFFSET $SG40839 ; ’* let’’s count from the begin:’
call edi ; printf
mov esi, DWORD PTR [ebx] ; operator++: get ->next pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40846 ; ’1st element: %d %d’
call edi ; printf
mov esi, DWORD PTR [esi] ; operator++: get ->next pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40848 ; ’2nd element: %d %d’
call edi ; printf
mov esi, DWORD PTR [esi] ; operator++: get ->next pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40850 ; ’3rd element: %d %d’
call edi ; printf
mov eax, DWORD PTR [esi] ; operator++: get ->next pointer
add esp, 64 ; 00000040H
push DWORD PTR [eax+12]
push DWORD PTR [eax+8]
push OFFSET $SG40852 ; ’element at .end(): %d %d’
call edi ; printf
push OFFSET $SG40853 ; ’* let’’s count from the end:’
call edi ; printf
push DWORD PTR [ebx+12] ; use x and y fields from the node $l$ variable points to
push DWORD PTR [ebx+8]
push OFFSET $SG40860 ; ’element at .end(): %d %d’
call edi ; printf
mov esi, DWORD PTR [ebx+4] ; operator--: get ->prev pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40862 ; ’3rd element: %d %d’
call edi ; printf
mov esi, DWORD PTR [esi+4] ; operator--: get ->prev pointer
push DWORD PTR [esi+12]
push DWORD PTR [esi+8]
push OFFSET $SG40864 ; ’2nd element: %d %d’
call edi ; printf
mov eax, DWORD PTR [esi+4] ; operator--: get ->prev pointer
push DWORD PTR [eax+12]
push DWORD PTR [eax+8]
push OFFSET $SG40866 ; ’1st element: %d %d’
call edi ; printf
add esp, 64 ; 00000040H
push OFFSET $SG40867 ; ’removing last element...’
call edi ; printf
mov edx, DWORD PTR [ebx+4]
add esp, 4
; prev=next?
; it is the only element, "garbage one"?
; if yes, do not delete it!
cmp edx, ebx
je SHORT $LN349@main
mov ecx, DWORD PTR [edx+4]
mov eax, DWORD PTR [edx]
mov DWORD PTR [ecx], eax
mov ecx, DWORD PTR [edx]
mov eax, DWORD PTR [edx+4]
push edx
mov DWORD PTR [ecx+4], eax
call ??3@YAXPAX@Z ; operator delete
add esp, 4
mov DWORD PTR _l$[esp+32], 2
$LN349@main:
lea eax, DWORD PTR _l$[esp+28]
push eax
call ?dump_List_val@@YAXPAI@Z ; dump_List_val
mov eax, DWORD PTR [ebx]
add esp, 4
mov DWORD PTR [ebx], ebx
mov DWORD PTR [ebx+4], ebx
cmp eax, ebx
je SHORT $LN412@main
$LL414@main:
mov esi, DWORD PTR [eax]
push eax
call ??3@YAXPAX@Z ; operator delete
add esp, 4
mov eax, esi
cmp esi, ebx
jne SHORT $LL414@main
$LN412@main:
push ebx
call ??3@YAXPAX@Z ; operator delete
add esp, 4
xor eax, eax
pop edi
pop esi
pop ebx
add esp, 16 ; 00000010H
ret 0
_main ENDP

与GCC不同,MSVC代码在函数开始,通过Buynode函数分配垃圾元素,这个方法也用于后续节点的申请(GCC将最早的节点分配在栈上)。

Listing 34.10 完整输出

* empty list:
_Myhead=0x003CC258, _Mysize=0
ptr=0x003CC258 _Next=0x003CC258 _Prev=0x003CC258 x=6226002 y=4522072
* 3-elements list:
_Myhead=0x003CC258, _Mysize=3
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0 x=6226002 y=4522072
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
ptr=0x003CC270 _Next=0x003CC2A0 _Prev=0x003CC288 x=1 y=2
ptr=0x003CC2A0 _Next=0x003CC258 _Prev=0x003CC270 x=5 y=6
node at .begin:
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
node at .end:
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0 x=6226002 y=4522072
* let’s count from the begin:
1st element: 3 4
2nd element: 1 2
3rd element: 5 6
element at .end(): 6226002 4522072
* let’s count from the end:
element at .end(): 6226002 4522072
3rd element: 5 6
2nd element: 1 2
1st element: 3 4
removing last element...
_Myhead=0x003CC258, _Mysize=2
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC270 x=6226002 y=4522072
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
ptr=0x003CC270 _Next=0x003CC258 _Prev=0x003CC288 x=1 y=2

33.3 C++ 11 std::forward_list

和std::list相似,但是包含一个指向下一个节点的指针。它所占用的内存空间更少,但是没有提供双向遍历链表的能力。

33.4 std::vector

我将std::vector称作c数组的安全封装。在内部实现上,它和std::string相似,包含指向缓冲区的指针,指向数组尾部的指针,以及指向缓冲区尾部的指针。 std::vector中的元素在内存中连续存放,和通常中的数组一样。在C++11包含一个新方法.data(),它返回指向缓冲区的指针,这和std::string中的.c_str()相同。 在堆中分配的缓冲区大小将超过数组自身大小。 MSVC和GCC的实现相似,只是结构体中元素名稍有不同,因此下面的代码在两种编译器上都能工作。下面是用于转储std::vector结构的类C代码。

#include <stdio.h>
#include <vector>
#include <algorithm>
#include <functional>
struct vector_of_ints
{
    // MSVC names:
    int *Myfirst;
    int *Mylast;
    int *Myend;
    // GCC structure is the same, names are: _M_start, _M_finish, _M_end_of_storage
};
void dump(struct vector_of_ints *in)
{
    printf ("_Myfirst=%p, _Mylast=%p, _Myend=%p\n", in->Myfirst, in->Mylast, in->Myend);
    size_t size=(in->Mylast-in->Myfirst);
    size_t capacity=(in->Myend-in->Myfirst);
    printf ("size=%d, capacity=%d\n", size, capacity);
    for (size_t i=0; i<size; i++)
    printf ("element %d: %d\n", i, in->Myfirst[i]);
};
int main()
{
    std::vector<int> c;
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(1);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(2);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(3);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(4);
    dump ((struct vector_of_ints*)(void*)&c);
    c.reserve (6);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(5);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(6);
    dump ((struct vector_of_ints*)(void*)&c);
    printf ("%d\n", c.at(5)); // bounds checking
    printf ("%d\n", c[8]); // operator[], no bounds checking
};

如果编译器是MSVC,下面是输出样例。

_Myfirst=00000000, _Mylast=00000000, _Myend=00000000
size=0, capacity=0
_Myfirst=0051CF48, _Mylast=0051CF4C, _Myend=0051CF4C
size=1, capacity=1
element 0: 1
_Myfirst=0051CF58, _Mylast=0051CF60, _Myend=0051CF60
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0051C278, _Mylast=0051C284, _Myend=0051C284
size=3, capacity=3
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0051C290, _Mylast=0051C2A0, _Myend=0051C2A0
size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0051B180, _Mylast=0051B190, _Myend=0051B198
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0051B180, _Mylast=0051B194, _Myend=0051B198
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0051B180, _Mylast=0051B198, _Myend=0051B198
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
6
6619158

可以看到,在main函数的头部也没有分配空间。当第一次push_back()调用结束后,缓冲区被分配。同时在每一次调用push_back()后,数组的大小和缓冲区容量都增大了。缓冲区地址也变化了,因为push_back()函数每次都会在堆中重新分配缓冲区。这是个耗时的操作,这就是为什么提前预测数组大小,同时调用.reserve()方法预留空间比较重要了。最后数据是垃圾数据,没有数组元素位于这个位置,因此打印出了随机的数据。这也说明了std::vector的[]操作并不校验数组下标是否越界。然而.at()方法会做相应检查,当出现越界时会抛出std::out_of_range。 让我们来看代码:

Listing 34.11: MSVC 2012 /GS- /Ob1

$SG52650 DB ’%d’, 0aH, 00H
$SG52651 DB ’%d’, 0aH, 00H
_this$ = -4 ; size = 4
__Pos$ = 8 ; size = 4
?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z PROC ; std::vector<int,std::allocator<int> >::
at, COMDAT
; _this$ = ecx
push ebp
mov ebp, esp
push ecx
mov DWORD PTR _this$[ebp], ecx
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR _this$[ebp]
mov edx, DWORD PTR [eax+4]
sub edx, DWORD PTR [ecx]
sar edx, 2
cmp edx, DWORD PTR __Pos$[ebp]
ja SHORT $LN1@at
push OFFSET ??_C@_0BM@NMJKDPPO@invalid?5vector?$DMT?$DO?5subscript?$AA@
call DWORD PTR __imp_?_Xout_of_range@std@@YAXPBD@Z
$LN1@at:
mov eax, DWORD PTR _this$[ebp]
mov ecx, DWORD PTR [eax]
mov edx, DWORD PTR __Pos$[ebp]
lea eax, DWORD PTR [ecx+edx*4]
$LN3@at:
mov esp, ebp
pop ebp
ret 4
?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z ENDP ; std::vector<int,std::allocator<int> >::
at
_c$ = -36 ; size = 12
$T1 = -24 ; size = 4
$T2 = -20 ; size = 4
$T3 = -16 ; size = 4
$T4 = -12 ; size = 4
$T5 = -8 ; size = 4
$T6 = -4 ; size = 4
_main PROC
push ebp
mov ebp, esp
sub esp, 36 ; 00000024H
mov DWORD PTR _c$[ebp], 0 ; Myfirst
mov DWORD PTR _c$[ebp+4], 0 ; Mylast
mov DWORD PTR _c$[ebp+8], 0 ; Myend
lea eax, DWORD PTR _c$[ebp]
push eax
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T6[ebp], 1
lea ecx, DWORD PTR $T6[ebp]
push ecx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std
::allocator<int> >::push_back
lea edx, DWORD PTR _c$[ebp]
push edx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T5[ebp], 2
lea eax, DWORD PTR $T5[ebp]
push eax
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std
::allocator<int> >::push_back
lea ecx, DWORD PTR _c$[ebp]
push ecx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T4[ebp], 3
lea edx, DWORD PTR $T4[ebp]
push edx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std
::allocator<int> >::push_back
lea eax, DWORD PTR _c$[ebp]
push eax
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T3[ebp], 4
lea ecx, DWORD PTR $T3[ebp]
push ecx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std
::allocator<int> >::push_back
lea edx, DWORD PTR _c$[ebp]
push edx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
push 6
lea ecx, DWORD PTR _c$[ebp]
call ?reserve@?$vector@HV?$allocator@H@std@@@std@@QAEXI@Z ; std::vector<int,std::
allocator<int> >::reserve
lea eax, DWORD PTR _c$[ebp]
push eax
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T2[ebp], 5
lea ecx, DWORD PTR $T2[ebp]
push ecx
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std
::allocator<int> >::push_back
lea edx, DWORD PTR _c$[ebp]
push edx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
mov DWORD PTR $T1[ebp], 6
lea eax, DWORD PTR $T1[ebp]
push eax
lea ecx, DWORD PTR _c$[ebp]
call ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std
::allocator<int> >::push_back
lea ecx, DWORD PTR _c$[ebp]
push ecx
call ?dump@@YAXPAUvector_of_ints@@@Z ; dump
add esp, 4
push 5
lea ecx, DWORD PTR _c$[ebp]
call ?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z ; std::vector<int,std::
allocator<int> >::at
mov edx, DWORD PTR [eax]
push edx
push OFFSET $SG52650 ; ’%d’
call DWORD PTR __imp__printf
add esp, 8
mov eax, 8
shl eax, 2
mov ecx, DWORD PTR _c$[ebp]
mov edx, DWORD PTR [ecx+eax]
push edx
push OFFSET $SG52651 ; ’%d’
call DWORD PTR __imp__printf
add esp, 8
lea ecx, DWORD PTR _c$[ebp]
call ?_Tidy@?$vector@HV?$allocator@H@std@@@std@@IAEXXZ ; std::vector<int,std::
allocator<int> >::_Tidy
xor eax, eax
mov esp, ebp
pop ebp
ret 0
_main ENDP

我们看到.at()方法如何进行辩解检查,当发生错误时将抛出异常。最后一个printf()调用只是从内存中读取数据,并没有做任何检查。 有人可能会问,为什么没有用类似std::string中size和capacity的变量,我猜想可能是为了让边界检查更快,但是我不确定。 GCC生成的代码基本上相同,但是.at()方法被内联了。

Listing34.12: GCC 4.8.1 -fno-inline-small-functions –O1

main proc near
push ebp
mov ebp, esp
push edi
push esi
push ebx
and esp, 0FFFFFFF0h
sub esp, 20h
mov dword ptr [esp+14h], 0
mov dword ptr [esp+18h], 0
mov dword ptr [esp+1Ch], 0
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov dword ptr [esp+10h], 1
lea eax, [esp+10h]
mov [esp+4], eax
lea eax, [esp+14h]
mov [esp], eax
call _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int
>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov dword ptr [esp+10h], 2
lea eax, [esp+10h]
mov [esp+4], eax
lea eax, [esp+14h]
mov [esp], eax
call _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int
>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov dword ptr [esp+10h], 3
lea eax, [esp+10h]
mov [esp+4], eax
lea eax, [esp+14h]
mov [esp], eax
call _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int
>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov dword ptr [esp+10h], 4
lea eax, [esp+10h]
mov [esp+4], eax
lea eax, [esp+14h]
mov [esp], eax
call _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int
>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov ebx, [esp+14h]
mov eax, [esp+1Ch]
sub eax, ebx
cmp eax, 17h
ja short loc_80001CF
mov edi, [esp+18h]
sub edi, ebx
sar edi, 2
mov dword ptr [esp], 18h
call _Znwj ; operator new(uint)
mov esi, eax
test edi, edi
jz short loc_80001AD
lea eax, ds:0[edi*4]
mov [esp+8], eax ; n
mov [esp+4], ebx ; src
mov [esp], esi ; dest
call memmove
loc_80001AD: ; CODE XREF: main+F8
mov eax, [esp+14h]
test eax, eax
jz short loc_80001BD
mov [esp], eax ; void *
call _ZdlPv ; operator delete(void *)
loc_80001BD: ; CODE XREF: main+117
mov [esp+14h], esi
lea eax, [esi+edi*4]
mov [esp+18h], eax
add esi, 18h
mov [esp+1Ch], esi
loc_80001CF: ; CODE XREF: main+DD
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov dword ptr [esp+10h], 5
lea eax, [esp+10h]
mov [esp+4], eax
lea eax, [esp+14h]
mov [esp], eax
call _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int
>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov dword ptr [esp+10h], 6
lea eax, [esp+10h]
mov [esp+4], eax
lea eax, [esp+14h]
mov [esp], eax
call _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int
>>::push_back(int const&)
lea eax, [esp+14h]
mov [esp], eax
call _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
mov eax, [esp+14h]
mov edx, [esp+18h]
sub edx, eax
cmp edx, 17h
ja short loc_8000246
mov dword ptr [esp], offset aVector_m_range ; "vector::_M_range_check"
call _ZSt20__throw_out_of_rangePKc ; std::__throw_out_of_range(char const*)
loc_8000246: ; CODE XREF: main+19C
mov eax, [eax+14h]
mov [esp+8], eax
mov dword ptr [esp+4], offset aD ; "%d\n"
mov dword ptr [esp], 1
call __printf_chk
mov eax, [esp+14h]
mov eax, [eax+20h]
mov [esp+8], eax
mov dword ptr [esp+4], offset aD ; "%d\n"
mov dword ptr [esp], 1
call __printf_chk
mov eax, [esp+14h]
test eax, eax
jz short loc_80002AC
mov [esp], eax ; void *
call _ZdlPv ; operator delete(void *)
jmp short loc_80002AC
; ---------------------------------------------------------------------------
mov ebx, eax
mov edx, [esp+14h]
test edx, edx
jz short loc_80002A4
mov [esp], edx ; void *
call _ZdlPv ; operator delete(void *)
loc_80002A4: ; CODE XREF: main+1FE
mov [esp], ebx
call _Unwind_Resume
; ---------------------------------------------------------------------------
loc_80002AC: ; CODE XREF: main+1EA
; main+1F4
mov eax, 0
lea esp, [ebp-0Ch]
pop ebx
pop esi
pop edi
pop ebp
locret_80002B8: ; DATA XREF: .eh_frame:08000510
; .eh_frame:080005BC
retn
main endp

.reserve()方法也被内联了。如果缓冲区大小小于新的size,则调用new()申请新缓冲区,调用memmove()拷贝缓冲区内容,然后调用delete()释放旧的缓冲区。 让你给我们看看通过GCC编译后程序的输出

_Myfirst=0x(nil), _Mylast=0x(nil), _Myend=0x(nil)
size=0, capacity=0
_Myfirst=0x8257008, _Mylast=0x825700c, _Myend=0x825700c
size=1, capacity=1
element 0: 1
_Myfirst=0x8257018, _Mylast=0x8257020, _Myend=0x8257020
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0x8257028, _Mylast=0x8257034, _Myend=0x8257038
size=3, capacity=4
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0x8257028, _Mylast=0x8257038, _Myend=0x8257038
size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x8257040, _Mylast=0x8257050, _Myend=0x8257058
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x8257040, _Mylast=0x8257054, _Myend=0x8257058
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0x8257040, _Mylast=0x8257058, _Myend=0x8257058
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
6
0

我们可以看到缓冲区大小的增长不同于MSVC中。 简单的实验说明MSVC中缓冲区每次扩大当前大小的50%,而GCC中则每次扩大100%。

33.5 std::map and std::set

二叉树是另一个基本的数据结构。它是一个树,但是每个节点最多包含两个指向其他节点的指针。每个节点包含键和/或值。 二叉树在键值字典的实现中是常用数据结构。 二叉树至少包含三个重要的属性: 所有的键的存储是排序的。 任何类型的键都容易存储。二叉树并不知道键的类型,因此需要键比较算法。 查找键的速度相比于链表和数组要快。 下面是一个非常简单的例子,我们在二叉树中存储下面这些数据0,1,2,3,5,6,9,10,11,12,20,99,100,101,107,1001,1010.

所有小于根节点键的键存储在树的左侧,所有大于根节点键的键存储在树的右侧。 因此,查找算法就很直接,当要查找的值小于当前节点的键的值,则查找左侧;如果大于当前节点的键的值,则查找右侧;当值相等时,则停止查找。这就是为什么通过键比较函数,算法可以查找数值、文本串等。 所有键的值都是唯一的。 如果这样,为了在n个键的平衡二叉树中查找一个键将需要log2 n步。1000个键需要10步,10000个键需要13步。但是这要求树总是平衡的,即键应该均匀的分布在树的不同层里。插入和删除操作需要保持树的平衡状态。 有一些流行的平衡算法可用,包括AVL树和红黑树。红黑树通过给节点增加一个color值来简化平衡过程,因此每个节点可能是红或者黑。 GCC和MSVC中的std::map和std::set模板的实现均采用了红黑树。 std::set只包含键。std::map是set的扩展,它在每个节点中包含一个值。

** MSVC **

#include <map>
#include <set>
#include <string>
#include <iostream>
// struct is not packed!
struct tree_node
{
    struct tree_node *Left;
    struct tree_node *Parent;
    struct tree_node *Right;
    char Color; // 0 - Red, 1 - Black
    char Isnil;
    //std::pair Myval;
    unsigned int first; // called Myval in std::set
    const char *second; // not present in std::set
};
struct tree_struct
{
    struct tree_node *Myhead;
    size_t Mysize;
};
void dump_tree_node (struct tree_node *n, bool is_set, bool traverse)
{
    printf ("ptr=0x%p Left=0x%p Parent=0x%p Right=0x%p Color=%d Isnil=%d\n", n, n->Left, n->Parent, n->Right, n->Color, n->Isnil);
    if (n->Isnil==0)
    {
        if (is_set)
            printf ("first=%d\n", n->first);
        else
            printf ("first=%d second=[%s]\n", n->first, n->second);
    }
    if (traverse)
    {
        if (n->Isnil==1)
            dump_tree_node (n->Parent, is_set, true);
        else
        {
            if (n->Left->Isnil==0)
                dump_tree_node (n->Left, is_set, true);
            if (n->Right->Isnil==0)
                dump_tree_node (n->Right, is_set, true);
        };
    };
};

const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";
void dump_as_tree (int tabs, struct tree_node *n, bool is_set)
{
    if (is_set)
        printf ("%d\n", n->first);
    else
        printf ("%d [%s]\n", n->first, n->second);
    if (n->Left->Isnil==0)
    {
        printf ("%.*sL-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->Left, is_set);
    };
    if (n->Right->Isnil==0)
    {
        printf ("%.*sR-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->Right, is_set);
    };
};
void dump_map_and_set(struct tree_struct *m, bool is_set)
{
    printf ("ptr=0x%p, Myhead=0x%p, Mysize=%d\n", m, m->Myhead, m->Mysize);
    dump_tree_node (m->Myhead, is_set, true);
    printf ("As a tree:\n");
    printf ("root----");
    dump_as_tree (1, m->Myhead->Parent, is_set);
};
int main()
{
    // map
    std::map<int, const char*> m;
    m[10]="ten";
    m[20]="twenty";
    m[3]="three";
    m[101]="one hundred one";
    m[100]="one hundred";
    m[12]="twelve";
    m[107]="one hundred seven";
    m[0]="zero";
    m[1]="one";
    m[6]="six";
    m[99]="ninety-nine";
    m[5]="five";
    m[11]="eleven";
    m[1001]="one thousand one";
    m[1010]="one thousand ten";
    m[2]="two";
    m[9]="nine";
    printf ("dumping m as map:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&m, false);
    std::map<int, const char*>::iterator it1=m.begin();
    printf ("m.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false);
    it1=m.end();
    printf ("m.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false);
    // set
    std::set<int> s;
    s.insert(123);
    s.insert(456);
    s.insert(11);
    s.insert(12);
    s.insert(100);
    s.insert(1001);
    printf ("dumping s as set:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s, true);
    std::set<int>::iterator it2=s.begin();
    printf ("s.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false);
    it2=s.end();
    printf ("s.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false);
};

Listing 34.13: MSVC 2012

dumping m as map:
ptr=0x0020FE04, Myhead=0x005BB3A0, Mysize=17
ptr=0x005BB3A0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB580 Color=1 Isnil=1
ptr=0x005BB3C0 Left=0x005BB4C0 Parent=0x005BB3A0 Right=0x005BB440 Color=1 Isnil=0
first=10 second=[ten]
ptr=0x005BB4C0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB520 Color=1 Isnil=0
first=1 second=[one]
ptr=0x005BB4A0 Left=0x005BB3A0 Parent=0x005BB4C0 Right=0x005BB3A0 Color=1 Isnil=0
first=0 second=[zero]
ptr=0x005BB520 Left=0x005BB400 Parent=0x005BB4C0 Right=0x005BB4E0 Color=0 Isnil=0
first=5 second=[five]
ptr=0x005BB400 Left=0x005BB5A0 Parent=0x005BB520 Right=0x005BB3A0 Color=1 Isnil=0
first=3 second=[three]
ptr=0x005BB5A0 Left=0x005BB3A0 Parent=0x005BB400 Right=0x005BB3A0 Color=0 Isnil=0
first=2 second=[two]
ptr=0x005BB4E0 Left=0x005BB3A0 Parent=0x005BB520 Right=0x005BB5C0 Color=1 Isnil=0
first=6 second=[six]
ptr=0x005BB5C0 Left=0x005BB3A0 Parent=0x005BB4E0 Right=0x005BB3A0 Color=0 Isnil=0
first=9 second=[nine]
ptr=0x005BB440 Left=0x005BB3E0 Parent=0x005BB3C0 Right=0x005BB480 Color=1 Isnil=0
first=100 second=[one hundred]
ptr=0x005BB3E0 Left=0x005BB460 Parent=0x005BB440 Right=0x005BB500 Color=0 Isnil=0
first=20 second=[twenty]
ptr=0x005BB460 Left=0x005BB540 Parent=0x005BB3E0 Right=0x005BB3A0 Color=1 Isnil=0
first=12 second=[twelve]
ptr=0x005BB540 Left=0x005BB3A0 Parent=0x005BB460 Right=0x005BB3A0 Color=0 Isnil=0
first=11 second=[eleven]
ptr=0x005BB500 Left=0x005BB3A0 Parent=0x005BB3E0 Right=0x005BB3A0 Color=1 Isnil=0
first=99 second=[ninety-nine]
ptr=0x005BB480 Left=0x005BB420 Parent=0x005BB440 Right=0x005BB560 Color=0 Isnil=0
first=107 second=[one hundred seven]
ptr=0x005BB420 Left=0x005BB3A0 Parent=0x005BB480 Right=0x005BB3A0 Color=1 Isnil=0
first=101 second=[one hundred one]
ptr=0x005BB560 Left=0x005BB3A0 Parent=0x005BB480 Right=0x005BB580 Color=1 Isnil=0
first=1001 second=[one thousand one]
ptr=0x005BB580 Left=0x005BB3A0 Parent=0x005BB560 Right=0x005BB3A0 Color=0 Isnil=0
first=1010 second=[one thousand ten]
As a tree:
root----10 [ten]
L-------1 [one]
L-------0 [zero]
R-------5 [five]
L-------3 [three]
L-------2 [two]
R-------6 [six]
R-------9 [nine]
R-------100 [one hundred]
L-------20 [twenty]
L-------12 [twelve]
L-------11 [eleven]
R-------99 [ninety-nine]
R-------107 [one hundred seven]
L-------101 [one hundred one]
R-------1001 [one thousand one]
R-------1010 [one thousand ten]
m.begin():
ptr=0x005BB4A0 Left=0x005BB3A0 Parent=0x005BB4C0 Right=0x005BB3A0 Color=1 Isnil=0
first=0 second=[zero]
m.end():
ptr=0x005BB3A0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB580 Color=1 Isnil=1
dumping s as set:
ptr=0x0020FDFC, Myhead=0x005BB5E0, Mysize=6
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB6A0 Color=1 Isnil=1
ptr=0x005BB600 Left=0x005BB660 Parent=0x005BB5E0 Right=0x005BB620 Color=1 Isnil=0
first=123
ptr=0x005BB660 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB680 Color=1 Isnil=0
first=12
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 Isnil=0
first=11
ptr=0x005BB680 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 Isnil=0
first=100
ptr=0x005BB620 Left=0x005BB5E0 Parent=0x005BB600 Right=0x005BB6A0 Color=1 Isnil=0
first=456
ptr=0x005BB6A0 Left=0x005BB5E0 Parent=0x005BB620 Right=0x005BB5E0 Color=0 Isnil=0
first=1001
As a tree:
root----123
L-------12
L-------11
R-------100
R-------456
R-------1001
s.begin():
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 Isnil=0
first=11
s.end():
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB6A0 Color=1 Isnil=1

结构体没有打包,因此所有的char类型都占用4个字节。 对于std::map,first和second变量可以看做是一个std::pair型变量。而在std::set中,std::pair只有一个变量。 当前树的节点个数总被保存着,这与MSVC中的std::list实现方式一样。 和std::list一样,迭代器只是指向节点的指针。.begin()迭代器指向最小的键。.begin()指针没有存储在某个位置(和list一样),树中最小的key总是可以找到。当节点有前一个和后一个节点时,- -和++操作会将迭代器移动到前一个或者后一个节点。关于这些操作的算法在文献七中进行了描述。 .end()迭代器指向根节点,它的Isnil的值为1,即这个节点没有键和值。

GCC

#include <stdio.h>
#include <map>
#include <set>
#include <string>
#include <iostream>
struct map_pair
{
    int key;
    const char *value;
};

struct tree_node
{
    int M_color; // 0 - Red, 1 - Black
    struct tree_node *M_parent;
    struct tree_node *M_left;
    struct tree_node *M_right;
};
struct tree_struct
{
    int M_key_compare;
    struct tree_node M_header;
    size_t M_node_count;
};
void dump_tree_node (struct tree_node *n, bool is_set, bool traverse, bool dump_keys_and_values)
{
    printf ("ptr=0x%p M_left=0x%p M_parent=0x%p M_right=0x%p M_color=%d\n", n, n->M_left, n->M_parent, n->M_right, n->M_color);
    void *point_after_struct=((char*)n)+sizeof(struct tree_node);
    if (dump_keys_and_values)
    {
        if (is_set)
            printf ("key=%d\n", *(int*)point_after_struct);
        else
        {
            struct map_pair *p=(struct map_pair *)point_after_struct;
            printf ("key=%d value=[%s]\n", p->key, p->value);
        };
    };
    if (traverse==false)
        return;
    if (n->M_left)
        dump_tree_node (n->M_left, is_set, traverse, dump_keys_and_values);
    if (n->M_right)
        dump_tree_node (n->M_right, is_set, traverse, dump_keys_and_values);
};
const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";
void dump_as_tree (int tabs, struct tree_node *n, bool is_set)
{
    void *point_after_struct=((char*)n)+sizeof(struct tree_node);
    if (is_set)
        printf ("%d\n", *(int*)point_after_struct);
    else
    {
        struct map_pair *p=(struct map_pair *)point_after_struct;
        printf ("%d [%s]\n", p->key, p->value);
    }
    if (n->M_left)
    {
        printf ("%.*sL-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_left, is_set);
    };
    if (n->M_right)
    {
        printf ("%.*sR-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_right, is_set);
    };
};
void dump_map_and_set(struct tree_struct *m, bool is_set)
{
    printf ("ptr=0x%p, M_key_compare=0x%x, M_header=0x%p, M_node_count=%d\n", m, m->M_key_compare, &m->M_header, m->M_node_count);
    dump_tree_node (m->M_header.M_parent, is_set, true, true);
    printf ("As a tree:\n");
    printf ("root----");
    dump_as_tree (1, m->M_header.M_parent, is_set);
};
int main()
{
    // map
    std::map<int, const char*> m;
    m[10]="ten";
    m[20]="twenty";
    m[3]="three";
    m[101]="one hundred one";
    m[100]="one hundred";
    m[12]="twelve";
    m[107]="one hundred seven";
    m[0]="zero";
    m[1]="one";
    m[6]="six";
    m[99]="ninety-nine";
    m[5]="five";
    m[11]="eleven";
    m[1001]="one thousand one";
    m[1010]="one thousand ten";
    m[2]="two";
    m[9]="nine";
    printf ("dumping m as map:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&m, false);
    std::map<int, const char*>::iterator it1=m.begin();
    printf ("m.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false, true);
    it1=m.end();
    printf ("m.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false, false);
    // set
    std::set<int> s;
    s.insert(123);
    s.insert(456);
    s.insert(11);
    s.insert(12);
    s.insert(100);
    s.insert(1001);
    printf ("dumping s as set:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s, true);
    std::set<int>::iterator it2=s.begin();
    printf ("s.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, true);
    it2=s.end();
    printf ("s.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, false);
};

dumping m as map:

ptr=0x0028FE3C, M_key_compare=0x402b70, M_header=0x0028FE40, M_node_count=17
ptr=0x007A4988 M_left=0x007A4C00 M_parent=0x0028FE40 M_right=0x007A4B80 M_color=1
key=10 value=[ten]
ptr=0x007A4C00 M_left=0x007A4BE0 M_parent=0x007A4988 M_right=0x007A4C60 M_color=1
key=1 value=[one]
ptr=0x007A4BE0 M_left=0x00000000 M_parent=0x007A4C00 M_right=0x00000000 M_color=1
key=0 value=[zero]
ptr=0x007A4C60 M_left=0x007A4B40 M_parent=0x007A4C00 M_right=0x007A4C20 M_color=0
key=5 value=[five]
ptr=0x007A4B40 M_left=0x007A4CE0 M_parent=0x007A4C60 M_right=0x00000000 M_color=1
key=3 value=[three]
ptr=0x007A4CE0 M_left=0x00000000 M_parent=0x007A4B40 M_right=0x00000000 M_color=0
key=2 value=[two]
ptr=0x007A4C20 M_left=0x00000000 M_parent=0x007A4C60 M_right=0x007A4D00 M_color=1
key=6 value=[six]
ptr=0x007A4D00 M_left=0x00000000 M_parent=0x007A4C20 M_right=0x00000000 M_color=0
key=9 value=[nine]
ptr=0x007A4B80 M_left=0x007A49A8 M_parent=0x007A4988 M_right=0x007A4BC0 M_color=1
key=100 value=[one hundred]
ptr=0x007A49A8 M_left=0x007A4BA0 M_parent=0x007A4B80 M_right=0x007A4C40 M_color=0
key=20 value=[twenty]
ptr=0x007A4BA0 M_left=0x007A4C80 M_parent=0x007A49A8 M_right=0x00000000 M_color=1
key=12 value=[twelve]
ptr=0x007A4C80 M_left=0x00000000 M_parent=0x007A4BA0 M_right=0x00000000 M_color=0
key=11 value=[eleven]
ptr=0x007A4C40 M_left=0x00000000 M_parent=0x007A49A8 M_right=0x00000000 M_color=1
key=99 value=[ninety-nine]
ptr=0x007A4BC0 M_left=0x007A4B60 M_parent=0x007A4B80 M_right=0x007A4CA0 M_color=0
key=107 value=[one hundred seven]
ptr=0x007A4B60 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x00000000 M_color=1
key=101 value=[one hundred one]
ptr=0x007A4CA0 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x007A4CC0 M_color=1
key=1001 value=[one thousand one]
ptr=0x007A4CC0 M_left=0x00000000 M_parent=0x007A4CA0 M_right=0x00000000 M_color=0
key=1010 value=[one thousand ten]
As a tree:
root----10 [ten]
L-------1 [one]
L-------0 [zero]
R-------5 [five]
L-------3 [three]
L-------2 [two]
R-------6 [six]
R-------9 [nine]
R-------100 [one hundred]
L-------20 [twenty]
L-------12 [twelve]
L-------11 [eleven]
R-------99 [ninety-nine]
R-------107 [one hundred seven]
L-------101 [one hundred one]
R-------1001 [one thousand one]
R-------1010 [one thousand ten]
m.begin():
ptr=0x007A4BE0 M_left=0x00000000 M_parent=0x007A4C00 M_right=0x00000000 M_color=1
key=0 value=[zero]
m.end():
ptr=0x0028FE40 M_left=0x007A4BE0 M_parent=0x007A4988 M_right=0x007A4CC0 M_color=0
dumping s as set:
ptr=0x0028FE20, M_key_compare=0x8, M_header=0x0028FE24, M_node_count=6
ptr=0x007A1E80 M_left=0x01D5D890 M_parent=0x0028FE24 M_right=0x01D5D850 M_color=1
key=123
ptr=0x01D5D890 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8B0 M_color=1
key=12
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 M_color=0
key=11
ptr=0x01D5D8B0 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 M_color=0
key=100
ptr=0x01D5D850 M_left=0x00000000 M_parent=0x007A1E80 M_right=0x01D5D8D0 M_color=1
key=456
ptr=0x01D5D8D0 M_left=0x00000000 M_parent=0x01D5D850 M_right=0x00000000 M_color=0
key=1001
As a tree:
root----123
L-------12
L-------11
R-------100
R-------456
R-------1001
s.begin():
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 M_color=0
key=11
s.end():
ptr=0x0028FE24 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8D0 M_color=0

GCC的实现也很类似。唯一一的不同点就是没有Isnil元素,因此占用的空间要小于MSVC的实现。跟节点也是作为.end()迭代器指向的位置,同时也不包含键和值。

重新平衡的演示

下面的例子将向我们展示插入节点后,树如何重新平衡。

#include <stdio.h>
#include <map>
#include <set>
#include <string>
#include <iostream>
struct map_pair
{
    int key;
    const char *value;
};
struct tree_node
{
    int M_color; // 0 - Red, 1 - Black
    struct tree_node *M_parent;
    struct tree_node *M_left;
    struct tree_node *M_right;
};
struct tree_struct
{
    int M_key_compare;
    struct tree_node M_header;
    size_t M_node_count;
};
const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";
void dump_as_tree (int tabs, struct tree_node *n)
{
    void *point_after_struct=((char*)n)+sizeof(struct tree_node);
    printf ("%d\n", *(int*)point_after_struct);
    if (n->M_left)
    {
        printf ("%.*sL-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_left);
    };
    if (n->M_right)
    {
        printf ("%.*sR-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_right);
    };
};
void dump_map_and_set(struct tree_struct *m)
{
    printf ("root----");
    dump_as_tree (1, m->M_header.M_parent);
};
int main()
{
    std::set<int> s;
    s.insert(123);
    s.insert(456);
    printf ("123, 456 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    s.insert(11);
    s.insert(12);
    printf ("\n");
    printf ("11, 12 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    s.insert(100);
    s.insert(1001);
    printf ("\n");
    printf ("100, 1001 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    s.insert(667);
    s.insert(1);
    s.insert(4);
    s.insert(7);
    printf ("\n");
    printf ("667, 1, 4, 7 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    printf ("\n");
};
123, 456 are inserted
root----123
R-------456
11, 12 are inserted
root----123
L-------11
R-------12
R-------456
100, 1001 are inserted
root----123
L-------12
L-------11
R-------100
R-------456
R-------1001
667, 1, 4, 7 are inserted
root----12
L-------4
L-------1
R-------11
L-------7
R-------123
L-------100
R-------667
L-------456
R-------1001

54.10位。

所有位操作工作,与其他的一些ISA(指令集架构)类似:

public static int set (int a, int b)
{
return a | 1<<b;
}
public static int clear (int a, int b)
{
return a & (~(1<<b));
}
public static int set(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=2
0: iload_0
1: iconst_1
2: iload_1
3: ishl
4: ior
5: ireturn
public static int clear(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=2
0: iload_0
1: iconst_1
2: iload_1
3: ishl
4: iconst_m1
5: ixor
6: iand
7: ireturn

926 iconst_m1将-1入栈,这数其实就是16进制的0xFFFFFFFF,将0xFFFFFFFF作为XOR-ing指令执行的操作数。起到的效果就是把所有bits位反向,(A.6.2在1406页)

我将所有数据类型,扩展成64为长整型。

public static long lset (long a, int b)
{
return a | 1<<b;
}
public static long lclear (long a, int b)
{
return a & (~(1<<b));
}
public static long lset(long, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=2
0: lload_0
1: iconst_1
2: iload_2
3: ishl
4: i2l
5: lor
6: lreturn
public static long lclear(long, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=3, args_size=2
0: lload_0
1: iconst_1
2: iload_2
3: ishl
4: iconst_m1
5: ixor
6: i2l
7: land
8: lreturn

代码是相同的,但是指令前面使用了前缀L,操作64位值,并且第二个函数参数还是int类型,并且32值需要升级为64位值,值被i21指令使用,本质上 就是把整型,扩展成64位长整型.

927页

54.12 switch()函数

switch()语句的实现是用tableswitch指令,

public static void f(int a)
{
    switch (a)
    {
        case 0: System.out.println("zero"); break;
        case 1: System.out.println("one\n"); break;
        case 2: System.out.println("two\n"); break;
        case 3: System.out.println("three\n"); break;
        case 4: System.out.println("four\n"); break;
        default: System.out.println("something unknown\n"); break;
    };
}

尽可能简单的例子

public static void f(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: tableswitch { // 0 to 4
0: 36
1: 47
2: 58
3: 69
4: 80
default: 91
}
36: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
39: ldc #3 // String zero
41: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
44: goto 99
47: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
50: ldc #5 // String one\n
52: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
55: goto 99
58: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
61: ldc #6 // String two\n
63: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
66: goto 99
69: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
72: ldc #7 // String three\n
74: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
77: goto 99
80: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
83: ldc #8 // String four\n
85: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
88: goto 99
91: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
94: ldc #9 // String ⤦
Ç something unknown\n
931
CHAPTER 54. JAVA 54.13. ARRAYS
96: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
99: return

930

931

54.14 字符串 54.14.1 第一个例子

字符串也是对象,和其他对象的构造方式相同。(还有数组)

public static void main(String[] args)
{
System.out.println("What is your name?");
String input = System.console().readLine();
System.out.println("Hello, "+input);
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String What is⤦
Ç your name?
5: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
8: invokestatic #5 // Method java/⤦
Ç lang/System.console:()Ljava/io/Console;
11: invokevirtual #6 // Method java/io⤦
Ç /Console.readLine:()Ljava/lang/String;
14: astore_1
15: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
18: new #7 // class java/⤦
Ç lang/StringBuilder
21: dup
22: invokespecial #8 // Method java/⤦
Ç lang/StringBuilder."<init>":()V
25: ldc #9 // String Hello,
27: invokevirtual #10 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
30: aload_1
31: invokevirtual #10 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
34: invokevirtual #11 // Method java/⤦
Ç lang/StringBuilder.toString:()Ljava/lang/String;
37: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
40: return

944 在11行偏移调用了readline()方法,字符串引用(由用户提供)被存储在栈顶,在14行偏移,字符串引用被存储在LVA的1号槽中。

用户输入的字符串在30行偏移处重新加载并和 “hello”字符进行了链接,使用的是StringBulder类,在17行偏移,构造的字符串被pirntln方法打印。

54.14.2 第二个例子 另外一个例子

public class strings
{
public static char test (String a)
{
return a.charAt(3);
};
public static String concat (String a, String b)
{
return a+b;
}
}
public static char test(java.lang.String);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: iconst_3
2: invokevirtual #2 // Method java/⤦
Ç lang/String.charAt:(I)C
5: ireturn

945

字符串的链接使用用StringBuilder类完成。

public static java.lang.String concat(java.lang.String, java.⤦
Ç lang.String);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: new #3 // class java/⤦
Ç lang/StringBuilder
3: dup
4: invokespecial #4 // Method java/⤦
Ç lang/StringBuilder."<init>":()V
7: aload_0
8: invokevirtual #5 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
11: aload_1
12: invokevirtual #5 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
15: invokevirtual #6 // Method java/⤦
Ç lang/StringBuilder.toString:()Ljava/lang/String;
18: areturn

另外一个例子

public static void main(String[] args)
{
String s="Hello!";
int n=123;
System.out.println("s=" + s + " n=" + n);
}

字符串构造用StringBuilder类,和它的添加方法,被构造的字符串被传递给println方法。

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: ldc #2 // String Hello!
2: astore_1
3: bipush 123
5: istore_2
6: getstatic #3 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
9: new #4 // class java/⤦
Ç lang/StringBuilder
12: dup
13: invokespecial #5 // Method java/⤦
Ç lang/StringBuilder."<init>":()V
16: ldc #6 // String s=
18: invokevirtual #7 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
21: aload_1
22: invokevirtual #7 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
25: ldc #8 // String n=
27: invokevirtual #7 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
30: iload_2
31: invokevirtual #9 // Method java/⤦
Ç lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
34: invokevirtual #10 // Method java/⤦
Ç lang/StringBuilder.toString:()Ljava/lang/String;
37: invokevirtual #11 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
40: return

946

54.16 类 简单类

清单 54.15: test.java

public class test
{
public static int a;
private static int b;
public test()
{
a=0;
b=0;
}
public static void set_a (int input)
{
a=input;
}
public static int get_a ()
{
return a;
}
public static void set_b (int input)
{
b=input;
}
public static int get_b ()
{
return b;
}
}

952

构造函数,只是把两个之段设置成0.

public test();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/⤦
Ç lang/Object."<init>":()V
4: iconst_0
5: putstatic #2 // Field a:I
8: iconst_0
9: putstatic #3 // Field b:I
12: return

a的设定器

public static void set_a(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: putstatic #2 // Field a:I
4: return

a的取得器

public static int get_a();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #2 // Field a:I
3: ireturn

b的设定器

public static void set_b(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: putstatic #3 // Field b:I
4: return

b的取得器

public static int get_b();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #3 // Field b:I
3: ireturn

953 类中的公有和私有字段代码没什么区别。 但是类型信息会在in.class 文件中表示,并且,无论如何私有变量是不可以被访问的。

让我们创建对象并调用方法: 清单 54.16: ex1.java

954 新指令创建对象,但不调用构造函数(它在4行偏移被调用)set_a()方法被在16行偏移被调用,字段访问使用的getstatic指令,在行偏移21。

Listing 54.16: ex1.java
public class ex1
{
public static void main(String[] args)
{
test obj=new test();
obj.set_a (1234);
System.out.println(obj.a);
}
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: new #2 // class test
3: dup
4: invokespecial #3 // Method test."<⤦
Ç init>":()V
7: astore_1
8: aload_1
9: pop
10: sipush 1234
13: invokestatic #4 // Method test.⤦
Ç set_a:(I)V
16: getstatic #5 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
19: aload_1
20: pop
21: getstatic #6 // Field test.a:I
24: invokevirtual #7 // Method java/io⤦
Ç /PrintStream.println:(I)V
27: return

5.4 章

54.1介绍 大家都知道,java有很多的反编译器(或是产生JVM字节码) 原因是JVM字节码比其他的X86低级代码更容易进行反编译。

a).多很多相关数据类型的信息。 b).JVM(java虚拟机)内存模型更严格和概括。 c).java编译器没有做任何的优化工作(JVM JIT不是实时),所以,类文件中的字节代码的通常更清晰易读。

JVM字节码知识什么时候有用呢?

a).文件的快速粗糙的打补丁任务,类文件不需要重新编译反编译的结果。 b).分析混淆代码 c).创建你自己的混淆器。 d).创建编译器代码生成器(后端)目标。

我们从一段简短的代码开始,除非特殊声明,我们用的都是JDK1.7

反编译类文件使用的命令,随处可见:javap -c -verbase.

在这本书中提供的很多的例子,都用到了这个。

54.3 简单的计算函数

让我们继续看简单的计算函数。

public class calc
{
public static int half(int a)
{
return a/2;
}
}

这种情况使用icont_2会被使用。

public static int half(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: iconst_2
2: idiv
3: ireturn

iload_0 将零给函数做参数,然后将其入栈。iconst_2将2入栈,这两个指令执行后,栈看上去是这个样子的。

+---+
TOS ->| 2 |
+---+
| a |
+---+

idiv携带两个值在栈顶, divides 只有一个值,返回结果在栈顶。

+--------+
TOS ->| result |
+--------+

ireturn取得比返回。 让我们处理双精度浮点整数。

public class calc
{
public static double half_double(double a)
{
return a/2.0;
}
}

清单54.7 常量区

...
#2 = Double 2.0d
...
public static double half_double(double);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=2, args_size=1
0: dload_0
1: ldc2_w #2 // double 2.0d
4: ddiv
5: dreturn

类似,只是ldc2_w指令是从常量区装载2.0,另外,所有其他三条指令有d前缀,意思是他们工作在double数据类型下。

我们现在使用两个参数的函数。

public class calc
{
public static int sum(int a, int b)
{
return a+b;
}
}
public static int sum(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: iadd
3: ireturn

iload_0加载第一个函数参数(a),iload_2 第二个参数(b)下面两条指令执行后,栈的情况如下:

+---+
TOS ->| b |
+---+
| a |
+---+

iadds 增加两个值,返回结果在栈顶。 +——–+ TOS ->| result | +——–+

让我们把这个例子扩展成长整型数据类型。

public static long lsum(long a, long b)
{
return a+b;
}

我们得到的是:

public static long lsum(long, long);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=4, args_size=2
0: lload_0
1: lload_2
2: ladd
3: lreturn

第二个(load指令从第二参数槽中,取得第二参数。这是因为64位长整型的值占用来位,用了另外的话2位参数槽。)

稍微复杂的例子

public class calc
{
public static int mult_add(int a, int b, int c)
{
return a*b+c;
}
}
public static int mult_add(int, int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=3
0: iload_0
1: iload_1
2: imul
3: iload_2
4: iadd
5: ireturn

第一是相乘,积被存储在栈顶。

+---------+
TOS ->| product |
+---------+

iload_2加载第三个参数(C)入栈。

+---------+
TOS ->| c |
+---------+
| product |
+---------+

现在iadd指令可以相加两个值。

915

54.5 简单的函数调用 mathrandom()返回一个伪随机数,函数范围在「0.0…1.0)之间,但对我们来说,由于一些原因,我们常常需要设计一个函数返回数值范围在「0.0…0.5)

public class HalfRandom
{
public static double f()
{
return Math.random()/2;
}
}

54.8 常量区

...
#2 = Methodref #18.#19 // java/lang/Math.⤦
Ç random:()D
6(Java) Local Variable Array

#3 = Double 2.0d
...
#12 = Utf8 ()D
...
#18 = Class #22 // java/lang/Math
#19 = NameAndType #23:#12 // random:()D
#22 = Utf8 java/lang/Math
#23 = Utf8 random
public static double f();
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: invokestatic #2 // Method java/⤦
Ç lang/Math.random:()D
3: ldc2_w #3 // double 2.0d
6: ddiv
7: dreturn

java本地变量数组 916 静态执行调用math.random()函数,返回值在栈顶。结果是被0.5初返回的,但函数名是怎么被编码的呢? 在常量区使用methodres表达式,进行编码的,它定义类和方法的名称。第一个methodref 字段指向表达式,其次,指向通常文本字符(“java/lang/math”) 第二个methodref表达指向名字和类型表达式,同时链接两个字符。第一个方法的名字式字符串“random”,第二个字符串是“()D”,来编码函数类型,它以为这两个值(因此D是字符串)这种方式1JVM可以检查数据类型的正确性:2)java反编译器可以从被编译的类文件中修改数据类型。

最后,我们试着使用“hello,world!”作为例子。

public class HelloWorld
{
public static void main(String[] args)
{
System.out.println("Hello, World");
}
}

54.9 常量区

917 常量区的ldc行偏移3,指向“hello,world!”字符串,并且将其入栈,在java里它被成为饮用,其实它就是指针,或是地址。

918

...
#2 = Fieldref #16.#17 // java/lang/System.⤦
Ç out:Ljava/io/PrintStream;
#3 = String #18 // Hello, World
#4 = Methodref #19.#20 // java/io/⤦
Ç PrintStream.println:(Ljava/lang/String;)V
...
#16 = Class #23 // java/lang/System
#17 = NameAndType #24:#25 // out:Ljava/io/⤦
Ç PrintStream;
#18 = Utf8 Hello, World
#19 = Class #26 // java/io/⤦
Ç PrintStream
#20 = NameAndType #27:#28 // println:(Ljava/⤦
Ç lang/String;)V
...
#23 = Utf8 java/lang/System
#24 = Utf8 out
#25 = Utf8 Ljava/io/PrintStream;
#26 = Utf8 java/io/PrintStream
#27 = Utf8 println
#28 = Utf8 (Ljava/lang/String;)V
...
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hello, ⤦
Ç World
5: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
8: return

常见的invokevirtual指令,从常量区取信息,然后调用pringln()方法,貌似我们知道的println()方法,适用于各种数据类型,我这种println()函数版本,预先给的是字符串类型。

但是第一个getstatic指令是干什么的?这条指令取得对象信息的字段的一个引用或是地址。输出并将其进栈,这个值实际更像是println放的指针,因此,内部的print method取得两个参数,输入1指向对象的this指针,2)“hello,world”字符串的地址,确实,println()在被初始化系统的调用,对象之外,为了方便,javap使用工具把所有的信息都写入到注释中。

54.7 线性同余伪随机数生成器 我们来试一个简单的伪随机函数生成器,我已经在这本书中用过一次了。(在500页20行) 919

public class LCG
{
public static int rand_state;
public void my_srand (int init)
{
rand_state=init;
}
public static int RNG_a=1664525;
public static int RNG_c=1013904223;

public int my_rand ()
{
rand_state=rand_state*RNG_a;
rand_state=rand_state+RNG_c;
return rand_state & 0x7fff;
}
}

一对类的字段,在最开始时被初始化。但是怎么能,在javap的输出中,发现类的构造呢?

static {};
flags: ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: ldc #5 // int 1664525
2: putstatic #3 // Field RNG_a:I
5: ldc #6 // int 1013904223
7: putstatic #4 // Field RNG_c:I
10: return

这种变量的初始化,RNG_a占用了3个参数槽,iRNG_C是4个,而puststatic指令是,用于设定常量。

my_srand()函数,只是将输入值,存储到rand_state中;

public void my_srand(int);
flags: ACC_PUBLIC
Code:
stack=1, locals=2, args_size=2
0: iload_1
1: putstatic #2 // Field ⤦
Ç rand_state:I
4: return

iload_1 取得输入值并将其入栈。但为什么不用iload_0?因为这个函数可能使用类的字段属性,因此这个变量被作为参数0传递给了函数,rand_state字段属性,在类中占用2个参数槽子。

现在my_rand():

public int my_rand();
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field ⤦
Ç rand_state:I
3: getstatic #3 // Field RNG_a:I
6: imul
7: putstatic #2 // Field ⤦
Ç rand_state:I
10: getstatic #2 // Field ⤦
Ç rand_state:I
13: getstatic #4 // Field RNG_c:I
16: iadd
17: putstatic #2 // Field ⤦
Ç rand_state:I
20: getstatic #2 // Field ⤦
Ç rand_state:I
23: sipush 32767
26: iand
27: ireturn

它仅是加载了所有对象字段的值。在20行偏移,操作和更新rand_state,使用putstatic指令。

rand_state 值被再次重载(因为之前,使用过putstatic指令,其被从栈中弃出)这种代码其实比较低效率,但是可以肯定的是,JVM会经常的,对其进行很好的优化。

54.9 传递参数值

我们来扩展一下min()/max()这个例子。

public class minmax
{
public static int min (int a, int b)
{
if (a>b)
return b;
return a;
}
public static int max (int a, int b)
{
if (a>b)
return a;
return b;
}
public static void main(String[] args)
{
int a=123, b=456;
int max_value=max(a, b);
int min_value=min(a, b);
System.out.println(min_value);
System.out.println(max_value);
}
}

924 这是main()函数的代码。

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: bipush 123
2: istore_1
3: sipush 456
6: istore_2
7: iload_1
8: iload_2
9: invokestatic #2 // Method max:(II⤦
Ç )I
12: istore_3
13: iload_1
14: iload_2
15: invokestatic #3 // Method min:(II⤦
Ç )I
18: istore 4
20: getstatic #4 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
23: iload 4
25: invokevirtual #5 // Method java/io⤦
Ç /PrintStream.println:(I)V
28: getstatic #4 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
31: iload_3
32: invokevirtual #5 // Method java/io⤦
Ç /PrintStream.println:(I)V
35: return

925 参数在栈中的被传给其他函数,返回值在栈顶。

54.11循环

public class Loop
{
public static void main(String[] args)
{
for (int i = 1; i <= 10; i++)
{
System.out.println(i);
}
}
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=1
0: iconst_1
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpgt 21
8: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
11: iload_1
12: invokevirtual #3 // Method java/io⤦
Ç /PrintStream.println:(I)V
15: iinc 1, 1
18: goto 2
21: return

icont_1将1推入到栈顶,istore_1将其存入到LVA的参数槽1,为什么没有零槽?因为main()函数只有一个参数,并且指向其的引用,就在第0号槽中。

因此,i本地变量总是在1号参数槽中。 指令在行3偏移和行5偏移,将i和10的比较。如果i大,执行流进入行21偏移,函数结束了,如果不被println调用。i在行11偏移进行了重新加载,之后给println使用。

多说一句,我们调用pringln打印数据类型是整型,我们看注释,“i,v”,i的意思是整型,v的意思是返回void。

当println函数结束,i是步进到行15偏移,指令第一个操作数是参数槽1的值。第二个是数值1与本地变量相加结果。

goto指令就是跳转,它跳转到循环体的开始地址,再行偏移2.

928页

让我们进行更复杂的例子。

public class Fibonacci
{
public static void main(String[] args)
{
int limit = 20, f = 0, g = 1;
for (int i = 1; i <= limit; i++)
{
f = f + g;
g = f - g;
System.out.println(f);
}
}
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: bipush 20
2: istore_1
3: iconst_0
4: istore_2
5: iconst_1
6: istore_3
7: iconst_1
8: istore 4
10: iload 4
12: iload_1
13: if_icmpgt 37
16: iload_2
17: iload_3
18: iadd
19: istore_2
20: iload_2
21: iload_3
22: isub
23: istore_3
24: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
27: iload_2
28: invokevirtual #3 // Method java/io⤦
Ç /PrintStream.println:(I)V
31: iinc 4, 1
34: goto 10
37: return

929 LVA槽中参数映射。 0-main()的唯一参数。 1-限制,总是20. 2-f 3-g 4-i

我们可以看到java编译器在LVA参数槽分配变量,并且是相同的顺序,就像在源代码中声明变量。

分离指令istore,是用于访问参数槽0123,但是不能大于4,因此,附加一些操作,在行2,8偏移,使用槽中数据作为操作数,类似于在偏移10位置的iload指令。

无可口非,分离其他的槽,限制变量总是20(其本质上就是一个常数),重加载值很经常吗?

JVM JIT 编译器经常可以对其优化的很好。在代码中人工的干预优化其实是没有什么太大价值的。

54.13数组

54.13.1简单的例子 我们首先创建一个长度是10的整型的数组,对其初始化。

public static void main(String[] args)
{
int a[]=new int[10];
for (int i=0; i<10; i++)
a[i]=i;
dump (a);
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: bipush 10
2: newarray int
4: astore_1
5: iconst_0
6: istore_2
7: iload_2
8: bipush 10
10: if_icmpge 23
13: aload_1
14: iload_2
15: iload_2
16: iastore
17: iinc 2, 1
20: goto 7
23: aload_1
24: invokestatic #4 // Method dump:([⤦
Ç I)V
27: return

newarray指令,创建了一个有10个整数元素的数组,数组的大小设置使用bipush指令,然后结果会返回到栈顶。数组类型用newarry指令操作符,进行设定。

932

newarray被执行后,引用(指针)到新创建的数据,栈顶的槽中,astore_1存储引用指向到LVA的一号槽,main()函数的第二个部分,是循环的存储值1到相应的素组元素。 aload_1得到数据的引用并放入到栈中。lastore将integer值从堆中存储到素组中,引用当前的栈顶。main()函数代用dump()的函数部分,参数是,准备给aload_1指令的(行偏移23)

现在我们进入dump()函数。

public static void dump(int a[])
{
for (int i=0; i<a.length; i++)
System.out.println(a[i]);
}
public static void dump(int[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: aload_0
4: arraylength
5: if_icmpge 23
8: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
11: aload_0
12: iload_1
13: iaload
14: invokevirtual #3 // Method java/io⤦
Ç /PrintStream.println:(I)V
17: iinc 1, 1
20: goto 2
23: return

到了引用的数组在0槽,a.length表达式在源代码中是转化到arraylength指令,它取得数组的引用,并且数组的大小在栈顶。 iaload在行偏移13被用于装载数据元素。 它需要在堆栈中的数组引用。用aload_0 11并且索引(用iload_1在行偏移12准备)

无可厚非,指令前缀可能会被错误的理解,就像数组指令,那样不正确,这些指令和对象的引用一起工作的。数组和字符串都是对象。

54.13.2 数组元素的求和

另外的例子

public class ArraySum
{
public static int f (int[] a)
{
int sum=0;
for (int i=0; i<a.length; i++)
sum=sum+a[i];
return sum;
}
}
public static int f(int[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: aload_0
6: arraylength
7: if_icmpge 22
10: iload_1
11: aload_0
12: iload_2
13: iaload
14: iadd
15: istore_1
16: iinc 2, 1
19: goto 4
22: iload_1
23: ireturn

LVA槽0是数组的引用,LVA槽1是本地变量和。

54.13.3 main()函数唯一的数据参数

让我们使用唯一的main()函数参数,字符串数组。

public class UseArgument
{
public static void main(String[] args)
{
System.out.print("Hi, ");
System.out.print(args[1]);
System.out.println(". How are you?");
}
}

934 0参(argument)第0个参数是程序(和C/C++类似)

因此第一个参数,而第一参数是拥护提供的。

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
3: ldc #3 // String Hi,
5: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.print:(Ljava/lang/String;)V
8: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
11: aload_0
12: iconst_1
13: aaload
14: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.print:(Ljava/lang/String;)V
17: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
20: ldc #5 // String . How ⤦
Ç are you?
22: invokevirtual #6 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
25: return

aload_0在11行加载,第0个LVA槽的引用(main()函数唯一的参数) iconst_1和aload在行偏移12,13,取得数组第一个元素的引用(从0计数) 字符串对象的引用在栈顶行14行偏移,给println方法。

54.1.34 初始化字符串数组

class Month
{

public static String[] months =
{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
public String get_month (int i)
{
return months[i];
};
}

935

get_month()函数很简单

public java.lang.String get_month(int);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: getstatic #2 // Field months:[⤦
Ç Ljava/lang/String;
3: iload_1
4: aaload
5: areturn

aaload操作数组引用,java字符串是一个对象,所以a_instructiong被用于操作他们.areturn返回字符串对象的引用。

month[]数值是如果初始化的?

static {};
flags: ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: bipush 12
2: anewarray #3 // class java/⤦
Ç lang/String
5: dup
6: iconst_0
7: ldc #4 // String January
9: aastore
10: dup
11: iconst_1
12: ldc #5 // String ⤦
Ç February
14: aastore
15: dup
16: iconst_2
17: ldc #6 // String March
19: aastore
20: dup
21: iconst_3
22: ldc #7 // String April
24: aastore
25: dup
26: iconst_4
27: ldc #8 // String May
29: aastore
30: dup
31: iconst_5
32: ldc #9 // String June
34: aastore
35: dup
36: bipush 6
38: ldc #10 // String July
40: aastore
41: dup
42: bipush 7
44: ldc #11 // String August
46: aastore
47: dup
48: bipush 8
50: ldc #12 // String ⤦
Ç September
52: aastore
53: dup
54: bipush 9
56: ldc #13 // String October
58: aastore
59: dup
60: bipush 10
62: ldc #14 // String ⤦
Ç November
64: aastore
65: dup
66: bipush 11
68: ldc #15 // String ⤦
Ç December
70: aastore
71: putstatic #2 // Field months:[⤦
Ç Ljava/lang/String;
74: return

936

937 anewarray 创建一个新数组的引用(a是一个前缀)对象的类型被定义在anewarray操作数中,它在这是“java/lang/string”文本字符串,在这之前的bipush 1L是设置数组的大小。 对于我们再这看到一个新指令dup,他是一个众所周知的堆栈操作的计算机指令。用于复制栈顶的值。(包括了之后的编程语言)它在这是用于复制数组的引用。因为aastore张玲玲 起到弹出堆栈中的数组的作用,但是之后,aastore需要在使用一次,java编译器,最好同dup代替getstatic指令,用于生成之前的每个数组的存贮操作。例如,月份字段。

54.13.5可变参数 可变参数 变长参数函数,实际上使用的就是数组,实际使用的就是数组。

public static void f(int... values)
{
for (int i=0; i<values.length; i++)
System.out.println(values[i]);
}
public static void main(String[] args)
{
f (1,2,3,4,5);
}
public static void f(int...);
flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
Code:
stack=3, locals=2, args_size=1
0: iconst_0
1: istore_1
2: iload_1
3: aload_0
4: arraylength
5: if_icmpge 23
8: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
11: aload_0
12: iload_1
13: iaload
14: invokevirtual #3 // Method java/io⤦
Ç /PrintStream.println:(I)V
17: iinc 1, 1
20: goto 2
23: return

f()函数,取得一个整数数组,使用的是aload_0 在行偏移3行。取得到了一个数组的大小,等等。

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=4, locals=1, args_size=1
0: iconst_5
1: newarray int
3: dup
4: iconst_0
5: iconst_1
6: iastore
7: dup
8: iconst_1
9: iconst_2
10: iastore
11: dup
12: iconst_2
13: iconst_3
14: iastore
15: dup
16: iconst_3
17: iconst_4
18: iastore
19: dup
20: iconst_4
21: iconst_5
22: iastore
23: invokestatic #4 // Method f:([I)V
26: return

素组在main()函数是构造的,使用newarray指令,被填充慢了之后f()被调用。

939 随便提一句,数组对象并不是在main()中销毁的,在整个java中也没有被析构。因为JVM的垃圾收集齐不是自动的,当他感觉需要的时候。 format()方法是做什么的?它用两个参数作为输入,字符串和数组对象。

public PrintStream format(String format, Object... args⤦)

让我们看一下。

public static void main(String[] args)
{
int i=123;
double d=123.456;
System.out.format("int: %d double: %f.%n", i, d⤦
Ç );
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=7, locals=4, args_size=1
0: bipush 123
2: istore_1
3: ldc2_w #2 // double 123.456⤦
Ç d
6: dstore_2
7: getstatic #4 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
10: ldc #5 // String int: %d⤦
Ç double: %f.%n
12: iconst_2
13: anewarray #6 // class java/⤦
Ç lang/Object
16: dup
17: iconst_0
18: iload_1
19: invokestatic #7 // Method java/⤦
Ç lang/Integer.valueOf:(I)Ljava/lang/Integer;
22: aastore
23: dup
24: iconst_1
25: dload_2
26: invokestatic #8 // Method java/⤦
Ç lang/Double.valueOf:(D)Ljava/lang/Double;
29: aastore
30: invokevirtual #9 // Method java/io⤦
Ç /PrintStream.format:(Ljava/lang/String;[Ljava/lang/Object⤦
Ç ;)Ljava/io/PrintStream;
33: pop
34: return

940

所以int和double类型是被首先普生为integer和double 对象,被用于方法的值。。。format()方法需要,对象雷翔的对象作为输入,因为integer和double类是继承于根类root。他们适合作为数组输入的元素, 另一方面,数组总是同质的,例如,同一个数组不能含有两种不同的数据类型。不能同时都把integer和double类型的数据同时放入的数组。

数组对象的对象在偏移13行,整型对象被添加到在行偏移22. double对象被添加到数组在29行。

倒数第二的pop指令,丢弃了栈顶的元素,因此,这些return执行,堆栈是的空的(平行)

54.13.6 二位数组

二位数组在java 中是一个数组去引用另外一个数组 让我们来创建二位素组。()

public static void main(String[] args)
{
int[][] a = new int[5][10];
a[1][2]=3;
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: iconst_5
1: bipush 10
3: multianewarray #2, 2 // class "[[I"
7: astore_1
8: aload_1
9: iconst_1
10: aaload
11: iconst_2
12: iconst_3
13: iastore
14: return

它创建使用的是multianewarry指令:对象类型和维数作为操作数,数组的大小(10*5),返回到栈中。(使用iconst_5和bipush指令)

行引用在行偏移10加载(iconst_1和aaload)列引用是选择使用iconst_2指令,在行偏移11行。值得写入和设定在12行,iastore在13 行,写入数据元素?

public static int get12 (int[][] in)
{
return in[1][2];
}
public static int get12(int[][]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: iconst_1
2: aaload
3: iconst_2
4: iaload
5: ireturn

引用数组在行2加载,列的设置是在行3,iaload加载数组。

54.13.7 三维数组 三维数组是,引用一维数组引用一维数组。

public static void main(String[] args)
{
int[][][] a = new int[5][10][15];
a[1][2][3]=4;
get_elem(a);
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: iconst_5
1: bipush 10
3: bipush 15
5: multianewarray #2, 3 // class "[[[I"
9: astore_1
10: aload_1
11: iconst_1
12: aaload
13: iconst_2
14: aaload
15: iconst_3
16: iconst_4
17: iastore
18: aload_1
19: invokestatic #3 // Method ⤦
Ç get_elem:([[[I)I
22: pop
23: return

942

它是用两个aaload指令去找right引用。

public static int get_elem (int[][][] a)
{
return a[1][2][3];
}
public static int get_elem(int[][][]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: iconst_1
2: aaload
3: iconst_2
4: aaload
5: iconst_3
6: iaload
7: ireturn

943

53.13.8总结 在java中可能出现栈溢出吗?不可能,数组长度实际就代表有多少个对象,数组的边界是可控的,而发生越界访问的情况时,会抛出异常。

54.15 异常 让我们稍微修改一下,月处理的那个例子(在932页的54.13.4)

清单 54.10: IncorrectMonthException.java

public class IncorrectMonthException extends Exception
{
private int index;
public IncorrectMonthException(int index)
{
this.index = index;
}
public int getIndex()
{
return index;
}
}

清单 54.11: Month2.java

class Month2
{
public static String[] months =
{
"January",
"February",
"March",
"April",
"May",
"June",
"July",
"August",
"September",
"October",
"November",
"December"
};
public static String get_month (int i) throws ⤦
Ç IncorrectMonthException
{
if (i<0 || i>11)
throw new IncorrectMonthException(i);
return months[i];
};
public static void main (String[] args)
{
try
{
System.out.println(get_month(100));
}
catch(IncorrectMonthException e)
{
System.out.println("incorrect month ⤦
Ç index: "+ e.getIndex());
e.printStackTrace();
}
};
}

本质上,IncorrectMonthExceptinClass类只是做了对象构造,还有访问器方法。 IncorrectMonthExceptinClass是继承于Exception类,所以,IncorrectMonth类构造之前,构造父类Exception,然后传递整数给IncorrectMonthException类作为唯一的属性值。

public IncorrectMonthException(int);
flags: ACC_PUBLIC
Code:
stack=2, locals=2, args_size=2
0: aload_0
1: invokespecial #1 // Method java/⤦
Ç lang/Exception."<init>":()V
4: aload_0
5: iload_1
6: putfield #2 // Field index:I
9: return

getIndex()只是一个访问器,引用到IncorrectMothnException类,被传到LVA的0槽(this指针),用aload_0指令取得, 用getfield指令取得对象的整数值,用ireturn指令将其返回。

public int getIndex();
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field index:I
4: ireturn

现在来看下month.class的get_month方法。

清单 54.12: Month2.class

public static java.lang.String get_month(int) throws ⤦
Ç IncorrectMonthException;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=1, args_size=1
0: iload_0
1: iflt 10
4: iload_0
5: bipush 11
7: if_icmple 19
10: new #2 // class ⤦
Ç IncorrectMonthException
13: dup
14: iload_0
15: invokespecial #3 // Method ⤦
Ç IncorrectMonthException."<init>":(I)V
18: athrow
19: getstatic #4 // Field months:[⤦
Ç Ljava/lang/String;
22: iload_0
23: aaload
24: areturn

949

iflt 在行偏移1 ,如果小于的话,

这种情况其实是无效的索引,在行偏移10创建了一个对象,对象类型是作为操作书传递指令的。(这个IncorrectMonthException的构造届时,下标整数是被通过TOS传递的。行15偏移) 时间流程走到了行18偏移,对象已经被构造了,现在athrow指令取得新构对象的引用,然后发信号给JVM去找个合适的异常句柄。

athrow指令在这个不返回到控制流,行19偏移的其他的个基本模块,和异常无关,我们能得到到行7偏移。 句柄怎么工作? main()在inmonth2.class

清单 54.13: Month2.class

public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=3, locals=2, args_size=1
0: getstatic #5 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
3: bipush 100
5: invokestatic #6 // Method ⤦
Ç get_month:(I)Ljava/lang/String;
8: invokevirtual #7 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
11: goto 47
14: astore_1
15: getstatic #5 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
18: new #8 // class java/⤦
Ç lang/StringBuilder
21: dup
22: invokespecial #9 // Method java/⤦
Ç lang/StringBuilder."<init>":()V
25: ldc #10 // String ⤦
Ç incorrect month index:
27: invokevirtual #11 // Method java/⤦
Ç lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/⤦
Ç StringBuilder;
30: aload_1
31: invokevirtual #12 // Method ⤦
Ç IncorrectMonthException.getIndex:()I
34: invokevirtual #13 // Method java/⤦
Ç lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
37: invokevirtual #14 // Method java/⤦
Ç lang/StringBuilder.toString:()Ljava/lang/String;
40: invokevirtual #7 // Method java/io⤦
Ç /PrintStream.println:(Ljava/lang/String;)V
43: aload_1
44: invokevirtual #15 // Method ⤦
Ç IncorrectMonthException.printStackTrace:()V
47: return
Exception table:
from to target type
0 11 14 Class IncorrectMonthException

950 这是一个异常表,在行偏移0-11(包括)行,一个IncorrectinMonthException异常可能发生,如果发生,控制流到达14行偏移,确实main程序在11行偏移结束,在14行异常开始, 没有进入此区域条件(condition/uncondition)设定,是不可能到打这个位置的。(PS:就是没有异常捕获的设定,就不会有异常流被调用执行。)

但是JVM会传递并覆盖执行这个异常case。 第一个astore_1(在行偏移14)取得,将到来的异常对象的引用,存储在LVA的槽参数1之后。getIndex()方法(这个异常对象) 会被在31行偏移调用。引用当前的异常对象,是在30行偏移之前。 所有的这些代码重置都是字符串操作代码:第一个整数值使用的是getIndex()方法,被转换成字符串使用的是toString()方法,它会和“正确月份下标”的文本字符来链接(像我们之前考虑的那样)。 println()和printStackTrace(1)会被调用,PrintStackTrace(1)调用 结束之后,异常被捕获,我们可以处理正常的函数,在47行偏移,return结束main()函数 , 如果没有发生异常,不会执行任何的代码。

这有个例子,IDA是如何显示异常范围:

清单54.14 我从我的计算机中找到 random.class 这个文件

.catch java/io/FileNotFoundException from met001_335 to ⤦
Ç met001_360\
using met001_360
.catch java/io/FileNotFoundException from met001_185 to ⤦
Ç met001_214\
using met001_214
.catch java/io/FileNotFoundException from met001_181 to ⤦
Ç met001_192\
using met001_195
951
CHAPTER 54. JAVA 54.16. CLASSES
.catch java/io/FileNotFoundException from met001_155 to ⤦
Ç met001_176\
using met001_176
.catch java/io/FileNotFoundException from met001_83 to ⤦
Ç met001_129 using \
met001_129
.catch java/io/FileNotFoundException from met001_42 to ⤦
Ç met001_66 using \
met001_69
.catch java/io/FileNotFoundException from met001_begin to ⤦
Ç met001_37\
using met001_37

[校准到这结束。]

54.17 简单的补丁。

54.17.1 第一个例子

让我们进入一个简单的修补任务。

public class nag
{
public static void nag_screen()
{
System.out.println("This program is not ⤦
Ç registered");
};
public static void main(String[] args)
{
System.out.println("Greetings from the mega-⤦
Ç software");
nag_screen();
}
}

我们如何去除“This program is registered”的打印输出.

最会在IDA中加载.class文件。

955

清单54.1: IDA

我们修补一下函数的第一个byte在177(返回指令操作码)

Figure 54.2 : IDA

这个在JDK1.7中不工作

Exception in thread "main" java.lang.VerifyError: Expecting a ⤦
Ç stack map frame
Exception Details:
Location:
nag.nag_screen()V @1: nop
Reason:
Error exists in the bytecode
Bytecode:
0000000: b100 0212 03b6 0004 b1
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java⤦
Ç :2615)
at java.lang.Class.getMethod0(Class.java:2856)
at java.lang.Class.getMethod(Class.java:1668)
at sun.launcher.LauncherHelper.getMainMethod(⤦
Ç LauncherHelper.java:494)
at sun.launcher.LauncherHelper.checkAndLoadMain(⤦
Ç LauncherHelper.java:486)

956 也许,JVM有一些其他检查,关联到栈映射。 好的,我们修补成不同的,去掉nag()函数调用。

清单:54.5 IDA NOP的操作码是0: 这个可以了!

54.17.2第二个例子

现在是另外一个简单的crackme例子。

public class password
{
public static void main(String[] args)
{
System.out.println("Please enter the password")⤦
Ç ;
String input = System.console().readLine();
if (input.equals("secret"))
System.out.println("password is correct⤦
Ç ");
957
CHAPTER 54. JAVA 54.17. SIMPLE PATCHING
else
System.out.println("password is not ⤦
Ç correct");
}
}

957

图54.4:IDA 我们看ifeq指令是怎么工作的,他的名字的意思是如果等于。 这是不恰当的,我更愿意命名if (ifz if zero) 如果栈顶值是0,他就会跳转,在我们这个例子,如果密码 不正确他就跳转。(equal方法返回的是0) 首先第一个方案就是修该这个指令… iefq是两个bytes的操作码 编码和跳转偏移,让这个指令定制,我们必须设定byte3 3byte(因为3是要添加当前地址结果,总是跳转同下一条指令) 因为ifeq的指令长度就是3bytes.

958 图54.5IDA

这个在JDK1.7中不工作

Exception in thread "main" java.lang.VerifyError: Expecting a ⤦
Ç stackmap frame at branch target 24
Exception Details:
Location:
password.main([Ljava/lang/String;)V @21: ifeq
Reason:
Expected stackmap frame at this location.
Bytecode:
0000000: b200 0212 03b6 0004 b800 05b6 0006 4c2b
0000010: 1207 b600 0899 0003 b200 0212 09b6 0004
0000020: a700 0bb2 0002 120a b600 04b1
Stackmap Table:
append_frame(@35,Object[#20])
same_frame(@43)
at java.lang.Class.getDeclaredMethods0(Native Method)
at java.lang.Class.privateGetDeclaredMethods(Class.java⤦
Ç :2615)
at java.lang.Class.getMethod0(Class.java:2856)
at java.lang.Class.getMethod(Class.java:1668)
at sun.launcher.LauncherHelper.getMainMethod(⤦
Ç LauncherHelper.java:494)
959
CHAPTER 54. JAVA 54.18. SUMMARY
at sun.launcher.LauncherHelper.checkAndLoadMain(⤦
Ç LauncherHelper.java:486)

不用说了,它工作在JRE1.6 我也尝试把所有的3 ifeq的所有操作码都用0替换(NOP),它仍然会工作,好,可能没有更多的堆栈映射在JRE1.7中被检查出来。

好的,我替换整个equal方法调用,使用icore_1指令加NOPS的修改。

(TOS)栈顶总是1,当ifeq指令被执行…所以ifeq也不会被执行。

可以了。

54.18总结

960 和C/C+比较java少了一些什么? 结构体:使用类 联合:使用类继承。 无符号数据类型,多说一句,还有一些在Java中实现的加密算法的硬编码。 函数指针。 54.2 返回一个值

可能最简单的java函数就是返回一些值,oh,并且我们必须注意,一边情况下,在java中没有孤立存在的函数,他们是“方法”(method),每个方法都是被关联到某些类,所以方法不会被定义在类外面, 但是我还是叫他们“函数” (function),我这么用。

public class ret
{
public static int main(String[] args)
{
return 0;
}
}

编译它。

javac ret.java

。。。使用Java标准工具反编译。

javap -c -verbose ret.class

会得到结果:

public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_0
1: ireturn

对于java开发者在编程中,0是使用频率最高的常量。 因为区分短一个短字节的 iconst_0指令入栈0,iconst_1指令(入栈),iconst_2等等,直到iconst5。也可以有iconst_m1, 推送-1。

就像在MIPS中,分离一个寄存器给0常数:3.5.2 在第三页。

栈在JVM中用于在函数调用时,传参和传返回值。因此, iconst_0是将0入栈,ireturn指令,(i就是integer的意思。)是从栈顶返回整数值。

[校准到这,未完待续…]

让我们写一个简单的例子, 现在我们返回1234:

public class ret
{
public static int main(String[] args)
{
return 1234;
}
}

我们得到:

清单: 54.2:jdk1.7(节选) public static int main(java.lang.String[]); flags: ACC_PUBLIC, ACC_STATIC Code: stack=1, locals=1, args_size=1 0: sipush 1234 3: ireturn

sipush(shot integer)如栈值是1234,slot的名字以为着一个16bytes值将会入栈。 sipush(短整型) 1234数值确认时候16-bit值。

public class ret
{
public static int main(String[] args)
{
return 12345678;
}
}

更大的值是什么?

清单 54.3 常量区

...
#2 = Integer 12345678
...

5栈顶

public static int main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATI
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // int 12345678
2: ireturn

907

操作码 JVM的指令码操作码不可能编码成32位数,开发者放弃这种可能。因此,32位数字12345678是被存储在一个叫做常量区的地方。让我们说(大多数被使用的常数(包括字符,对象等等车)) 对我们而言。

对JVM来说传递常量不是唯一的,MIPS ARM和其他的RISC CPUS也不可能把32位操作编码成32位数字,因此 RISC CPU(包括MIPS和ARM)去构造一个值需要一系列的步骤,或是他们保存在数据段中: 28。3 在654页.291 在695页。

MIPS码也有一个传统的常量区,literal pool(原语区) 这个段被叫做“lit4”(对于32位单精度浮点数常数存储) 和lit8(64位双精度浮点整数常量区)

布尔型

public class ret
{
public static boolean main(String[] args)
{
return true;
}
}



public static boolean main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iconst_1

这个JVM字节码是不同于返回的整数学 ,32位数据,在形参中被当成逻辑值使用。像C/C++,但是不能像使用整型或是viceversa返回布尔型,类型信息被存储在类文件中,在运行时检查。

16位短整型也是一样。

908

public class ret
{

public static short main(String[] args)
{
return 1234;
}
}
public static short main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: sipush 1234
3: ireturn

还有char 字符型?

public class ret
{
public static char main(String[] args)
{
return 'A';
}
}
public static char main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: bipush 65
2: ireturn

bipush 的意思“push byte”字节入栈,不必说java的char是16位UTF16字符,和short 短整型相等,单ASCII码的A字符是65,它可能使用指令传输字节到栈。

让我们是试一下byte。

public class retc
{
public static byte main(String[] args)
{
return 123;
}
}
public static byte main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC

Code:
stack=1, locals=1, args_size=1
0: bipush 123
2: ireturn

909

也许会问,位什么费事用两个16位整型当32位用?为什么char数据类型和短整型类型还使用char.

答案很简单,为了数据类型的控制和代码的可读性。char也许本质上short相同,但是我们快速的掌握它的占位符,16位的UTF字符,并且不像其他的integer值符。使用 short,为各位展现一下变量的范围被限制在16位。在需要的地方使用boolean型也是一个很好的主意。代替C样式的int也是为了相同的目的。

在java中integer的64位数据类型。

public class ret3
{
public static long main(String[] args)
{
return 1234567890123456789L;
}
}

清单54.4常量区

...
#2 = Long 1234567890123456789l
...
public static long main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w #2 // long ⤦
Ç 1234567890123456789l
3: lreturn

64位数也被在存储在常量区,ldc2_w 加载它,lreturn返回它。 ldc2_w指令也是从内存常量区中加载双精度浮点数。(同样占64位)

public class ret
{
public static double main(String[] args)
{
return 123.456d;
}
}

清单54.5常量区

...
#2 = Double 123.456d
...
public static double main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc2_w #2 // double 123.456⤦
Ç d
3: dreturn

dreturn 代表 “return double”

最后,单精度浮点数:

public class ret
{
public static float main(String[] args)
{
return 123.456f;
}
}

清单54.6 常量区

...
#2 = Float 123.456f
...
public static float main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: ldc #2 // float 123.456f
2: freturn

此处的ldc指令使用和32位整型数据一样,从常量区中加载。freturn 的意思是“return float”

那么函数还能返回什么呢?

911

public class ret
{
public static void main(String[] args)
{
return;
}
}
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=0, locals=1, args_size=1
0: return

这以为着,使用return控制指令确没有返回实际的值,知道这一点就非常容易的从最后一条指令中演绎出函数(或是方法)的返回类型。

54.4 JVM内存模型

X86和其他低级环境系统使用栈传递参数和存储本地变量,JVM稍微有些不同。

主要体现在: 本地变量数组(LVA)被用于存储到来函数的参数和本地变量。iload_0指令是从其中加载值,istore存储值在其中,首先,函数参数到达:开始从0 或者1(如果0参被this指针用。),那么本地局部变量被分配。

每个槽子的大小都是32位,因此long和double数据类型都占两个槽。

操作数栈(或只是“栈”),被用于在其他函数调用时,计算和传递参数。不像低级X86的环境,它不能去访问栈,而又不明确的使用pushes和pops指令,进行出入栈操作。

54.6 调用beep()函数 这可能是最简单的,不使用参数的调用两个函数。

public static void main(String[] args)
{
java.awt.Toolkit.getDefaultToolkit().beep();
};
public static void main(java.lang.String[]);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: invokestatic #2 // Method java/⤦
Ç awt/Toolkit.getDefaultToolkit:()Ljava/awt/Toolkit;
3: invokevirtual #3 // Method java/⤦
Ç awt/Toolkit.beep:()V
6: return

首先,invokestatic在0行偏移调用javaawt.toolkit. getDefaultTookKit()函数,返回toolkit类对象的引用,invokedvirtualIFge指令在3行偏移,调用这个类的beep()方法。

54.8 条件跳转 让我们进入条件跳转

public class abs
{
public static int abs(int a)
{
if (a<0)
return -a;
return a;
}
}
public static int abs(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=1
0: iload_0
1: ifge 7
4: iload_0
5: ineg
6: ireturn
7: iload_0
8: ireturn

921

ifge跳转到7行偏移,如果栈顶的值大于等于0,别忘了,任何IFXX指令从栈中pop出栈值(用于进行比较)

另外一个例子

public static int min (int a, int b)
{
if (a>b)
return b;
return a;
}

我们得到的是:

public static int min(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: if_icmple 7
5: iload_1
6: ireturn
7: iload_0
8: ireturn

if_icmple出栈两个值并比较他们,如果第三个子值比第一个值小(或者等于)发生跳转到行偏移7.

当我们定义max()函数。

public static int max (int a, int b)
{
if (a>b)
return a;
return b;
}

。。。结果代码是是一样的,但是最后两个iload指令(行偏移5和行偏移7)被跳转了。

public static int max(int, int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=2, args_size=2
0: iload_0
1: iload_1
2: if_icmple 7
5: iload_0
6: ireturn
7: iload_1
8: ireturn

922 更复杂的例子。。

public class cond
{
public static void f(int i)
{
if (i<100)
System.out.print("<100");
if (i==100)
System.out.print("==100");
if (i>100)
System.out.print(">100");
if (i==0)
System.out.print("==0");
}
}
public static void f(int);
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: iload_0
1: bipush 100
3: if_icmpge 14
6: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
9: ldc #3 // String <100
11: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.print:(Ljava/lang/String;)V
14: iload_0
15: bipush 100
17: if_icmpne 28
20: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
23: ldc #5 // String ==100
25: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.print:(Ljava/lang/String;)V
28: iload_0
29: bipush 100
31: if_icmple 42
34: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
37: ldc #6 // String >100
39: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.print:(Ljava/lang/String;)V
42: iload_0
43: ifne 54
46: getstatic #2 // Field java/⤦
Ç lang/System.out:Ljava/io/PrintStream;
49: ldc #7 // String ==0
51: invokevirtual #4 // Method java/io⤦
Ç /PrintStream.print:(Ljava/lang/String;)V
54: return

923 if_icmpge出栈两个值,并且比较它们,如果第的二个值大于第一个,发生跳转到行偏移14,if_icmpne和if_icmple做的工作类似,但是使用不同的判断条件。

在行偏移43的ifne指令,它的名字不是很恰当,我要愿意把它命名为ifnz

如果栈定的值不是0跳转,但是这是怎么做的,总跳转到行偏移54,如果输入的值不是另,如果是0,执行流程进入行偏移46,“==”字符串被打印。

N.BJVM没有无符号数据类型,所以,比较指令的操作数,只有还有符号整数值。

V 寻找代码中有趣或者重要的部分

现代软件设计中,极简不是特别重要的特性。

并不是因为程序员编写的代码多,而是由于许多库通常都会静态链接到可执行文件中。如果所有的外部库都移入了外部DLL文件中,情况将有所不同。(C++使用STL和其他模版库的另一个原因)

因此,确定函数的来源很重要,是否来源于标准库或者其他著名的库(比如Boostlibpng),是否与我们在代码中寻找的东西相关。

通过重写所有的C/C++代码来寻找我们想要的东西是不现实的。

逆向工程师的一个主要的任务是迅速定位到目标代码。

IDA反汇编工具允许我们搜索文本字符串,字节序列和常量。甚至可以导出为.lst或者.asm文件,然后使用grep,awk等工具进一步分析。

当你尝试去理解某些代码的功能时,一些开源库比如libpng会容易理解一些。当你觉得某些常量或者文本字符串眼熟时,值得用google搜索一下。如果你发现他们在某些地方使用了开源项目时,那么只要对比一下函数就可以了。这些方法能够解决部分问题。

举个例子,如果一个程序使用XML文件,那么第一步是确定使用了哪个XML库。通常情况下使用的是标准库(或者有名的库)而非自编写的库。

再举个例子,有一次我尝试去理解SAP 6.0中网络包如何压缩与解压。整个软件很大,但手头有一个包含详细debug信息的.PDB文件,非常方便。最后我找到一个负责解压网络包的函数,叫CsDecomprLZC。我马上就用google搜索了函数名,发现MaxDB(一个开源SAP项目)也使用了这个函数。http://www.google.com/search?q=CsDecomprLZC

然后惊奇的发现,MaxDB和SAP 6.0 使用同样的代码来处理压缩和解压网络包。

第55章

识别可执行文件

55.1 Microsoft Visual C++

可导入的MSVC版本和DLL文件如下图:

msvcp*.dll包含C++相关函数,因此如果导入了这类dll,便可推测是C++程序。

55.1.1命名管理

命名通常以问号?开始。

获取更多关于MSVC命令管理的信息:51.1.1节

55.2 GCC

除了*NIX环境,Win32下也有GCC,需要Cygwin和MinGW。

55.2.1 命名管理

命名通常以_Z符号开头。

更多关于GCC命名管理的信息:51.1.1节

55.2.2 Cygwin

cygwin1.dll经常被导入。

55.2.3 MinGW

msvcrt.dll可能会被导入。

55.3 Intel FORTRAN

libifcoremd.dll,libifportmd.dll和libiomp5md.dll(OpenMP支持)可能会被导入。

libifcoremd.dll中许多函数以前缀名for_开始,表示FORTRAN。

55.4Watcom,OpenWatcom

55.4.1 命名管理

命名通常以W符号开始。

举个例子,下面是“class”类名为“method”的方法没有任何参数并且返回void的加密:

W?method$_class$n__v

55.5 Borland

这里有一个有关Borland Delphi和C++开发者命名管理的例子:

@TApplication@IdleAction$qv@TApplication@ProcessMDIAccels$qp6tagMSG@TModule@$bctr$qpcpvt1@TModule@$bdtr$qv@TModule@ValidWindow$qp14TWindowsObject@TrueColorTo8BitN$qpviiiiiit1iiiiii@TrueColorTo16BitN$qpviiiiiit1iiiiii@DIB24BitTo8BitBitmap$qpviiiiiit1iiiii@TrueBitmap@$bctr$qpcl@TrueBitmap@$bctr$qpvl@TrueBitmap@$bctr$qiilll

命名通常以@符号开始,然后是类名、方法名、加密方法的参数类型。

这些名称会被导入到.exe,.dll和debug信息内等等。

Borland Visual Component Libarary(VCL)存储在.bpl文件中,而不是.dll。比如vcl50.dll,rtl60.dll。

其他可能导入的DLL:BORLNDMM.DLL。

55.5.1 Delphi

几乎所有的Delphi可执行文件的代码段都以“Boolean”字符串开始,和其他类型名称一起。 下面是一个典型的Delphi程序的代码段开头,这个块紧接着win32 PE文件头:

00000400  04 10 40 00 03 07 42 6f  6f 6c 65 61 6e 01 00 00  |..@...Boolean...|
00000410  00 00 01 00 00 00 00 10  40 00 05 46 61 6c 73 65  |........@..False|
00000420  04 54 72 75 65 8d 40 00  2c 10 40 00 09 08 57 69  |.True.@.,.@...Wi|
00000430  64 65 43 68 61 72 03 00  00 00 00 ff ff 00 00 90  |deChar..........|
00000440  44 10 40 00 02 04 43 68  61 72 01 00 00 00 00 ff  |D.@...Char......|
00000450  00 00 00 90 58 10 40 00  01 08 53 6d 61 6c 6c 69  |....X.@...Smalli|
00000460  6e 74 02 00 80 ff ff ff  7f 00 00 90 70 10 40 00  |nt..........p.@.|
00000470  01 07 49 6e 74 65 67 65  72 04 00 00 00 80 ff ff  |..Integer.......|
00000480  ff 7f 8b c0 88 10 40 00  01 04 42 79 74 65 01 00  |......@...Byte..|
00000490  00 00 00 ff 00 00 00 90  9c 10 40 00 01 04 57 6f  |..........@...Wo|
000004a0  72 64 03 00 00 00 00 ff  ff 00 00 90 b0 10 40 00  |rd............@.|
000004b0  01 08 43 61 72 64 69 6e  61 6c 05 00 00 00 00 ff  |..Cardinal......|
000004c0  ff ff ff 90 c8 10 40 00  10 05 49 6e 74 36 34 00  |......@...Int64.|
000004d0  00 00 00 00 00 00 80 ff  ff ff ff ff ff ff 7f 90  |................|
000004e0  e4 10 40 00 04 08 45 78  74 65 6e 64 65 64 02 90  |..@...Extended..|
000004f0  f4 10 40 00 04 06 44 6f  75 62 6c 65 01 8d 40 00  |..@...Double..@.|
00000500  04 11 40 00 04 08 43 75  72 72 65 6e 63 79 04 90  |..@...Currency..|
00000510  14 11 40 00 0a 06 73 74  72 69 6e 67 20 11 40 00  |..@...string .@.|
00000520  0b 0a 57 69 64 65 53 74  72 69 6e 67 30 11 40 00  |..WideString0.@.|
00000530  0c 07 56 61 72 69 61 6e  74 8d 40 00 40 11 40 00  |..Variant.@.@.@.|
00000540  0c 0a 4f 6c 65 56 61 72  69 61 6e 74 98 11 40 00  |..OleVariant..@.|
00000550  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000560  00 00 00 00 00 00 00 00  00 00 00 00 98 11 40 00  |..............@.|
00000570  04 00 00 00 00 00 00 00  18 4d 40 00 24 4d 40 00  |.........M@.$M@.|
00000580  28 4d 40 00 2c 4d 40 00  20 4d 40 00 68 4a 40 00  |(M@.,M@. M@.hJ@.|
00000590  84 4a 40 00 c0 4a 40 00  07 54 4f 62 6a 65 63 74  |.J@..J@..TObject|
000005a0  a4 11 40 00 07 07 54 4f  62 6a 65 63 74 98 11 40  |..@...TObject..@|
000005b0  00 00 00 00 00 00 00 06  53 79 73 74 65 6d 00 00  |........System..|
000005c0  c4 11 40 00 0f 0a 49 49  6e 74 65 72 66 61 63 65  |..@...IInterface|
000005d0  00 00 00 00 01 00 00 00  00 00 00 00 00 c0 00 00  |................|
000005e0  00 00 00 00 46 06 53 79  73 74 65 6d 03 00 ff ff  |....F.System....|
000005f0  f4 11 40 00 0f 09 49 44  69 73 70 61 74 63 68 c0  |..@...IDispatch.|
00000600  11 40 00 01 00 04 02 00  00 00 00 00 c0 00 00 00  |.@..............|
00000610  00 00 00 46 06 53 79 73  74 65 6d 04 00 ff ff 90  |...F.System.....|
00000620  cc 83 44 24 04 f8 e9 51  6c 00 00 83 44 24 04 f8  |..D$...Ql...D$..|
00000630  e9 6f 6c 00 00 83 44 24  04 f8 e9 79 6c 00 00 cc  |.ol...D$...yl...|
00000640  cc 21 12 40 00 2b 12 40  00 35 12 40 00 01 00 00  |.!.@.+.@.5.@....|
00000650  00 00 00 00 00 00 00 00  00 c0 00 00 00 00 00 00  |................|
00000660  46 41 12 40 00 08 00 00  00 00 00 00 00 8d 40 00  |FA.@..........@.|
00000670  bc 12 40 00 4d 12 40 00  00 00 00 00 00 00 00 00  |..@.M.@.........|
00000680  00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00  |................|
00000690  bc 12 40 00 0c 00 00 00  4c 11 40 00 18 4d 40 00  |..@.....L.@..M@.|
000006a0  50 7e 40 00 5c 7e 40 00  2c 4d 40 00 20 4d 40 00  |P~@.\~@.,M@. M@.|
000006b0  6c 7e 40 00 84 4a 40 00  c0 4a 40 00 11 54 49 6e  |l~@..J@..J@..TIn|
000006c0  74 65 72 66 61 63 65 64  4f 62 6a 65 63 74 8b c0  |terfacedObject..|
000006d0  d4 12 40 00 07 11 54 49  6e 74 65 72 66 61 63 65  |..@...TInterface|
000006e0  64 4f 62 6a 65 63 74 bc  12 40 00 a0 11 40 00 00  |dObject..@...@..|
000006f0  00 06 53 79 73 74 65 6d  00 00 8b c0 00 13 40 00  |..System......@.|
00000700  11 0b 54 42 6f 75 6e 64  41 72 72 61 79 04 00 00  |..TBoundArray...|
00000710  00 00 00 00 00 03 00 00  00 6c 10 40 00 06 53 79  |.........l.@..Sy|
00000720  73 74 65 6d 28 13 40 00  04 09 54 44 61 74 65 54  |stem(.@...TDateT|
00000730  69 6d 65 01 ff 25 48 e0  c4 00 8b c0 ff 25 44 e0  |ime..%H......%D.|

数据段(DATA)最开始的四字节可能是00 00 00 00,32 13 8B C0或者FF FF FF FF。在处理加壳/加密的 Delphi可执行文件时这个信息很有用。

55.6其他有名的DLLs

  • vcomp*.dll Microsoft实现的OpenMP

第56章

与外部世界通信(win32)

有时理解函数的功能通过观察函数的输入与输出就足够了。这样可以节省时间。

文件和注册访问:对于最基本的分析,SysInternals的Process Monitor工具很有用。

对于基本网络访问分析,Wireshark很有帮助。

但接下来你仍需查看内部。

第一步是查看使用的是OS的API哪个函数,标准库是什么。

如果程序被分为主要的可执行文件和一系列DLL文件,那么DLL文件中的函数名可能会有帮助。

如果我们对指定文本调用MessageBox()的细节感兴趣,我们可以在数据段中查找这个文本,定位文本引用处,以及控制权交给我们感兴趣的MessageBox()的地方。

如果我们在谈论电子游戏,并且对里面的事件的随机性感兴趣,那么我们可以查找rand()函数或者类似函数(比如马特赛特旋转演算法),然后定位调用这些函数的地方,更重要的是,函数执行结果如何被使用。

但如果不是一个游戏,并且仍然使用了rand()函数,找出原因也很有意思。这里有一些关于在数据压缩算法中意外出现rand()函数调用的例子(模仿加密):blog.yurichev.com

56.1 Windows API中常用的函数

下面这些函数可能会被导入。值得注意的是并不是每个函数都在代码中使用。许多函数可能被库函数和CRT代码调用。

  • 注册表访问(advapi32.dll):RegEnumKeyEx, RegEnumValue, RegGetValue7, RegOpenKeyEx, RegQueryValueEx
  • .ini-file访问(kernel32.dll): GetPrivateProfileString
  • 资源访问(68.2.8): (user32.dll): LoadMen
  • TCP/IP网络(ws2_32.dll): WSARecv, WSASend
  • 文件访问(kernel32.dll): CreateFile, ReadFile, ReadFileEx, WriteFile, WriteFileEx
  • Internet高级访问(wininet.dll): WinHttpOpen
  • 可执行文件数字签名(wintrust.dll): WinVerifyTrust
  • 标准MSVC库(如果是动态链接的) (msvcr*.dll): assert, itoa, ltoa, open, printf, read, strcmp, atol, atoi, fopen, fread, fwrite, memcmp, rand, strlen, strstr, strchr

56.2 tracer:拦截所有函数特殊模块

这里有一个INT3断点,只触发了一次,但可以为指定DLL中的所有函数设置。

--one-time-INT3-bp:somedll.dll!.*

我们给所有前缀是xml的函数设置INT3断点吧:

--one-time-INT3-bp:somedll.dll!xml.*

另一方面,这样的断点只会触发一次。

Tracer会在函数调用发生时显示调用情况,但只有一次。但查看函数参数是不可能的。

尽管如此,在你知道这个程序使用了一个DLL,但不知道实际上使用了哪个函数并且有许多的函数的情况下,这个特性还是很有用的。

举个例子,我们来看看,cygwin的uptime工具使用了什么:

tracer -l:uptime.exe --one-time-INT3-bp:cygwin1.dll!.*

我们可以看见所有的至少调用了一次的cygwin1.dll库函数,以及位置:

One-time INT3 breakpoint: cygwin1.dll!__main (called from uptime.exe!OEP+0x6d (0x40106d))
One-time INT3 breakpoint: cygwin1.dll!_geteuid32 (called from uptime.exe!OEP+0xba3 (0x401ba3))
One-time INT3 breakpoint: cygwin1.dll!_getuid32 (called from uptime.exe!OEP+0xbaa (0x401baa))
One-time INT3 breakpoint: cygwin1.dll!_getegid32 (called from uptime.exe!OEP+0xcb7 (0x401cb7))
One-time INT3 breakpoint: cygwin1.dll!_getgid32 (called from uptime.exe!OEP+0xcbe (0x401cbe))
One-time INT3 breakpoint: cygwin1.dll!sysconf (called from uptime.exe!OEP+0x735 (0x401735))
One-time INT3 breakpoint: cygwin1.dll!setlocale (called from uptime.exe!OEP+0x7b2 (0x4017b2))
One-time INT3 breakpoint: cygwin1.dll!_open64 (called from uptime.exe!OEP+0x994 (0x401994))
One-time INT3 breakpoint: cygwin1.dll!_lseek64 (called from uptime.exe!OEP+0x7ea (0x4017ea))
One-time INT3 breakpoint: cygwin1.dll!read (called from uptime.exe!OEP+0x809 (0x401809))
One-time INT3 breakpoint: cygwin1.dll!sscanf (called from uptime.exe!OEP+0x839 (0x401839))
One-time INT3 breakpoint: cygwin1.dll!uname (called from uptime.exe!OEP+0x139 (0x401139))
One-time INT3 breakpoint: cygwin1.dll!time (called from uptime.exe!OEP+0x22e (0x40122e))
One-time INT3 breakpoint: cygwin1.dll!localtime (called from uptime.exe!OEP+0x236 (0x401236))
One-time INT3 breakpoint: cygwin1.dll!sprintf (called from uptime.exe!OEP+0x25a (0x40125a))
One-time INT3 breakpoint: cygwin1.dll!setutent (called from uptime.exe!OEP+0x3b1 (0x4013b1))
One-time INT3 breakpoint: cygwin1.dll!getutent (called from uptime.exe!OEP+0x3c5 (0x4013c5))
One-time INT3 breakpoint: cygwin1.dll!endutent (called from uptime.exe!OEP+0x3e6 (0x4013

第57章

字符串

57.1 文本字符串

57.1.1 C/C++

普通的C字符串是以零结束的(ASCIIZ字符串)。

C字符串格式(以零结束)是这样的是出于历史原因。[Rit79中]:

A minor difference was that the unit of I/O was the word, not the byte, because the PDP-7 was a word- addressed machine. In practice this meant merely that all programs dealing with character streams ignored null characters, because null was used to pad a file to an even number of characters.

在Hiew或者FAR Manager中,这些字符串看上去是这样的:

int main()
{
    printf ("Hello, world!\n");
};

57.1.2 Borland Delphi

在Pascal和Borland Delphi中字符串为8-bit或者32-bit长。

举个例子:

CODE:00518AC8                 dd 19h
CODE:00518ACC aLoading___Plea db 'Loading... , please wait.',0
...
CODE:00518AFC                 dd 10h
CODE:00518B00 aPreparingRun__ db 'Preparing run...',0

57.1.3 Unicode

通常情况下,称Unicode是一种编码字符串的方法,每个字符占用2个字节或者16bit。这是一种常见的术语错误。在许多语言系统中,Unicode是一种用于给每个字符分配数字的标准,而不是用于描述编码的方法。

最常用的编码方法是:UTF-8(在Internet和*NIX系统中使用较多)和UTF-16LE(在Windows中使用)。

UTF-8

UTF-8是最成功的字符编码方法之一。所有拉丁符号编码成ASCII,而超出ASCII表的字符的编码使用多个字节。0的编码方式和以前一样,所有的标准C字符串函数处理UTF-8字符串和处理其他字符串一样。

我们来看看不同语言中的符号在UTF-8中是如何被编码的,在FAR中看上去又是什么样的,使用437内码表:

就像你看到的一样,英语字符串看上去和ASCII编码的一样。匈牙利语使用一些拉丁符号加上变音标志。这些符号使用多个字节编码。我用红色下划线标记出来了。对于冰岛语和波兰语也是一样的。我在开始处使用“Euro”通行符号,编码为3个字节。这里剩下的语言系统与拉丁文没有联系。至少在俄语、阿拉伯语、希伯来语和印地语中我们可以看到相同的字节,这并不稀奇:语言系统的所有符号通常位于同一个Unicode表中,所以他们的号码前几个数字相同。

之前在“How much?”前面,我们看到了3个字节,这实际上是BOM。BOM定义了使用的编码系统。

UTF-16LE

在Windows中,许多win32函数带有后缀 -A和-W。第一种类型的函数用于处理普通字符串,第二种类型的函数用于处理UTF-16LE(wide),每个符号存储类型通常为16比特的short。 UTF-16中拉丁符号在Hiew和FAR中看上去插入了0字节:

int wmain()
{
    wprintf (L"Hello, world!\n");
};

在Windows NT系统中经常可以看见这样的:

在IDA中,占两个字节通常被称为Unicode:

.data:0040E000 aHelloWorld:
.data:0040E000                 unicode 0, <Hello, world!>
.data:0040E000                 dw 0Ah, 0

下面是俄语字符串在UTF-16LE中如何被编码:

容易发现的是,符号被插入了方形字符(ASCII码为4).实际上,西里尔符号位于Unicode第四个平面。因此,在UTF-16LE中,西里尔符号的范围为0x400到0x4FF.

我们回到使用多种语言书写的字符串的例子中吧。下面是他们在UTF-16LE中的样子。

这里我们也能看到开始处有一个BOM。所有的拉丁字符都被插入了一个0字节。我也给一些带有变音符号的字符标注了红色下划线(匈牙利语和冰岛语)。

57.1.4 Base64

Base64编码方法多用于需要将二进制数据以文本字符串的形式传输的情况。实际上,这种算法将3个二进制字节编码为4个可打印字符:所有拉丁字母(包括大小写)、数字、加号、除号共64个字符。

Base64字符串一个显著的特性是他们经常(并不总是)以1个或者2个等号结尾,举个例子:

AVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuWs=
WVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuQ==

等号不会在base-64编码的字符串中间出现。

57.2 Error/debug messages

调试信息非常有帮助。在某种程度上,调试信息报告了程序当前的行为。通常这些printf类函数,写入信息到log文件中,在release模式下不写任何东西但会显示调用信息。如果本地或全局变量dump到了调试信息中,可能会有帮助,至少能获取变量名。比如在Oracle RDBMS中就有这样一个函数 ksdewt()。

文本字符串常常很有帮助。IDA反汇编器可以展示指定字符串被哪个函数在哪里使用。经常会出现有趣的状况。

错误信息也很有帮助。在Oracle RDBMS中,错误信息会报告使用的一系列函数。

更多相关信息:blog.yurichev.com

快速获知哪个函数在什么情况下报告了错误信息是可以做到的。顺便说一句,这也是copy-protection系统为什么要设置模糊而难懂的错误信息或错误码。没有人会为软件破解者仅仅通过错误信息就快速找到了copy-protection被触发的原因而感到高兴。

一个关于错误信息编码的例子:78.2节

57.3 Suspicious magic strings

一些幻数字符串通常使用在后门中,看上去很神秘。举个例子,下面有一个TP-Link WR740路由器的后门。使用下面的URL可以激活后门:http://192.168.0.1/userRpmNatDebugRpm26525557/start_art.html

实际上,“userRpmNatDebugRpm26525557”字符串会在硬件中显示。在后门信息泄漏前,这个字符串并不能被google到。你在任何RFC中都找不到这个。你也无法在任何计算机科学算法中找到使用了这个奇怪字节序列的地方。此外,这看上去也不像错误信息或者调试信息。因此,调查这样一个奇怪字符串的用途是明智的。有时像这样的字符串可能使用了base64编码。所以解码后再看一遍是明智的,甚至扫一眼就够了。

更确切的说,这种隐藏后门的方法称为“security through obscurity”。

第58章

调用assert

有时,assert()宏的出现也是有用的:通常这个宏会泄漏源文件名,行号和条件。

最有用的信息包含在assert的条件中,我们可以从中推断出变量名或者结构体名。另一个有用的信息是文件名。我们可以从中推断出使用了什么类型的代码。并且也可能通过文件名识别出有名的开源库。

.text:107D4B29 mov  dx, [ecx+42h]
.text:107D4B2D cmp  edx, 1
.text:107D4B30 jz   short loc_107D4B4A
.text:107D4B32 push 1ECh
.text:107D4B37 push offset aWrite_c ; "write.c"
.text:107D4B3C push offset aTdTd_planarcon ; "td->td_planarconfig == PLANARCONFIG_CON"...
.text:107D4B41 call ds:_assert
...
.text:107D52CA mov  edx, [ebp-4]
.text:107D52CD and  edx, 3
.text:107D52D0 test edx, edx
.text:107D52D2 jz   short loc_107D52E9
.text:107D52D4 push 58h
.text:107D52D6 push offset aDumpmode_c ; "dumpmode.c"
.text:107D52DB push offset aN30     ; "(n & 3) == 0"
.text:107D52E0 call ds:_assert
...
.text:107D6759 mov  cx, [eax+6]
.text:107D675D cmp  ecx, 0Ch
.text:107D6760 jle  short loc_107D677A
.text:107D6762 push 2D8h
.text:107D6767 push offset aLzw_c   ; "lzw.c"
.text:107D676C push offset aSpLzw_nbitsBit ; "sp->lzw_nbits <= BITS_MAX"
.text:107D6771 call ds:_assert

同时google一下条件和文件名是明智的,可能会因此找到开源库。举个例子,如果我们google查找“sp->lzw_nbits <= BITS_MAX”,将会显示一些与LZW压缩有关的开源代码。

第59章

常量

通常人们在生活中或者程序员在编写代码时喜欢使用像10,100,1000这样的整数。

有经验的逆向工程师会对这些数字的十六进制形式很熟悉:10=0xA, 100=0x64, 1000=0x3E8, 10000=0x2710。

常量 0xAAAAAAAA (10101010101010101010101010101010)和0x55555555 (01010101010101010101010101010101)也很常用——构成alternating bits。举个例子,0x55AA在引导扇区,MBR,IBM兼容扩展卡中使用过。

某些算法,特别是密码学方面的使用的常量很有代表性,我们可以在IDA中轻松找到。

举个例子,MD5算法这样初始化内部变量:

var int h0 := 0x67452301 var int h1 := 0xEFCDAB89 var int h2 := 0x98BADCFE var int h3 := 0x10325476

如果你在代码中某行发现这四个常量,那么极有可能该处函数与MD5有关。

另一个有关CRC16/CRC32算法的例子,通常使用预先计算好的表来计算:

/** CRC table for the CRC-16. The poly is 0x8005 (x^16 + x^15 + x^2 + 1) */
u16 const crc16_table[256] = {
0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
...

CRC3预计算表同见:第37节

59.1 幻数

许多文件格式定义了标准的文件头,使用了幻数。

举个例子,所有的Win32和MS-DOS可执行文件以“MZ”这两个字符开始。

MIDI文件的开始有“MThd”标志。如果我们有一个使用MIDI文件的程序,它很有可能会检查至少4字节的文件头来确认文件类型。

可以这样实现:

(buf指向内存文件加载的开始处)

cmp [buf], 0x6468544D ; "MThd"
    jnz _error_not_a_MIDI_file

也可能会调用某个函数比如memcmp()或者等同于CMPSB指令(A.6.3节)的代码用于比对内存块。

当你发现这样的地方,你就可以确定的MIDI文件加载的开始处,同时我们可以看到缓冲区存放MIDI文件内容的地方,什么内容被使用以及如何使用。

59.1.1 DHCP

上面的方法对于网络协议也同样适用。举个例子,DHCP协议网络包包含了magic cookie:0x6353826。任何生成DHCP包的代码在某处一定将这个常量嵌入了包中。它在代码中出现的地方可能就与执行这些操作有关,或者不仅是如此。任何接收DHCP的包都会检查这个magic cookie,比对是否相同。

举个例子,我们在Windows 7 x64的dhcpcore.dll文件中搜索这个常量。找到两处:看上去这个常量在名为DhcpExtractOptionsForValidation()和 DhcpExtractFullOptions()函数中使用:

.rdata:000007FF6483CBE8 dword_7FF6483CBE8 dd 63538263h ; DATA XREF: ⤦ 
    DhcpExtractOptionsForValidation+79
.rdata:000007FF6483CBEC dword_7                       DATA XREF: ⤦ 
    DhcpExtractFullOptions+97

下面是常量被引用的地址:

.text:000007FF6480875F  mov eax, [rsi]
.text:000007FF64808761  cmp eax, cs:dword_7FF6483CBE8
.text:000007FF64808767  jnz loc_7FF64817179

还有:

.text:000007FF648082C7  mov eax, [r12]
.text:000007FF648082CB  cmp eax, cs:dword_7FF6483CBEC
.text:000007FF648082D1  jnz loc_7FF648173AF

59.2 搜索常量

在IDA中很容易:使用ALT-B或者ALT-I。如果是在大量文件或者在不可执行文件中搜索常量,我会使用自己编写一个叫binary grep的小工具。

第60章

寻找合适的指令

如果程序使用了FPU指令但使用不多,你可以尝试用调试器手工逐个检查。

举个例子,我们可能会对用户如何在微软的Excel中输入计算公式感兴趣,比如除法操作。

如果我们加载excel.exe(Offic 2010)版本为14.0.4756.1000 到IDA中,列出所有的条目,查找每一条FDIV指令(除了使用常量作为第二个操作数的——显然不是我们所关心的):

cat EXCEL.lst | grep fdiv | grep -v dbl_ > EXCEL.fdiv

然后我们就会看到有144条相关结果。

我们可以在Excel中输入像“=(1/3)”这样的字符串然后对指令进行检查。

通过使用调试器或者tracer(一次性检查4条指令)检查指令,我们幸运地发现目标指令是第14个:

.text:3011E919 DC 33        fdiv    qword ptr [ebx]
PID=13944|TID=28744|(0) 0x2f64e919 (Excel.exe!BASE+0x11e919)
EAX=0x02088006 EBX=0x02088018 ECX=0x00000001 EDX=0x00000001
ESI=0x02088000 EDI=0x00544804 EBP=0x0274FA3C ESP=0x0274F9F8
EIP=0x2F64E919
FLAGS=PF IF
FPU ControlWord=IC RC=NEAR PC=64bits PM UM OM ZM DM IM
FPU StatusWord=
FPU ST(0): 1.000000

ST(0)存放了第一个参数,[EBX]存放了第二个参数。

FDIV(FSTP)之后的指令在内存中写入了结果:

.text:3011E91B DD 1E        fstp    qword ptr [esi]

如果我们设置一个断点,就可以看到结果:

PID=32852|TID=36488|(0) 0x2f40e91b (Excel.exe!BASE+0x11e91b)
EAX=0x00598006 EBX=0x00598018 ECX=0x00000001 EDX=0x00000001
ESI=0x00598000 EDI=0x00294804 EBP=0x026CF93C ESP=0x026CF8F8
EIP=0x2F40E91B
FLAGS=PF IF
FPU ControlWord=IC RC=NEAR PC=64bits PM UM OM ZM DM IM
FPU StatusWord=C1 P
FPU ST(0): 0.333333

我们也可以恶作剧地修改一下这个值:

tracer -l:excel.exe bpx=excel.exe!BASE+0x11E91B,set(st0,666)
PID=36540|TID=24056|(0) 0x2f40e91b (Excel.exe!BASE+0x11e91b)
EAX=0x00680006 EBX=0x00680018 ECX=0x00000001 EDX=0x00000001
ESI=0x00680000 EDI=0x00395404 EBP=0x0290FD9C ESP=0x0290FD58
EIP=0x2F40E91B
FLAGS=PF IF
FPU ControlWord=IC RC=NEAR PC=64bits PM UM OM ZM DM IM
FPU StatusWord=C1 P
FPU ST(0): 0.333333
Set ST0 register to 666.000000

Excel在这个单元中显示666,我们也可以确信的确找到了正确的位置。

如果我们尝试使用同样的Excel版本,但是是64位的,会发现只有12个FDIV指令,我们的目标指令在第三个。

tracer.exe -l:excel.exe bpx=excel.exe!BASE+0x1B7FCC,set(st0,666)

看起来似乎许多浮点数和双精度类型的除法操作都被编译器用SSE指令比如DIVSD(DIVSD总共出现了268次)替换了。

第61章

可疑的代码模式

61.1 XOR 指令

像XOR op这样的指令,op为寄存器(比如,xor eax,eax)通常用于将寄存器的值设置为零,但如果操作数不同,“互斥或”运算将被执行。在普通的程序中这种操作较罕见,但在密码学中应用较广,包括业余的。如果第二个操作数是一个很大的数字,那么就更可疑了。可能会指向加密/解密操作或校验和的计算等等。

而这种观察也可能是无意义的,比如“canary”(18.3节)。canary的产生和检测通常使用XOR指令。

下面这个awk脚本可用于处理IDA的.list文件:

gawk -e '$2=="xor" { tmp=substr($3, 0, length($3)-1); if (tmp!=$4) if($4!="esp") if ($4!="ebp")⤦􏰀 {print$1,$2,tmp,",",$4}}'filename.lst

61.2 Hand-written assembly code

现代编译器不会emit LOOP和RCL指令。另一方面,这些指令对于直接用汇编语言编程的程序员来说很熟悉。如果你发现了这些指令,可以猜测这部分代码极有可能是手工编写的。这样的代码在这个指令列表中用(M)标记:A.6节。

同时函数prologue/epilogue通常不会以手工编写的汇编的形式呈现。

通常情况下,手工编写的代码中参数传递给函数没有固定的系统。

Windows 2003 内核(ntoskrnl.exe 文件)的例子:

MultiplyTest    proc near           ; CODE XREF: Get386Stepping
                xor     cx, cx
loc_620555:                         ; CODE XREF: MultiplyTest+E
                push    cx
                call    Multiply
                pop     cx
                jb      short locret_620563
                loop    loc_620555
                clc
locret_620563:                      ; CODE XREF:MultiplyTest+C
                retn
MultiplyTest endp

Multiply        proc near           ;CODE XREF:MultiplyTest+5
                mov ecx,81h
                mov eax,417A000h
                mul ecx
                cmp edx,2
                stc
                jnz short locret_62057F
                cmp  eax,0FE7A000h
                stc
                jnz short locret_62057F
                clc
locret_62057F:                      ; CODE XREF:Multiply+10
                                    ; Multiply+18
                retn
Multiply        endp

事实上,如果我们查看WRK v1.2源码,上面的代码在WRK-v1.2386.asm文件中很容易找到。 #第62章
##跟踪时使用幻数

通常情况下,我们的主要目标是理解程序从文件读取或从网络中接收的值的用途。手动跟踪某个值常常是个体力活。最简单应对技术之一(尽管不是百分之百靠谱)是使用自定义的幻数。

这在某种程度上类似于X射线计算机断层扫描:造影剂注射到病人的血液中,增强患者的内部结构在X射线下的能见度。例如,健康人的血液在肾脏渗透是众所周知的,如果血液中有介质则可以很容易在断层中看到血液如何渗透的,是否有结石或肿瘤。

我们可以使用一个32比特的数字,比如0x0badf00d,或者某人的生日0x11101979并将这个4字节数字写到我们目标程序使用的文件的某个位置。

然后使用code coverage模式下的tracer的跟踪这个程序,再用grep工具或仅仅是文本搜索(跟踪结果),就可以轻松看到值的位置以及如何被使用。

使用cc模式下tracer的结果,可使用grep:

0x150bf66 (_kziaia+0x14), e=        1 [MOV EBX, [EBP+8]] [EBP+8]=0xf59c934
0x150bf69 (_kziaia+0x17), e=        1 [MOV EDX, [69AEB08h]] [69AEB08h]=0
0x150bf6f (_kziaia+0x1d), e=        1 [FS: MOV EAX, [2Ch]]
0x150bf75 (_kziaia+0x23), e=        1 [MOV ECX, [EAX+EDX*4]] [EAX+EDX*4]=0xf1ac360
0x150bf78 (_kziaia+0x26), e=        1 [MOV [EBP-4], ECX] ECX=0xf1ac360

对于网络包中也同样适用。很重要的一点是,幻数必须独特保证没有在该程序中出现过。

除了tracer,heavydebug模式下的DosBox(MS-DOS仿真器)也能将每条指令执行后寄存器状态写入到一个文本文件中,因此,这种技术对于DOS程序也是很有用的。

第63章

其他

63.1 基本思想

一个逆向工程师应该尽可能多地去尝试站在程序开发者的角度,并思考开发者碰见某些特殊情况会如何解决。

63.2 C++

RTTI(51.1.5)的数据对于C++类定义可能会有帮助。

63.3 某些二进制文件模式

有时我们可以在十六进制编辑器中清楚地看到16/32/64比特值的数组。下面是一个非常典型的MIPS代码。每一个MIPS(还有ARM或ARM64模式的ARM)指令都是32比特(4字节),构成32比特值的数组。通过查看快照可以看到这种模式。为了显示更清晰我加了红色的下划线:

另一个这种模式的例子:第86节

63.4 内存快照比对

将两个内存快照直接比对来查看变化的技术常用于做8比特的PC游戏的高分游戏挂。

举个例子,如果你在8比特的电脑上加载了一个游戏(这里的内存不多,但游戏需要的内存通常更少),假设你知道你现在有100发子弹,你可以给内存做个快照放到某处。然后打一发,子弹数变为99,然后再做一个快照进行比对:某处一定会有一个字节一开始是100,现在变成了99。考虑到这些8比特的游戏通常用汇编语言编写,并且这样的变量通常是全局变量,可以确定内存中确有某个地址包含了子弹数目。如果你在反汇编后的游戏代码中搜索了所有有关这个地址的引用,那么找到减少子弹数的代码并不难,然后使用NOP指令替换掉,这样在游戏里子弹数就会一直保持100。通常情况下8比特PC游戏加载地址不变,并且每个游戏的不同版本不多(通常一个版本就会流行很长一段时间),所以游戏爱好者常常知道哪些地址的哪些字节需要覆盖(使用BASIC指令POKE)。由此形成了一个包含了POKE指令游戏挂,发布在和8比特游戏有关的杂志上。见:wikipedia

同样的,修改高分文件也很容易,并且不仅仅是处理8比特游戏了。记下你的得分数并且将文件备份。当高分变化后将两个文件进行比对,使用DOS的FC工具就可以(高分文件通常是二进制形式)。某处一定会有部分字节不同,发现哪些字节包含了得分数很容易。然而,游戏开发者为了防范这些游戏挂可能会采取一些措施。

这本书中其他类似的例子:第85节

63.4.1 Windows注册表

在程序安装前后比对注册表的变化也是可行的,常用于寻找与程序有关的注册表元素。这也可能是“windows registry cleaner”共享软件如此受欢迎的原因吧。

64章 传递参数的方法

64.1 cdcel

这种传递参数的方法在C/C++语言里面比较流行。

如下的代码片段所示,调用者反序地把参数压到栈中:最后一个参数,倒数第二个参数,第一个参数。调用者还必须在函数返回之后把栈指针(ESP)还原为初始状态。

Listing 64.1: cdecl

push arg3
push arg2
push arg1
call function
add esp, 12 ; returns ESP

64.2 stdcall

该调用方法与cdecl差不多,除了被调用者必须通过RET x指令代替RET指令将ESP指针设置为初始化状态,其中x = arguments number * sizeof(int)。调用者无需调整栈指针(ESP)。

Listing 64.2: stdcall

push arg3
push arg2
push arg1
call function
function:
... do something ...
ret 12

这种调用方式在win32的标准库无处不在,但win64并不使用该调用方法(具体参见下文win64一节)。

举个例子,我们可以稍微把在91页中8.1的示例代码修改一下,增加一个__stdcall修饰符。

int __stdcall f2 (int a, int b, int c)
{
    return a*b+c;
};

编译出来的结果跟8.2几乎一模一样,但你可以看到它是通过RET 12而不是RET返回的。同时,调用者并没有调整栈指针(ESP)。

因此,很容易通过RETN n指令推导出函数参数的数量(n除以四)。

Listing 64.3: MSVC 2010

_a$ = 8 ; size = 4
_b$ = 12 ; size = 4
_c$ = 16 ; size = 4
_f2@12 PROC
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _a$[ebp]
    imul eax, DWORD PTR _b$[ebp]
    add eax, DWORD PTR _c$[ebp]
    pop ebp
    ret 12 ; 0000000cH
_f2@12 ENDP
; ...
    push 3
    push 2
    push 1
    call _f2@12
    push eax
    push OFFSET $SG81369
    call _printf
    add esp, 8

64.2.1 可变参数的函数

printf()系列的函数大概是C/C++里面唯一一系列具有可变参数的函数了,在这些函数的帮助下很容易理清cdecl和stdcall两种调用方式之间的重要区别。让我们先假设编译器知道每个调用printf()函数的参数的个数,无论如何,当我们调用printf()的时候,它已经存在于编译好的MSVCRT.DLL之中(我们讨论的是Windows),并没有任何关于传递多少个参数的信息,剩下的办法就是通过它的格式字符串获取得到参数个数。因此,如果printf()函数是一个stdcall调用方式的函数,它必须通过格式字符串计算参数个数用于恢复栈指针(ESP),这是一种相当危险的情况,程序员的一个错别字就可以导致程序崩溃。因此此类函数使用cdecl调用方式远比使用stdcall调用方式适合。

64.3 fastcall

这是一种将部分参数通过寄存器传入,其余参数通过栈方式传入的方法。它的执行效率在一些旧时CPU比cdecl/stdcall要好(因为小栈的压力)。然而,在现代的CPU中使用该调用方式不一定能获得更好的性能。

fastcall并没有一个标准化,因此不同的编译器的实现可以不同。这是一个众所周知的警告:如果你有两个DLL,其中第一个DLL调用第二个DLL的函数,它们是又分别不同的编译器使用fastcall调用方式编译出来的,则会有不可预期的后果。

MSVC和GCC两个编译器都是通过ECX和EDX来传递第一个和第二个参数,通过栈进行传递其余参数。栈指针必须被被调用者恢复为初始状态(与stdcall类似)。

Listing 64.4: fastcall

push arg3
mov edx, arg2
mov ecx, arg1
call function
function:
.. do something ..
ret 4

举个例子,我们可以稍微把8.1的示例代码修改一下,增加一个__fastcall修饰符。

int __fastcall f3 (int a, int b, int c)
{
    return a*b+c;
};

下面它编译出来的结果:

Listing 64.5: Optimizing MSVC 2010 /Ob0

_c$ = 8         ; size = 4
@f3@12 PROC
; _a$ = ecx
; _b$ = edx
    mov eax, ecx
    imul eax, edx
    add eax, DWORD PTR _c$[esp-4]
    ret 4
@f3@12 ENDP
; ...
    mov edx, 2
    push 3
    lea ecx, DWORD PTR [edx-1]
    call @f3@12
    push eax
    push OFFSET $SG81390
    call _printf
    add esp, 8

我们可以看到被调用者使用RET N指令来调整栈指针(ESP)。这意味着,我们可以通过这条指令来推断出参数的个数。

64.3.1 GCC regparm

这是一种对fastcall调用方式的某种优化。使用-mregparm编译选项可以设置多少个参数是通过寄存器传递的(最大为3个)。因此,EAX,EDX和ECX寄存器将被使用。

当然,如果指定通过寄存器传参的参数数量小于三个的时候,并没有使用完这三个寄存器。

调用者需要把栈指针恢复为初始状态。

相关例子请参看(19.1.1)。

64.3.2 Watcom/OpenWatcom 编译器

在这里,它被成为“寄存器调用约定”,头四个参数通过EAX,EDX,EBX和ECX传递。其余参数通过栈传递。通过在函数名上添加下划线来区分那些不同的调用约定。

64.4 thiscall

这是C++里面传递this指针的成员函数调用约定。

在MSVC里面,this指针通过ECX寄存器来传递。

在GCC里面,this指针是通过第一个参数进行传递的。因此很明显,在所有成员函数里面都会多出一个额外的参数。

相关例子请查看(51.1.1)。

64.5 x86-64

64.5.1 Windows x64

在Win64里面传递函数参数的方法类似fastcall调用约定。前四个参数通过RCX,RDX,R8和R9寄存器传参,其余参数通过栈进行传递。调用者还必须预留32个字节或者4个64位的空间,让被调用者可以保存前四个参数。短函数可能直接使用通过寄存器传过来的值,但更大的可能是保存那些值后在进一步使用。

调用者还必须负责还原栈指针。

这个调用约定也用于Windows x86-64位系统上的DLL(而不是Win32的stdcall)。

例子

#include <stdio.h>
void f1(int a, int b, int c, int d, int e, int f, int g)
{
    printf ("%d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);
};
int main()
{
    f1(1,2,3,4,5,6,7);
};

Listing 64.6: MSVC 2012 /0b

$SG2937 DB '%d %d %d %d %d %d %d', 0aH, 00H
main PROC
    sub rsp, 72                     ; 00000048H
    mov DWORD PTR [rsp+48], 7
    mov DWORD PTR [rsp+40], 6
    mov DWORD PTR [rsp+32], 5
    mov r9d, 4
    mov r8d, 3
    mov edx, 2
    mov ecx, 1
    call f1
    xor eax, eax
    add rsp, 72                     ; 00000048H
    ret 0
main ENDP
a$ = 80
b$ = 88
c$ = 96
d$ = 104
e$ = 112
f$ = 120
g$ = 128
f1 PROC
$LN3:
    mov DWORD PTR [rsp+32], r9d
    mov DWORD PTR [rsp+24], r8d
    mov DWORD PTR [rsp+16], edx
    mov DWORD PTR [rsp+8], ecx
    sub rsp, 72                     ; 00000048H
    mov eax, DWORD PTR g$[rsp]
    mov DWORD PTR [rsp+56], eax
    mov eax, DWORD PTR f$[rsp]
    mov DWORD PTR [rsp+48], eax
    mov eax, DWORD PTR e$[rsp]
    mov DWORD PTR [rsp+40], eax
    mov eax, DWORD PTR d$[rsp]
    mov DWORD PTR [rsp+32], eax
    mov r9d, DWORD PTR c$[rsp]
    mov r8d, DWORD PTR b$[rsp]
    mov edx, DWORD PTR a$[rsp]
    lea rcx, OFFSET FLAT:$SG2937
    call printf
    add rsp, 72                     ; 00000048H
    ret 0
    f1 ENDP

在这里我们可以清楚看到这7个参数是如何传递的:4个参数通过寄存器传递而其余3个通过栈传递。f1()的反汇编代码一开始就把参数保存到“预留”的栈空间之中,这样做的目的是编译器并不能保证有足够的寄存器可以使用,如果不这样做的话这四个寄存器将被参数占用到函数执行结束。最后,预留栈空间是调用者的职责。

Listing 64.7: Optimizing MSVC 2012 /0b

$SG2777 DB '%d %d %d %d %d %d %d', 0aH, 00H
a$ = 80
b$ = 88
c$ = 96
d$ = 104
e$ = 112
f$ = 120
g$ = 128
f1 PROC
$LN3:
    sub rsp, 72                     ; 00000048H
    mov eax, DWORD PTR g$[rsp]
    mov DWORD PTR [rsp+56], eax
    mov eax, DWORD PTR f$[rsp]
    mov DWORD PTR [rsp+48], eax
    mov eax, DWORD PTR e$[rsp]
    mov DWORD PTR [rsp+40], eax
    mov DWORD PTR [rsp+32], r9d
    mov r9d, r8d
    mov r8d, edx
    mov edx, ecx
    lea rcx, OFFSET FLAT:$SG2777
    call printf
    add rsp, 72                     ; 00000048H
    ret 0
f1 ENDP
main PROC
    sub rsp, 72                     ; 00000048H
    mov edx, 2
    mov DWORD PTR [rsp+48], 7
    mov DWORD PTR [rsp+40], 6
    lea r9d, QWORD PTR [rdx+2]
    lea r8d, QWORD PTR [rdx+1]
    lea ecx, QWORD PTR [rdx-1]
    mov DWORD PTR [rsp+32], 5
    call f1
    xor eax, eax
    add rsp, 72                     ; 00000048H
    ret 0
main ENDP

如果我们使用了编译优化的开关去编译上面的例子,它的反汇编码几乎是相同的,但是预留的栈空间将不被使用,因为在这里并不需要使用到预留的栈空间。

而且可以看到MSVC 2012是如何利用LEA指令来优化代码(A.6.2)。

我也不确定是否值得这么做。

更多的例子请看(74.1)

this指针的传递(C/C++)

this指针通过RCX传递,成员函数的第一个参数通过RDX传递,更多例子请看(51.1.1)。

64.5.2 Linux x64

Linux x86-64传递参数的方式几乎和Windows一样。但是是通过6个寄存器代替4个寄存器来传参(RDI,RSI,RDX,RCX,R8,R9),另外并没有预留的栈空间这回事。虽然,如果它需要/想要的话,可以把寄存器的值保存到栈之中。

Listing 64.8: Optimizing GCC 4.7.3

.LC0:
    .string "%d %d %d %d %d %d %d\n"
f1:
    sub rsp, 40
    mov eax, DWORD PTR [rsp+48]
    mov DWORD PTR [rsp+8], r9d
    mov r9d, ecx
    mov DWORD PTR [rsp], r8d
    mov ecx, esi
    mov r8d, edx
    mov esi, OFFSET FLAT:.LC0
    mov edx, edi
    mov edi, 1
    mov DWORD PTR [rsp+16], eax
    xor eax, eax
    call __printf_chk
    add rsp, 40
    ret
main:
    sub rsp, 24
    mov r9d, 6
    mov r8d, 5
    mov DWORD PTR [rsp], 7
    mov ecx, 4
    mov edx, 3
    mov esi, 2
    mov edi, 1
    call f1
    add rsp, 24
    ret

注意:这里的值是写入到32-bit的寄存器(EAX…)而不是整个64-bit寄存器(RAX…)。这是因为写入到32-bit寄存器的时候会自动清空高32-bit。据说,这是为了方便把代码移植到x86-64。

64.6 返回float和double类型的值

除了Win64之外,其它返回float和double类型的值都是通过FPU里面的ST(0)寄存器返回的。 在Win64里面,返回float和double类型的值是通过XMM0寄存器返回。

64.7 修改参数

有时候,C/C++程序员(虽然不仅仅是这些人)可能会问,如果他们碰巧修改了参数会怎样?答案非常简单,这些参数是保存在栈里面的,修改参数的时候是修改这个栈里面的内容,调用者并没有在被调用函数退出之后再使用它们(至少在我的实践中没有遇到这种情况)。

#include <stdio.h>
void f(int a, int b)
{
    a=a+b;
    printf ("%d\n", a);
};

Listing 64.9: MSVC 2012

_a$ = 8     ; size = 4
_b$ = 12    ; size = 4
_f PROC
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _a$[ebp]
    add eax, DWORD PTR _b$[ebp]
    mov DWORD PTR _a$[ebp], eax
    mov ecx, DWORD PTR _a$[ebp]
    push ecx
    push OFFSET $SG2938 ; '%d', 0aH
    call _printf
    add esp, 8
    pop ebp
    ret 0
_f END

是的,可以随便修改参数。当然,这得它不是C++的引用(references)(51.3),而且你如果不修改通过指针指向的数据。那么修改参数是不会影响到当前函数的。

从理论上来讲,被调用者的函数返回之后,调用者可以获取并修改和使用它。如果它是直接使用汇编语言编写的。但C/C++并不提供任何方式可以访问它们。

64.8 使用指针的函数参数

…更有意思的是,有可能在程序中,取一个函数参数的指针并将其传递给另外一个函数。

#include <stdio.h>
// located in some other file
void modify_a (int *a);
void f (int a)
{
    modify_a (&a);
    printf ("%d\n", a);
};

很难理解它是如果实现的,直到我们看到它的反汇编码:

Listing 64.10: Optimizing MSVC 2010

$SG2796 DB '%d', 0aH, 00H
_a$ = 8
_f PROC
    lea eax, DWORD PTR _a$[esp-4]   ; just get the address of value in local stack
    push eax                        ; and pass it to modify_a()
    call _modify_a
    mov ecx, DWORD PTR _a$[esp]     ; reload it from the local stack
    push ecx                        ; and pass it to printf()
    push OFFSET $SG2796             ; '%d'
    call _printf
    add esp, 12
    ret 0
_f ENDP

传递到另一个函数是a在栈空间上的地址,该函数修改了指针指向的值然后再调用printf()来打印出修改之后的值。

细心的读者可能会问,使用寄存器传参的调用约定是如何传递函数指针参数的?

这是一种利用了影子空间的情况,输入的参数值先从寄存器复制到局部栈中的影子空间,然后再讲这个地址传递给其他函数。

Listing 64.11: Optimizing MSVC 2012 x64

$SG2994 DB '%d', 0aH, 00H
a$ = 48
f PROC
    mov DWORD PTR [rsp+8], ecx      ; save input value in Shadow Space
    sub rsp, 40
    lea rcx, QWORD PTR a$[rsp]      ; get address of value and pass it to modify_a()
    call modify_a
    mov edx, DWORD PTR a$[rsp]      ; reload value from Shadow Space and pass it to printf()
    lea rcx, OFFSET FLAT:$SG2994    ; '%d'
    call printf
    add rsp, 40
    ret 0
f ENDP

GCC同样将传入的参数存储在本地栈空间:

Listing 64.12: Optimizing GCC 4.9.1 x64

.LC0:
.string "%d\n"
f:
    sub rsp, 24
    mov DWORD PTR [rsp+12], edi     ; store input value to the local stack
    lea rdi, [rsp+12]               ; take an address of the value and pass it to modify_a()
    call modify_a
    mov edx, DWORD PTR [rsp+12]     ; reload value from the local stack and pass it to printf()
    mov esi, OFFSET FLAT:.LC0       ; '%d'
    mov edi, 1
    xor eax, eax
    call __printf_chk
    add rsp, 24
    ret

ARM64的GCC也做了同样的事情,但这个空间称为寄存器保护区:

f:
    stp x29, x30, [sp, -32]!
    add x29, sp, 0      ; setup FP
    add x1, x29, 32     ; calculate address of variable in Register Save Area
    str w0, [x1,-4]!    ; store input value there
    mov x0, x1          ; pass address of variable to the modify_a()
    bl modify_a
    ldr w1, [x29,28]    ; load value from the variable and pass it to printf()
    adrp x0, .LC0 ; '%d'
    add x0, x0, :lo12:.LC0
    bl printf ; call printf()
    ldp x29, x30, [sp], 32
    ret
.LC0:
    .string "%d\n"

顺便提一下,一个类似影子空间的使用在这里也被提及过(46.1.2)。 #65章 线程局部存储

TLS是每个线程特有的数据区域,每个线程可以把自己需要的数据存储在这里。一个著名的例子是C标准的全局变量errno。多个线程可以同时使用errno获取返回的错误码,如果是全局变量它是无法在多线程环境下正常工作的。因此errno必须保存在TLS。

C++11标准里面新添加了一个thread_local修饰符,标明每个线程都属于自己版本的变量。它可以被初始化并位于TLS中。

Listing 65.1: C++11

#include <iostream>
#include <thread>
thread_local int tmp=3;
int main()
{
    std::cout << tmp << std::endl;
};

使用MinGW GCC 4.8.1而不是MSVC2012编译。

如果我们查看它的PE文件,可以看到tmp变量被放到TLS section。

65.1 线性同余发生器

前面第20章的纯随机数生成器有一个缺陷:它不是线程安全的,因为它的内部状态变量可以被不同的线程同时读取或修改。

65.1.1 Win32

未初始化的TLS数据

一个全局变量如果添加了_declspec(thread)修饰符,那么它会被分配在TLS。

#include <stdint.h>
#include <windows.h>
#include <winnt.h>

// from the Numerical Recipes book
#define RNG_a 1664525
#define RNG_c 1013904223

__declspec( thread ) uint32_t rand_state;

void my_srand (uint32_t init)
{
    rand_state=init;
}

int my_rand ()
{
    rand_state=rand_state*RNG_a;
    rand_state=rand_state+RNG_c;
    return rand_state & 0x7fff;
}

int main()
{
    my_srand(0x12345678);
    printf ("%d\n", my_rand());
};

使用Hiew可以看到PE文件多了一个section:.tls。

Listing 65.2: Optimizing MSVC 2013 x86

_TLS SEGMENT
    _rand_state DD 01H DUP (?)
_TLS ENDS

_DATA SEGMENT
    $SG84851 DB '%d', 0aH, 00H
_DATA ENDS

_TEXT SEGMENT

_init$ = 8  ; size = 4

_my_srand PROC
; FS:0=address of TIB
    mov eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
    mov ecx, DWORD PTR __tls_index
    mov ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
    mov eax, DWORD PTR _init$[esp-4]
    mov DWORD PTR _rand_state[ecx], eax
    ret 0
_my_srand ENDP

_my_rand PROC
; FS:0=address of TIB
    mov eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
    mov ecx, DWORD PTR __tls_index
    mov ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
    imul eax, DWORD PTR _rand_state[ecx], 1664525
    add eax, 1013904223 ; 3c6ef35fH
    mov DWORD PTR _rand_state[ecx], eax
    and eax, 32767 ; 00007fffH
    ret 0
_my_rand ENDP

_TEXT ENDS

rand_state现在处于TLS段,而且这个变量每个线程都拥有属于自己版本。它是这么访问的:从FS:2Ch加载TIB(Thread Information Block)的地址,然后添加一个额外的索引(如果需要的话),接着计算出在TLS段的地址。

最后可以通过ECX寄存器来访问rand_state变量,它指向每个线程特定的数据区域。

FS:这是每个逆向工程师都很熟悉的选择子了。它专门用于指向TIB,因此访问线程特定数据可以很快完成。

GS: 该选择子用于Win64,0x58的地址是TLS。

Listing 65.3: Optimizing MSVC 2013 x64

_TLS SEGMENT
    rand_state DD 01H DUP (?)
_TLS ENDS

_DATA SEGMENT
    $SG85451 DB '%d', 0aH, 00H
_DATA ENDS

_TEXT SEGMENT
init$ = 8

my_srand PROC
    mov edx, DWORD PTR _tls_index
    mov rax, QWORD PTR gs:88 ; 58h
    mov r8d, OFFSET FLAT:rand_state
    mov rax, QWORD PTR [rax+rdx*8]
    mov DWORD PTR [r8+rax], ecx
    ret 0
my_srand ENDP

my_rand PROC
    mov rax, QWORD PTR gs:88 ; 58h
    mov ecx, DWORD PTR _tls_index
    mov edx, OFFSET FLAT:rand_state
    mov rcx, QWORD PTR [rax+rcx*8]
    imul eax, DWORD PTR [rcx+rdx], 1664525 ;0019660dH
    add eax, 1013904223 ; 3c6ef35fH
    mov DWORD PTR [rcx+rdx], eax
    and eax, 32767 ; 00007fffH
    ret 0
my_rand ENDP

_TEXT ENDS

初始化TLS数据

比方说,我们想为rand_state设置一些固定的值以避免程序员忘记初始化。

#include <stdint.h>
#include <windows.h>
#include <winnt.h>

// from the Numerical Recipes book
#define RNG_a 1664525
#define RNG_c 1013904223

__declspec( thread ) uint32_t rand_state=1234;

void my_srand (uint32_t init)
{
   rand_state=init;
}

int my_rand ()
{
   rand_state=rand_state*RNG_a;
   rand_state=rand_state+RNG_c;
   return rand_state & 0x7fff;
}

int main()
{
    printf ("%d\n", my_rand());
};

代码除了给rand_state设定初始值外与之前的并没有什么不同,但在IDA我们看到:

.tls:00404000 ; Segment type: Pure data
.tls:00404000 ; Segment permissions: Read/Write
.tls:00404000 _tls segment para public 'DATA' use32
.tls:00404000 assume cs:_tls
.tls:00404000 ;org 404000h
.tls:00404000 TlsStart db 0 ; DATA XREF: .rdata:TlsDirectory
.tls:00404001 db 0
.tls:00404002 db 0
.tls:00404003 db 0
.tls:00404004 dd 1234
.tls:00404008 TlsEnd db 0 ; DATA XREF: .rdata:TlsEnd_pt
...

每次一个新的线程运行的时候,会分配新的TLS给它,然后包括1234所有数据将被拷贝过去。

这是一个典型的场景: - 线程A开始运行,然后分配给它一个TLS,并把1234拷贝到rand_state。 - 线程A里面多次调用my_rand()函数,rand_state已经不是1234。 - 线程B开始运行,然后分配给它一个TLS,并把1234拷贝到rand_state,这时候可以观察到两个线程使用同一个变量,但它们的值是不一样的。

TLS callbacks

如果我们想给TLS赋一个变量值呢?比方说:程序员忘记调用my_srand()函数来初始化PRNG,但是随机数生成器在开始的时候必须使用一个真正的随机数值而不是1234。这种情况下则可以使用TLS callbaks。

下面的代码的可移植性很差,原因你应该明白。我们定义了一个函数(tls_callback()),它在进程/线程开始执行前调用,该函数使用GetTickCount()函数的返回值来初始化PRNG。

#include <stdint.h>
#include <windows.h>
#include <winnt.h>

// from the Numerical Recipes book
#define RNG_a 1664525
#define RNG_c 1013904223

__declspec( thread ) uint32_t rand_state;

void my_srand (uint32_t init)
{
    rand_state=init;
}

void NTAPI tls_callback(PVOID a, DWORD dwReason, PVOID b)
{
    my_srand (GetTickCount());
}

#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()

int my_rand ()
{
    rand_state=rand_state*RNG_a;
    rand_state=rand_state+RNG_c;
    return rand_state & 0x7fff;
}
int main()
{
    // rand_state is already initialized at the moment (using GetTickCount())
    printf ("%d\n", my_rand());
};

用IDA看一下:

Listing 65.4: Optimizing MSVC 2013

.text:00401020 TlsCallback_0 proc near ; DATA XREF: .rdata:TlsCallbacks
.text:00401020     call ds:GetTickCount
.text:00401026     push eax
.text:00401027     call my_srand
.text:0040102C     pop ecx
.text:0040102D     retn 0Ch
.text:0040102D TlsCallback_0 endp
...
.rdata:004020C0 TlsCallbacks dd offset TlsCallback_0 ; DATA XREF: .rdata:TlsCallbacks_ptr
...
.rdata:00402118 TlsDirectory dd offset TlsStart
.rdata:0040211C TlsEnd_ptr dd offset TlsEnd
.rdata:00402120 TlsIndex_ptr dd offset TlsIndex
.rdata:00402124 TlsCallbacks_ptr dd offset TlsCallbacks
.rdata:00402128 TlsSizeOfZeroFill dd 0
.rdata:0040212C TlsCharacteristics dd 300000h

TLS callbacks函数时常用于隐藏解包处理过程。为此有些人可能会困惑,为什么一些代码可以偷偷地在OEP(Original Entry Point)之前执行。

65.1.2 Linux

下面是GCC声明线程局部存储的方式:

__thread uint32_t rand_state=1234;

这不是标准C/C++的修饰符,但是是GCC的一个扩展特性。

GS:该选择子同样用于访问TLS,但稍微有点区别:

Listing 65.5: Optimizing GCC 4.8.1 x86

.text:08048460 my_srand proc near
.text:08048460
.text:08048460 arg_0 = dword ptr 4
.text:08048460
.text:08048460     mov eax, [esp+arg_0]
.text:08048464     mov gs:0FFFFFFFCh, eax
.text:0804846A     retn
.text:0804846A my_srand endp
.text:08048470 my_rand proc near
.text:08048470     imul eax, gs:0FFFFFFFCh, 19660Dh
.text:0804847B     add eax, 3C6EF35Fh
.text:08048480     mov gs:0FFFFFFFCh, eax
.text:08048486     and eax, 7FFFh
.text:0804848B     retn
.text:0804848B my_rand endp

更多例子:ELF Handling For Thread-Local Storage #66章 系统调用(syscall-s)

众所周知,所有运行的进程在操作系统里面分为两类:一类拥有访问全部硬件设备的权限(内核空间)而另一类无法直接访问硬件设备(用户空间)。

操作系统内核和驱动程序通常是属于第一类的。

而应用程序通常是属于第二类的。

举个例子,Linux kernel运行于内核空间,而Glibc运行于用户空间。

这种分离对与操作系统的安全性是至关重要的:它最重要的一点是,不给任何进程有破坏到其它进程甚至是系统内核的机会。另一方面,一个错误的驱动或系统内核错误都会造成系统崩溃或者蓝屏。

保护模式下的x86处理器允许使用4个保护等级(ring)。但Linux和Windows两个操作系统都只使用了两个:ring0(内核空间)和ring3(用户空间)。

系统调用(syscall-s)是两个运行空间的连接点。可以说,这是提供给应用程序主要的API。

在Windows NT,系统调用表存在于SSDT。

通过系统调用实现shellcode在计算机病毒作者之间非常流行。因为很难确定所需函数在系统库里面的地址,但系统调用很容易确定。然而,由于系统调用属于比较底层的API,所以需要编写更多的代码。最后值得一提的是,在不同的操作系统版本里面,系统调用号是有可能不同的。

66.1 Linux

在Linux系统中,系统调用通常使用int 0x80中断进行调用。通过EAX寄存器传递调用号,再通过其它寄存器传递所需参数。

Listing 66.1: A simple example of the usage of two syscalls

section .text
global _start
_start:
    mov edx,len ; buf len
    mov ecx,msg ; buf
    mov ebx,1   ; file descriptor. stdout is 1
    mov eax,4   ; syscall number. sys_write is 4
    int 0x80
    mov eax,1   ; syscall number. sys_exit is 4
    int 0x80
section .data
msg db 'Hello, world!',0xa
len equ $ - msg

编译:

nasm -f elf32 1.s
ld 1.o

Linux所有的系统调用在这里可以查看:http://go.yurichev.com/17319

在Linux中可以使用strace(71章)对系统调用进行跟踪或者拦截。

66.2 Windows

Windows系统使用int 0x2e中断或x86下特有的指令SYSENTER调用用系统调用服务。

Windows所有的系统调用在这里可以查看:http://go.yurichev.com/17320

扩展阅读:“Windows Syscall Shellcode” by Piotr Bania #67章 Linux

67.1 位置无关代码

在分析Linux共享库的时候(.so)的时候,可能会经常看到类似下面的代码:

Listing 67.1: libc-2.17.so x86

.text:0012D5E3 __x86_get_pc_thunk_bx proc near ; CODE XREF: sub_17350+3
.text:0012D5E3 ; sub_173CC+4 ...
.text:0012D5E3     mov ebx, [esp+0]
.text:0012D5E6     retn
.text:0012D5E6 __x86_get_pc_thunk_bx endp
...
.text:000576C0 sub_576C0 proc near ; CODE XREF: tmpfile+73
...
.text:000576C0     push ebp
.text:000576C1     mov ecx, large gs:0
.text:000576C8     push edi
.text:000576C9     push esi
.text:000576CA     push ebx
.text:000576CB     call __x86_get_pc_thunk_bx
.text:000576D0     add ebx, 157930h
.text:000576D6     sub esp, 9Ch
...
.text:000579F0     lea eax, (a__gen_tempname - 1AF000h)[ebx] ; "__gen_tempname"
.text:000579F6     mov [esp+0ACh+var_A0], eax
.text:000579FA     lea eax, (a__SysdepsPosix - 1AF000h)[ebx] ; "../sysdeps/posix/tempname.c"
.text:00057A00     mov [esp+0ACh+var_A8], eax
.text:00057A04     lea eax, (aInvalidKindIn_ - 1AF000h)[ebx] ; "! \"invalid KIND in __gen_tempname\""
.text:00057A0A     mov [esp+0ACh+var_A4], 14Ah
.text:00057A12     mov [esp+0ACh+var_AC], eax
.text:00057A15     call __assert_fail

在每个函数开始处,所有指向字符串的指针都需要通过EBX和一些常量值来修正地址。这就是所谓的PIC(位置无关代码),它的目的是让这段代码即使随机地放在内存中某个位置都能正确地执行。这也是为什么不能使用绝对地址的原因。

PIC(位置无关代码)对于早期的操作系统和现在那些没有虚拟内存支持的嵌入式系统来说至关重要(所有进程都放在同一个连续的内存块)。此外,它还用于*NIX系统的共享库。这样共享库只需要加载一次到内存之后就可以让所有需要的进程使用,而且这些进程可以把同一个共享库映射到各自不同的内存地址上。这也是为什么共享库不使用绝对地址也能够正常地工作。

让我们做一个简单的实验:

#include <stdio.h>
int global_variable=123;
int f1(int var)
{
    int rt=global_variable+var;
    printf ("returning %d\n", rt);
    return rt;
};

用GCC 4.7.3编译它并用IDA查看.so文件的反汇编代码:

gcc -fPIC -shared -O3 -o 1.so 1.c
.text:00000440 public __x86_get_pc_thunk_bx
.text:00000440 __x86_get_pc_thunk_bx proc near ; CODE XREF: _init_proc+4
.text:00000440 ; deregister_tm_clones+4 ...
.text:00000440     mov ebx, [esp+0]
.text:00000443     retn
.text:00000443 __x86_get_pc_thunk_bx endp
.text:00000570 public f1
.text:00000570 f1 proc near
.text:00000570
.text:00000570 var_1C = dword ptr -1Ch
.text:00000570 var_18 = dword ptr -18h
.text:00000570 var_14 = dword ptr -14h
.text:00000570 var_8 = dword ptr -8
.text:00000570 var_4 = dword ptr -4
.text:00000570 arg_0 = dword ptr 4
.text:00000570
.text:00000570     sub esp, 1Ch
.text:00000573     mov [esp+1Ch+var_8], ebx
.text:00000577     call __x86_get_pc_thunk_bx
.text:0000057C     add ebx, 1A84h
.text:00000582     mov [esp+1Ch+var_4], esi
.text:00000586     mov eax, ds:(global_variable_ptr - 2000h)[ebx]
.text:0000058C     mov esi, [eax]
.text:0000058E     lea eax, (aReturningD - 2000h)[ebx] ; "returning %d\n"
.text:00000594     add esi, [esp+1Ch+arg_0]
.text:00000598     mov [esp+1Ch+var_18], eax
.text:0000059C     mov [esp+1Ch+var_1C], 1
.text:000005A3     mov [esp+1Ch+var_14], esi
.text:000005A7     call ___printf_chk
.text:000005AC     mov eax, esi
.text:000005AE     mov ebx, [esp+1Ch+var_8]
.text:000005B2     mov esi, [esp+1Ch+var_4]
.text:000005B6     add esp, 1Ch
.text:000005B9     retn
.text:000005B9 f1 endp

如上所示:每个函数执行时都会矫正“returning %d”和global_variable的地址。__x86_get_pc_thunk_bx()函数通过EBX返回一个指向自身的指针(返回的是0x57C)。这是一种获取程序计数器(EIP)的简单方法。0x1A84常量是这个函数开始处到(Global Offset Table Procedure Linkage Table(GOT PLT))它们之间的距离差。IDA会把这些偏移处理成更容易理解后再显示出来,所以实际上的代码是:

.text:00000577 call __x86_get_pc_thunk_bx
.text:0000057C add ebx, 1A84h
.text:00000582 mov [esp+1Ch+var_4], esi
.text:00000586 mov eax, [ebx-0Ch]
.text:0000058C mov esi, [eax]
.text:0000058E lea eax, [ebx-1A30h]

这里的EBX指向了GOT PLT section。当计算global_variable(存储在GOT)的地址时须减去0x0C偏移量。当计算“returning %d”字符串的地址时须减去0x1A30偏移量。

顺便说一下,AMD64的指令支持使用RIP用于相对寻址,这使得它可以产生出更简洁的PIC代码。

让我们用相同的GCC编译器编译相同的C代码,但使用x64平台。

IDA会简化了反汇编代码,造成我们无法看到使用RIP相对寻址的细节,所以我在这里使用了objdump来查看反汇编代码:

0000000000000720 <f1>:
720: 48 8b 05 b9 08 20 00    mov rax,QWORD PTR [rip+0x2008b9] # 200fe0 <_DYNAMIC+0x1d0>
727: 53                      push rbx
728: 89                      fb mov ebx,edi
72a: 48 8d 35 20 00 00 00    lea rsi,[rip+0x20] #751 <_fini+0x9>
731: bf 01 00 00 00          mov edi,0x1
736: 03 18                   add ebx,DWORD PTR [rax]
738: 31 c0                   xor eax,eax
73a: 89 da                   mov edx,ebx
73c: e8 df fe ff ff          call 620 <__printf_chk@plt>
741: 89 d8                   mov eax,ebx
743: 5b                      pop rbx
744: c3                      ret

0x2008b9是0x720处指令地址到global_variable地址的差,0x20是0x72a处指令地址到“returning %d”字符串地址的差。

你可能会看到,频繁重新计算地址会导致执行效率变差(虽然在x64会更好)。所以如果你比较关心性能的话最好还是使用静态链接。

67.1.1 Windows

Windows的DLL并没有使用PIC机制。如果Windows加载器需加载DLL到另外一个基地址,它需要把DLL在内存中的“重定位段”(在固定的位置)里所有地址都调整为正确的。这意味着多个Windows进程不能在不同进程内存块的不同地址共享一份DLL,因为每个实例加载在内存后只固定在这些地址工作。

67.2 LD_PRELOAD hack in Linux

Linux允许让我们自己的动态链接库加载在其它动态链接库之前,甚至是系统库(如 libc.so.6)。

反过来想,也就是允许我们用自己写的函数去“代替”系统库的函数。举个例子,我们可以很容易地拦截掉time(),read(),write()等等这些函数。

来瞧瞧我们是如何愚弄uptime这个程序的。我们知道,该程序显示计算机已经工作了多长时间。借助strace的帮助可以看到,该程序通过/proc/uptime文件获取到计算机的工作时长。

$ strace uptime
...
open("/proc/uptime", O_RDONLY) = 3
lseek(3, 0, SEEK_SET) = 0
read(3, "416166.86 414629.38\n", 2047) = 20
...

/proc/uptime并不是存放在磁盘的真实文件。而是由Linux Kernel产生的一个虚拟的文件。它有两个数值:

$ cat /proc/uptime
416690.91 415152.03

我们可以用wikipedia来看一下它的含义:

第一个数值是系统运行总时长,第二个数值是系统空闲的时间。都以秒为单位表示。

我们来写一个含open(),read(),close()函数的动态链接库。

首先,我们的open()函数会比较一下文件名是不是我们所想要打开的,如果是,则将文件描述符记录下来。然后,read()函数会判断如果我们调用的是不是我们所保存的文件描述符,如果是则代替它输出,否则调用libc.so.6里面原来的函数。最后,close()函数会关闭我们所保存的文件描述符。

在这里我们借助了dlopen()和dlsym()函数来确定原先在libc.so.6的函数的地址,因为我们需要控制“真实”的函数。

题外话,如果我们的程序想劫持strcmp()函数来监控每个字符串的比较,则需要我们自己实现一个strcmp()函数而不能用原先的函数。

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>

void *libc_handle = NULL;
int (*open_ptr)(const char *, int) = NULL;
int (*close_ptr)(int) = NULL;
ssize_t (*read_ptr)(int, void*, size_t) = NULL;
bool inited = false;

_Noreturn void die (const char * fmt, ...)
{
    va_list va;
    va_start (va, fmt);
    vprintf (fmt, va);
    exit(0);
};

static void find_original_functions ()
{
    if (inited)
        return;
    libc_handle = dlopen ("libc.so.6", RTLD_LAZY);
    if (libc_handle==NULL)
        die ("can't open libc.so.6\n");
    open_ptr = dlsym (libc_handle, "open");
    if (open_ptr==NULL)
        die ("can't find open()\n");
    close_ptr = dlsym (libc_handle, "close");
    if (close_ptr==NULL)
        die ("can't find close()\n");
    read_ptr = dlsym (libc_handle, "read");
    if (read_ptr==NULL)
        die ("can't find read()\n");
    inited = true;
}

static int opened_fd=0;

int open(const char *pathname, int flags)
{
    find_original_functions();
    int fd=(*open_ptr)(pathname, flags);
    if (strcmp(pathname, "/proc/uptime")==0)
        opened_fd=fd; // that's our file! record its file descriptor
    else
        opened_fd=0;
    return fd;
};

int close(int fd)
{
    find_original_functions();
    if (fd==opened_fd)
        opened_fd=0; // the file is not opened anymore
    return (*close_ptr)(fd);
};

ssize_t read(int fd, void *buf, size_t count)
{
    find_original_functions();
    if (opened_fd!=0 && fd==opened_fd)
    {
        // that's our file!
        return snprintf (buf, count, "%d %d", 0x7fffffff, 0x7fffffff)+1;
    };
    // not our file, go to real read() function
    return (*read_ptr)(fd, buf, count);
};

把它编译成动态链接库:

gcc -fpic -shared -Wall -o fool_uptime.so fool_uptime.c -ldl

运行uptime,并让它在加载其它库之前加载我们的库:

LD_PRELOAD=`pwd`/fool_uptime.so uptime

可以看到:

01:23:02 up 24855 days, 3:14, 3 users, load average: 0.00, 0.01, 0.05

如果LD_PRELOAD环境变量一直指向我们的动态链接库文件名,其它程序在启动的时候也会加载我们的动态链接库。

更多的例子请看:

68.1 CRT(win32)

程序一开始就从main()函数执行的?事实并非如此。如果我们用IDA或者HIEW打开一个可执行文件,我们可以看到OEP(Original Entry Point)指向了其它代码块。这些代码做了一些维护和准备工作之后再把控制流交给我们的代码。这就是所谓的startup-code或叫CRT code(C RunTime)。

main()函数通过一个数组接收命令行传递过来的参数,环境变量与此类似。通常情况下,传递一个字符串到程序之后,CRT code会用空格来分割它们。CRT code同样也准备了一个envp来存放环境变量。如果是GUI版本的win32程序,入口函数需要使用WinMain()来代替main()函数,它也有自己的参数。

int CALLBACK WinMain(
    _In_ HINSTANCE hInstance,
    _In_ HINSTANCE hPrevInstance,
    _In_ LPSTR lpCmdLine,
    _In_ int nCmdShow
);

CRT code同样会准备好它所需要的所有参数。

此外,main()函数的返回值是它的退出码。CRT code将它作为ExitProcess()的参数。

通常,每个编译器都有它自己的CRT code。

下面是MSVC 2008特有的CRT code。

___tmainCRTStartup proc near

var_24 = dword ptr -24h
var_20 = dword ptr -20h
var_1C = dword ptr -1Ch
ms_exc = CPPEH_RECORD ptr -18h

    push 14h
    push offset stru_4092D0
    call __SEH_prolog4
    mov eax, 5A4Dh
    cmp ds:400000h, ax
    jnz short loc_401096
    mov eax, ds:40003Ch
    cmp dword ptr [eax+400000h], 4550h
    jnz short loc_401096
    mov ecx, 10Bh
    cmp [eax+400018h], cx
    jnz short loc_401096
    cmp dword ptr [eax+400074h], 0Eh
    jbe short loc_401096
    xor ecx, ecx
    cmp [eax+4000E8h], ecx
    setnz cl
    mov [ebp+var_1C], ecx
    jmp short loc_40109A


loc_401096: ; CODE XREF: ___tmainCRTStartup+18
            ; ___tmainCRTStartup+29 ...
    and [ebp+var_1C], 0

loc_40109A: ; CODE XREF: ___tmainCRTStartup+50
    push 1
    call __heap_init
    pop ecx
    test eax, eax
    jnz short loc_4010AE
    push 1Ch
    call _fast_error_exit
    pop ecx

loc_4010AE: ; CODE XREF: ___tmainCRTStartup+60
    call __mtinit
    test eax, eax
    jnz short loc_4010BF
    push 10h
    call _fast_error_exit
    pop ecx

loc_4010BF: ; CODE XREF: ___tmainCRTStartup+71
    call sub_401F2B
    and [ebp+ms_exc.disabled], 0
    call __ioinit
    test eax, eax
    jge short loc_4010D9
    push 1Bh
    call __amsg_exit
    pop ecx

loc_4010D9: ; CODE XREF: ___tmainCRTStartup+8B
    call ds:GetCommandLineA
    mov dword_40B7F8, eax
    call ___crtGetEnvironmentStringsA
    mov dword_40AC60, eax
    call __setargv
    test eax, eax
    jge short loc_4010FF
    push 8
    call __amsg_exit
    pop ecx

loc_4010FF: ; CODE XREF: ___tmainCRTStartup+B1
    call __setenvp
    test eax, eax
    jge short loc_401110
    push 9
    call __amsg_exit
    pop ecx

loc_401110: ; CODE XREF: ___tmainCRTStartup+C2
    push 1
    call __cinit
    pop ecx
    test eax, eax
    jz short loc_401123
    push eax
    call __amsg_exit
    pop ecx
loc_401123: ; CODE XREF: ___tmainCRTStartup+D6
    mov eax, envp
    mov dword_40AC80, eax
    push eax ; envp
    push argv ; argv
    push argc ; argc
    call _main
    add esp, 0Ch
    mov [ebp+var_20], eax
    cmp [ebp+var_1C], 0
    jnz short $LN28
    push eax ; uExitCode
    call $LN32

$LN28: ; CODE XREF: ___tmainCRTStartup+105
    call __cexit
    jmp short loc_401186


$LN27: ; DATA XREF: .rdata:stru_4092D0
    mov eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 401044
    mov ecx, [eax]
    mov ecx, [ecx]
    mov [ebp+var_24], ecx
    push eax
    push ecx
    call __XcptFilter
    pop ecx
    pop ecx

$LN24:
    retn

$LN14: ; DATA XREF: .rdata:stru_4092D0
    mov esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 401044
    mov eax, [ebp+var_24]
    mov [ebp+var_20], eax
    cmp [ebp+var_1C], 0
    jnz short $LN29
    push eax ; int
    call __exit

$LN29: ; CODE XREF: ___tmainCRTStartup+135
    call __c_exit

loc_401186: ; CODE XREF: ___tmainCRTStartup+112
    mov [ebp+ms_exc.disabled], 0FFFFFFFEh
    mov eax, [ebp+var_20]
    call __SEH_epilog4
    retn

在这里我们看到代码调用了GetCommandLineA(),setargv()和setenvp()去填充argc,argv,envp全局变量。

最后,使用这些参数去调用main()函数。

有些函数调用了与自身类似的函数,如heap_init(),ioinit()。

如果你尝试在CRT code代码中使用malloc(),它将异常退出下面的错误:

runtime error R6030
- CRT not initialized

在C++中,全局对象的初始化也同样发生在main()函数执行之前的CRT:51.4.1。

main()函数的返回值传给cexit()或$LN32,后者调用doexit()。

能否摆脱CRT?这个当然,如果你知道你在做什么的话。

MSVC的链接器可以通过/ENTRY选项设置入口函数。

#include <windows.h>
int main()
{
    MessageBox (NULL, "hello, world", "caption", MB_OK);
};

让我们用MSVC 2008来编译它。

cl no_crt.c user32.lib /link /entry:main

我们可以获得一个大小为2560字节的runnable.exe。它有一个PE头,调用MessageBox的指令,数据段中有两串字符串,而MessageBox函数导入自user32.DLL。

这个程序能够正常运行,但你不能在main()函数里面使用WinMain()的四个参数。准确点来说你能,但是这些参数并没有在执行的时候准备好。

cl no_crt.c user32.lib /link /entry:main /align:16

它会报一个链接警告:

LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run

我们可以获得一个720字节的exe文件。它可以在Windows 7 x86上正常运行,但是没办法在x64上运行(当你运行它的时候会将先是一条错误信息)。更多的优化可能可以提高执行效率,但如你所见,很快就出现了兼容问题。

68.2 Win32 PE

PE是Windows下的可执行文件格式。

.exe,.dll,.sys文件它们之间的区别是,.exe和.sys文件通常没有导出表,只有导入表。

DLL文件和其它PE文件类似,有一个入口点(OEP)(DllMain()函数),但一般情况下很少DLL带有这个函数。

.sys通常是一个设备驱动程序。

作为驱动程序,Windows需要检验它的PE文件并保证它是正确的。

从Windows Vista开始,一个驱动程序文件必须拥有数字签名,否则它会被拒绝加载。

每个PE文件都由一段打印“This program cannot be run in DOS mode.”的DOS程序块开始。如果你的程序运行于DOS或者Windows 3.1(这些OS并不识别PE文件格式),这个DOS程序块将被执行打印。

68.2.1 术语

  • Module(模块) - 一个exe/dll文件。
  • Process(进程) - 加载到内存中并正在运行的程序,通常由一个exe文件和多个dll文件组成。
  • Process memory(进程内存) - 进程所在容所。每个进程都拥有自己的内存。通常是加载的模块,栈内存,堆内存等等。
  • VA(虚拟地址) - 可以被程序所使用的地址。
  • Base address(基地址) - 模块被加载到进程内存后的地址。
  • RVA(相对虚拟地址) - VA地址减去基地址后的地址。PE文件中有许多地址使用RVA地址。
  • IAT(导入地址表)- 一个导入符号地址的数组。通常由一个IMAGE_DIRECTORY_ENTRY_IAT数据目录指向IAT。值得注意的是,IDA可会给IAT分配一个名为.idata的pseudo-section,即使IAT是其它section的一部分。
  • INT(导入名称表) - 一个导入符号名的数组。

68.2.2 Base address

问题是,模块(DLL)的开发者不可能事先知道哪些地址分配给哪些模块使用的。

这就是为什么两个具有相同基地址的DLL需要一个加载到这个基地址而另外一个加载到进程的其它空闲内存处并调整第二个DLL的虚拟地址。

通常情况下,MSVC链接器生成.exe文件的基地址是0x400000,并把代码段安排在0x401000。这意味着该代码段的RVA地址是0x1000。DLL的基地址通常被MSVC链接器安排在0x10000000。

还有一种情况下加载模块时会导致基地址浮动。

这就是ASLR(Address Space Layout Randomization(地址空间布局随机化))。

一个shellcode想要执行必须调用到系统的函数。

在老的操作系统当中(如果是WindowsNT,则在Windows Vista之前),系统的DLL(如kernel32.dll,user32.dll)总是加载到已知的地址。如果我们还记得的话,它们的版本是很少有变动的。因为函数的地址是固定的,shellcode可以直接调用它们。

为了避免这种情况,ASLR每次在加载模块的时候都会随机安排它们的基地址。

支持ASLR的程序在PE头中会设置IMAGE_DLL_CHARACTERISTICS_DYNAMIC_BASE标识表明其支持ASLR。

68.2.3 Subsystem

还有一个subsystem字段, 通常是: - native (sys驱动程序) - console (控制台程序) - GUI (图形程序)

68.2.4 OS version

PE文件还规定了可以加载它的最小Windows版本号。有一个表保存了PE的版本号和相应的Windows开发代号。

举个例子,MSVC 2005编译的.exe文件运行在Windows NT4(version 4.00)。但MSVC 2008不是(生成文件的版本是5.00,至少运行于Windows 2000)。

MSVC 2012生成的.exe文件默认是6.00版本,最低平台要求至少是Windows Vista。但可以通过更改编译选项,强制编译器支持Windows XP。

68.2.5 Sections

一部分section似乎存在于所有可执行文件格式里面。

下面的标志位用于区分代码和常量数据:

  • 当IMAGE_SCN_CNT_CODE或IMAGE_SCN_MEM_EXECUTE被置位,表示该section是一个可执行代码。
  • 在数据section中,IMAGE_SCN_CNT_INITIALIZED_DATA,IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_WRITE被置位。
  • 在未初始化section和空section中,IMAGE_SCN_CNT_UNINITIALIZED_DATA, IMAGE_SCN_MEM_READ和IMAGE_SCN_MEM_WRITE被置位。
  • 在常量数据section(写保护)中,IMAGE_SCN_CNT_INITIALIZED_DATA和IMAGE_SCN_MEM_READ被置位,但不可以置位 IMAGE_SCN_MEM_WRITE。当一个进程尝试在这个section写数据时,进程会崩溃掉。

每个section在PE文件可能有一个名字,但是它并不是很重要。通常(但不总是)代码section的名字是.text,数据section是.data,常量数据section是.rdata(readable data)。其它流行的名字还有:

  • .idata—imports section(导入section)。IDA可能会创建一个类似(68.2.1)的pseudo-section。
  • .edata—exports section(导出section)。
  • .pdata—在Windows NT(MIPS,IA64,x64)包含了所有异常信息。
  • .reloc—relocs section(重定位section)
  • .bss—uninitialized data(未初始化数据(BSS))
  • .tls—thread local storage(线程局部存储(TLS))
  • .rsrc—resources(资源)
  • .CRT—可能存在古老的MSVC版本编译出来的二进制文件里面。

PE文件的打包器/加密器经常打乱section名字或者把名字替换为自己的。

MSVC允许你任意命名section。

一些编译器和链接器可以添加一个用于调试符号和其他调试信息的section(例如MinGW)。但不包括MSVC现在的版本(提供单独的PDB文件用于这个目的)。

这是PE文件的section结构体定义:

typedef struct _IMAGE_SECTION_HEADER {
    BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
    union {
        DWORD PhysicalAddress;
        DWORD VirtualSize;
    } Misc;
    DWORD VirtualAddress;
    DWORD SizeOfRawData;
    DWORD PointerToRawData;
    DWORD PointerToRelocations;
    DWORD PointerToLinenumbers;
    WORD NumberOfRelocations;
    WORD NumberOfLinenumbers;
    DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

一些相关的字段的解释:PointerToRawData是在磁盘文件中的偏移,VirtualAddress在Hiew中是装载到内存中的RVA。

68.2.6 Relocations (relocs)

也称为FIXUP-s(在Hiew)。

他们也存在于几乎所有的可执行文件格式。

显然,模块可以被加载到各种基地地址,但如何处理全局变量?一个解决方案是使用位置无关代码(67.1章),但它并不是总是有用的。

这就是重定位表存在的理由:当模块加载到不同的基地址的时候,它们的入口地址都需要修正。

举个例子,有一个全局变量的地址是0x410000,它是这样访问的:

A1 00 00 41 00    mov eax, [000410000]

模块的基地址是0x400000,全局变量的RVA地址是0x10000。

如果模块加载到0x500000这个基地址,那么全局变量实际的地址必须是0x510000。

我们可以看到,在0xA1字节之后,变量的地址编码到MOV指令中的。

这就是为什么0xA1字节之后的4个字节地址写在了重定位表。

如果模块加载到不同的基地址,操作系统加载器枚举重定位表中所有地址,查找每个32位的地址,减去原来的基地址(我们这里得到了RVA),并添加新的基地址。

如果模块加载到原来的基地址,那么不做任何事情。

所有的全局变量都可以这样处理。

重定位表可能有各种类型,但是在x86处理器的Windows中,通常是IMAGE_REL_BASED_HIGHLOW。

顺便说一下,重定位表在Hiew是隐藏的。相关例子请查看(Figure 7.12)。

OllyDbg会用下划线标识哪些使用了重定位表。相关例子请查看(Figure 13.11)。

68.2.7 Exports and imports

众所周知,任何可执行文件都必须使用操作系统提供的服务和其它一些动态链接库。

可以说,一个模块(通常是DLL)的函数通常都是导出提供给其它模块使用(.exe文件或其它DLL)。

这种情况下,每个DLL都有一个导出(exports)表,由模块的函数加它们的地址组成。

每个exe或dll文件也有一个导入(imports)表,里面包含了程序执行所需函数对应的DLL文件名。

在加载main.exe文件之后,操作系统加载器开始处理导入表:它加载所需的DLL文件,接着在DLL的导入表查找对应函数名字的地址,然后把它们的地址写到main.exe模块的IAT((Import Address Table)导入表)。

我们可以看到,加载器必须大量比较函数名,但字符串比较效率并不是很高。所以有一个支持“ordinals”或“hints”的东西,表示函数存储在表中的序号,用于代替它们的函数名。

这使得它们可以更快地加载DLL。Ordinals在导出表中永远都存在。

举个例子:一个使用MFC库的程序都是通过ordinals加载mfc*.dll,在这种程序中,INT(Import Name Table)是不存在MFC函数名字的。

使用IDA加载这类程序的时候,如果告诉它mfc*.dll文件路径,则可以看到函数名。如果不告诉IDA这些DLL路径,它会显示诸如mfc80_123而不是函数名。

Imports section

编译器通常会给导入表及其相关内容分配一个单独的section(名字类似.idata),但这不是一个强制规定。

因为术语混乱,导入表是一个比较令人困惑的地方。让我们尝试一下整理这些信息。

Figure 68.1: A scheme that unites all PE-file structures related to imports
Figure 68.1: A scheme that unites all PE-file structures related to imports

Figure 68.1: A scheme that unites all PE-file structures related to imports

里面主要的结构是IMAGE_IMPORT_DESCRIPTOR数组。每个被加载进来的DLL占用一个元素。

每个元素包含一个文本字符串(DLL名字)的RVA地址。

OriginalFirstThink是INT表的RVA地址。这是一个RVA地址的数组,里面每个成员都指向一个函数名的文本字符串。每个函数名的字符串之前是一个16位的(“hint”)-“ordinal”整数。

加载的时候,如果可以通过ordinal找到函数,那么就不需要使用字符串比较来查找函数。数组的最后一个元素是0。还有一个FirstThunk字段指向IAT表,这个地方是加载器重写需要重新解析函数的地址的RVA地址。

需要加载器重写地址的函数在IDA中加了诸如这种标记:__imp_CreateFileA。

加载器至少有两种方法重写地址:

  • 代码会有诸如调用_impCreateFileA的指令,因为导入函数的地址在某种意义上是一个全局变量,当模块加载到不同的基地址时,call指令的地址被添加到重定位表中。 但是,显然这种方法可能会扩大重定位表。因为有可能从这个模块大量调用导入的函数。而且,重定位表太大的话会减慢模块的加载速度。

  • 每个导入函数给它分配一条jmp指令,使用jmp指令加上重定位表的地址跳转到导入函数。这些入口点被称之为“thunks”,所有调用导入函数仅需要调用相对应的“thunk”,这种情况下不需要额外的重定位操作,因为这些CALL都使用相对地址,不需要额外的调整操作。

这两种方法可以组合使用。可能的话,链接器给那些被调用太多次的函数创建一个“thunk”,然而默认情况下不是这样。

顺便说一下,FirstThunk指向的函数地址数组不必要位于IAT section。举个例子,我曾经写的PE_add_import工具可以给.exe文件添加一个导入函数。在早些时候,这个工具可以让你的函数调用其它DLL文件的函数。我的工具添加了类似下面的代码:

MOV EAX, [yourdll.dll!function]
JMP EAX

FirstThunk指向第一条指令,换句话说,当加载yourdll.dll的时候,加载器在代码中写入function函数的正确地址。

还值得注意的是代码段通常是写保护的,因此我的工具在code section添加了一个IMAGE_SCN_MEM_WRITE标志位。否则,程序在加载的时候会爆出错误码为5(访问失败)的异常错误。

有人可能会问:如果我提供一个程序与一组不变的DLL文件,是有可能加快加载过程?

是的,它可以提前把函数的地址写入到导入表的FirstThunk数组。IMAGE_IMPORT_DESCRIPTOR结构有一个Timestamp字段。如果这个变量存在,则加载器会比较这个变量和DLL文件日期时间。如果它们相等,那么加载器不做任何事情,所以加载过程可以很快完成。这就是所谓的“old-style binding”。为了加快程序的加载,Matt Pietrek. “An In-Depth Look into the Win32 Portable Executable File Format”,建议你的程序安装在最终用户的计算机后不久做捆绑。

PE文件的打包器/加密器也可以压缩/加密导入表。在这种情况下,Windows的加载器当然不会加载所有需要的DLL。因此打包器/加密器只能通过LoadLibrary()和GetProcAddress()来获取所需函数。

安装在Windows系统中的标准DLL文件,IAT往往是位于PE文件的开头。据说,这是一种优化。加载时.exe文件不是全部加载到内存,它是“映射”和加载部分需要被访问到的内存。可能微软的开发者认为这样加载比较快。

68.2.8 Resources

资源在PE文件只是一组图标,图片,文本字符串,对话框描述。因为把它们从主代码分离了出来,所以多国语言程序很容易实现,只需要根据操作系统设置的语言去选择文本或图片的语言。

作为一个副作用,通过使用诸如ResHack的编辑器,即使在没有专业知识的情况下,也可以轻松地编辑和保存可执行文件的资源。

68.2.9 .NET

.NET的程序并不编译成机器码,而是编译成字节码。严格地说,是在.exe文件里面使用字节码代替x86机器。然而,进入入口点(OEP)还是需要一小段x86机器码:

jmp mscoree.dll!_CorExeMain

.NET的加载器位于mscoree.dll,由它来处理PE文件。它存在于之前的所有Windows XP操作系统。从XP启动的时候,OS的加载器能够探测.NET文件并通过JMP指令执行。

68.2.10 TLS

这个section包含了初始化TLS的数据(65章)(如果需要的话)。当一个新线程启动的时候,它的TLS数据使用这个section的数据进行初始化。

除此之外,PE文件规范还提供了TLS的初始化!当section,TLS callbacks存在,它们会在传递控制权到主入口点(OEP)之前被调用。这个功能广泛用于PE文件的打包和加密。

68.2.11 工具

  • objdump - cygwin版本可以反汇编PE文件
  • Hiew - (参考73章)
  • pefile - 一个处理PE文件的Python库
  • ResHack AKA Resource Hacker — 资源编辑器
  • PE_add_import — 添加符号到导入表的简易工具
  • PE_patcher — 修补PE文件的简易工具
  • PE_search_str_refs — 查找函数在PE文件里对应的字符串的简易工具

68.2.12 扩展阅读

Daniel Pistelli — The .NET File Format

68.3 Windows SEH

68.3.1 让我们先忘了MSVC

在Windows,SEH(Structured Exception Handling(结构化异常处理))是异常处理的一种机制。然而,它是语言无关的,不管是C++或者其它OOP语言。我们可以看到SEH(从C++和MSVC扩展)是独立实现的。

每个运行的进程都有一个SEH处理链,TIB有它最后的处理程序的地址。当异常发生时(除零,错误的地址访问,用户通过调用RaiseException()函数引发异常),操作系统在TIB找到最后的处理程序并调用它,获取异常时CPU的状态信息(如寄存器的值等等)。处理程序当前的异常能否修复,如果能,则修复该异常。如果不能,它通知操作系统无法处理它并由操作系统调用异常处理链中的下一个处理程序,直到处理程序能够处理的异常被发现。

在异常处理链的结尾处有一个标准的处理程序,它显示一个对话框用于通知用户进程崩溃,然后把一些崩溃时CPU的状态信息,收集起来并将其发送给微软开发商。

Figure 68.2: Windows XP
Figure 68.2: Windows XP

Figure 68.2: Windows XP

Figure 68.3: Windows XP
Figure 68.3: Windows XP

Figure 68.3: Windows XP

Figure 68.4: Windows 7
Figure 68.4: Windows 7

Figure 68.4: Windows 7

Figure 68.5: Windows 8.1
Figure 68.5: Windows 8.1

Figure 68.5: Windows 8.1

早些时候,这个处理程序被称为Dr.Watson。

顺便说一句,有些开发人员会在自己的处理程序发送程序崩溃的信息。通过SetUnhandledExceptionFilter()函数注册异常处理程序,如果操作系统没有任何其它方式处理异常,则调用它。一个例子是Oracle RDBMS,它保存了CPU所有可能有用的信息和内存状态的巨大转储文件。

让我们写一个自己的primitive exception handler:

#include <windows.h>
#include <stdio.h>

DWORD new_value=1234;

EXCEPTION_DISPOSITION __cdecl except_handler(
    struct _EXCEPTION_RECORD *ExceptionRecord,
    void * EstablisherFrame,
    struct _CONTEXT *ContextRecord,
    void * DispatcherContext )
{
    unsigned i;

    printf ("%s\n", __FUNCTION__);
    printf ("ExceptionRecord->ExceptionCode=0x%p\n", ExceptionRecord->ExceptionCode);
    printf ("ExceptionRecord->ExceptionFlags=0x%p\n", ExceptionRecord->ExceptionFlags);
    printf ("ExceptionRecord->ExceptionAddress=0x%p\n", ExceptionRecord->ExceptionAddress);
    if (ExceptionRecord->ExceptionCode==0xE1223344)
    {
        printf ("That's for us\n");
        // yes, we "handled" the exception
        return ExceptionContinueExecution;
    }
    else if (ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VIOLATION)
    {
        printf ("ContextRecord->Eax=0x%08X\n", ContextRecord->Eax);
        // will it be possible to 'fix' it?
        printf ("Trying to fix wrong pointer address\n");
        ContextRecord->Eax=(DWORD)&new_value;
        // yes, we "handled" the exception
        return ExceptionContinueExecution;
    }
    else
    {
        printf ("We do not handle this\n");
        // someone else's problem
        return ExceptionContinueSearch;
    };
}
int main()
{
    DWORD handler = (DWORD)except_handler; // take a pointer to our handler
    // install exception handler
    __asm
    { // make EXCEPTION_REGISTRATION record:
        push handler // address of handler function
        push FS:[0] // address of previous handler
        mov FS:[0],ESP // add new EXECEPTION_REGISTRATION
    }
    RaiseException (0xE1223344, 0, 0, NULL);
    // now do something very bad
    int* ptr=NULL;
    int val=0;
    val=*ptr;
    printf ("val=%d\n", val);
    // deinstall exception handler
    __asm
    { // remove our EXECEPTION_REGISTRATION record
        mov eax,[ESP] // get pointer to previous record
        mov FS:[0], EAX // install previous record
        add esp, 8 // clean our EXECEPTION_REGISTRATION off stack
    }
    return 0;
}

FS段寄存器:在Win32指向TIB。在TIB的第一个元素是指向异常处理指针链中的最后一个处理程序,我们将自己的异常处理程序的地址保存在这里。异常处理链的结点结构体名字是_EXCEPTION_REGISTRATION,这是一个单链表实现的栈容器。

Listing 68.1: MSVC/VC/crt/src/exsup.inc

\_EXCEPTION\_REGISTRATION struc
    prev dd ?
    handler dd ?
\_EXCEPTION\_REGISTRATION ends

每个结点的handler字段指向一个异常处理程序,每个结点的prev字段指向在栈中的上一个结点。最后一个结点的prev指向0xFFFFFFFF(-1)。

Figure 68.5: Windows 8.1
Figure 68.5: Windows 8.1

我们的处理程序安装后,我们调用RaiseException()。这是一个用户异常。处理程序检查异常代码,如果异常代码是0xE1223344,它返回ExceptionContinueExecution。这意味着处理程序修复了CPU的状态(通常是EIP/ESP寄存器),操作系统可以恢复运行。如果你稍微修改一下代码,处理程序返回ExceptionContinueSearch,那么操作系统将调用下一个处理程序,如果没有找到处理程序(因为没人捕获该异常),你会看到标准的Windows进程崩溃对话框。

系统异常和用户异常之间的区别是什么?这里有系统的:

as defined in WinBase.h
EXCEPTION_ACCESS_VIOLATION
EXCEPTION_DATATYPE_MISALIGNMENT
EXCEPTION_BREAKPOINT
EXCEPTION_SINGLE_STEP
EXCEPTION_ARRAY_BOUNDS_EXCEEDED
EXCEPTION_FLT_DENORMAL_OPERAND
EXCEPTION_FLT_DIVIDE_BY_ZERO
EXCEPTION_FLT_INEXACT_RESULT
EXCEPTION_FLT_INVALID_OPERATION
EXCEPTION_FLT_OVERFLOW
EXCEPTION_FLT_STACK_CHECK
EXCEPTION_FLT_UNDERFLOW
EXCEPTION_INT_DIVIDE_BY_ZERO
EXCEPTION_INT_OVERFLOW
EXCEPTION_PRIV_INSTRUCTION
EXCEPTION_IN_PAGE_ERROR
EXCEPTION_ILLEGAL_INSTRUCTION
EXCEPTION_NONCONTINUABLE_EXCEPTION
EXCEPTION_STACK_OVERFLOW
EXCEPTION_INVALID_DISPOSITION
EXCEPTION_GUARD_PAGE
EXCEPTION_INVALID_HANDLE
EXCEPTION_POSSIBLE_DEADLOCK
CONTROL_C_EXIT

这些异常码的定义规则是:

31
S

S是一个基本代码: 11—error; 10—warning; 01—informational; 00—success;U表示是否是用户代码。

这就是为什么我选择了0xE1223344,0xE(1110b)意味着1)user exception(用户异常);2)error(错误)。

当我们尝试读取地址为0的内存时。因为这个地址在win32中并不被使用,所以会引发一个异常。通过检查异常码是否等于EXCEPTION_ACCESS_VIOLATION常量。

读0地址内存的代码看起来像这样:

Listing 68.2: MSVC 2010

...
    xor eax, eax
    mov eax, DWORD PTR [eax] ; exception will occur here
    push eax
    push OFFSET msg
    call _printf
    add esp, 8
...

能否修复“on the fly”这个错误然后继续执行程序?当然,我们的异常处理程序可以修复EAX值然后让操作系统继续执行下去。这是我们该做的。printf()将打印1234,因为我们的处理程序执行后EAX不是0,而是全局变量new_value的地址。

若内存管理器有一个关于CPU的错误信号,CPU会暂停线程,在Windows内核查找异常处理程序,然后一个一个调用SEH链的handler。

我在这里使用MSVC 2010,当然,没有任何保证EAX将用于这个指针。

这个地址替换的技巧非常的漂亮,我经常使用它插入到SEH内部中。不过,我忘记了在哪里用它修复“on the fly”错误。

为什么SHE相关的记录存储在栈上而不是其它地方?据说这是因为操作系统不需要在函数执行完成之后关心这些信息。但我不能100%肯定。这有点类似alloca()。

68.3.2 现在让我们回到MSVC

据说,微软的程序员需要在C语言而不是C++上使用异常,所以它们在MSVC上添加了一个非标准的C扩展。它与C++的异常没有任何关联。

__try
{
    ...
}
__except(filter code)
{
    handler code
}

“Finally”块也许能代替handler code:

__try
{
    ...
}
__finally
{
    ...
}

filte code是一个表达式,告诉handler code是否对应引发的异常。如果你的filte code太大而无法使用一个表达式,可以定义一个单独的filte函数。

在Windows内核有很多这样的结构,下面是几个例子(WRK(Windows Research Kernel)):

Listing 68.3: WRK-v1.2/base/ntos/ob/obwait.c

try {
    KeReleaseMutant( (PKMUTANT)SignalObject,
                      MUTANT_INCREMENT,
                      FALSE,
                      TRUE );
} except((GetExceptionCode () == STATUS_ABANDONED ||
          GetExceptionCode () == STATUS_MUTANT_NOT_OWNED)?
          EXCEPTION_EXECUTE_HANDLER :
          EXCEPTION_CONTINUE_SEARCH) {
    Status = GetExceptionCode();
    goto WaitExit;
}

Listing 68.4: WRK-v1.2/base/ntos/cache/cachesub.c

try {
    RtlCopyBytes( (PVOID)((PCHAR)CacheBuffer + PageOffset),
                   UserBuffer,
                   MorePages ?
                  (PAGE_SIZE - PageOffset) :
                  (ReceivedLength - PageOffset) );
} except( CcCopyReadExceptionFilter( GetExceptionInformation(), Status ) ) {

这里是一个filter code的例子:

Listing 68.5: WRK-v1.2/base/ntos/cache/copysup.c

LONG
CcCopyReadExceptionFilter(
    IN PEXCEPTION_POINTERS ExceptionPointer,
    IN PNTSTATUS ExceptionCode
)

/*++

Routine Description:
    This routine serves as a exception filter and has the special job of
    extracting the "real" I/O error when Mm raises STATUS_IN_PAGE_ERROR
    beneath us.
Arguments:
    ExceptionPointer - A pointer to the exception record that contains
    the real Io Status.
    ExceptionCode - A pointer to an NTSTATUS that is to receive the real
    status.
Return Value:
    EXCEPTION_EXECUTE_HANDLER

--*/
{
    *ExceptionCode = ExceptionPointer->ExceptionRecord->ExceptionCode;
    if ( (*ExceptionCode == STATUS_IN_PAGE_ERROR) &&
         (ExceptionPointer->ExceptionRecord->NumberParameters >= 3) ) {
        *ExceptionCode = (NTSTATUS) ExceptionPointer->ExceptionRecord->ExceptionInformation[2];
    }
    ASSERT( !NT_SUCCESS(*ExceptionCode) );
    return EXCEPTION_EXECUTE_HANDLER;
}

在内部,SEH是操作系统支持的异常扩展。但是处理函数是_except_handler3(对于SEH3)或_except_handler4(对于SEH4)。 这个处理函数的代码是与MSVC相关的,它位于它的库或在msvcr*.dll文件。其他的Win32编译器可以提供与之完全不同的机制。

SEH3

SEH3有一个_except_handler3处理函数,而且扩展了_EXCEPTION_REGISTRATION表,并添加了一个指向scope table和previous try level变量。SEH4扩展了scope table缓冲溢出保护。

scope table是一个表,包含了指向filter和handler code的块和每个try/except嵌套。

scopetable
scopetable

再者,操作系统只关心prev/handle字段。excepthandler3函数的工作是读取其他字段和scope table,并决定由哪些处理程序来执行。

excepthandler3函数的源代码是闭源的。然而,Sanos操作系统的win32兼容性层重新实现相同的功能。其它类似的实现有Wine和ReactOS。

如果filter指针为NULL,handler指针则指向finally代码块。

执行期间,栈中的previous try level变量发生变化,所以_except_handler3可以获取当前嵌套级的信息,才知道要使用scope table哪一表项。

SEH3: 一个try/except块例子

#include <stdio.h>
#include <windows.h>
#include <excpt.h>
int main()
{
    int* p = NULL;
    __try
    {
        printf("hello #1!\n");
        *p = 13; // causes an access violation exception;
        printf("hello #2!\n");
    }
    __except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
             EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
        printf("access violation, can't recover\n");
    }
}

Listing 68.6: MSVC 2003

$SG74605 DB 'hello #1!', 0aH, 00H
$SG74606 DB 'hello #2!', 0aH, 00H
$SG74608 DB 'access violation, can''t recover', 0aH, 00H
_DATA ENDS

; scope table

CONST SEGMENT
$T74622 DD 0ffffffffH   ; previous try level
    DD FLAT:$L74617     ; filter
    DD FLAT:$L74618     ; handler
CONST ENDS

_TEXT SEGMENT
$T74621 = -32     ; size = 4
_p$ = -28         ; size = 4
__$SEHRec$ = -24  ; size = 24
_main PROC NEAR
    push ebp
    mov ebp, esp
    push -1                              ; previous try level
    push OFFSET FLAT:$T74622             ; scope table
    push OFFSET FLAT:__except_handler3   ; handler
    mov eax, DWORD PTR fs:__except_list
    push eax                             ; prev
    mov DWORD PTR fs:__except_list, esp
    add esp, -16
    push ebx                             ; saved 3 registers
    push esi                             ; saved 3 registers
    push edi                             ; saved 3 registers
    mov DWORD PTR __$SEHRec$[ebp], esp
    mov DWORD PTR _p$[ebp], 0
    mov DWORD PTR __$SEHRec$[ebp+20], 0   ; previous try level
    push OFFSET FLAT:$SG74605             ; 'hello #1!'
    call _printf
    add esp, 4
    mov eax, DWORD PTR _p$[ebp]
    mov DWORD PTR [eax], 13
    push OFFSET FLAT:$SG74606             ; 'hello #2!'
    call _printf
    add esp, 4
    mov DWORD PTR __$SEHRec$[ebp+20], -1  ; previous try level
    jmp SHORT $L74616
    ; filter code
$L74617:
$L74627:
    mov ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov edx, DWORD PTR [ecx]
    mov eax, DWORD PTR [edx]
    mov DWORD PTR $T74621[ebp], eax
    mov eax, DWORD PTR $T74621[ebp]
    sub eax, -1073741819; c0000005H
    neg eax
    sbb eax, eax
    inc eax
$L74619:
$L74626:
    ret 0
    ; handler code
$L74618:
    mov esp, DWORD PTR __$SEHRec$[ebp]
    push OFFSET FLAT:$SG74608             ; 'access violation, can''t recover'
    call _printf
    add esp, 4
    mov DWORD PTR __$SEHRec$[ebp+20], -1  ; setting previous try level back to -1
$L74616:
    xor eax, eax
    mov ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov DWORD PTR fs:__except_list, ecx
    pop edi
    pop esi
    pop ebx
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP
_TEXT ENDS
END

在这里我们可以看到SEH帧是如果在栈中构建出来的,scope table位于CONST segment-事实上,这些字段是不被改变的。一件有趣的事情是如何改变previous try level变量。它的初始化值是0xFFFFFFFF(-1)。当进入try语句块的时候,变量赋值为0。当try语句块结束的时候,写回-1。我们还能看到filter和handler code的地址。因此,我们可以很容易在函数里看到try/except是如何构造的。

由于函数序言的SEH安装代码被多个函数共享,有时候编译器会在函数序言插入调用SEH_prolog()函数,这就完成了这个任务。该SEH回收代码是SEH_epilog()函数。

让我们尝试用tracer运行这个例子:

tracer.exe -l:2.exe --dump-seh

Listing 68.7: tracer.exe output

EXCEPTION_ACCESS_VIOLATION at 2.exe!main+0x44 (0x401054) ExceptionInformation[0]=1
EAX=0x00000000 EBX=0x7efde000 ECX=0x0040cbc8 EDX=0x0008e3c8
ESI=0x00001db1 EDI=0x00000000 EBP=0x0018feac ESP=0x0018fe80
EIP=0x00401054
FLAGS=AF IF RF
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x401204 (2.exe!_except_handler
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x401070 (2.exe!main+0x60) handler=0x401088 (2.exe!main+0x78)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x401204 (2.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x401531 (2.exe!mainCRTStartup+0x18d) handler=0x401545 (2.exe!mainCRTStartup+0x1a1)
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header: GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll!___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!_TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!_FinalExceptionHandler@16)

我们看到,SEH链包含4个handler。

前面两个是我们的例子。两个?但是我们只有一个?是的,一个是CRT的_mainCRTStartup()函数设置的。并至少作为FPU异常的处理。它的源码可以在MSVC的安装目录找到:crt/src/winxfltr.c。

第三个是ntdll.dll的SEH4,第四个handler也位于ntdll.dll,跟MSVC没什么关系,它有一个自描述函数名。

正如你所见,在一个链中有三种类型的处理函数:一个跟MSVC(最后一个)没什么关系和两个与MSVC关联的:SEH3和SEH4。

SEH3: 两个try/except块例子

#include <stdio.h>
#include <windows.h>
#include <excpt.h>
int filter_user_exceptions (unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
    printf("in filter. code=0x%08X\n", code);
    if (code == 0x112233)
    {
        printf("yes, that is our exception\n");
        return EXCEPTION_EXECUTE_HANDLER;
    }
    else
    {
        printf("not our exception\n");
        return EXCEPTION_CONTINUE_SEARCH;
    };
}
int main()
{
    int* p = NULL;
    __try
    {
        __try
        {
            printf ("hello!\n");
            RaiseException (0x112233, 0, 0, NULL);
            printf ("0x112233 raised. now let's crash\n");
            *p = 13; // causes an access violation exception;
        }
        __except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
                 EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
        {
            printf("access violation, can't recover\n");
        }
    }
    __except(filter_user_exceptions(GetExceptionCode(), GetExceptionInformation()))
    {
        // the filter_user_exceptions() function answering to the question
        // "is this exception belongs to this block?"
        // if yes, do the follow:
        printf("user exception caught\n");
    }
}

现在有两个try块,所以scope table现在有两个元素,每个块占用一个。Previous try level随着try块的进入或退出而改变。

Listing 68.8: MSVC 2003

$SG74606 DB 'in filter. code=0x%08X', 0aH, 00H
$SG74608 DB 'yes, that is our exception', 0aH, 00H
$SG74610 DB 'not our exception', 0aH, 00H
$SG74617 DB 'hello!', 0aH, 00H
$SG74619 DB '0x112233 raised. now let''s crash', 0aH, 00H
$SG74621 DB 'access violation, can''t recover', 0aH, 00H
$SG74623 DB 'user exception caught', 0aH, 00H
_code$ = 8 ; size = 4
_ep$ = 12 ; size = 4
_filter_user_exceptions PROC NEAR
    push ebp
    mov ebp, esp
    mov eax, DWORD PTR _code$[ebp]
    push eax
    push OFFSET FLAT:$SG74606 ; 'in filter. code=0x%08X'
    call _printf
    add esp, 8
    cmp DWORD PTR _code$[ebp], 1122867; 00112233H
    jne SHORT $L74607
    push OFFSET FLAT:$SG74608 ; 'yes, that is our exception'
    call _printf
    add esp, 4
    mov eax, 1
    jmp SHORT $L74605
$L74607:
    push OFFSET FLAT:$SG74610 ; 'not our exception'
    call _printf
    add esp, 4
    xor eax, eax
$L74605:
    pop ebp
    ret 0
_filter_user_exceptions ENDP

    ; scope table

CONST SEGMENT
$T74644 DD 0ffffffffH ; previous try level for outer block
    DD FLAT:$L74634 ; outer block filter
    DD FLAT:$L74635 ; outer block handler
    DD 00H ; previous try level for inner block
    DD FLAT:$L74638 ; inner block filter
    DD FLAT:$L74639 ; inner block handler
CONST ENDS

$T74643 = -36 ; size = 4
$T74642 = -32 ; size = 4
_p$ = -28 ; size = 4
__$SEHRec$ = -24 ; size = 24
_main PROC NEAR
    push ebp
    mov ebp, esp
    push -1 ; previous try level
    push OFFSET FLAT:$T74644
    push OFFSET FLAT:__except_handler3
    mov eax, DWORD PTR fs:__except_list
    push eax
    mov DWORD PTR fs:__except_list, esp
    add esp, -20
    push ebx
    push esi
    push edi
    mov DWORD PTR __$SEHRec$[ebp], esp
    mov DWORD PTR _p$[ebp], 0
    mov DWORD PTR __$SEHRec$[ebp+20], 0 ; outer try block entered. set previous try level to 0
    mov DWORD PTR __$SEHRec$[ebp+20], 1 ; inner try block entered. set previous try level to 1
    push OFFSET FLAT:$SG74617 ; 'hello!'
    call _printf
    add esp, 4
    push 0
    push 0
    push 0
    push 1122867 ; 00112233H
    call DWORD PTR __imp__RaiseException@16
    push OFFSET FLAT:$SG74619 ; '0x112233 raised. now let''s crash'
    call _printf
    add esp, 4
    mov eax, DWORD PTR _p$[ebp]
    mov DWORD PTR [eax], 13
    mov DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0
    jmp SHORT $L74615
    ; inner block filter
$L74638:
$L74650:
    mov ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov edx, DWORD PTR [ecx]
    mov eax, DWORD PTR [edx]
    mov DWORD PTR $T74643[ebp], eax
    mov eax, DWORD PTR $T74643[ebp]
    sub eax, -1073741819; c0000005H
    neg eax
    sbb eax, eax
    inc eax
$L74640:
$L74648:
    ret 0
    ; inner block handler
$L74639:
    mov esp, DWORD PTR __$SEHRec$[ebp]
    push OFFSET FLAT:$SG74621 ; 'access violation, can''t recover'
    call _printf
    add esp, 4
    mov DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0
$L74615:
    mov DWORD PTR __$SEHRec$[ebp+20], -1 ; outer try block exited, set previous try level back to -1
    jmp SHORT $L74633
    ; outer block filter
$L74634:
$L74651:
    mov ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov edx, DWORD PTR [ecx]
    mov eax, DWORD PTR [edx]
    mov DWORD PTR $T74642[ebp], eax
    mov ecx, DWORD PTR __$SEHRec$[ebp+4]
    push ecx
    mov edx, DWORD PTR $T74642[ebp]
    push edx
    call _filter_user_exceptions
    add esp, 8
$L74636:
$L74649:
    ret 0
    ; outer block handler
$L74635:
    mov esp, DWORD PTR __$SEHRec$[ebp]
    push OFFSET FLAT:$SG74623 ; 'user exception caught'
    call _printf
    add esp, 4
    mov DWORD PTR __$SEHRec$[ebp+20], -1 ; both try blocks exited. set previous try level back to -1
$L74633:
    xor eax, eax
    mov ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov DWORD PTR fs:__except_list, ecx
    pop edi
    pop esi
    pop ebx
    mov esp, ebp
    pop ebp
    ret 0
_main ENDP

如果我们在handler中调用的printf()函数设置一个断点,可以看到另一个SEH handler如何被添加。同样,我们还可以看到scope table包含两个元素。

tracer.exe -l:3.exe bpx=3.exe!printf --dump-seh

Listing 68.9: tracer.exe output

(0) 3.exe!printf
EAX=0x0000001b EBX=0x00000000 ECX=0x0040cc58 EDX=0x0008e3c8
ESI=0x00000000 EDI=0x00000000 EBP=0x0018f840 ESP=0x0018f838
EIP=0x004011b6
FLAGS=PF ZF IF
* SEH frame at 0x18f88c prev=0x18fe9c handler=0x771db4ad (ntdll.dll!ExecuteHandler2@20+0x3a)
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=1
scopetable entry[0]. previous try level=-1, filter=0x401120 (3.exe!main+0xb0) handler=0x40113b (3.exe!main+0xcb)
scopetable entry[1]. previous try level=0, filter=0x4010e8 (3.exe!main+0x78) handler=0x401100 (3.exe!main+0x90)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x40160d (3.exe!mainCRTStartup+0x18d) handler=0x401621 (3.exe!mainCRTStartup+0x1a1
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header: GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll!___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!_TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!_FinalExceptionHandler@16)

SEH4

在缓冲区攻击期间(18.2章),scope table的地址可以被重写。所以从MSVC 2005开始,SEH3升级到SEH4后有了缓冲区溢出保护。现在scope table指针与一个security cookie(一个随机值)做异或运算。scope table扩展了包含两个指向security cookie指针的头部。每个元素都有另一个栈内偏移值:栈帧的地址(EBP)与security_cookie异或。该值将在异常处理过程中读取并检查其正确性。栈中的security cookie每次都是随机的,所以远程攻击者无法预测到它。

SEH4的previous try level初始化值是-2而不是-1。

seh4
seh4

这里有两个使用MSVC编译的SEH4例子:

Listing 68.10: MSVC 2012: one try block example

$SG85485 DB    'hello #1!', 0aH, 00H
$SG85486 DB    'hello #2!', 0aH, 00H
$SG85488 DB    'access violation, can''t recover', 0aH, 00H

; scope table:
xdata$x          SEGMENT
__sehtable$_main DD 0fffffffeH   ; GS Cookie Offset
    DD           00H             ; GS Cookie XOR Offset
    DD           0ffffffccH      ; EH Cookie Offset
    DD           00H             ; EH Cookie XOR Offset
    DD           0fffffffeH      ; previous try level
    DD           FLAT:$LN12@main ; filter
    DD           FLAT:$LN8@main  ; handler
xdata$x          ENDS

$T2 = -36        ; size = 4
_p$ = -32        ; size = 4
tv68 = -28       ; size = 4
__$SEHRec$ = -24 ; size = 24
_main    PROC
    push   ebp
    mov    ebp, esp
    push   -2
    push   OFFSET __sehtable$_main
    push   OFFSET __except_handler4
    mov    eax, DWORD PTR fs:0
    push   eax
    add    esp, -20
    push   ebx
    push   esi
    push   edi
    mov    eax, DWORD PTR ___security_cookie
    xor    DWORD PTR __$SEHRec$[ebp+16], eax ; xored pointer to scope table
    xor    eax, ebp
    push   eax                              ; ebp ^ security_cookie
    lea    eax, DWORD PTR __$SEHRec$[ebp+8] ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
    mov    DWORD PTR fs:0, eax
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
    push   OFFSET $SG85485 ; 'hello #1!'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    push   OFFSET $SG85486 ; 'hello #2!'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
    jmp    SHORT $LN6@main

; filter:
$LN7@main:
$LN12@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T2[ebp], eax
    cmp    DWORD PTR $T2[ebp], -1073741819 ; c0000005H
    jne    SHORT $LN4@main
    mov    DWORD PTR tv68[ebp], 1
    jmp    SHORT $LN5@main
$LN4@main:
    mov    DWORD PTR tv68[ebp], 0
$LN5@main:
    mov    eax, DWORD PTR tv68[ebp]
$LN9@main:
$LN11@main:
    ret    0

; handler:
$LN8@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85488 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
$LN6@main:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:0, ecx
    pop    ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
_main    ENDP

Listing 68.11: MSVC 2012: two try blocks example

$SG85486 DB    'in filter. code=0x%08X', 0aH, 00H
$SG85488 DB    'yes, that is our exception', 0aH, 00H
$SG85490 DB    'not our exception', 0aH, 00H
$SG85497 DB    'hello!', 0aH, 00H
$SG85499 DB    '0x112233 raised. now let''s crash', 0aH, 00H
$SG85501 DB    'access violation, can''t recover', 0aH, 00H
$SG85503 DB    'user exception caught', 0aH, 00H

xdata$x    SEGMENT
__sehtable$_main DD 0fffffffeH         ; GS Cookie Offset
                 DD    00H             ; GS Cookie XOR Offset
                 DD    0ffffffc8H      ; EH Cookie Offset
                 DD    00H             ; EH Cookie Offset
                 DD    0fffffffeH      ; previous try level for outer block
                 DD    FLAT:$LN19@main ; outer block filter
                 DD    FLAT:$LN9@main  ; outer block handler
                 DD    00H             ; previous try level for inner block
                 DD    FLAT:$LN18@main ; inner block filter
                 DD    FLAT:$LN13@main ; inner block handler
xdata$x    ENDS

$T2 = -40        ; size = 4
$T3 = -36        ; size = 4
_p$ = -32        ; size = 4
tv72 = -28       ; size = 4
__$SEHRec$ = -24 ; size = 24
_main    PROC
    push   ebp
    mov    ebp, esp
    push   -2  ; initial previous try level
    push   OFFSET __sehtable$_main
    push   OFFSET __except_handler4
    mov    eax, DWORD PTR fs:0
    push   eax ; prev
    add    esp, -24
    push   ebx
    push   esi
    push   edi
    mov    eax, DWORD PTR ___security_cookie
    xor    DWORD PTR __$SEHRec$[ebp+16], eax       ; xored pointer to scope table
    xor    eax, ebp                                ; ebp ^ security_cookie
    push   eax
    lea    eax, DWORD PTR __$SEHRec$[ebp+8]        ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
    mov    DWORD PTR fs:0, eax
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; entering outer try block, setting previous try level=0
    mov    DWORD PTR __$SEHRec$[ebp+20], 1 ; entering inner try block, setting previous try level=1
    push   OFFSET $SG85497 ; 'hello!'
    call   _printf
    add    esp, 4
    push   0
    push   0
    push   0
    push   1122867 ; 00112233H
    call   DWORD PTR __imp__RaiseException@16
    push   OFFSET $SG85499 ; '0x112233 raised. now let''s crash'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, set previous try level back to 0
    jmp    SHORT $LN2@main

; inner block filter:
$LN12@main:
$LN18@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T3[ebp], eax
    cmp    DWORD PTR $T3[ebp], -1073741819 ; c0000005H
    jne    SHORT $LN5@main
    mov    DWORD PTR tv72[ebp], 1
    jmp    SHORT $LN6@main
$LN5@main:
    mov    DWORD PTR tv72[ebp], 0
$LN6@main:
    mov    eax, DWORD PTR tv72[ebp]
$LN14@main:
$LN16@main:
    ret    0

; inner block handler:
$LN13@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85501 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, setting previous try level back to 0
$LN2@main:
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
    jmp    SHORT $LN7@main

; outer block filter:
$LN8@main:
$LN19@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T2[ebp], eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    push   ecx
    mov    edx, DWORD PTR $T2[ebp]
    push   edx
    call   _filter_user_exceptions
    add    esp, 8
$LN10@main:
$LN17@main:
    ret    0

; outer block handler:
$LN9@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85503 ; 'user exception caught'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
$LN7@main:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:0, ecx
    pop    ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
_main    ENDP

_code$ = 8  ; size = 4
_ep$ = 12   ; size = 4
_filter_user_exceptions PROC
    push   ebp
    mov    ebp, esp
    mov    eax, DWORD PTR _code$[ebp]
    push   eax
    push   OFFSET $SG85486 ; 'in filter. code=0x%08X'
    call   _printf
    add    esp, 8
    cmp    DWORD PTR _code$[ebp], 1122867 ; 00112233H
    jne    SHORT $LN2@filter_use
    push   OFFSET $SG85488 ; 'yes, that is our exception'
    call   _printf
    add    esp, 4
    mov    eax, 1
    jmp    SHORT $LN3@filter_use
    jmp    SHORT $LN3@filter_use
$LN2@filter_use:
    push   OFFSET $SG85490 ; 'not our exception'
    call   _printf
    add    esp, 4
    xor    eax, eax
$LN3@filter_use:
    pop    ebp
    ret    0
_filter_user_exceptions ENDP

这里是cookie的含义:Cookie Offset用于区分栈中saved_EBP的地址和EBP⊕security_cookie。附加的Cookie XOR Offset用于区分EBP⊕security_cookie是否保存在栈中。如果这个等式不为true,会由于栈受到破坏而停止这个过程。

security_cookie⊕(Cookie XOR Offset+address_of_saved_EBP) == stack[address_of_saved_EBP + CookieOffset]

如果Cookie Offset为-2,这意味着它不存在。

在我的tracer工具也实现了Cookie检查,具体请看Github

MSVC 2005之后的编译器开启/GS选项仍可能会回滚到SEH3。不过,CRT的代码总是使用SEH4。

68.3.3 Windows x64

正如你所认为的,每个函数序言在设置SEH帧效率不高。另一个性能问题是,函数执行期间多次尝试改变previous try level。这种情况在x64完全改变了:现在所有指向try块,filter和handler函数都保存在PE文件的.pdata段,由它提供给操作系统异常处理所需信息。

这里有两个使用x64编译的例子:

Listing 68.12: MSVC 2012

$SG86276 DB 'hello #1!', 0aH, 00H
$SG86277 DB 'hello #2!', 0aH, 00H
$SG86279 DB 'access violation, can''t recover', 0aH, 00H
pdata SEGMENT
$pdata$main DD imagerel $LN9
    DD imagerel $LN9+61
    DD imagerel $unwind$main
pdata ENDS
pdata SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
    DD imagerel main$filt$0+32
    DD imagerel $unwind$main$filt$0
pdata ENDS
xdata SEGMENT
$unwind$main DD 020609H
    DD 030023206H
    DD imagerel __C_specific_handler
    DD 01H
    DD imagerel $LN9+8
    DD imagerel $LN9+40
    DD imagerel main$filt$0
    DD imagerel $LN9+40
$unwind$main$filt$0 DD 020601H
    DD 050023206H
xdata ENDS
_TEXT SEGMENT
main PROC
$LN9:
    push rbx
    sub rsp, 32
    xor ebx, ebx
    lea rcx, OFFSET FLAT:$SG86276 ; 'hello #1!'
    call printf
    mov DWORD PTR [rbx], 13
    lea rcx, OFFSET FLAT:$SG86277 ; 'hello #2!'
    call printf
    jmp SHORT $LN8@main
$LN6@main:
    lea rcx, OFFSET FLAT:$SG86279 ; 'access violation, can''t recover'
    call printf
    npad 1 ; align next label
$LN8@main:
    xor eax, eax
    add rsp, 32
    pop rbx
    ret 0
main ENDP
_TEXT ENDS

text$x SEGMENT
main$filt$0 PROC
    push rbp
    sub rsp, 32
    mov rbp, rdx
$LN5@main$filt$:
    mov rax, QWORD PTR [rcx]
    xor ecx, ecx
    cmp DWORD PTR [rax], -1073741819; c0000005H
    sete cl
    mov eax, ecx
$LN7@main$filt$:
    add rsp, 32
    pop rbp
    ret 0
    int 3
main$filt$0 ENDP
text$x ENDS

Listing 68.13: MSVC 2012

$SG86277 DB 'in filter. code=0x%08X', 0aH, 00H
$SG86279 DB 'yes, that is our exception', 0aH, 00H
$SG86281 DB 'not our exception', 0aH, 00H
$SG86288 DB 'hello!', 0aH, 00H
$SG86290 DB '0x112233 raised. now let''s crash', 0aH, 00H
$SG86292 DB 'access violation, can''t recover', 0aH, 00H
$SG86294 DB 'user exception caught', 0aH, 00H

pdata SEGMENT
$pdata$filter_user_exceptions DD imagerel $LN6
    DD imagerel $LN6+73
    DD imagerel $unwind$filter_user_exceptions
$pdata$main DD imagerel $LN14
    DD imagerel $LN14+95
    DD imagerel $unwind$main
pdata ENDS
pdata SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
    DD imagerel main$filt$0+32
    DD imagerel $unwind$main$filt$0
$pdata$main$filt$1 DD imagerel main$filt$1
    DD imagerel main$filt$1+30
    DD imagerel $unwind$main$filt$1
pdata ENDS
xdata SEGMENT
$unwind$filter_user_exceptions DD 020601H
    DD 030023206H
$unwind$main DD 020609H
    DD 030023206H
    DD imagerel __C_specific_handler
    DD 02H
    DD imagerel $LN14+8
    DD imagerel $LN14+59
    DD imagerel main$filt$0
    DD imagerel $LN14+59
    DD imagerel $LN14+8
    DD imagerel $LN14+74
    DD imagerel main$filt$1
    DD imagerel $LN14+74
$unwind$main$filt$0 DD 020601H
    DD 050023206H
$unwind$main$filt$1 DD 020601H
    DD 050023206H
xdata ENDS

_TEXT SEGMENT
main PROC
$LN14:
    push rbx
    sub rsp, 32
    xor ebx, ebx
    lea rcx, OFFSET FLAT:$SG86288 ; 'hello!'
    call printf
    xor r9d, r9d
    xor r8d, r8d
    xor edx, edx
    mov ecx, 1122867 ; 00112233H
    call QWORD PTR __imp_RaiseException
    lea rcx, OFFSET FLAT:$SG86290 ; '0x112233 raised. now let''s crash'
    call printf
    mov DWORD PTR [rbx], 13
    jmp SHORT $LN13@main
$LN11@main:
    lea rcx, OFFSET FLAT:$SG86292 ; 'access violation, can''t recover'
    call printf
    npad 1 ; align next label
$LN13@main:
    jmp SHORT $LN9@main
$LN7@main:
    lea rcx, OFFSET FLAT:$SG86294 ; 'user exception caught'
    call printf
    npad 1 ; align next label
$LN9@main:
    xor eax, eax
    add rsp, 32
    pop rbx
    ret 0
main ENDP

text$x SEGMENT
main$filt$0 PROC
    push rbp
    sub rsp, 32
    mov rbp, rdx
$LN10@main$filt$:
    mov rax, QWORD PTR [rcx]
    xor ecx, ecx
    cmp DWORD PTR [rax], -1073741819; c0000005H
    sete cl
    mov eax, ecx
$LN12@main$filt$:
    add rsp, 32
    pop rbp
    ret 0
    int 3
main$filt$0 ENDP
main$filt$1 PROC
    push rbp
    sub rsp, 32
    mov rbp, rdx
$LN6@main$filt$:
    mov rax, QWORD PTR [rcx]
    mov rdx, rcx
    mov ecx, DWORD PTR [rax]
    call filter_user_exceptions
    npad 1 ; align next label
$LN8@main$filt$:
    add rsp, 32
    pop rbp
    ret 0
    int 3
main$filt$1 ENDP
text$x ENDS

_TEXT SEGMENT
code$ = 48
ep$ = 56
filter_user_exceptions PROC
$LN6:
    push rbx
    sub rsp, 32
    mov ebx, ecx
    mov edx, ecx
    lea rcx, OFFSET FLAT:$SG86277 ; 'in filter. code=0x%08X'
    call printf
    cmp ebx, 1122867; 00112233H
    jne SHORT $LN2@filter_use
    lea rcx, OFFSET FLAT:$SG86279 ; 'yes, that is our exception'
    call printf
    mov eax, 1
    add rsp, 32
    pop rbx
    ret 0
$LN2@filter_use:
    lea rcx, OFFSET FLAT:$SG86281 ; 'not our exception'
    call printf
    xor eax, eax
    add rsp, 32
    pop rbx
    ret 0
filter_user_exceptions ENDP
_TEXT ENDS

Sko12获取更多详细的信息。

除了异常信息,.pdata还包含了几乎所有函数的开始和结束地址,因此它可能对于自动化分析工具有用。

68.3.4 更多关于SEH的信息

Matt Pietrek. “A Crash Course on the Depths of Win32™ Structured Exception Handling”. In: MSDN magazine (). URL: http://go.yurichev.com/17293.

Igor Skochinsky. Compiler Internals: Exceptions and RTTI. Also available as http://go.yurichev.com/ 17294. 2012.

68.4 Windows NT: Critical section

临界区在任何操作系统多线程环境中都是非常重要的,它保证一个线程在某一时刻访问一些数据的时候,阻塞其它正要访问这些数据的线程。

下面是Windows NT操作系统的CRITICAL_SECTION声明:

Listing 68.14: (Windows Research Kernel v1.2) public/sdk/inc/nturtl.h

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;
    //
    // The following three fields control entering and exiting the critical
    // section for the resource
    //
    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread; // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount; // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

下面展示了EnterCriticalSection()函数的运行过程:

Listing 68.15: Windows 2008/ntdll.dll/x86 (begin)

_RtlEnterCriticalSection@4
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
arg_0 = dword ptr 8
    mov edi, edi
    push ebp
    mov ebp, esp
    sub esp, 0Ch
    push esi
    push edi
    mov edi, [ebp+arg_0]
    lea esi, [edi+4] ; LockCount
    mov eax, esi
    lock btr dword ptr [eax], 0
    jnb wait ; jump if CF=0
loc_7DE922DD:
    mov eax, large fs:18h
    mov ecx, [eax+24h]
    mov [edi+0Ch], ecx
    mov dword ptr [edi+8], 1
    pop edi
    xor eax, eax
    pop esi
    mov esp, ebp
    pop ebp
    retn 4
... skipped

在这段代码中最重要的指令是BTR(带LOCK前缀):把目的操作数中由源操作数所指定位的值送往标志位CF,并将目的操作数中的该位置0。这是一个原子操作,会阻塞掉其它同时想要访问这段内存的CPU(参看BTR指令的LOCK前缀)。如果LockCount是1,则重置并返回:我们现在正处于临界区。如果不是,则表示其它线程正在占用,将进入等待状态。

使用WaitForSingleObject()进入等待状态。

下面展示了LeaveCriticalSection()函数的运行过程:

Listing 68.16: Windows 2008/ntdll.dll/x86 (begin)

_RtlLeaveCriticalSection@4 proc near
arg_0 = dword ptr 8
    mov edi, edi
    push ebp
    mov ebp, esp
    push esi
    mov esi, [ebp+arg_0]
    add dword ptr [esi+8], 0FFFFFFFFh ;RecursionCount
    jnz short loc_7DE922B2
    push ebx
    push edi
    lea edi, [esi+4] ; LockCount
    mov dword ptr [esi+0Ch], 0
    mov ebx, 1
    mov eax, edi
    lock xadd [eax], ebx
    inc ebx
    cmp ebx, 0FFFFFFFFh
    jnz loc_7DEA8EB7
    loc_7DE922B0:
    pop edi
    pop ebx
loc_7DE922B2:
    xor eax, eax
    pop esi
    pop ebp
    retn 4
... skipped

XADD指令功能是:交换并相加。这种情况下,LockCount加1并把结果保存到EBX寄存器,同时把1赋值给LockCount。这个操作是原子的,因为它使用了LOCK前缀,这意味着系统会阻塞其它CPU或CPU核心同时访问这块内存。

LOCK前缀是非常重要的:如果两个线程,每个都工作在不同的CPU或CPU核心,它们都能够进入critical section并修改内存数据,这种行为将导致不确定的后果。

第69章

反汇编器

69.1 IDA

较老的可下载的免费版本:http://go.yurichev.com/17031

短热键列表(第977页)

第70章

调试器

70.1 tracer

我用tracer代替调试器。

我最终不再使用调试器是因为我所需要的只是在代码执行的时候找到函数的参数,或者寄存器在某点的状态。每次加载调试器的时间太长,因此我编写了一个小工具tracer。它有控制台接口,运行在命令行下,允许我们给函数下断,查看寄存器状态,修改值等等。

但出于学习的目的更建议在调试器中手动跟踪代码,观察寄存器状态是怎么变化的(比如经典的SoftICE,Ollydbg,Windbg寄存器值发生变化会高亮),手动修改标志位,数据然后观察效果。

70.2 OllyDbg

非常流行的win32用户态调试器 http://go.yurichev.com/17032 短热键列表(第977页)

70.3 GDB

GDB在逆向工程师中并不非常流行,但用起来非常舒适。部分命令(第978页) #第71章 ##系统调用跟踪

71.0.1 stace/dtruss

显示当前进程的系统调用(第697页)。比如:

# strace df -h
...
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220\232\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1770984, ...}) = 0
mmap2(NULL, 1780508, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb75b3000

Mac OS X 的dtruss也有这个功能。

Cygwin也有strace,但如果我理解正确的话,它只为cygwin环境编译exe文件工作。

第72章

反编译器

只有一个已知的,公开的,高质量的反编译C代码的反编译器:Hex-Rays

http://go.yurichev.com/17033

第73章

其他工具

Microsoft Visual Studio Express1:Visual Studio精简版,方便做简单的实验。部分有用的选项(第978页)

Hiew:适用于二进制文件小型修改

binary grep:大量文件中搜索常量(或者任何有序字节)的小工具,也可以用于不可执行文件:GitHub #第84章

简单异或加密

84.1

Norton Guide这款工具在MS-DOS时代很受欢迎,作为超文本参考手册程序常驻在系统中。 Norton Guide的数据库文件扩展名是.ng,内容看上去是加密的:

为什么我说内容是加密的而不是压缩的呢?可以看到,0x1A字节(看起来是“→”)经常出现,而在压缩文件中不会有这种情况,所以这是个加密文件。同时我们也发现大段只包含拉丁字母的部分,看上去就像未知语言的字符串。

0x1A字节出现得频率很高,我们可以尝试解密这个文件,先假设它是用最简单的异或加密。如果我们用0x1A和Hiew中的每个字节异或,我们就能看见熟悉的英文字符串:

与单个固定字节异或是最简单的可能的加密方法,有时可能会碰到。

现在我们理解了为什么0x1A出现的频率如此高了:因为文件包含了大量0字节,加密之后替换成了0x1A。

但是常量可能会不同。在这个例子中,我们可以尝试0到255之间的每一个常量,在解密文件中寻找熟悉的内容,256就不行了。

更多关于Norton Guide文件格式内容:http://go.yurichev.com/17317

84.1.1 熵

像这样简单的加密系统一个很重要的特性就是加密/解密块的信息熵是一样的。下面是我用 Wolfram Mathematica 10的分析。

In[1]:= input = BinaryReadList["X86.NG"];
In[2]:= Entropy[2, input] // N
Out[2]= 5.62724
In[3]:= decrypted = Map[BitXor[#, 16^^1A] &, input];
In[4]:= Export["X86_decrypted.NG", decrypted, "Binary"];
In[5]:= Entropy[2, decrypted] // N
Out[5]= 5.62724
In[6]:= Entropy[2, ExampleData[{"Text", "ShakespearesSonnets"}]] // N
Out[6]= 4.42366

我所做是加载文件,获取它的熵,解密保存之后再次获取它的熵(竟然是一样的!)。Mathematica也提供一些著名了英文文本来分析。所以我获取了莎士比亚十四行诗的熵,它很接近我们所分析的文件。我们分析文件包含了英文句子,和莎士比亚十四行诗的语言接近。使用了异或的英文文本有相同的熵。

然而当文件使用大于一个字节来异或时就不可信了。

我们分析的文件可以在这里下载到http://go.yurichev.com/17350

关于熵的基数多说一点

Wolfram Mathematica使用e(自然对数)为基数计算,UNIX的ent工具使用2为基数。所以我在熵命令中将2设为基数,所以Mathematica获得的结果和ent一样。

84.2 最简单4字节异或加密

如果异或加密的时候使用了更长的模式,比如,4字节模式,那么也很容易发现。下面这个例子是kernel32.dll文件的起始部分(Windows Server 2008 32位版本):

下面是使用4字节密钥“加密”的结果:

容易发现有四个字符重复出现。事实上,PE文件头有许多0字节填充区,这也是密钥能被看出来的原因。

下面是十六进制形式PE头的开头:

下面是“加密”后:

容易发现密钥是这四个字节:8C 61 D3 63 . 使用这个信息解密整个文件很容易。因此记住PE文件的这些特性是很重要的:1)PE头有许多0字节填充区;2)每页有4096字节,所有的PE区段用0补齐,经常可以看到所有的区段后出现很长的0字节填充区。

一些其他的文件格式可能包含长0字节填充区,对于科学和工程软件文件来说非常典型。

想自己分析这些文件可以到这里下载:http://go.yurichev.com/

84.2.1 练习

作为一个练习尝试解密下面这个文件。当然,密钥已经改变。http://go.yurichev.com/17353

第85章

Millenium 游戏存档文件

“Millenium Return to Earth”是一款很老的DOS游戏(1991),你可以挖掘矿产资源,修建船只,在其他星球上装备它们等等。

就像其他游戏一样,你可以将游戏状态存入一个文件中。

咱们来看看能不能找到点什么。

下面这是游戏中的一处矿井。有些星球的矿井工作更快,也有工作慢的。资源的设置也不同。来看看现在底下埋的是什么样的资源:

我保存了游戏状态。存档文件大小为9538字节。

我在游戏中等了“几天”,现在我们可以从矿井中得到更多的资源:

我再次保存了游戏状态。

下面我们使用简单的DOS/Windows FC 工具来比较二进制存档文件。

FC /b 2200save.i.v1 2200SAVE.I.V2
Comparing files 2200save.i.v1 and 2200SAVE.I.V2
00000016: 0D 04
00000017: 03 04
0000001C: 1F 1E
00000146: 27 3B
00000BDA: 0E 16
00000BDC: 66 9B
00000BDE: 0E 16
00000BE0: 0E 16
00000BE6: DB 4C
00000BE7: 00 01
00000BE8: 99 E8
00000BEC: A1 F3
00000BEE: 83 C7
00000BFB: A8 28
00000BFD: 98 18
00000BFF: A8 28
00000C01: A8 28
00000C07: D8 58
00000C09: E4 A4
00000C0D: 38 B8
00000C0F: E8 68
...

这里的输出并不完整,有许多不同之处,我截取了结果来展示最有趣的部分。

在第一个状态中,我有14个单位的氢气,102个单位的氧气。在第二个状态中则分别是22和155个单位。如果这些值保存到了存档文件中,我们会看到差异。的确如此,较老存档的0XBDA处的值为0x0E(14),而在新存档中则变成了0x16(22)。这可能是氢气。同样,老存档的0XBDC处的值为0x66(102),新存档中值变为0x9B(155)。这看上去是氧气。我将两个文件都放在我的网站上,想要自己实验了解更多信息的请戳这里:beginners.re

下面是在Hiew中显示的新存档文件,我将游戏中与资源相关的值标记了出来:

我检查后确认它们是16-bit值,不是16-bit DOS软件中什么奇怪的东西,16-bit的DOS软件int类型为16比特。

下面来验证咱们的假设吧。我在第一个位置(氢气的位置)写入1234(0x4D2):

然后我加载这个已改变的文件到游戏中,查看矿井的数据:

这就对了。

现在我们尝试让这个游戏更快结束,把值设为最大:

0xFFFF就是65536,现在我们拥有了许多资源:

我在游戏中跳过了“几天”,结果有些资源变少了:

溢出发生了。游戏开发者可能没考虑到会出现这么多的资源的情况,没有设置溢出检查,但游戏中的矿井仍然在工作,资源在增加,所以导致了溢出。我想我不应该那么贪婪。

可能大量的数值都保存在了这个文件中。

这就是非常简单的游戏欺骗方法。高分文件可以通过这样打补丁轻松得到。

更多关于文件和内存快照的比对:63.4 第681页

第86章

Oracle RDBMS: .SYM-files

当一个Oracle RDBMS进程出于某种原因崩溃时,会将许多信息写入日志文件,包括栈回溯,就像这样:

----- Call Stack Trace -----
calling             call    entry                   argument values in hex
location            type    point                   (? means dubious value)
------------------- -------- --------------------   ----------------------------
_kqvrow()                   00000000
_opifch2()+2729     CALLptr 00000000                23D4B914 E47F264 1F19AE2
                                                    EB1C8A8 1
_kpoal8()+2832      CALLrel _opifch2()              89 5 EB1CC74
_opiodr()+1248      CALLreg 00000000                5E 1C EB1F0A0
_ttcpip()+1051      CALLreg 00000000                5E 1C EB1F0A0 0
_opitsk()+1404      CALL??? 00000000                C96C040 5E EB1F0A0 0 EB1ED30
                                                    EB1F1CC 53E52E 0 EB1F1F8
_opiino()+980       CALLrel _opitsk()               0   0
_opiodr()+1248      CALLreg 00000000                3C 4 EB1FBF4
_opidrv()+1201      CALLrel _opiodr()               3C 4 EB1FBF4 0
_sou2o()+55         CALLrel _opidrv()               3C 4 EB1FBF4
_opimai_real()+124  CALLrel _opimai_real()          2 EB1FC2C
_OracleThreadStart@ CALLrel _opimai()               2 EB1FF6C 7C88A7F4 EB1FC34 0
4()+830                                             EB1FD04
77E6481C            CALLreg 00000000                E41FF9C 0 0 E41FF9C 0 EB1FFC4
00000000            CALL??? 00000000

当然Oracle RDBMS 可执行文件肯定拥有某种调试信息,或者带有符号信息(或者类似信息)的映射文件。

WindowsNT Oracle RDBMS 的符号信息包含在具有.SYM扩展名的文件中,具有专门的格式。(平文文件挺好的,但是需要额外解析,因此获取速度更慢)

咱们来看看能不能理解它的格式。我选了最短的orawtc8.sym文件,来自于Oracle 8.1.7 版本orawtc8.dll 文件。

下面是Hiew加载后的效果:

通过与其他.SYN文件的对比,我们可以快速发现“OSYM”总是头(和尾),因此这可能就是文件的标志。

同时也可以看出,文件格式是:OSYM+一些二进制数据+以0为界定符的字符串+OSYM。这些字符串显然是函数和全局变量名。

我标记了OSYM标志和字符串:

咱们来看看。我在Hiew中标记了整个字符串块(除了末尾的OSYM),然后把它放进单独的文件中。然后我运行UNIX的strings和wc工具分析字符串

strings strings_block | wc -l
66

有66个文本字符串,请记住这个数字。

可以这么说,常规情况下,数量值会被存储在一个单独的二进制文件中。的确也是如此,我们可以在文件开头找到这个66数字(0x42),就在OSYM这个标志右边:

$ hexdump -C orawtc8.sym
00000000  4f 53 59 4d 42 00 00 00  00 10 00 10 80 10 00 10  |OSYMB...........|
00000010  f0 10 00 10 50 11 00 10  60 11 00 10 c0 11 00 10  |....P...`.......|
00000020  d0 11 00 10 70 13 00 10  40 15 00 10 50 15 00 10  |....p...@...P...|
00000030  60 15 00 10 80 15 00 10  a0 15 00 10 a6 15 00 10  |`...............|
....

当然,这里的0x42不是一个字节,更像是一个32比特的值,小端,后面至少跟着3个0字节。

为什么我认为是32位呢?因为Oracle RDBMS的符号文件比较大。主要的oracle.exe可执行文件(10.2.0.4版本)的oracle.sym文件包含0x3A38E(238478)个符号。一个16位的值在这里明显不够。

我检查了其他.SYM文件,证实了我的猜想:OSYM符号后面的32位值总表示文件字符串的数量。

这对于所有的二进制文件来说几乎是通用的:文件头包含标志和文件其他信息。

现在我们来进一步调查二进制块是什么。再次使用Hiew,我把从块头8个字节(32位计数值后面)开始一直到字符串块结尾的内容放入单独的文件中。

在Hiew中看看这个二进制块:

有一个明显的规律。

我用红线划分了这个块:

Hiew,就像其他的十六进制编辑器一样,每行显示16个字节。所以规律很容易看出来:每行有4个32位的值。

这个规律容易看出来的原因是其中的一些值(地址0x104之前)总是具有0x1000xxxx的格式,以0x10和0字节开始。其他值(从地址0x108开始)都是0x0000xxxx的格式,总是以两个0字节开始。

我们把这个块当作32位值的数组dump出来:

$ od -v -t x4 binary_block
0000000 10001000 10001080 100010f0 10001150
0000020 10001160 100011c0 100011d0 10001370
0000040 10001540 10001550 10001560 10001580
0000060 100015a0 100015a6 100015ac 100015b2
0000100 100015b8 100015be 100015c4 100015ca
0000120 100015d0 100015e0 100016b0 10001760
0000140 10001766 1000176c 10001780 100017b0
0000160 100017d0 100017e0 10001810 10001816
0000200 10002000 10002004 10002008 1000200c
0000220 10002010 10002014 10002018 1000201c
0000240 10002020 10002024 10002028 1000202c
0000260 10002030 10002034 10002038 1000203c
0000300 10002040 10002044 10002048 1000204c
0000320 10002050 100020d0 100020e4 100020f8
0000340 1000210c 10002120 10003000 10003004
0000360 10003008 1000300c 10003098 1000309c
0000400 100030a0 100030a4 00000000 00000008
0000420 00000012 0000001b 00000025 0000002e
0000440 00000038 00000040 00000048 00000051
0000460 0000005a 00000064 0000006e 0000007a
0000500 00000088 00000096 000000a4 000000ae
0000520 000000b6 000000c0 000000d2 000000e2
0000540 000000f0 00000107 00000110 00000116
0000560 00000121 0000012a 00000132 0000013a
0000600 00000146 00000153 00000170 00000186
0000620 000001a9 000001c1 000001de 000001ed
0000640 000001fb 00000207 0000021b 0000022a
0000660 0000023d 0000024e 00000269 00000277
0000700 00000287 00000297 000002b6 000002ca
0000720 000002dc 000002f0 00000304 00000321
0000740 0000033e 0000035d 0000037a 00000395
0000760 000003ae 000003b6 000003be 000003c6
0001000 000003ce 000003dc 000003e9 000003f8
0001020

这里有132个值,也就是66*2。或许每一个符号有两个32位的值,或者有两个数组呢?咱们接着看。

从0x1000开始的值可能是地址。毕竟这是dll的.SYM文件,win32 DLL默认的基址是0x10000000,代码通常从0x10001000开始。

我用IDA打开orawtc8.dll文件时发现基址并不相同,不过没关系,第一个函数是:

.text:60351000 sub_60351000 proc near
.text:60351000
.text:60351000 arg_0        = dword ptr 8
.text:60351000 arg_4        = dword ptr 0Ch
.text:60351000 arg_8        = dword ptr 10Ch
.text:60351000
.text:60351000              push    ebp
.text:60351001              mov     ebp,esp
.text:60351003              mov     eax, dword_60353014
.text:60351008              cmp     eax, 0FFFFFFFFh
.text:6035100B              jnz     short loc_6035104F
.text:6035100D              mov     ecx, hModule
.text:60351013              xor     eax, eax
.text:60351015              cmp     ecx, 0FFFFFFFFh
.text:60351018              mov     dword_60353014, eax
.text:6035101D              jnz     short loc_60351031
.text:6035101F              call    sub_603510F0
.text:60351024              mov     ecx, eax
.text:60351026              mov     eax, dword_60353014
.text:6035102B              mov     hModule, ecx
.text:60351031
.text:60351031 loc_60351031:                    ; CODE XREF: sub_60351000+1D
.text:60351031              test    ecx, ecx
.text:60351033              jbe     short loc_6035104F
.text:60351035              push    offset ProcName ; "ax_reg"
.text:6035103A              push    ecx             ; hModule
.text:6035103B              call    ds:GetProcAddress
...

哇,“ax_reg”字符串看起来很熟悉。它的确是字符串块的第一个字符串。所以函数名是“ax_reg”。

第二个函数是:

.text:60351080 sub_60351080     proc near
.text:60351080
.text:60351080 arg_0            = dword ptr  8
.text:60351080 arg_4            = dword ptr  0Ch
.text:60351080
.text:60351080                  push    ebp
.text:60351081                  mov     ebp, esp
.text:60351083                  mov     eax, dword_60353018
.text:60351088                  cmp     eax, 0FFFFFFFFh
.text:6035108B                  jnz     short loc_603510CF
.text:6035108D                  mov     ecx, hModule
.text:60351093                  xor     eax, eax
.text:60351095                  cmp     ecx, 0FFFFFFFFh
.text:60351098                  mov     dword_60353018, eax
.text:6035109D                  jnz     short loc_603510B1
.text:6035109F                  call    sub_603510F0
.text:603510A4                  mov     ecx, eax
.text:603510A6                  mov     eax, dword_60353018
.text:603510AB                  mov     hModule, ecx
.text:603510B1
.text:603510B1 loc_603510B1:                             ; CODE XREF: sub_60351080+1D
.text:603510B1                  test    ecx, ecx
.text:603510B3                  jbe     short loc_603510CF
.text:603510B5                  push    offset aAx_unreg ; "ax_unreg"
.text:603510BA                  push    ecx              ; hModule
.text:603510BB                  call    ds:GetProcAddress
...

“ax_unreg”字符串也是字符串块的第二个字符串!第二个函数的开始地址是0x60351080,二进制块的第二个值是10001080.因此就是这个地址,但对于DLL加上了默认基地址

现在可以快速的检查然后确定数组开始的66个值(也就是数组前一半)只是DLL中的函数地址,包括一些标签等等。那么另一半是什么呢?剩余的66个值都是以0x0000开始的,看上去范围是[0…0x3FB8]。并且他们看上去不像位域:序列的数量在增长。最后一个十六进制数字看上去是随机的。因此不像是地址(如果它是4字节,8字节,16字节则可除尽)

我们问问自己吧:Oracle RDBMS的开发者还会在文件中保存什么呢?随便猜猜:可能是文本字符串(函数名)的地址。可以迅速验证这一点,是的,每个数字代表的就是字符串在这个块中第一个字符的位置。

就是这样,完成了!

我写了一个工具将这些.SYM文件转换到IDA脚本中,然后我可以加载.IDC脚本,设置函数名:

#include <stdio.h>
#include <stdint.h>
#include <io.h>
#include <assert.h>
#include <malloc.h>
#include <fcntl.h>
#include <string.h>
int main (int argc, char *argv[])
{
    uint32_t sig, cnt, offset;
    uint32_t *d1, *d2;
    int     h, i, remain, file_len;
    char    *d3;
    uint32_t array_size_in_bytes;
    assert (argv[1]); // file name
    assert (argv[2]); // additional offset (if needed)
    // additional offset
    assert (sscanf (argv[2], "%X", &offset)==1);
    // get file length
    assert ((h=open (argv[1], _O_RDONLY | _O_BINARY, 0))!=-1);
    assert ((file_len=lseek (h, 0, SEEK_END))!=-1);
    assert (lseek (h, 0, SEEK_SET)!=-1);
    // read signature
    assert (read (h, &sig, 4)==4);
    // read count
    assert (read (h, &cnt, 4)==4);
    assert (sig==0x4D59534F); // OSYM
    // skip timedatestamp (for 11g)
    //_lseek (h, 4, 1);
    array_size_in_bytes=cnt*sizeof(uint32_t);
    // load symbol addresses array
    d1=(uint32_t*)malloc (array_size_in_bytes);
    assert (d1);
    assert (read (h, d1, array_size_in_bytes)==array_size_in_bytes);
    // load string offsets array
    d2=(uint32_t*)malloc (array_size_in_bytes);
    assert (d2);
    assert (read (h, d2, array_size_in_bytes)==array_size_in_bytes);
    // calculate strings block size
    remain=file_len-(8+4)-(cnt*8);
    // load strings block
    assert (d3=(char*)malloc (remain));
    assert (read (h, d3, remain)==remain);
    printf ("#include <idc.idc>\n\n");
    printf ("static main() {\n");
    for (i=0; i<cnt; i++)
    printf ("\tMakeName(0x%08X, \"%s\");\n", offset + d1[i], &d3[d2[i]]);
    printf ("}\n");
    close (h);
    free (d1); free (d2); free (d3);

下面是它工作的一个例子:

#include <idc.idc>
static main() {
    MakeName(0x60351000, "_ax_reg");
    MakeName(0x60351080, "_ax_unreg");
    MakeName(0x603510F0, "_loaddll");
    MakeName(0x60351150, "_wtcsrin0");
    MakeName(0x60351160, "_wtcsrin");
    MakeName(0x603511C0, "_wtcsrfre");
    MakeName(0x603511D0, "_wtclkm");
    MakeName(0x60351370, "_wtcstu");
    ... }

我使用的例子可以在这里找到:beginners.re

咱们来试试64位的Oracle RDBMS。相应的,地址应该为64位,对么?

8字节的规律看上去更加明显了:

是的,所有的表含有64位的元素,甚至是字符串的偏移。现在标志也变成了OSYMAM64,猜测是用于区分目标平台的。

就是这样了。连接Oracle RDBMS-SYM文件我用的函数的库:GitHub

第87章

Oracle RDBMS:.MSB-files

这个二进制文件包含了错误信息和对应的错误码。我们来理解它的格式然后找到unpack方法。

这里有文本格式的Oracle RDBMS错误信息,因此我们可以比对文本和pack后的二进制文件。

下面是ORAUS.MSG文本文件的开头,一些无关紧要的注释已经去掉:

00000, 00000, "normal, successful completion"
00001, 00000, "unique constraint (%s.%s) violated"
00017, 00000, "session requested to set trace event"
00018, 00000, "maximum number of sessions exceeded"
00019, 00000, "maximum number of session licenses exceeded"
00020, 00000, "maximum number of processes (%s) exceeded"
00021, 00000, "session attached to some other process; cannot switch session"
00022, 00000, "invalid session ID; access denied"
00023, 00000, "session references process private memory; cannot detach session"
00024, 00000, "logins from more than one process not allowed in single-process mode"
00025, 00000, "failed to allocate %s"
00026, 00000, "missing or invalid session ID"
00027, 00000, "cannot kill current session"
00028, 00000, "your session has been killed"
00029, 00000, "session is not a user session"
00030, 00000, "User session ID does not exist."
00031, 00000, "session marked for kill"
...

第一个数字是错误码,第二个可能是附加的标志,我不太确定。

现在我们来打开ORAUS.MSB二进制文件,找到这些文本字符串。这里有:

可以看到,这些文本字符串之间(包括ORAUS.MSG文件开头的那些)插入了一些二进制值。通过快速调查分析可发现二进制文件的主要部分按0x200(512)字节的大小进行分割。

咱们来看看第一个块的内容:

这里可以看到第一条错误信息文本。同时也看到错误信息之间没有0字节。这意味着没有以null结尾的c字符串。因此,每一条错误信息的长度值肯定以某种形式加密了。我们再来找找错误码。ORAUS.MSG文件这样开始:0,1,17(0x11),18 (0x12), 19 (0x13), 20 (0x14), 21 (0x15), 22 (0x16), 23 (0x17), 24 (0x18)…我在块头找到这些数字并且用红线标注出来了。错误码的间隔是6个字节。这意味着可能有6个字节分配给每条错误信息。

第一个16位值(0xA或者10)表示每个块的消息数量:我通过分析其他块证实了这一点,的确是这样:错误信息大小不定,有的长有的短。但是块大小总是固定的,所以你永远也不知道每个块可以pack多少条文本信息。

我注意到,既然这些c字符串不以null结尾,那么他们的大小一定在某处被加密了。第一个字符串“normal, successful completion”的大小是29(0x1D)字节。第二个字符串“unique constraint (%s.%s) violated”的大小是34(0x22)字节。在块里面找不到这些值(0x1D和0x22)。

还有一点,Oracle RDBMS需要知道需要加载的字符串在块中的位置,对么?第一个字符串“normal, successful completion”从地址0x14444(如果我们从文件开始处计数的话)或者0x44(从块开始处计数)开始。第二个字符串“unique constraint (%s.%s) violated”从0x1461(从文件开始处计算)或者0x61(从块开始处计算)开始。这些数字(0x44和0x61)看上去很熟悉!我们能在块的开始处找到他们。

因此,每个6字节块是:

可以通过快速核对其他值证明我是对的。然后这里还有最后一个6字节块,错误码为0,从最后一条错误信息的最后一个字符后开始。也许这就是确定文本信息长度的方法?我们刚刚枚举了6字节块来寻找我们需要的错误码,然后我们找到了文本字符串的位置,接着我们通过查看下一个6字节块获取文本字符串的位置。这样我们就找到了字符串的边界。这种方法通过不保存文本字符串的大小节省了一些空间。我不敢说它特别省,但是这是一个聪明的技巧。

我们再回到.MSB文件的头部:

可以迅速找到文件中记录块数量的值(用红线标注出来了),然后检查了其他.MSB文件,结果发现都是这样的。这里还有很多其他值,但我没有查看他们,因为我的工作已经完成了(一个unpack工具)。如果我要写一个.MSB文件packer,那么我可能需要理解其他值的含义。

头的后面接着一个可能包含16比特值的表:

其大小可以直观的划出来(我用红线画出)。在dump这些值的过程中,我发现每个16比特的值是每个块最后一个错误码。

这就是如何快速找到Oracle RDBMS错误信息的方法:

这是我编写的unpack.MSB文件的c程序:beginners.re

这是我用作实例的两个文件(Oracle RDBMS 11.1.0.6):beginners.re,beginners.re

87.1 总结

这种方法对于许多现代计算机来说也许太老了,假如这个文件格式是80年代中期某个具有内存/硬盘空间节省意识的硬件开发者设计的。尽管如此,这仍是一个有趣又简单的任务,因为不需要分析Oracle RDBMS的代码就能理解特殊文件的格式。