免费注册 登录
»专业资料»IT/计算机»计算机软件及应用»逆向工程入门指南.pdf
收起/展开

逆向工程入门指南

文档类型:pdf 上传时间:2018-06-10 文档页数:633页 文档大小:9.42 M 文档浏览:13313次 文档下载:5次 所需积分:0 学币 文档评分:3.0星

逆向工程入门指南内容摘要: 目錄逆向工程入门指南 1.1Part I 代码模式 1.2 CPU简介 1.2.1 最简单的函数 1.2.2 Hello,world! 1.2.3 函数的开始和结束 1.2.4 栈 1.2.5 printf()与参数处理 1.2.6 scanf() 1.2.7 访问实参 1.2.8 一个或者多个字的返回值 1.2.9 指针 1.2.10 GOTO操作符 1.2.11 条件转跳 1.2.12 选择结构switch()/case/default 1.2.13 循环 1.2.14 对C-Strings的简单处理 1.2.15 用其他东西代替算数操作符 1.2.16 浮点数单元 1.2.17 数组 1.2.18 操纵特定的bit 1.2.19 用线性同余生成器来产生伪随机数 1.2.20 结构体 1.2.21 联合体 1.2.22 指向函数的指针 1.2.23 在32位环境中的64位值 1.2.24 SIMD 1.2.25 64位化 1.2.26 使用SIMD来处理浮点数 1.2.27 关于ARM的特殊细节 1.2.28 关于MIPS的特殊细节 1.2.29Part II 重要的基础知识 1.3 有符号数的表示 1.3.1 字节序 1.3.2 内存 1.3.3 2 CPU 1.3.4 哈希函数 1.3.5Part III 更高级些的例子 1.4 温度转换 1.4.1 斐波那契数列 1.4.2 CRC32的计算实例 1.4.3 网址的计算实例 1.4.4 循环:几个迭代器 1.4.5 Duff’s device 1.4.6 除以9 1.4.7 将字符串转化为数字(atoi()) 1.4.8 内联函数 1.4.9 C99 的约束 1.4.10 无分支的abs()函数 1.4.11 参数可变的函数 1.4.12 字符串截取 1.4.13 toupper()函数 1.4.14 不正确的反汇编代码 1.4.15 花指令 1.4.16 C++ 1.4.17 负的数组引索 1.4.18 Windoes 16-bit 1.4.19Part IV JAVA 1.5 Java 1.5.1Part V 在代码里面寻找重要又有趣的东西 1.6 可执行文件的识别 1.6.1 和外部世界的交流(win32) 1.6.2 字符串 1.6.3 调用断言 1.6.4 常量 1.6.5 找到真正的指令 1.6.6 可疑代码的模式 1.6.7 在追踪时使用Magic numbers 1.6.8 其他东西 1.6.9Part VI 操作系统的特性 1.7 参数传递方法(调用规则) 1.7.1 本地线程储存区 1.7.2 系统调用 1.7.3 Linux 1.7.4 3 Windows-NT 1.7.5Part VII 工具 1.8 反汇编器 1.8.1 调试器 1.8.2 系统调用的追踪 1.8.3 反编译器 1.8.4 其他工具 1.8.5Part IX 逆向文件格式的例子 1.9 基本的异或加密 1.9.1 Millenium 的存档文件 1.9.2 Oracle RDBMS SYM-files 1.9.3 Oracle RDBMS MSB-files 1.9.4后记 1.10 附录 1.10.1附录 1.11 x86 1.11.1 ARM 1.11.2 MIPS 1.11.3 一些GCC库函数 1.11.4 一些MIPS库函数 1.11.5 速查表 1.11.6缩略词表 1.12杂项 1.13快速引索 1.14参考文献 1.15 4逆向工程入门指南逆向工程入门指南Reverse Engineering for BeginnersDennis Yurichev CC-署名-非商业使用-禁止演绎©2013-2015, Dennis Yurichev.《Reverse-Engineering-for-Beginners》中文翻 5逆向工程入门指南译版一些说明 本分支是基于乌云所翻译的《RE4B》所衍生的翻译版本。经过乌云允许后, 我们fork了它并进行一些错误的修复和更新。 人邮社出版的《RE4B》翻译版与本分支无任何关系,是由Archer和Anti团队一 起翻译而成。 和人邮社沟通后,因翻译版权问题,本分支只会对已翻译的内容进行错误修 复。不会再对作者的主干分支进行同步更新。 有兴趣的可以加QQ群一起交流逆向工程:565270515 请勿再fork本分支进行传播参与过的翻译人员(如有遗漏烦请提醒一声) 瞌睡龙、糖果、blast、magix526、Larryxi、左懒、DM_、Zing、inkydragon、 xqin 6Part I 代码模式Part I 代码模式 7CPU简介Part I 代码片段 Everything is comprehended in comparison - Author unknown我在开始学C/C++的时候,经常写一些小段的代码编译一下,然后观察输出的汇编代码。这种习惯让我很容易理解代码背后到底发生了什么。这种习惯让C/C++代码和编译器产生的汇编语言的关系深深地印在我的脑海里,对我来说很容易就能通过汇编代码想出C代码和函数粗略的样子。或许这个技巧对其他初学者能有所帮助。本书有时候会用到一些旧的编译器,这是为了尽可能得到最短的(最简单)代码片段。关于练习作者在学习C语言的时候,经常些写一些C语言的小函数,然后逐渐的将他们重写成汇编语言,并尝试让代码尽可能的短。现在这种做法不是很值得提倡,因为很难在效率上和现代编译器相竞争。不过这是一种深入理解汇编语言的好方法。因此,你可以放轻松一些,随便在这本书里找一段汇编代码,然后尝试者让它更短一些。当然不要忘记测试你所写的汇编代码。优化等级和调试信息源代码可以用不同的编译器,以不同的优化等级来编译。 典型的编译器有三种优化等级,其中0级代表不优化。优化既可以针对代码的体积,也可以针对代码的运行速度。一个无优化的编译器编译会更快一点,生成的代码也更容易理解一些(虽然很冗长)。反之一个带优化的编译器会运行的更慢,并编译出运行的更快的代码(但代码并不会更紧凑)。除了优化的级别和方向外,一个编译器还可能在生成的文件里包含一些调试信息,这样的代码更容易调试。个编译器的一个重要特性是,在输出文件里面,可能会有源代码到机器码地址的连接。另一方面,带优化的编译器,更倾向于将所有的源代码优化掉后再输出,因此源代码不会出现在输出的机器码里。一个逆向工程师这两种情况都有可能遇到,因为有的开发者会打开优化,有的不会。所以,在这本书里,我们会尽可能关注每个例子的调试和发行版本的代码特征。第一章CPU简介CPU是一种可以执行由机器码组成的程序的设备。 8CPU简介词汇表:Instruction:用于控制CPU的指令。最简单的例子有:在寄存器之间进行数据转移操作,内存操作,算术操作。原则上每种CPU会有自己独特的一套指令构架(Instruction Set Architecture(ISA))。Machine code: 机器码,CPU能直接处理的代码。每条指令都会被译成几个字节的机器码。Assembly Language: 汇编语言,助记码和其他一些像宏那样的扩展组成的、便于程序员编写的语言。CPU register:CPU寄存器,每个CPU都有一些通用寄存器(General PurposeRegisters(GPR))。X86有8个,x86-64(amd64)有16个,ARM有16个,最简单的理解寄存器的方法就是,把寄存器想成一个未指定类型的临时变量。想象你在使用高级语言编程,并且只能用8个32bit(或 64-bit)的变量。但是只用这些可以完成非常多的事情。那么你可能想知道,机器码跟程序语言有什么区别呢?答案主要在于人类和CPU的思维方式并不类似。对于人类来讲,使用例如C/C++, Java, Python这样高级语言会比较简单,但是CPU更喜欢低级抽象的东西。也许有一天CPU也能直接执行高级语言的语句,但那样的CPU肯定会变得比天的要复杂好几倍。类似的,人类之所以使用汇编语言会感觉不很方便,是因为它非常的低级,而且很难用它写很长的代码而不出错。将高级语言转换成汇编语言的程序,被称为编译器。1.1 关于不同指令集的几点x86构架一直都带有可变长度的操作码,因此64位的世纪到来时,x64的扩展对这个构架并没有太大的影响。事实上x86构架还包含着很多最早在16位8086 CPU里出现的指令,他们在今天的处理器中依旧可以被使用。ARM是一种带定长操作符的精简指令集的CPU,它过去有很多优点。最初,ARM所有的指令都被编码为4个字节。这现在被称为“ARM模式”。但是人们发现这样做并不像他们一开始所想的那样节约。事实上现实中最常用的CPU指令都可以用更少的字节来编码。因此他们又增加了一种每个指令只以2字节编码的构架,叫做Thumb。这现在被称为“Thumb模式”。然而并不是所有的ARM构架都能被编码为2字节,所以Thumb构架在某些方面是有限制的。值得注意的是以ARM模式或Thumb模式编译得代码,有可能同时出现在一个程序里。ARM的设计记者认为Thumb可以作为一种扩展存在,这就产生了Thumb-2,它首次在ARMv7里出现。Thumb-2依旧使用2字节的指令集,但是它也有一些4字节的新指令。有一种很常见的错误观念,认为Thumb-2是ARM和Thumb的混合物。这是不对的,Thumb-2扩展了对所有处理器特性的支持,所以他可以和ARM模式相竞争———很显然这个目标被很好的实现了。主要的iPod/iPhone/iPad应用是用Thumb-2指令集编译的(公认的,这主要是因为Xcode把这个设为默认模式)。 9CPU简介之后,64位的ARM发布了,这种构架有4字节的操作码,而且也不需要任何附加的Thumb模式。即便如此64位的要求也影响了构架,导致了现在有3种ARM构架:ARM 模式、Thumb 模式(包括Thumb-2)和ARM64。这些构架有部分交叉,可以说他们是不同的构架,但不能说他们是一个构架的的不同变种。因此在这本书里,我们会试着加入全部三种ARM构架的代码片段。顺带提一下,还有很多带32位变长操作码的、精简指令集的构架。例如:MIPS,PowerPC and Alpha AXP. 10最简单的函数第二章最简单的函数最简单的函数可能只需要返回一个常数值。 这有个例子: 清单 2.1: C/C++ 代码 int f() { return 123; };让我们编译一下!2.1 x86下面是带优化的GCC和MSVC编译器在x86平台上的输出: 清单 2.2: 带优化的GCC/MSVC (汇编输出) f: mov eax, 123 ret这里只有两个函数:第一个把123放入 EAX 寄存器里, EAX 通常被用作存放函数的返回值。第二个是 RET , RET 把控制权交给主调函数。主调函数会从 EAX 里取出返回值。2.2 ARM在ARM平台上会有一点点区别。清单 2.3: 带优化的 Keil 6/2013 (ARM mode) ASM 输出 f PROC MOV r0,#0x7b ; 123 BX lr ENDPARM 用 R0 来储存函数的返回值,所以123被复制进 R0 . 11最简单的函数在ARM构架里返回值的地址不是保存在局部堆栈里,而是放在链接寄存器里,所以 BX LR 指令转跳到那个地址,这有效地把控制权转交给了主调函数。值得注意的是,对于x86和ARM构架来说, MOV 是一个容易令人误解性的名称。数据事实上没有被移动,而是被复制了。MIPS在MIPS的世界里有两种寄存器命名的形式:用数字(从 $0 到 $31 )或者用别名( $V0 , $A0 , 等等)。GCC汇编输出中会像下面列表中那样用数字表示寄存器:清单 2.4: 带优化的 GCC 4.4.5 (汇编输出) j $31 li $2,123 # 0x7b而IDA会把它转换成别名: 清单 2.5: 带优化的 GCC 4.4.5 (IDA) jr $ra li $v0, 0x7B $2 (或 $V0 )被用来储存函数的返回值。 LI 代表“立即加载”,这也是MIPS里 MOV 的一个的等价用法。剩下的指令是转跳指令( J 或 JR ),它把控制权交给主调函数,并转跳到 $2(或 $V0 )寄存器里的地址。这个寄存器类似于 ARM 里的LR寄存器。你可能想知道为什么加载指令( LI )和转跳指令( J 或 JR )的位置被交换了。这都是由于RISC中被称为“分支延迟槽”的特性。对于这种现象发生的原因有个借口:这是一些MIPS构架编译器的一个怪癖。但这对我们的目的来说并不重要,我们只需记住在MIPS里是这样的:在转跳指令之后的指令会先比转跳指令本身先执行。2.3.1 关于MIPS指令/寄存器命名的一点在MIPS的世界里,寄存器和指令名习惯上使用小写。但为了一致性,我们坚持使用大写,并在这本书里,把这点当做一个其他编译器都遵守的约定。 12Hello,world!第三章Hello,world!让我们用《C语言程序设计》中最著名的例子开始吧[Ker88]: #include int main() { printf("hello, world\n"); return 0; }3.1 x863.1.1 MSVC让我们在MSVC 2010中编译一下: cl 1.cpp /Fa1.asm(/Fa 选项表示让编译器生产汇编代码文件)代码清单 3.1: MSVC 2010 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 13Hello,world!MSVC生成的汇编代码用的是Intel的汇编语法。Intel语法与AT&T语法的区别将会在3.1.3讨论。编译器会生成连接到 1.exe 的 1.obj 文件。在我们的例子当中,该文件包含两个部分: CONST (放数据常量)和 _TEXT (放代码)。字符串 "hello, world" 在C/C++的类型为 const char[] [Str13, 7.3.2],,然而它没有自己的变量名。编译器需要处理这个字符串,于是就自己给他定义了一个内部名称 $SG3830 。所以我们的例子可以重写为下面这样: #include const char $SG3830[]="hello, world\n"; int main() { printf($SG3830); return 0; }让我们回到汇编代码,正如我们看到的,字符串是由0字节结束的,这是标准的C/C++字符串。关于C/C++字符串见:57.1.1在代码部分, _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的寄存器内容会被改写。IntelC++编译器可能用 POP ECX ,因为这条指令比 ADD ESP, X 更短,( POP ——1字节对应 ADD ——3字节)。这里有一个在Oracle RDBMS中用 POP 而不用 ADD 的例子。 清单 3.2: OracleRDBMS 10.2 Linux (app.o 文件) 14Hello,world! .text:0800029A push ebx .text:0800029B call qksfroChild .text:080002A0 pop ecx在调用printf()之后,原来的C/C++代码执行 return 0 ,返回0当做main()函数的返回结果。在生成的代码中,这被编译成指令 XOR EAX, EAX 。XOR事实上就是异或,但是编译器经常用它来代替 MOV EAX, 0 原因就是它需要的字节更短( XOR 需要2字节对应 MOV 需要5字节)。有些编译器则用 SUB EAX, EAX 就是EXA的值减去EAX,也就是返回0。最后的 RET 指令返回控制权给调用者。通常这是C/C++的库函数代码,它会按顺序,把控制权返还给操作系统。3.1.2 GCC现在我们尝试在linux中用GCC 4.4.1编译同样的C/C++代码 gcc 1.c -o 1下一步,在IDA反汇编的帮助下,我们看看main()函数是如何被创建的。IDA与MSVC一样,也是使用Intel语法。代码清单 3.3:IDA里的代码 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 ,这条指令将ESP寄存器中的值对齐为16字节。这让堆栈中的所有值,都以相同的方式对齐。(如果分配的内存地址大小被对齐为4或16字节,CPU的性能会更好。) 15Hello,world! 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, ... )。3.1.3 GCC:AT&T 语法我们来看一看在AT&T当中的汇编语法,这个语法在UNIX类系统当中更普遍。 代码清单 3.4: 让我们用 GCC 4.7.3 编译 gcc -S 1_1.c我们将得到这个:代码清单 3.5: GCC 4.7.3 16Hello,world! .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-string一样,用于编码一个以null结尾的字符序列)。然后,我们将看到这个:代码清单 3.6: GCC 4.7.3 .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 17Hello,world!在Intel与AT&T语法当中一些主要的区别就是: 操作数写在后面 在Intel语法中:\ \ \ 在AT&T语法中:\ \ \ 有一个简单的记住它们的方法: 当你面对intel语法的时候,你可以想象把等号 (=)放到2个操作数中间,当面对AT&T语法的时候,你可以放一个右箭头(→) 到两个操作数之间。 AT&T: 在寄存器名之前需要写一个百分号(%)并且在数字前面需要加上美元符 ($)。并用圆括号替代方括号。 AT&T: 以下是一些添加到操作符后,用来表示数据形式的后缀: – q — quad (64 bits) – l — long (32 bits) – w — word (16 bits) – b — byte (8 bits)让我们回到上面的编译结果:它和在IDA里看到的是一样的。只有一点不同: 0FFFFFFF0h 被写成了 $-16 。但这是其实是一样的,10进制的16在16进制里表示为0x10。-0x10就等同于0xFFFFFFF0(针对于32位的数据类型)。另外:返回值通常用 MOV 置0,而不用 XOR 。MOV仅仅加载(load)了一个值到寄存器。这条指令的名称是个误称(数据没有被移动,而是被复制了)。在其他的构架上,这条指令会被称作“LOAD” 、 “STORE”或其他类似的名称。3.2 x86-643.2.1 MSVC-x86-64让我们来试试64-bit的MSVC: 代码清单 3.7: MSVC 2012 x64 $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 寄存器被用来传递函数的4个参数,在这里我们可以看到指向给printf()函数的字符串的指针,没有用通过栈,而是用了 RCX 来传递。这些指针现在是64位的,所以他们通过64位寄存器来传递(带有R-前缀),并且为了 18Hello,world!向后兼容,依旧可以使用E-前缀,来访问32位的部分。如下图所示,这是 RAX/EAX/AX/AL 在x86-64构架里的情况 ​main()函数会返回一个int类型的值,在C/C++里为了兼容和移植性,依旧是32位的。这就是问什么,是 EAX 而不是 RAX (即寄存器的低32位部分)在函数最后会被清0。在寄存器里也有40字节被分配给了局部堆栈。这被称为“影子空间”。这一点我们之后会提及:8.2.1 。3.2.2 GCC-x86-64这次在64位的Linux里试试GCC:代码清单 3.8: GCC 4.4.6 x64 .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位部分)。但是为什么不用RDI的64位部分呢?记住这一点很重要: MOV 指令在64位模式下,对低32位进行写入操作的时候,会清空高32位的内容[Int13]。比如 MOV EAX,011223344h 将会把值写到RAX里,并且清空RAX的高32位区域。如果我们打开编译好的对象文件(object file[.o]),我们会看到所有的指令的操作符:代码清单 3.9: GCC 4.4.6 x64 19Hello,world! .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就像看到的那样,在 0x4004D4 那行写入 EDI 花了5个字节。如果把这句换成给 EDI 写入64位的值,会花掉7个字节。显然,GCC在试图节省空间,除此之外,数据段(data segment)中包含的字串不会分配到高于4GiB的地址。可以看到在调用printf()函数前, EAX 被清空了,这是因为在x86-64的 *NIX 系统上, 使用过的向量寄存器的数量会被存入 EAX [Mit13]。3.3 关于GCC 额外的一点(3.1.1),并且匿名的C字符串带有常量的类型C字符串在常量段被分配的地址是一定不变的。基于这样的事实,就有一个有趣的结论:编译器可能只用了字符串的某一部分。让我们看看这个例子: #include int f1() { printf ("world\n"); } int f2() { printf ("hello world\n"); } int main() { f1(); f2(); }一般的C/C++编译器(包括MSVC)会分别分配给地址两个字符串,但是让我们看看GCC干了什么: 代码清单 3.10: GCC 4.8.1 + IDA listing 20Hello,world! f1 proc near s = dword ptr -1Ch sub esp, 1Ch mov [esp+1Ch+s], offset s ; "world\n" call _puts add esp, 1Ch retn f1 endp f2 proc near s = dword ptr -1Ch sub esp, 1Ch mov [esp+1Ch+s], offset aHello ; "hello " call _puts add esp, 1Ch retn f2 endp aHello db 'hello ' s db 'world',0xa,0实际上,当我们打印"hello world"字符串时,这两个单词被放在内存里相邻的位置。函数f2()中调用的 puts() 并不知道字符串已经被分开了。事实上字符串并没有被真正分开,只是在代码里被假装分开了。当 puts() 被f1()调用时,他使用“world”字符串加上一个0, puts() 并不清楚字符串之后还有什么。这个聪明的小技巧至少在GCC里被经常使用,他能够节省一些内存。3.4 ARM作者根据自身对ARM处理器的经验,选择了几款流行的编译器: 嵌入式领域很流行的:Keil Release 6/2013 苹果的Xcode 4.6.3 IDE(其中使用了LLVM-GCC4.2编译器) GCC 4.9 (Linaro) (for ARM64) 可用的32位.exe32位ARM的代码(包括Thumb 和 Thumb-2模式)被用在这本书的所有例子里,如果未做其他提示,我们谈论64位ARM时会叫它ARM64.3.3.1 未进行代码优化的Keil 6/2013 编译:ARM模式让我们在Keil里编译我们的例子 21Hello,world! armcc.exe –arm –c90 –O0 1.carmcc编译器可以生成intel语法的汇编程序列表,但是里面有高级的ARM处理器相关的宏,对我们来讲更希望看到“指令原来的样子”,所以让我们看看IDA反汇编之后的结果。代码清单 3.11: 无优化的 Keil 6/2013 (ARM 模式) IDA .text:00000000 main .text:00000000 10 40 2D E9 STMFD SP!, {R4,LR} .text:00000004 1E 0E 8F E2 ADR R0, aHelloWorld ; "h ello, 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在例子中,我们可以发现所有指令都是4字节的,因为我们编译的时候选择了ARM模式,而不是Thumb模式。最开始的指令是 STMFD SP!, {R4, LR} ,这条指令类似x86平台的 PUSH 指令,它会把2个寄存器(R4和LR)的值写到栈里。不过为了简化,在 armcc 编译器输出的汇编代码里会写成 PUSH {R4, LR} ,但这并不准确,因为 PUSH 命令只在Thumb模式下可用,所以为了减少混乱,我们用IDA来做反汇编工具。这指令首先会减少 SP 的值,这样它在栈中指向的空间就被释放,以留给新条目使用,然后将R4和LR的值存入被修改后的 SP 的储存区域中。这条指令(类似于Thumb模式的PUSH)允许一次压入好几个寄存器的值,非常实用。顺带说一下,在x86里面它没有等价的指令。还有一点值得注意的是: STMFD 指令是广义的 PUSH 指令(扩展了它的功能),因为他能操作任何寄存器,不只是 SP 。换句话说, STMFD 可以用于将一组寄存器储存在特定的内存地址上。 ADR R0, aHelloWorld 这条指令将 "hello, world" 字串的地址偏移加上或减去PC寄存器的值。有人会问, PC 寄存器在这里有什么用呢?这被称作浮动地址码(position-independet code),这样的代码可以在内存中非固定的地址上运行。换句话说,这是和 PC 寄存器相关的寻址。ADR这条指令,考虑了指令的地址和字符串真正所在的地址的差异。无论操作系统把我们的代码加载到哪里,这个差值(偏移)总是相同的。这也是为什么,我们每次都要加上当前的指令地址(从 PC 里),以获取内存中字串的绝对地址。 BL __2print 这条指令用于调用printf()函数,以下是这条指令是如何工作的: 将BL指令(0xC)后面的地址写入LR寄存器; 然后把printf()函数的入口地址写入PC寄存器,将控制权交给printf()函数。 22Hello,world!当printf()函数执行完之后,它必须知道该把控制权返回谁。这就是为什么,每个函数都会把控制权交给 LR 寄存器中的地址。函数返回地址的存放位置,也正是“纯”-RISC处理器(例如ARM)和CISC处理器(例如x86)的区别。另外,一个32位地址或者偏移量不能被编码到32位BL指令里,因为BL指令只有24位的空间。我们应该还记得,所有的ARM模式下的指令都是4字节的(32位)。因此,指令占用了4位的地址。这也就意味着最后2bits(这里总会被设置成0)被忽略了,总的来说,我们有26位可用于偏移编码。这足够去访问大约 当前_PC ±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寄存器。在函数的最后,这个值会被直接写入 PC 寄存器,完成函数的返回操作。因为在C/C++里 main() 一般是主函数,控制权会返回给系统加载器或者CRT里面的指针或其他类似的东西。所有的这些都允许在函数的结尾忽略 BX LR 指令。汇编代码里的 DCB 关键字用来定义ASCII字串数组,就像x86汇编里的 DB 关键字。3.4.2未进行代码优化的Keil 6/2013 编译: (Thumb模式)让我们用下面的指令,将相同的例子用Keil的Thumb模式来编译一下。 armcc.exe –thumb –c90 –O0 1.c我们可以在IDA里得到下面这样的代码: 代码清单 3.12: Non-optimizing Keil6/2013 (Thumb mode) + IDA 23Hello,world! .text:00000000 main .text:00000000 10 B5 PUSH {R4,LR} .text:00000002 C0 A0 ADR R0, aHelloWorld ; "hell o, 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我们首先就能注意到指令都是2字节(16位)的了,这正是Thumb模式的特征。但BL指令是2由个16位的指令来构成的。因为不可能只用16位操作符里的小空间,去加载printf()的偏移量。因此,第一个16位指令,用来加载函数偏移的高10位,第二个指令加载函数偏移的低11位。正如我说过的,所有的Thumb模式下的指令都是2字节(16位)的。这就意味着一个Thumb指令,无论如何不可能在奇数位的地址上。基于以上因素,地址的最后一位将会在编码指令时省略。总的来讲, BL 在Thumb模式下可以访问 当前_PC ±2M的地址。至于在这个函数中的其他指令: PUSH 和 POP ,它们跟上面讲到的 STMFD/LDMFD 很类似,但这里不需要指定 SP 寄存器, ADR 指令也跟上面的工作方式相同。 MOVS 指令将函数的返回值0写到了 R0 寄存器里,让函数返回0。3.4.3 开启代码优化的Xcode(LLVM)(ARM模式)Xcode 4.6.3不开启代码优化的情况下,会产生非常多冗余的代码,所以我们学习一个优化过的版本。这个版本所用的指令的数量会尽可能的少。开启 -O3 编译选项Listing 3.13: Optimizing Xcode 4.6.3 (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 worl d!", 0我们已经非常熟悉 STMFD 和 LDMFD 指令了,这里就跳过不讲。 24Hello,world!下一条, MOV 指令就是将数字 0x1686 写入 R0 寄存器里。这个值是字符串”Hello world!”的指针偏移量。 R7 寄存器(在[App10]里这是个标准)是一个帧指针,在之后的章节我们会介绍它。 MOVT R0, #0 ( MOVe Top )指令时向寄存器 R0 的高16位写入0。这是因为在ARM模式下, MOV 这条指令,只对低16位进行操作。记住!在ARM模式下,所有的指令都被限定在32位以内。当然这个限制并不影响,数据在2个寄存器之间的直接的转移。这也是 MOVT 这种向高16位(包含第16~31位)写入的附加指令存在的意义。但在这里它其实是多余的,因为 MOVS R0,#0x1686 这条指令也能把寄存器的高16位清0。这或许就是相对于人脑来说编译器的不足。 ADD R0,PC,R0 指令把 PC 寄存器的值相到 R0 里,用来计算 "Hello world!" 字符串的绝对地址。这如我们所知的,这里采用浮动地址码,所以这个修正还是有必要的。 BL 指令调用了 puts() 函数,而不是 printf() 。GCC将第一个 printf() 函数替换成了 puts() 。因为 printf() 函数只有单一参数时,跟 puts() 函数是类似的。在大多数情况下, printf() 的字符串参数里,没有以 % 开头的特殊控制符的时候,两个函数的会输出相同的结果。如果不是这样,这两个函数的功能会有所差别。为什么编译器会替换 printf() 为 puts() 呢?这或许是因为 puts() 更快一些。因为 puts() 只是做了字串的标准输出( stdout ),而不需要将字符串逐位与 % 相比较。下一条语句,我们可以看到了熟悉的 "MOV R0, #0" 指令,用来将 R0 寄存器设为0。3.4.4 开启代码优化的Xcode(LLVM)编译Thumb-2模式在默认情况下,Xcode4.6.3会生成如下的Thumb-2代码代码清单 3.14: 带优化的 Xcode 4.6.3 (LLVM) (Thumb-2 模式) 25Hello,world! __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 wor ld!",0xA,0正如我们刚刚回忆过的, BL 和 BLX 指令在Thumb模式下,被编码为一对16位的指令。在Thumb-2模式下这操作符些会被这样扩展,所以新的指令会被编码成32位的指令。很容易就能发现,Thumb-2的操作码总是以 0xFx 或 0xEx 开头。但是在IDA的反汇编代码里,操作符的位置被交换过了。对于ARM处理器来说,这是因为指令以以下方式编码:最后一个字节在最前面,接下来是第一个字符(在Thumb和 Thumb-2模式里),对于四个字节的操作符则是:首先是第四个字节,然后是第三个,接下来是第二个,最后才是第一个字节(这是由于不同的字节序)。下面是在IDA里,字节是如何排列的: ARM 和 ARM64 模式: 4-3-2-1; Thumb 模式: 2-1; Thumb-2 模式里的一对16位指令 : 2-1-4-3.所以我们能看出来, MOVW , MOVT.W 和 BLX 这几个指令都是以 0xFx 开始。在Thumb-2指令里有一条是 MOVW R0, #0x13D8 ,它的作用是将16位的值,写到 R0 的低16位里面,并将高位清零。 MOVT.W R0, #0 的作用类似与前面讲到的 MOVT 指令,但它工作在Thumb-2模式下。还有些其他的差异,比如 BLX 指令替代了上面用到的 BL 指令。这样做的区别在于:这条指令除了将 RA 存入到寄存器 LR 里,还将控制全交给 puts() 函数,并且处理器也从Thumb/Thumb-2模式转换到了ARM模式(或者相反)。这条指令放在这里,是因为跳转到了像下面这样的位置(下面的代码以ARM模式编码)。 __symbolstub1:00003FEC _puts ; CODE XREF: _hello_wo rld+E __symbolstub1:00003FEC 44 F0 9F E5 LDR PC, =__imp__puts这本质上是个到 puts() 导入地址的转跳。 26Hello,world!可能会有细心的读者要问了:为什么不在需要的时候,直接调用 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 寄存器里,并将控制权交给那个地址。所以为了减少系统加载器完成这个过程所需的时间,最好将所有符号的地址一次性写到一个特定的地方。另外,我们前面也指出过,我们没办法只用一条指令,并且在不访问内存的情况下,就将一个32位的值保存到寄存器里。因此,最好的办法就是,单独分出一个函数,用来在ARM模式下将控制权交给动态链接库,这样做一些类似与上面这样单一指令的函数(称做Thunk function),然后从Thumb模式里也能去调用。在先前的例子中(以ARM模式编译的例子), BL 指令也是跳转到了同一个Thunkfunction里。尽管没有进行模式的转变(所以指令里不存在那个”X”)。关于形实转换函数形实转换函数很难理解,表面上看是因为它的具有误导性的名字。理解它最简单的方法是把他看做一个适配器,或者将一种插口转换为另一种的转换器。举个例子,一个适配器允许一个英式的电源插头插入一个美式的插座,反之亦然。形实转换函数有时被称作封装器。以下是对该函数的一些描述: 27Hello,world! P. Z. Ingerman说这个函数是"提供地址的一段代码",他于1961年,发明了形实 转换函数,并作为Algol-60 程序调用里,将实参转换为标准定义的一种方式。 如果调用一个带有表达式形参的程序,编译器会生成一个形实转换函数来计算 表达式的值,并将结果的地址放在某些标准位置上。 ... Microsoft 和 IBM 都在 他们的基于Intel的系统里面定义了一个“16-位的环境”(带有讨厌的段寄存器和 64K的内存限制)和一个“32-位的环境”(带有平坦寻址和半实时的内存管理)。 这 两种环境都能在相同的电脑和操作系统上运行(感谢我们在Microsoft世界里称之 为WOW的东西,WOW代表着Windows On Windows)。 MS 和 IBM都决定将 16位到32位和相反的转换过程称为一个"thunk";对于Windows 95来说,甚至 有个叫做“Thunk编译器”的工具——THUNK.EXE。(The Jargon File)3.4.5 ARM64GCC让我们在ARM64 上用GCC 4.8.1编译一下这个程序。代码清单 3.15:无优化的 GCC 4.8.1 + objdump 1 0000000000400590 : 2 400590: a9bf7bfd stp x29, x30, [sp,#-16]! 3 400594: 910003fd mov x29, sp 4 400598: 90000000 adrp x0, 400000 5 40059c: 91192000 add x0, x0, #0x648 6 4005a0: 97ffffa0 bl 400420 7 4005a4: 52800000 mov w0, #0x0 // #0 8 4005a8: a8c17bfd ldp x29, x30, [sp],#16 9 4005ac: d65f03c0 ret 10 11 ... 12 13 Contents of section .rodata: 14 400640 01000200 00000000 48656c6c 6f210a00 ........Hello! ..代码清单 3.16: main() 返回uint64_t类型的值 28Hello,world! #include #include uint64_t main() { printf ("Hello!\n"); return 0; }结果是相似的,下面是在那一行, MOV 看起来是怎么样的:代码清单 3.17: 无优化的 GCC 4.8.1 + objdump 4005a4: d2800000 mov x0, #0x0 // #03.5 MIPS3.5.1 关于全局指针 LDA 负载对然后恢复了 X29 和 X30 寄存器。3.5.2 带优化的GCC让我们看看下面这个例子,他说明了全局指针的概念:代码清单 3.18: 带优化的 GCC 4.4.5 (汇编输出) 29Hello,world! 1 $LC0: 2 ; \000 is zero byte in octal base: 3 .ascii "Hello, world!\012\000" 4 main: 5 ; function prologue. 6 ; set the GP: 7 lui $28,%hi(__gnu_local_gp) 8 addiu $sp,$sp,-32 9 addiu $28,$28,%lo(__gnu_local_gp) 10 ; save the RA to the local stack: 11 sw $31,28($sp) 12 ; load the address of the puts() function from the GP to $25: 13 lw $25,%call16(puts)($28) 14 ; load the address of the text string to $4 ($a0): 15 lui $4,%hi($LC0) 16 ; jump to puts(), saving the return address in the link r egister: 17 jalr $25 18 addiu $4,$4,%lo($LC0) ; branch delay slot 19 ; restore the RA: 20 lw $31,28($sp) 21 ; copy 0 from $zero to $v0: 22 move $2,$0 23 ; return by jumping to the RA: 24 j $31 25 ; function epilogue: 26 addiu $sp,$sp,32 ; branch delay slot代码清单 3.19: 带优化的 GCC 4.4.5 (IDA) 30Hello,world! 1 .text:00000000 main: 2 .text:00000000 3 .text:00000000 var_10 = -0x10 4 .text:00000000 var_4 = -4 5 .text:00000000 6 ; function prologue. 7 ; set the GP: 8 .text:00000000 lui $gp, (__gnu_local_g p >> 16) 9 .text:00000004 addiu $sp, -0x20 10 .text:00000008 la $gp, (__gnu_loc al_gp & 0xFFFF) 11 ; save the RA to the local stack: 12 .text:0000000C sw $ra, 0x20+var_4 ($sp) 13 ; save the GP to the local stack: 14 ; for some reason, this instruction is missing in the GCC assembly output: 15 .text:00000010 sw $gp, 0x20+var_1 0($sp) 16 ; load the address of the puts() function from the GP to $t9: 17 .text:00000014 lw $t9, (puts & 0x FFFF)($gp) 18 ; form the address of the text string in $a0: 19 .text:00000018 lui $a0, ($LC0 >> 16) # "Hello, world!" 20 ; jump to puts(), saving the return address in the link r egister: 21 .text:0000001C jalr $t9 22 .text:00000020 la $a0, ($LC0 & 0x FFFF) # "Hello, world!" 23 ; restore the RA: 24 .text:00000024 lw $ra, 0x20+var_4( $sp) 25 ; copy 0 from $zero to $v0: 26 .text:00000028 move $v0, $zero 27 ; return by jumping to the RA: 28 .text:0000002C jr $ra 29 ; function epilogue: 30 .text:00000030 addiu $sp, 0x203.5.3 无优化的 GCC无优化的GCC会产生更冗长的代码:代码清单 3.20: 无优化的 GCC 4.4.5 (汇编输出) 31Hello,world! 1 $LC0: 2 .ascii "Hello, world!\012\000" 3 main: 4 ; function prologue. 5 ; save the RA ($31) and FP in the stack: 6 addiu $sp,$sp,-32 7 sw $31,28($sp) 8 sw $fp,24($sp) 9 ; set the FP (stack frame pointer): 10 move $fp,$sp 11 ; set the GP: 12 lui $28,%hi(__gnu_local_gp) 13 addiu $28,$28,%lo(__gnu_local_gp) 14 ; load the address of the text string: 15 lui $2,%hi($LC0) 16 addiu $4,$2,%lo($LC0) 17 ; load the address of puts() using the GP: 18 lw $2,%call16(puts)($28) 19 nop 20 ; call puts(): 21 move $25,$2 22 jalr $25 23 nop ; branch delay slot 24 25 ; restore the GP from the local stack: 26 lw $28,16($fp) 27 ; set register $2 ($V0) to zero: 28 move $2,$0 29 ; function epilogue. 30 ; restore the SP: 31 move $sp,$fp 32 ; restore the RA: 33 lw $31,28($sp) 34 ; restore the FP: 35 lw $fp,24($sp) 36 addiu $sp,$sp,32 37 ; jump to the RA: 38 j $31 39 nop ; branch delay slot代码清单 3.21: 无优化的 GCC 4.4.5 (IDA) 1 .text:00000000 main: 2 .text:00000000 3 .text:00000000 var_10 = -0x10 4 .text:00000000 var_8 = -8 5 .text:00000000 var_4 = -4 6 .text:00000000 7 ; function prologue. 8 ; save the RA and FP in the stack: 9 .text:00000000 addiu $sp, -0x20 32Hello,world! 10 .text:00000004 sw $ra, 0x20+v ar_4($sp) 11 .text:00000008 sw $fp, 0x20+v ar_8($sp) 12 ; set the FP (stack frame pointer): 13 .text:0000000C move $fp, $sp 14 ; set the GP: 15 .text:00000010 la $gp, __gnu_ local_gp 16 .text:00000018 sw $gp, 0x20+v ar_10($sp) 17 ; load the address of the text string: 18 .text:0000001C lui $v0, (aHelloWo rld >> 16) # "Hello, world!" 19 .text:00000020 addiu $a0, $v0, (a HelloWorld & 0xFFFF) # "Hello, world!" 20 ; load the address of puts() using the GP: 21 .text:00000024 lw $v0, (puts & 0xFFFF)($gp) 22 .text:00000028 or $at, $zero ; NOP 23 ; call puts(): 24 .text:0000002C move $t9, $v0 25 .text:00000030 jalr $t9 26 .text:00000034 or $at, $zero ; NOP 27 ; restore the GP from local stack: 28 .text:00000038 lw $gp, 0x20+v ar_10($fp) 29 ; set register $2 ($V0) to zero: 30 .text:0000003C move $v0, $zero 31 ; function epilogue. 32 ; restore the SP: 33 .text:00000040 move $sp, $fp 34 ; restore the RA: 35 .text:00000044 lw $ra, 0x20+v ar_4($sp) 36 ; restore the FP: 37 .text:00000048 lw $fp, 0x20+v ar_8($sp) 38 .text:0000004C addiu $sp, 0x20 39 ; jump to the RA: 40 .text:00000050 jr $ra 41 .text:00000054 or $at, $zero ; NOP3.5.4 堆栈结构在本例里面的作用文本字符串的地址是通过寄存器传递的。那为什么要设置一个局部堆栈呢?这样做的原因是寄存器 RA 和 GP 的值必须被储存在某个地方(因为 printf() 被调用了),局部堆栈就是用于这个目的的。如果这是个末端函数,那么有可能除去他的函 33Hello,world!数开始和函数结尾,例如:2.33.5.5 带优化的 GCC:把它加载到GDB代码清单 3.22: GDB session 的例子 34Hello,world! root@debian-mips:~# gcc hw.c -O3 -o hw root@debian-mips:~# gdb hw GNU gdb (GDB) 7.0.1-debian Copyright (C) 2009 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute i t. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "mips-linux-gnu". For bug reporting instructions, please see: ... Reading symbols from /root/hw...(no debugging symbols found)...d one. (gdb) b main Breakpoint 1 at 0x400654 (gdb) run Starting program: /root/hw Breakpoint 1, 0x00400654 in main () (gdb) set step-mode on (gdb) disas Dump of assembler code for function main: 0x00400640 : lui gp,0x42 0x00400644 : addiu sp,sp,-32 0x00400648 : addiu gp,gp,-30624 0x0040064c : sw ra,28(sp) 0x00400650 : sw gp,16(sp) 0x00400654 : lw t9,-32716(gp) 0x00400658 : lui a0,0x40 0x0040065c : jalr t9 0x00400660 : addiu a0,a0,2080 0x00400664 : lw ra,28(sp) 0x00400668 : move v0,zero 0x0040066c : jr ra 0x00400670 : addiu sp,sp,32 End of assembler dump. (gdb) s 0x00400658 in main () (gdb) s 0x0040065c in main () (gdb) s 0x2ab2de60 in printf () from /lib/libc.so.6 (gdb) x/s $a0 0x400820: "hello, world" (gdb)3.5.5 小结 35Hello,world!x86/ARM 和 x64/ARM64 代码的主要区别是:x64中指向字符串的指针是64位长度的。现代CPU是64位的主要原因是:内存成本的下降和各种应用对64位的强烈需求。我们现在能够给电脑加很多内存,以至于远远超过了32位指针能够寻址的范围。正因如此,现在所有的指针都是64位的了。3.7 练习 http://challenges.re/48 http://challenges.re/49 36函数的开始和结束第四章函数的序幕和清尾C语言的函数通常使用类似下面的代码片段作为序幕: push ebp mov ebp, esp sub esp, X这些指令将EBP寄存器的值入栈,然后把ESP赋值给EBP,最后在栈中为局部变量分配一段空间。在函数执行过程中EBP是固定的,可以用来作为访问局部变量和函数参数的基址。虽然也可以使用ESP,但在函数运行过程中,ESP可能会变化,使用起来不太方便。函数清尾主要包括:释放栈中分配的空间,恢复EBP寄存器中的值,最后把控制流交由调用者: mov esp, ebp pop ebp ret 0函数的序幕和清尾代码片段通常被反汇编器作为函数定义的检测代码。4.1 递归函数的序幕和清尾代码片段可能会影响到递归函数的性能。更多的信息请查看36.3一章。 37栈第五章栈栈是计算机科学中最基本的一种数据结构。从技术上讲,栈只是在x86中被 ESP 寄存器、x64中被 RSP 寄存器、或ARM中被 SP 寄存器指向的一块程序内存。在x86和ARM Thumb模式中,访问栈最常用的指令是 PUSH 和 POP 。 PUSH 指令在32位模式下,会将 ESP/RSP/SP 的值减去4(在64位系统中,会减去8),然后将它唯一的参数写入到 ESP/RSP/SP 指向的内存地址。POP 是 PUSH 的逆向操作:从 SP 指向的内存地址中获取数据,然后存入指定的参数中(一般为寄存器), 然后将栈指针加4(或8)。在栈分配过后,栈指针指向栈底。 PUSH 减少栈指针; POP 指令增加栈指针。栈底实际上是栈分配到的内存的起始地址。这看起来很奇怪,但事实就是这样。ARM 支持递增堆栈和递减堆栈。举几个例子: STMFD/LDMFD , STMED/LDMED 指令是用来处理递减堆栈的(向下增长,从高址开始向低址增长)。 STMFA/LDMFA , STMEA/LDMEA 指令是用来处理递增堆栈的(向上增长,从低址开始向高址增长)。5.1 为什么栈会反向增长?从直觉上来说,我们会认为栈像其它数据结构一样,是向高地址正向增长的。栈反向增长是有历史原因的。在计算机十分巨大,需要占据整个房间的年代,人们可以很容易的把内存分为两部分,一部分给堆,另一部分给栈。当然,在程序运行期间,我们并不知道堆栈各需要多大的空间。这时最简单的解决方法可能是:在[RT74]中我们可以看到: 38栈 映像文件的用户核心部分可以被划分为三个逻辑段。程序代码段在虚拟内存里 从0位置开始。 在程序运行过程中,这部分是具有写保护的,同一程序的所有进程 都共享代码段的一个副本。 在虚拟内存地址中,程序代码段开始的8k字节,是 私有的的可写数据段,这个段的大小可以通过系统调用来扩大。 从虚拟内存的 高位地址开始是堆栈段,这部分像硬件栈指针大小的变动一样,可以自动的向 下增长。以上可以使我们联想到:一些学生在一个笔记本中写两门课的笔记:将第一门课的笔记正常写下,由于厌恶,而把另一门的笔记从后往前写。两种笔记有可能因缺少空间,而在中间的某处相遇。5.2 栈可以用来做什么?5.2.1 保存函数的返回地址x86当使用 CALL 指令去调用一个函数时, CALL 后面一条指令的地址会被保存到栈中,然后使用无条件跳转指令跳转到 CALL 中执行。CALL 指令等价于 PUSH address_after_call / JMP operand 这对指令。 RET 指令从栈中取出一个值并转跳到这个值上,这等价于 POP tmp / JMP tmp 指令对。栈溢出是很容易的。只需要执行无止尽的递归。 void f() { f(); };MSVC 2008报告了这个问题: c:\tmp6>cl ss.cpp /Fass.asm Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.210 22.08 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. ss.cpp c:\tmp6\ss.cpp(4) : warning C4717: ’f’ : recursive on all contro l paths, function will cause ⤦ Ç runtime stack overflow...但它还是生成了正确的代码: 39栈 ?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 ; fGCC 4.4.1 在这两种条件下,会生成同样的代码,并且不会有任何警告。ARMARM程序员也使用栈来保存返回地址,但稍有不同。正如我们在“Hello,World!(3.4)里提到过的, RA 被保存在 LR (链接寄存器)中。然而,如果有时需要调用另外一个函数,并且要多次使用 LR 寄存器,它的值必须被保存起来。通常它会在被保存到函数的开头。像我们经常看到的指令 PUSH R4-R7, LR ,与在函数结尾处的指令 POP R4-R7, PC ,在函数中使用到的寄存器的值,包括 LR ,会被保存到栈中。然而,如果一个函数从未调用其它函数,它在 RISC 术语中被叫作叶子函数。因此,叶子函数不需要保存 LR 寄存器(因为他们并不修改它)。如果这样的函数很小,并只使用了少量的寄存器,它可能完全不需要用到栈。因此,可以不使用栈而调用叶子函数。这样做比在老x86机器上运行要快,因为不需要为栈留出额外的内存。在留给栈的内存尚未分配或不可用的情况下,这种方式是非常有用的。一些叶子函数的例子:8.3.2 , 8.3.3 , 19.17 , 19.33 , 19.5.4 , 15.4 , 15.2 , 17.3 。5.2.2 传递函数参数在x86中,最常见的传参方式是 cdecl : 40栈 push arg3 push arg2 push arg1 call f add esp, 12 ; 4*3=12被调用函数通过栈指针得到参数。因此,以下就是在函数f()的第一条指令执行之前,栈中参数的值是如何排列的。关于对调用约定参见(64)。值得注意的是,没有任何东西强迫程序员一定要使用栈来传递参数。这并不是必需的,一个程序员完全可以不使用栈,而通过其它方式来实现参数传递。例如,可以为参数分配一部分堆空间,存入参数,然后通过 EAX 寄存器里指向这个块的指针,将参数传递给函数。这样是可行的。然而,在x86和ARM中,使用栈传递参数还是更加方便的。另外,被调函数并不知道有多少参数被传递进来。C语言中有些函数可以传递不同个数的参数(如: printf() ),他们一般通过使用格式字符串(以 % 开始)来判断参数个数。如果我们可以这样些: printf("%d %d %d", 1234);printf()会输出入1234,然后另外输出和栈相邻的,两个另外的随机数字。这就是为什么我们如何声明 main() 函数是不重要的,像 main() , main(int argc, char *argv[]) 或 main(int argc, char *argv[], char *。事实上, CRT 模式 大致是这样调用main()函数的: push envp push argv push argc call main ... 41栈即使你将 main() 声明为不带参数的main()函数。它们仍然在栈中,只是没被使用。如果你将 main() 声明为 main(int argc, char *argv[]) ,你就可以使用前两个参数,并且第三个参数在你的函数仍然是"不可见的"。还有,如果你声明为 main(int argc) 这样,它同样是可以正常运行的。5.2.3 存放局部变量一个函数可以在栈中分配空间,用于储存局部变量。这只需要将栈指针向栈底增加。因此,无论你需要定义多少局部变量,这样都很快。在栈中存放局部变量并不是一个硬性的要求。你可以将局部变量存到任何你想存的地方,但从传统上来说,大家更喜欢这样做。5.2.4 x86: alloca() 函数这里值得注意的是 alloca() 函数。该函数的作用类似于 malloc() ,但它会直接在栈上分配内存。它分配的内存块,并不需要调用像 free() 这样的函数来释放(4)。当函数运行结束, ESP 的值还原时,这部分内存会自动释放。值得注意的是 alloca() 函数的实现。简而言之,这个函数就是根据你所需要的内存大小,将 ESP 指针指向栈底移位,然后将 ESP 指向所分配的内存块。让我们试一下: #ifdef __GNUC__ #include // GCC #else #include // MSVC #endif #include void f() { char *buf=(char*)alloca (600); #ifdef __GNUC__ snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // GCC #else _snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3); // MSVC #endif puts (buf); }; _snprintf() 函数作用与 printf() 函数基本相同,不同的地方是 printf() 会将结果输出到的标准输出中(例如:终端和控制台), _snprintf() 会将结果保存到缓冲区中, puts() 会将缓冲区的内容复制到标准输出。当然,后面两行代码可以 42栈使只用一个 printf() 调用替换,但我们必须说明小缓冲区的用途。MSVC让我们来编译 (MSVC 2010):代码清单 5.1: 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 ... alloca() 的唯一一个参数通过 EAX 来传递(而不是把他压入栈)。在函数调用结束时, ESP 会指向 600字节的内存,我们可以像使用缓冲区数组一样来使用它。GCC + Intel格式GCC 4.4.1不需要调用额外的函数,就可以实现相同的功能:代码清单 5.2: GCC 4.7.3 43栈 .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 poi nter 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 ; s call puts mov ebx, DWORD PTR [ebp-4] leave retGCC + AT&T 语法我们来看看使用了AT&T语法的相同的代码:代码清单 5.3: GCC 4.7.3 44栈 .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这里的代码与上面的那个代码清单是相同的。另外:在Intel语法中, movl $3, 20(%esp) 与 mov DWORD PTR [esp + 20],3 是等价的。在AT&T语法中,register+offset形式的内存地址表示为: offset(%register) 。5.2.5 (Windows) 结构化异常处理 (SEH)(如果存在) SEH 记录也是存放在栈中的。 想了解更多,参看(68.3)。5.2.6 缓冲区溢出保护想了解更多,参看(18.2)。5.2.7 栈内数据的自动回收也许把临时变量和 SHE 记录存在栈中,是因为他们会在函数的结尾会被自动的释放,而且只需要用一条指令就能还原栈指针(通常是 ADD )。函数的参数也会在函数的结尾被释放。相对的,储存在堆中的任何东西都必须被明确的释放。5.3 典型的堆栈布局 45栈以下是32位的环境中,第一个函数开始执行前,栈典型的布局:5.4 栈内的'噪声'在这本书里,我们经常提到栈中的"噪声"值和内存中的"垃圾"值,他们从哪里来?他们通常是上一个函数执行完而留下的值。简短的例子: #include void f1() { int a=1, b=2, c=3; }; void f2() { int a, b, c; printf ("%d, %d, %d\n", a, b, c); }; int main() { f1(); f2(); };编译后:代码清单 5.4: 无优化的 MSVC 2010 46栈$SG2752 DB '%d, %d, %d', 0aH, 00H_c$ = -12 ; size = 4_b$ = -8 ; size = 4_a$ = -4 ; size = 4_f1 PROC push ebp mov ebp, esp sub esp, 12 mov DWORD PTR _a$[ebp], 1 mov DWORD PTR _b$[ebp], 2 mov DWORD PTR _c$[ebp], 3 mov esp, ebp pop ebp ret 0_f1 ENDP_c$ = -12 ; size = 4_b$ = -8 ; size = 4_a$ = -4 ; size = 4_f2 PROC push ebp mov ebp, esp sub esp, 12 mov eax, DWORD PTR _c$[ebp] push eax mov ecx, DWORD PTR _b$[ebp] push ecx mov edx, DWORD PTR _a$[ebp] push edx push OFFSET $SG2752 ; '%d, %d, %d' call DWORD PTR __imp__printf add esp, 16 mov esp, ebp pop ebp ret 0_f2 ENDP_main PROC push ebp mov ebp, esp call _f1 call _f2 xor eax, eax pop ebp ret 0_main ENDP编译器会有一些小怨言: 47栈 c:\Polygon\c>cl st.c /Fast.asm /MD Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.402 19.01 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. st.c c:\polygon\c\st.c(11) : warning C4700: uninitialized local varia ble 'c' used c:\polygon\c\st.c(11) : warning C4700: uninitialized local varia ble 'b' used c:\polygon\c\st.c(11) : warning C4700: uninitialized local varia ble 'a' used Microsoft (R) Incremental Linker Version 10.00.40219.01 Copyright (C) Microsoft Corporation. All rights reserved. /out:st.exe st.obj但当我们运行编译好的程序时: c:\Polygon\c>st 1, 2, 3啊!这太奇怪了!在 f2() 里,我们并没有为任何变量赋值。这些幽灵般的值仍留在栈里。让我们在OllyDbg里加载这个例子:图 5.1: OllyDbg: f1()当 f1() 分配变量a、b和c时,他们的值被存到 0x1FF860 等几个地址里。 48栈然后当 f2() 执行的时候:图 5.2: OllyDbg: f2()... f2() 中的a、b和c分到了相同的地址!并且没有一个值被覆盖了,到目前为止他们的值未受影响。为了让这中情况发生,一定有几个函数被依次调用,并且在每个函数分支中 SP 都有相同的值(例如,他们都有相同的参数)。然后这些临时变量就会被分配到栈中相同的位置上。总的来说,栈中(和内存中)所有的值中总有几个,是先前的函数执行后留下的。严格的来说他们并不是随机的,但是他们的值是不可预测的。还有其他可能吗?也许可以在每个函数执行完后,清除栈中一部分的值,但这会产生很多额外的(而且没必要的)工作。5.4.1 MSVC 2013这个例子是在MSVC 2010里编译的。但有些读者会尝试在MSVC 2013 里编译、运行它。然后会得到三个数字颠倒后的结果: c:\Polygon\c>st 3, 2, 1为什么?我也在MSVC 2013中编译了这个程序:代码清单 5.5: MSVC 2013 49栈 _a$ = -12 ; size = 4 _b$ = -8 ; size = 4 _c$ = -4 ; size = 4 _f2 PROC ... _f2 ENDP _c$ = -12 ; size = 4 _b$ = -8 ; size = 4 _a$ = -4 ; size = 4 _f1 PROC ... _f1 ENDP不像MSVC 2010 ,在MSVC 2013中 f2() 中的变量 a/b/c 会被以相反的顺序分配空间。但这样做是完全正确的,因为C/C++标准里并没有规定要以何种顺序来分配栈中的变量。产生这个区别的原因是:MSVC 2010有他自己的分配变量的方式,而在MSVC 2013中,可能有什么事改变了编译器内在的东西,所以结果稍有区别。5.5 Exercises http://challenges.re/51 http://challenges.re/52 50printf()与参数处理第六章printf() 与参数处理现在让我们扩展"hello, world"(2)中的示例,将其中main()函数中printf的部分替换成这样 #include int main() { printf("a=%d; b=%d; c=%d", 1, 2, 3); return 0; };6.1 x866.1.1 x86: 3个参数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。 51printf()与参数处理当然,这只涉及__cdecl函数调用方式。也可以在最后一个函数调用后,把几个 ADD ESP, X 指令合并成一个。 push a1 push a2 call ... ... push a1 call ... ... push a1 push a2 push a3 call ... add esp, 24MSVC 与 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同时也在改变,因为它是指向栈的栈中的数据又在哪?那么看一下调试器右下方的窗口:图 6.1然后我们可以看到有三列,栈的地址,元组数据,以及一些OllyDbg的注释,OllyDbg可以识别像printf()这样的字符串,以及后面的三个值。 52printf()与参数处理右击选中字符串,然后点击”follow in dump”,然后字符串就会出现在左侧显示内存数据的地方,这些内存的数据可以被编辑。我们可以修改这些字符串,之后这个例子的结果就会变的不同,现在可能并不是很实用。但是作为练习却非常好,可以体会每部分是如何工作的。再按一次F8(step over)然后我们就可以看到输出图6.2 执行printf()函数让我们看看寄存器和栈是怎样变化的 见图5.5EAX寄存器现在是0xD(13).这是正确的,printf()返回打印的字符,EIP也变了——事实上现在指向CALL printf之后下一条指令的地址.ECX和EDX的值也改变了。显然,printf()函数的内部机制对它们进行了使用。很重要的一点ESP的值并没有发生变化,栈的状态也是!我们可以清楚地看到字符串和相应的3个值还是在那里,实际上这就是cdecl调用方式。被调用的函数并不清楚栈中参数,因为这是调用体的任务。再按一下F8执行 ADD ESP, 0 见图5.6ESP改变了,但是值还是在栈中!当然 没有必要用0或者别的数据填充这些值。因为在栈指针寄存器之上的数据都是无用的。 53printf()与参数处理图6.3 OllyDbg:main()初始处图6.4 OllyDbg:printf()执行时 54printf()与参数处理图6.5 Ollydbg:printf()执行后图6.6 OllyDbg ADD ESP, 10执行完后GCC现在我们将同样的程序在linux下用GCC4.4.1编译后放入IDA看一下: 55printf()与参数处理 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 endpMSVC与GCC编译后代码的不同点只是参数入栈的方法不同,这里GCC不用PUSH/POP而是直接对栈操作。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 This is free software: you are free to change and redistribute i t. 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: ... Reading symbols from /home/dennis/polygon/1...done. 56printf()与参数处理表6.1 在printf()处设置断点 (gdb) b printf Breakpoint 1 at 0x80482f0Run 这里没有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 : mov $0x0,%eax 0x804844f : leave 0x8048450 : 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()完。 57printf()与参数处理 (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 = 13GDB显示了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 ...反汇编当前的指令 (gdb) disas Dump of assembler code for function main: 0x0804841d : push %ebp 0x0804841e : mov %esp,%ebp 0x08048420 : and $0xfffffff0,%esp 0x08048423 : sub $0x10,%esp 0x08048426 : movl $0x3,0xc(%esp) 0x0804842e : movl $0x2,0x8(%esp) 0x08048436 : movl $0x1,0x4(%esp) 0x0804843e : movl $0x80484f0,(%esp) 0x08048445 : call 0x80482f0 => 0x0804844a : mov $0x0,%eax 0x0804844f : leave 0x08048450 : ret End of assembler dump. 58printf()与参数处理GDB默认使用AT&T语法显示,当然也可以转换至intel: (gdb) set disassembly-flavor intel (gdb) disas Dump of assembler code for function main: 0x0804841d : push ebp 0x0804841e : mov ebp,esp 0x08048420 : and esp,0xfffffff0 0x08048423 : sub esp,0x10 0x08048426 : mov DWORD PTR [esp+0xc],0x3 0x0804842e : mov DWORD PTR [esp+0x8],0x2 0x08048436 : mov DWORD PTR [esp+0x4],0x1 0x0804843e : mov DWORD PTR [esp],0x80484f0 0x08048445 : call 0x80482f0 => 0x0804844a : mov eax,0x0 0x0804844f : leave 0x08048450 : 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 ...6.1.2 x64: 8个参数为了看其他参数如何通过栈传递的,我们再次修改代码将参数个数增加到9个(printf()格式化字符串和8个int 变量) 59printf()与参数处理 #include 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; };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 `表6.2:msvc 2010 x64GCC在*NIX系统,对于x86-64这也是同样的原理,除了前6个参数传递给了RDI,RSI,RDX,RCX,R8,R9寄存器。GCC将生成的代码字符指针写入了EDI而不是RDI(如果有的话)——我们在2.2.2节看到过这部分 60printf()与参数处理同样我们也看到在寄存器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表6.3:GCC 4.4.6 –o 3 x64GCC + GDB让我们在GDB中尝试这个例子。 $ gcc -g 2.c -o 2反编译: 61printf()与参数处理 $ gdb 2 GNU gdb (GDB) 7.6.1-ubuntu Copyright (C) 2013 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute i t. 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: ... 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 ... 62printf()与参数处理表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 : push rbp 0x000000000040052e : mov rbp,rsp 0x0000000000400531 : sub rsp,0x20 0x0000000000400535 : mov DWORD PTR [rsp+0x10],0x8 0x000000000040053d : mov DWORD PTR [rsp+0x8],0x7 0x0000000000400545 : mov DWORD PTR [rsp],0x6 0x000000000040054c : mov r9d,0x5 0x0000000000400552 : mov r8d,0x4 0x0000000000400558 : mov ecx,0x3 0x000000000040055d : mov edx,0x2 0x0000000000400562 : mov esi,0x1 0x0000000000400567 : mov edi,0x400628 0x000000000040056c : mov eax,0x0 0x0000000000400571 : call 0x400410 0x0000000000400576 : mov eax,0x0 0x000000000040057b : leave 0x000000000040057c : ret End of assembler dump.执行完printf()后,就会清零EAX,然后发现EAX早已为0,RIP现在则指向LEAVE指令。 63printf()与参数处理 (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 ...6.2 ARM6.3 ARM:3个参数习惯上,ARM传递参数的规则(参数调用)如下:前4个参数传递给了R0-R3寄存器,其余的参数则在栈中。这和fastcall或者win64传递参数很相似32-bit ARMNon-optimizing Keil + ARM mode(非优化keil编译模式 + ARM环境) 64printf()与参数处理 .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. 到目前为止没有什么不寻常的。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()函数外没有其他函数了。另外,除了这个调用外,我们不需要再做别的。这就是为什么这样编译是可行的。Optimizing Keil + thumb mode 65printf()与参数处理 .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}表6.8:Optimizing Keil + thumb mode和non-optimized for ARM mode代码没什么明显的区别Optimizing Keil 6/2013 (ARM mode) + 让我们移除 returnARM 64Non-optimizing GCC (Linaro) 4.96.2.2 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); };Optimizing Keil: ARM mode 66printf()与参数处理 .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: 67printf()与参数处理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指针返回到之前的指针处,因此清除了栈,当然,栈中之前写入的数据还在那,但是当后来的函数被调用时那里则会被重写。 “LDRPC, [SP+4+var_4],#4"指令将LR中储存的值载入到PC指针,因此函数结束。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 68printf()与参数处理 __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”指令却可以同时执行,因为执行效果并没有任何冲突。 大概,编译器就是这样尝试编译的,可能。Optimizing Xcode (LLVM): thumb-2 mode 69printf()与参数处理 __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在这里被替代使用了ARM 64无优化的 GCC (Linaro) 4.96.3 MIPS6.3.1 3个参数带优化的 GCC 4.4.5无优化的 GCC 4.4.56.3.2 8个参数 70printf()与参数处理带优化的 GCC 4.4.5无优化的 GCC 4.4.56.4 结论6.5 By the way值得一提的是,这些x86,x64,fastcall和ARM传递参数的不同表现了CPU并不在意函数参数是怎样传递的,同样也假想编译器可能用特殊的结构传送参数而一点也不是通过栈。 71scanf()第七章scanf()现在我们来使用scanf()。7.1 简单的例子 #include int main() { int x; printf ("Enter X: "); scanf ("%d", &x); printf ("You entered %d... ", x); return 0; };如今使用scanf()作为用户交互非常不明智,但是我们还是可以说明如何把指针传递给int变量。7.1.1 关于指针指针是计算机科学中最基础的概念之一。通常,大数组、结构或对象作为参数被传递给其它函数花费太大,而传递它们的地址要相对简单的多。此外:如果调用函数要修改作为参数传进来的数组或结构中的数据,并将其整体返回,那这种情况就太荒唐了。因此最简单的办法就是把数组或结构的地址传递给函数,让函数进行修改。在C/C++中指针就是某处内存的地址。在x86中,地址是以32位数表示的(占4字节);在x86-64中是64位数(占8字节)。顺便一说,这也是为什么有些人在改用x86-64时感到愤怒——x64架构中所有的指针需要的空间是原来的两倍。通过某种方法,只使用无类型指针也是可行的。例如标准C函数memcpy(),用于把一个区块复制到另外一个区块上,需要两个void*型指针作为输入,因为你无法预知,也无需知道要复制区块的类型,区块的大小才是重要的。当函数需要一个以上的返回值时也经常用到指针(等到第十章再讲)。scanf()就是这样,函数除了要显示成功读入的字符个数外,还要返回全部值。 72scanf()在C/C++中,指针类型只是用于在编译阶段进行类型检查。本质上,在已编译的代码中并不包含指针类型的信息。7.1.2 x86MSVCMVSC 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 ENDSX是局部变量。 73scanf()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作用是"取有效地址",通常用来生成一个地址(A.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()。7.1.3 MSVC+OllyDbg 74scanf()我们在OllyDbg中使用这个例子。首先载入程序,按F8直到进入我们的可执行文件而不是ntdll.dll。往下滚动屏幕找到main()。点击第一条指令(PUSH EBP),按F2设置断点,再按F9执行,触发main()开始处的断点。让我们来跟随到准备变量x的地址的位置。图7.1 OllyDbg:计算局部变量的地址可以右击寄存器窗口的EAX,再点击"堆栈窗口中跟随"。这个地址会在堆栈窗口中显示。观察,这是局部栈中的一个变量。我在图中用红色箭头标出。这里是一些无用数据(0x77D478)。PUSH指令将会把这个栈元素的地址压入栈中。然后按F8直到scanf()函数执行完。在scanf()执行时,我们要在命令行窗口中输入,例如输入123。图7.2 命令行输出scanf()在这里执行。图7.3:OllyDbg:scanf()执行 75scanf()scanf()在EAX中返回1,这意味着成功读入了一个值。现在我们关心的那个栈元素中的值是0x7B(123)。接下来,这个值从栈中复制到ECX寄存器中,然后传递给printf()。图7.4:OllyDbg:准备把值传递给printf()GCC让我们在Linux GCC 4.4.1下编译这段代码 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_20], offset aEnterX ; "Ente r X:" call _puts mov eax, offset aD ; "%d" lea edx, [esp+20h+var_4] mov [esp+20h+var_1C], edx mov [esp+20h+var_20], eax call ___isoc99_scanf mov edx, [esp+20h+var_4] mov eax, offset aYouEnteredD___ ; "You enter ed %d...\n" mov [esp+20h+var_1C], edx mov [esp+20h+var_20], eax call _printf mov eax, 0 leave retn main endp 76scanf()GCC把第一个调用的printf()替换成了puts(),原因在3.4.3节中讲过了。和在MSVC例子中一样,参数都是用MOV指令放入栈中。By the way顺带一说,这个简单的例子是编译器将C/C++表达式翻译成指令列表的真实演示。C/C++表达式间没任何联系。编译器并没有神奇之处,只不过把编程语言逐行翻译成对应的机器码代码而已。7.1.4 x64和原来一样,只是传递参数时不使用栈而使用寄存器。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 ENDSGCC 77scanf() .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 ret7.1.5 ARMkeil优化+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___ ; "Y ou entered %d... " .text:00000056 06 F0 CB F8 BL __2printf .text:0000005A 00 20 MOVS R0, #0 .text:0000005C 08 BD POP {R3,PC} 78scanf()必须把一个指向int变量的指针传递给scanf(),这样才能通过这个指针返回一个值。Int是一个32位的值,所以我们在内存中需要4字节存储,并且正好符合32位的寄存器。局部变量x的空间分配在栈中,IDA把他命名为var_8。然而并不需要分配空间,因为栈指针(SP)指向的空间可以被立即使用。所以栈指针的值被复制到R1寄存器中,然后和格式化字符串一起送入scanf()。然后LDR指令将这个值从栈中送入R1寄存器,用以送入printf()中。用ARM-mode和Xcode LLVM编译的代码区别不大,这里略去。ARM64 .LC0: .string "Enter X:" .LC1: .string "%d" .LC2: .string "You entered %d...\n" scanf_main: ; subtract 32 from SP, then save FP and LR in stack frame: stp x29, x30, [sp, -32]! ; set stack frame (FP=SP) add x29, sp, 0 ; load pointer to the "Enter X:" string adrp x0, .LC0 add x0, x0, :lo12:.LC0 ; X0=pointer to the "Enter X:" string ; print it: bl puts ; load pointer to the "\%d" string: adrp x0, .LC1 add x0, x0, :lo12:.LC1 ; find a space in stack frame for "x" variable (X1=FP+28): add x1, x29, 28 ; X1=address of "x" variable' ; pass the address to scanf() and call it: bl __isoc99_scanf ; load 32-bit value from the variable in stack frame: ldr w1, [x29,28] ; W1=x ; load pointer to the "You entered \%d...\n" string ; printf() will take text string from X0 and "x" variable from X 1 (or W1) adrp x0, .LC2 add x0, x0, :lo12:.LC2 bl printf ; _return 0 mov w0, 0 ; restore FP and LR, then add 32 to SP: ldp x29, x30, [sp], 32 ret 79scanf()在栈帧上申请了32字节空间,比它需要的要大,可能是因为内存地址对齐问题?最有趣的是寻找栈帧上x变量的空间(代码22行),为什么是加28?因为编译器是在栈帧的结束而不是开始的时间决定变量的空间。传递给scanf()的地址上储存这用户输入的值。32位值的类型是int,在代码27行中拿到然后传递给printf()。7.1.6 MIPS用$sp+24指向栈上申请的x变量的地址,然后将地址传给scanf(),用户输入的值使用LW(Load Word)指令传递给printf()。Listing 7.4: Optimizing GCC 4.4.5 (assembly output) $LC0: $LC1: $LC2: .ascii "Enter X:\000" .ascii "%d\000" .ascii "You entered %d...\012\000" main: ; function prologue: lui $28,%hi(__gnu_local_gp) addiu $sp,$sp,-40 addiu $28,$28,%lo(__gnu_local_gp) sw $31,36($sp) ; call puts(): lw $25,%call16(puts)($28) lui $4,%hi($LC0) jalr $25 addiu $4,$4,%lo($LC0) ; branch delay slot ; call scanf(): lw $28,16($sp) lui $4,%hi($LC1) lw $25,%call16(__isoc99_scanf)($28) ; set 2nd argument of scanf(), $a1=$sp+24: addiu $5,$sp,24 jalr $25 addiu $4,$4,%lo($LC1) ; branch delay slot ; call printf(): lw $28,16($sp) ; set 2nd argument of printf(), ; load word at address $sp+24:IDA 中显示的栈帧情况如下: .text:00000000 main: .text:00000000 .text:00000000 var_18 = -0x18 .text:00000000 var_10 = -0x10 .text:00000000 var_4 = -4 80scanf() .text:00000000 ; function prologue: .text:00000000 lui $gp, (__gnu_local_gp >> 1 6) .text:00000004 addiu $sp, -0x28 .text:00000008 la $gp, (__gnu_local_gp & 0x FFFF) .text:0000000C sw $ra, 0x28+var_4($sp) .text:00000010 sw $gp, 0x28+var_18($sp) ; call puts(): .text:00000014 lw $t9, (puts & 0xFFFF)($gp) .text:00000018 lui $a0, ($LC0 >> 16) # "Ent er X:" .text:0000001C jalr $t9 .text:00000020 la $a0, ($LC0 & 0xFFFF) # " Enter X:" ; branch delay slot ; call scanf(): .text:00000024 lw $gp, 0x28+var_18($sp) .text:00000028 lui $a0, ($LC1 >> 16) # "%d" .text:0000002C lw $t9, (__isoc99_scanf & 0x FFFF)($gp) ; set 2nd argument of scanf(), $a1=$sp+24: .text:00000030 addiu $a1, $sp, 0x28+var_10 .text:00000034 jalr $t9 ; branch delay slot .text:00000038 la $a0, ($LC1 & 0xFFFF) # " %d" ; call printf(): .text:0000003C lw $gp, 0x28+var_18($sp) ; set 2nd argument of printf(), ; load word at address $sp+24: .text:00000040 lw $a1, 0x28+var_10($sp) .text:00000044 lw $t9, (printf & 0xFFFF)($g p) .text:00000048 lui $a0, ($LC2 >> 16) # "You entered %d...\n" .text:0000004C jalr $t9 .text:00000050 la $a0, ($LC2 & 0xFFFF) # " You entered %d...\n" ; branch delay slot ; function epilogue: .text:00000054 lw $ra, 0x28+var_4($sp) ; set return value to 0: .text:00000058 move $v0, $zero ; _return: .text:0000005C jr $ra .text:00000060 addiu $sp, 0x28 ; branch delay slot7.2 全局变量 81scanf()如果之前的例子中的x变量不再是本地变量而是全局变量呢?那么就有可能从任何地方访问它,不仅仅是函数体,全局变量被认为anti-pattern(通常被认为是一个不好的习惯),但是为了试验,我们可以这样做。 #include int x; int main() { printf ("Enter X: "); scanf ("%d", &x); printf ("You entered %d... ", x); return 0; };7.2.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 82scanf()现在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+1 0 .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_Hea pAlloc+13 .data:0040FA90 ; __calloc_impl+72 .data:0040FA94 dword_40FA94 dd ? ; DATA XREF: ___sbh_ free_block+2FE被_x替换了?其它变量也并未要求初始化,这也就是说在载入exe至内存后,在这里有一块针对所有变量的空间,并且还有一些随机的垃圾数据。但在exe中这些没有初始化的变量并不影响什么,比如它适合大数组。 83scanf()7.2.2 MSVC: x86 + OllyDbg到这里事情就变得简单了表7.5 OllyDbg: scanf()执行后变量都在data部分里,在PUSH指令(压入x的地址)被执行后,地址将会在栈中显示,那么右击元组数据,点击"Fllow in dump",然后变量就会在左侧内存窗口显示.在命令行窗口中输入123后,这里就会显示0x7B但是为什么第一个字节是7B?合理的猜测,这里会有一组00 00 7B,被称为是字节顺序,然后在x86中使用的是小端,也就是说低位字节先写,高位字节后写。回到例子中,这里的32-bit值就会载入到EAX中,然后被传递给printf().X变量地址是0xDC3390。在OllyDbg中我们看进程内存映射(Alt-M),然后发现这个地址在PE文件.data结构处。 84scanf()表7.6: OllyDbg 进程内存映射7.2.3 GCC: x86这和linux中几乎是一样的,除了segment的名称和属性:未初始化变量被放置在_bss部分。在ELF文件格式中,这部分数据有这样的属性: ; Segment type: Uninitialized ; Segment permissions: Read/Write如果静态的分配一个值,比如10,它将会被放在_data部分,这部分有下面的属性: ; Segment type: Pure data ; Segment permissions: Read/Write7.2.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 85scanf()几乎和x86中的代码是一样的,注意x变量的地址传递给scanf()用的是LEA指令,尽管第二处传递给printf()变量时用的是MOV指令,"DWORD PTR"——是汇编语言中的一部分(和机器码没有联系)。这就表示变量数据类型是32-bit,于是MOV指令就被编码了。7.2.5 ARM:Optimizing Keil 6/2013 (Thumb mode) 86scanf() .text:00000000 ; Segment type: Pure code .text:00000000 AREA .text, CODE ... .text:00000000 main .text:00000000 PUSH {R4,LR} .text:00000002 ADR R0, aEnterX ; "E nter 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___ ; "Y ou entered %d... " .text:00000016 BL __2printf .text:0000001A MOVS R0, #0 .text:0000001C POP {R4,PC} ... .text:00000020 aEnterX DCB "Enter X:",0xA,0 ; DA TA XREF: main+2 .text:0000002A DCB 0 .text:0000002B DCB 0 .text:0000002C off_2C DCD x ; DA TA XREF: main+8 .text:0000002C ; main+10 .text:00000030 aD DCB "%d",0 ; DA TA 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 ; DA TA XREF: main+8 .data:00000048 ; ma in+10 .data:00000048 ; .d ata ends 87scanf()那么,现在x变量以某种方式变为全局的,现在被放置在另一个部分中。命名为data块(.data)。有人可能会问,为什么文本字符串被放在了代码块(.text),而且x可以被放在这?因为这是变量,而且根据它的定义,它可以变化,也有可能会频繁变化,不频繁变化的代码块可以被放置在ROM中,变化的变量在RAM中,当有ROM时在RAM中储存不变的变量是不利于节约资源的。此外,RAM中数据部分常量必须在之前初始化,因为在RAM使用后,很明显,将会包含杂乱的信息。继续向前,我们可以看到,在代码片段,有个指针指向X变量(0ff_2C)。然后所有关于变量的操作都是通过这个指针。这也是x变量可以被放在远离这里地方的原因。所以他的地址一定被存在离这很近的地方。LDR指令在thumb模式下只可访问指向地址在1020bytes内的数据。同样的指令在ARM模式下——范围就达到了-4095bytes~ +4095bytes,也就是x变量地址一定要在这附近的原因。因为没法保证链接时会把这个变量放在附近,它甚至可能在外部存储芯片里!另外,如果变量以const声明,Keil编译环境下则会将变量放在.constdata部分,大概从那以后,链接时就可以把这部分和代码块放在ROM里了。7.2.6 ARM64 88scanf() .comm x,4,4 .LC0: .string "Enter X:" .LC1: .string "%d" .LC2: .string "You entered %d...\n" f5: ; save FP and LR in stack frame: stp x29, x30, [sp, -16]! ; set stack frame (FP=SP) add x29, sp, 0 ; load pointer to the "Enter X:" string: adrp x0, .LC0 add x0, x0, :lo12:.LC0 bl puts ; load pointer to the "\%d" string: adrp x0, .LC1 add x0, x0, :lo12:.LC1 ; form address of x global variable: adrp x1, x add x1, x1, :lo12:x bl __isoc99_scanf ; form address of x global variable again: adrp x0, x add x0, x0, :lo12:x ; load value from memory at this address: ldr w1, [x0] ; load pointer to the "You entered \%d...\n" string: adrp x0, .LC2 add x0, x0, :lo12:.LC2 bl printf ; _return 0 mov w0, 0 ; restore FP and LR: ldp x29, x30, [sp], 16 ret本例中,x变量声明为了全局变量,它的地址通过 ADRP/ADD 指令对来计算。(代码 21 和 25 行).7.2.7 MIPS未初始化的全局变量已初始化的全局变量 89scanf()7.3 scanf()结果检查正如我之前所见的,现在使用scanf()有点过时了,但是如过我们不得不这样做时,我们需要检查scanf()执行完毕时是否发生了错误。 #include 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?7.3.1 MSVC: x86我们可以得到这样的汇编代码(msvc2010): 90scanf() 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 youentered? 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".7.3.2 MSVC: x86:IDA 91scanf()现在是时候打开IDA然后尝试做些什么了,顺便说一句。对于初学者来说使用在MSVC中使用/MD是个非常好的主意。这样所有独立的函数不会从可执行文件中link,而是从MSVCR*.dll。因此这样可以简单明了的发现函数在哪里被调用。当在IDA中分析代码时,建议一定要做笔记。比如在分析这个例子的时候,我们看到了JNZ将要被设置为error,所以点击标注,然后标注为"error"。另外一处标注在"exit": 92scanf() .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 enter ed %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隐藏: 93scanf()我隐藏了两部分然后分别给它们命名: .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):这个非常的有用。可以这么说,逆向工程师很重要的一点就是缩小他所有的信息。 94scanf()图7.7: IDA 图形模式图7.8: Graph mode in IDA with 3 nodes folded 95scanf()7.3.3 MSVC: x86 + OllyDbg让我们继续在OllyDbg中看这个范例程序,使它认为scanf()怎么运行都不会出错。当本地变量地址被传递给scanf()时,这个变量还有一些垃圾数据。这里是0x4CD478:图7.10 OllyDbg:传递变量地址给printf()当scanf()执行时,我在命令行窗口输入了一些不是数字的东西,像"asdasd".scanf()结束后eax变为了0.也就意味着有错误发生:图7.11 OllyDbg:scanf() 返回错误我们也可以发现栈中的本地变量并没有发生变化,scanf()会在那里写入什么呢?其实什么都没有,只是返回了0.现在让我们尝试修改这个程序,右击EAX,在选项中有个"set to 1",这正是我们所需要的。现在EAX是1了。那么接下来的检查就会按照我们的需求执行,然后printf()将会打印出栈上的变量。按下F9我们可以在窗口中看到: 96scanf()图7.9实际上,5035128是栈上一个数据(0x4CD478)的十进制表示!7.3.4 MSVC: x86 + Hlew这也是一个关于可执行文件patch的简单例子,我们之前尝试patch程序,所以程序总是打印数字,不管我们输入什么。假设编译时并没有使用/MD,我们可以在.text开始的地方找到main()函数,现在让我们在Hiew中打开执行文件。找到.text的开始处(enter,F8,F6,enter,enter)我们可以看到这个:图7.12:main()函数Hiew 找到 ASCIIZ 字符串并显示,引入的函数名字也同样显示。移动光标到地址 .00401027 (这里是 JNZ 指令, 我们需要绕过它), 按下 F3, 然后输入 “9090” (表示两个 NOP): 97scanf()图7.13:Hiew 用两个NOP替换JNZ然后按下F9(update),现在文件保存在了磁盘中,它将会按照我们希望的那样执行。两个NOP可能看起来并不是那么完美,另一个方法是把0写在第二处(jumpoffset),所以JNZ就可以总是跳到下一个指令了。另外我们也可以这样做:替换第一个字节为EB,这样就不修改第二处(jumpoffset),这样就会无条件跳转,不管我们输入什么,错误信息都可以打印出来了。7.3.5 MSVC: x64因为我们这里处理的是无整型变量。在x86-64中还是32bit,我们可以看出32bit的寄存器(前缀为E)在这种情况下是怎样使用的,然而64bit的寄存也有被使用(前缀R) 98scanf() _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? Hu h?’ call printf $LN1@main: ; return 0 xor eax, eax add rsp, 56 ret 0 main ENDP _TEXT ENDS END7.3.6 ARMARM:Optimizing Keil 6/2013 (Thumb mode) 99scanf() 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,或者Zflag是1。和x86中的JZ是一样的。其他的都很简单,执行流程分为两个方向,当R0被写入0后,两个方向则会合并,作为函数的返回值,然后函数结束。ARM64 100scanf() .LC0: .string "Enter X:" .LC1: .string "%d" .LC2: .string "You entered %d...\n" .LC3: .string "What you entered? Huh?" f6: ; save FP and LR in stack frame: stp x29, x30, [sp, -32]! ; set stack frame (FP=SP) add x29, sp, 0 ; load pointer to the "Enter X:" string: adrp x0, .LC0 add x0, x0, :lo12:.LC0 bl puts ; load pointer to the "\%d" string': adrp x0, .LC1 add x0, x0, :lo12:.LC1 ; calculate address of x variable in the local stack add x1, x29, 28 bl __isoc99_scanf ; scanf() returned result in W0. ; check it: cmp w0, 1 ; BNE _is Branch if Not Equal ; so if W00, jump to L2 will be occurred bne .L2 ; at this moment W0=1, meaning no error ; load x value from the local stack ldr w1, [x29,28] ; load pointer to the "You entered \%d...\n" string: adrp x0, .LC2 add x0, x0, :lo12:.LC2 bl printf ; skip the code, which print the "What you entered? Huh?" string : b .L3 .L2: ; load pointer to the "What you entered? Huh?" string: adrp x0, .LC3 add x0, x0, :lo12:.LC3 bl puts .L3: ; _return 0 mov w0, 0 ; restore FP and LR: ldp x29, x30, [sp], 32 ret 101scanf()7.3.7 MIPS .text:004006A0 main: .text:004006A0 .text:004006A0 var_18 = -0x18 .text:004006A0 var_10 = -0x10 .text:004006A0 var_4 = -4 .text:004006A0 .text:004006A0 lui $gp, 0x42 .text:004006A4 addiu $sp, -0x28 .text:004006A8 li $gp, 0x418960 .text:004006AC sw $ra, 0x28+var_4($sp) .text:004006B0 sw $gp, 0x28+var_18($sp) .text:004006B4 la $t9, puts .text:004006B8 lui $a0, 0x40 .text:004006BC jalr $t9 ; puts .text:004006C0 la $a0, aEnterX # "Enter X:" .text:004006C4 lw $gp, 0x28+var_18($sp) .text:004006C8 lui $a0, 0x40 .text:004006CC la $t9, __isoc99_scanf .text:004006D0 la $a0, aD # "%d" .text:004006D4 jalr $t9 ; __isoc99_scanf .text:004006D8 addiu $a1, $sp, 0x28+var_10 # branch delay slot .text:004006DC li $v1, 1 .text:004006E0 lw $gp, 0x28+var_18($sp) .text:004006E4 beq $v0, $v1, loc_40070C .text:004006E8 or $at, $zero # branch delay slot, NOP .text:004006EC la $t9, puts .text:004006F0 lui $a0, 0x40 .text:004006F4 jalr $t9 ; puts .text:004006F8 la $a0, aWhatYouEntered # " What you entered? Huh?" .text:004006FC lw $ra, 0x28+var_4($sp) .text:00400700 move $v0, $zero .text:00400704 jr $ra .text:00400708 addiu $sp, 0x28 .text:0040070C loc_40070C: .text:0040070C la $t9, printf .text:00400710 lw $a1, 0x28+var_10($sp) .text:00400714 lui $a0, 0x40 .text:00400718 jalr $t9 ; printf .text:0040071C la $a0, aYouEnteredD___ # " You entered %d...\n" .text:00400720 lw $ra, 0x28+var_4($sp) .text:00400724 move $v0, $zero .text:00400728 jr $ra .text:0040072C addiu $sp, 0x28 102scanf()scanf()在$V0寄存器中返回结果。通过对比$V0和$V1的值检查地址0x004006E4。BEQ 表示 “Branch Equal”。 如果值相等 (i.e., success), 程序执行将跳至0x0040070C。7.3.8 练习我们可以看见,JNE/JNZ 指令可以很容易的被 JE/JZ 指令替代,反之亦然。 但是之后基础区块也被交换了。尝试在练习中做做吧。7.4 练习 http://challenges.re/53 103访问实参第八章访问传递参数现在我们来看函数调用者通过栈把参数传递到被调用函数。被调用函数是如何访问这些参数呢? #include int f (int a, int b, int c) { return a*b+c; }; int main() { printf ("%d ", f(1, 2, 3)); return 0; };8.1 X868.1.1 MSVC如下为相应的反汇编代码(MSVC 2010 Express)Listing 8.2 MSVC 2010 Express 104访问实参 _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 的宏,同样,在函数内部访问局部变量也使用了类似的形式,不同的是访问参数时偏移值(为正值)。因此EBP寄存器的值加上宏_a$的值指向压栈参数。_a$[ebp]的值被存储在寄存器eax中,IMUL指令执行后,eax的值为eax与_b$[ebp]的乘积,然后eax与_c$[ebp]的值相加并将和放入eax寄存器中,之后返回eax的值。返回值作为printf()的参数。8.1.2 MSVC+OllyDbg我们在OllyDbg中观察,跟踪到函数f()使用第一个参数的位置,可以看到寄存器EBP指向栈底,图中使用红色箭头标识。栈帧中第一个被保存的是EBP的值,第二个是返回地址(RA),第三个是参数1,接下来是参数2,以此类推。因此,当我们访问第一个参数时EBP应该加8(2个32-bit字节宽度)。 105访问实参Figure 8.1: OllyDbg: 函数f()内部8.1.3 GCC使用GCC4.4.1编译后在IDA中查看Listing 8.3: GCC 4.4.1 106访问实参 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)会自动恢复栈指针。8.2 X64 107访问实参x86-64架构下有点不同,函数参数(4或6)使用寄存器传递,被调用函数通过访问寄存器来访问传递进来的参数。8.2.1 MSVCMSVC优化后:Listing 8.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 8.5: MSVC 2012 x64 108访问实参 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”。8.2.2 GCCGCC优化后的代码:Listing 8.6: GCC 4.4.6 -O3 x64 109访问实参 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 retGCC无优化代码:Listing 8.7: GCC 4.4.6 x64 110访问实参 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 retSystem V *NIX [21]没有“shadow space”,但被调用者可能会保存参数,这也是造成寄存器短缺的原因。8.2.3 GCC: uint64_t instead int我们例子使用的是32位int,寄存器也为32位寄存器(前缀为E-)。为处理64位数值内部会自动调整为64位寄存器: 111访问实参 #include #include 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 8.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)。8.3 ARM8.3.1 未优化的Keil + ARM mode 112访问实参 .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模式,或者反之。8.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寄存器。8.3.3 Optimizing Keil + thumb mode 113访问实参 .text:0000005E 48 43 MULS R0, R1 .text:00000060 80 18 ADDS R0, R0, R2 .text:00000062 70 47 BX LRThumb模式下没有MLA指令,编译器做了两次间接处理,MULS指令使R0寄存器的值与R1寄存器的值相乘并将结果存入R0。ADDS指令将R0与R2的值相加并将结果存入R0。8.3.4 ARM64Optimizing GCC (Linaro) 4.9Non-optimizing GCC (Linaro) 4.98.4 MIPS 114一个或者多个字的返回值第九章一个或者多个字的返回值X86架构下通常返回EAX寄存器的值,如果是单字节char,则只使用EAX的低8位AL。如果返回float类型则使用FPU寄存器ST(0)。ARM架构下通常返回寄存器R0。9.1 尝试用函数的返回值返回void假如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 void main() { printf ("Hello, world!"); };我们在linux下编译。GCC 4.8.1会使用puts()替代printf()(看前面章节2.3.3),没有关系,因为puts()会返回打印的字符数,就行printf()一样。请注意,main()结束时EAX寄存器的值是非0的,这意味着main()结束时保留puts()返回时EAX的值。Listing 9.1: GCC 4.8.1 115一个或者多个字的返回值 .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 9.2: tst.sh #!/bin/sh ./hello_world echo $?运行: $ tst.sh Hello, world! 1414为打印的字符数。9.2 如果我们不使用返回值会发生什么? 9.3 返回一个结构体回到返回值是EAX寄存器值的事实,这也就是为什么老的C编译器不能够创建返回信息无法拟合到一个寄存器(通常是int型)的函数。如果必须这样,应该通过指针来传递。现在可以这样,比如返回整个结构体,这种情况应该避免。如果必须要返回大的结构体,调用者必须开辟存储空间,并通过第一个参数传递指针,整个过程对程序是透明的。像手动通过第一个参数传递指针一样,只是编译器隐藏了这个过程。小例子: 116一个或者多个字的返回值 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}; }; 117一个或者多个字的返回值Listing 9.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我们可以看到,函数仅仅填充调用者申请的结构体空间的相应字段。因此没有性能缺陷。 118指针第十章指针指针通常被用作函数返回值(recall scanf() case (6)).例如,当函数返回两个值时。10.1 Global variables example #include 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 10.1: Optimizing MSVC 2010 (/Ox /Ob0) 119指针 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. 10.4. 120指针Figure 10.1: OllyDbg: 全局变量地址被传递进f1()Figure 10.2: OllyDbg: f1()开始Figure 10.3: OllyDbg: f1()完成 121指针Figure 10.4: OllyDbg: 全局变量被传递进printf()Figure 10.5: OllyDbg: memory map10.2 Local variables example让我们修改一下例子:Listing 10.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 10.3: Optimizing MSVC 2010 (/Ox /Ob0) 122指针 _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. 10.6.f1()开始的时候,随机栈地址为0x35FCF4和0x35FCF8 fig. 10.7.f1()完成时结果0xDB18和0x243存放在地址0x35FCF4和0x35FCF8。Figure 10.6: OllyDbg: 局部变量地址被圧栈 123指针Figure 10.7: OllyDbg: f1()startingFigure 10.8: OllyDbg: f1()finished10.3 小结f1()可以返回结果到内存的任何地方,这是指针的本质和特性。顺便提一下,C++引用的工作方式和这个类似。详情阅读相关内容(33)。 124GOTO操作符第十一章GOTO操作符 125条件转跳第十二章条件跳转12.1 简单的例子现在我们来了解条件跳转。 #include void f_signed (int a, int b) { if (a>b) printf ("a>b"); if (a==b) printf ("a==b"); if (ab"); if (a==b) printf ("a==b"); if (ab " .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 ; "ab ”的地址写入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时才被触发,这样之后函数才执行完成。但是如果ab " .text:0000007E 06 F0 B7 F8 BL __2printf .text:00000082 .text:00000082 loc_82 ; CODE XREF: f_s igned+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_s igned+12 .text:0000008C A5 42 CMP R5, R4 .text:0000008E 02 DA BGE locret_96 .text:00000090 A3 A0 ADR R0, aAB_1 ; "ab) return a; return b; };17.7.1 x86无优化的MSVC 188浮点数单元尽管这个函数很简单,但是理解它的工作原理并不容易。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。 189浮点数单元 如果 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变量的值会被加载 但是还没有结束。关于检查C2-FlagFirst OllyDbg example: a=1.2 and b=3.4Second OllyDbg example: a=5.6 and b=-4msvc2010优化模式 190浮点数单元 _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 ENDPFCOM区别于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没有从栈中抛出值的指令并且没有地方存储。 但是,还没有结束。First OllyDbg example: a=1.2 and b=3.4Second OllyDbg example: a=5.6 and b=-4GCC 4.4.1 191浮点数单元d_max proc nearb =qword ptr -10ha =qword ptr -8a_first_half = dword ptr 8a_second_half = dword ptr 0Chb_first_half = dword ptr 10hb_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_8048456loc_8048453: fld [ebp+b]locret_8048456: leave retnd_max endp 192浮点数单元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:-:CFb,CPU的标志位会被设为:ZF=0,PF=0,CF=0 如果ab然后one将会被存入AL中,接下来JZ不会被触发,函数将返回_a。在其他的情况下,返回的是_b。带优化的 GCC 4.4.1GCC 4.4.1-03优化选项turned开关 193浮点数单元 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 sta ck, 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而不需要额外的交换。ARMARM+优化Xcode(LLVM)+ARM模式 194浮点数单元 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寄存去中的值来返回。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”指令。 195浮点数单元IT指令定义所谓的if-then块。指令后面最多放置四条指令是可能的,判断后缀会被加上。在我们的例子中,“IT GT”意味着下一条指令会被执行,如果GT(GreaterThan)条件为真。下面是一段更加复杂的代码,来源于"愤怒的小鸟"(ios版) ITE NE VMOVNE R2, R3, D16 VMOVEQ R2, R3, D17ITE意味着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, R0ITTE(if-then-then-else)意味着第一条第二条指令将会被执行,如果LE(Less orEqual)条件为真,反之第三条指令将会执行。 196浮点数单元编译器通常不生成所有的组合。举个例子,在“愤怒的小鸟”中提到的(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指令和相应的标志。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的变量存储在本地栈中,还有返回值ARM+优化模式keil+thumb模式 197浮点数单元 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(标志位设置 - 大于或等于)指令可能有效并且无需额外的代码。17.7.3 ARM64Optimizing GCC (Linaro) 4.9Non-optimizing GCC (Linaro) 4.9ExerciseOptimizing GCC (Linaro) 4.9—float17.7.4 MIPS17.8 栈,计算器和逆波兰表示法17.9 x6417.10 练习 198浮点数单元 199数组第十八章数组数组是在内存中连续排列的一组变量,这些变量具有相同类型1。18.1 小例子 #include 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; };18.1.1 x86MSVC编译后:Listing 18.1: MSVC 200数组 _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这段代码主要有两个循环:第一个循环填充数组,第二个循环打印数组元素。shlecx,1指令使ecx的值乘以2,更多关于左移请参考17.3.1。 在堆栈上为数组分配了80个字节的空间,包含20个元素,每个元素4字节大小。 201数组Let’s try this example in OllyDbg 缺漏 =GCCGCC 4.4.1编译后为:Listing 18.2: GCC 4.4.1 202数组 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++表达式。 203数组18.1.2 ARMARM + 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 aga in ; second loop MOV R4, #0 ; i B loc_4C4 loc_4B0 LDR R2, [SP,R4,LSL#2] ; (second printf argumen t) 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 aga in MOV R0, #0 ; value to return ADD SP, SP, #0x50 ; deallocate place, allo cated for 20 int variables LDMFD SP!, {R4,PC}int类型长度为32bits即4字节,20个int变量需要80(0x50)字节,因此“subsp,sp,#0x50”指令为在栈上分配存储空间。 两个循环迭代器i被存储在R4寄存器中。 值i2被写入数组,通过将i值左移1位实现乘以2的效果,整个过程通过”MOVR0,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)。 204数组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 bo dy 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) (sa me 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 bo dy again MOVS R0, #0 ; value to return ; deallocate place, allocated for 20 int variables + one more va riable ADD SP, SP, #0x54 POP {R4,R5,PC}Thumb代码也是非常类似的。Thumb模式计算数组偏移的移位操作使用特定的指令LSLS。 编译器在堆栈中申请的数组空间更大,但是最后4个字节的空间未使用。Non-optimizing GCC 4.9.1 (ARM64)18.1.3 MIPS18.2 缓冲区溢出 205数组18.2.1 读取外部数组的边界Array[index]中index指代数组索引,仔细观察下面的代码,你可能注意到代码没有index是否小于20。如果index大于20?这是C/C++经常被批评的特征。 以下代码可以成功编译可以工作: #include 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): 206数组 _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),但是这可能影响运行速度。 我们可以从栈上非法读取数值,是否可以写入数值呢? 下面我们将写入数值: 207数组 #include 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 obviou sly 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足以完成任务。 我们用它看被调试进程崩溃的地方: 208数组 generic tracer 0.4 (WIN32), http://conus.info/gt New process: C:PRJ...1.exe, PID=7988 EXCEPTION_ACCESS_VIOLATION: 0x15 (), 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编译后的同类代码: 209数组 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) 210数组寄存器的值与win32例子略微不同,因为堆栈布局也不太一样。18.2.2 Writing beyond array boundsMSVCGCC18.3 防止缓冲区溢出的方法下面一些方法防止缓冲区溢出。MSVC使用以下编译选项: /RTCs Stack Frame runtime checking /GZ Enable stack checks (/RTCs)一种方法是在函数局部变量和序言之间写入随机值。在函数退出之前检查该值。如果该值不一致则挂起而不执行RET。进程将被挂起。 该随机值有时被称为“探测值”。 如果使用MSVC编译简单的例子(18.1),使用RTC1和RTCs选项,将能看到函数调用@_RTC_CheckStackVars@8函数来检测“探测值“。我们来看GCC如何处理这些。我们使用alloca()(4.2.4)例子: #include #include void f() { char *buf=(char*)alloca (600); _snprintf (buf, 600, "hi! %d, %d, %d", 1, 2, 3); puts (buf); };我们不使用任何附加编译选项,只使用默认选项,GCC 4.7.3将插入“探测“检测代码:Listing 18.3: GCC 4.7.3 211数组 .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)将输出以下信息: 212数组 *** 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)[0xb7606e5 c] /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-gn u/libgcc_s.so.1 b757b000-b757c000 r--p 0001a000 08:01 1048602 /lib/i386-linux-gn u/libgcc_s.so.1 b757c000-b757d000 rw-p 0001b000 08:01 1048602 /lib/i386-linux-gn u/libgcc_s.so.1 b7592000-b7593000 rw-p 00000000 00:00 0 b7593000-b7740000 r-xp 00000000 08:01 1050781 /lib/i386-linux-gn u/libc-2.17.so b7740000-b7742000 r--p 001ad000 08:01 1050781 /lib/i386-linux-gn u/libc-2.17.so b7742000-b7743000 rw-p 001af000 08:01 1050781 /lib/i386-linux-gn u/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-gn u/ld-2.17.so b777e000-b777f000 r--p 0001f000 08:01 1050794 /lib/i386-linux-gn u/ld-2.17.so b777f000-b7780000 rw-p 00020000 08:01 1050794 /lib/i386-linux-gn u/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版本)。18.3.1 Optimizing Xcode (LLVM) + thumb-2 mode 213数组我们回头看简单的数组例子(18.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 214数组 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 beginloc_2F14 ADDS R0, R5, #1 LDR.W R2, [R6,R5,LSL#2] MOV R5, R0loc_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 215数组首先可以看到,LLVM循环展开写入数组,LLVM认为先计算出数组元素的值速度更快。 在函数的结尾我们能看到“探测值“的检测—局部存储的值与R8指向的标准值对比。如果相等4指令块将通过”ITTTT EQ“触发,R0写入0,函数退出。如果不相等,指令块将不会被触发,跳向___stack_chk_fail函数,结束进程。18.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)。18.5 指向字符串的数组18.5.1 x6432-bit MSVC18.5.2 32-bit ARMARM in ARM modeARM in Thumb mode18.5.3 ARM6418.5.4 MIPS18.5.5 数组溢出数组溢出保护 216数组18.6 多维数组多维数组和线性数组在本质上是一样的。 因为计算机内存是线性的,它是一维数组。但是一维数组可以很容易用来表现多维的。 比如数组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等。列优先的意思是:先写入第一列,然后是第二列,…,最后是最后一列。18.6.1 二维数组的例子行填充的例子列填充的例子18.6.2 像一位数组那样访问二维数组18.6.3 多维数组多维数组与此类似。 我们看个例子:Listing 18.4: simple example 217数组 #include int a[10][20][30]; void insert(int x, int y, int z, int value) { a[x][y][z]=value; };x86MSVC 2010:Listing 18.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 18.6: GCC 4.4.1 218数组 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 endpGCC使用的不同的计算方法。为计算第一个操作值30y,GCC没有使用乘法指令。GCC的做法是:(???? + ????) ≪ 4 − (???? + ????) = (2????) ≪ 4 − 2???? = 2・ 16 ・ ???? − 2???? = 32???? − 2???? = 30????。因此30y的计算仅使用加法和移位操作,这样速度更快。ARM + Non-optimizing Xcode (LLVM) + thumb modeListing 18.7: Non-optimizing Xcode (LLVM) + thumb mode 219数组 _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代码在栈中保存了所有变量,这是冗余的。元素地址的计算我们通过公式已经找到了。ARM + Optimizing Xcode (LLVM) + thumb modeListing 18.8: Optimizing Xcode (LLVM) + thumb mode 220数组 _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 - point er to a. R0=x*2400 + ptr to a ADD.W R0, R0, R1,LSL#3 ; R0 = R0+R1<<3 = R0+R1*8 = x*24 00 + 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),这里什么都没有做,是冗余的。显然,编译器没有优化它。MIPS18.6.4 更多的例子18.7 讲打包的字符串作为数组18.7.1 32-bit ARM18.7.2 ARM6418.7.3 MIPS18.7.4 Conclusion18.8 结论18.9 练习 221数组 222操纵特定的bit第十九章操纵特殊的bit很多函数参数的输入标志使用了位域。当然,可以使用bool类型来替代,只是有点浪费。19.1 Specific bit checkingx86Win32 API 例子: HANDLE fh; fh=CreateFile("file", GENERIC_WRITE | GENERIC_READ, FILE_SHA RE_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) 223操纵特定的bit容易看出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 #include void main() { int handle; handle=open ("file", O_RDWR | O_CREAT); };我们得到: Listing 17.4: GCC 4.4.1 224操纵特定的bit 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中编译:makevmlinux,在IDA中打开,找到do_filp_open()函数。在开始部分我们可以看到(注释个人添加): Listing 17.6:do_filp_open() (linux kernel 2.6.31) 225操纵特定的bit 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_flagGCC保存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, 2O_CREAT宏等于0x40,如果open_flag为0x40,标志位被置1,接下来的JNZ指令将被触发。ARMLinux kernel3.8.0检测O_CREAT过程有点不同。 Listing 17.8: linux kernel 3.8.0 226操纵特定的bit struct file *do_filp_open(int dfd, struct filename *pathname, co nst struct open_flags *op) { ... filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RC U); ... } static struct file *path_openat(int dfd, struct filename *pathna me, 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, stru ct 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) 227操纵特定的bit ... .text:C0169EA8 MOV R9, R3 ; R3 - (4th argument) open_f lag ... .text:C0169ED4 LDR R6, [R9] ; R6 - open_flag ... .text:C0169F68 TST R6, #0x40 ; jumptable C0169F00 defa ult 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.1 4+DC .text:C016A128 MOV R0, R4 .text:C016A12C BL complete_walk ...TST指令类似于x86下的TEST指令。 这段代码来自do_last()函数源码,有两个分支lookup_fast()和complete_walk()。这里O_CREAT宏也等于0x40。19.2 Specific bit setting/clearing例如: 228操纵特定的bit #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; };19.2.1 x86Non-optimizing MSVCMSVC 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 ENDPOR指令添加一个或多个bit位而忽略了其余位。 AND用来重置一个bit位。OllyDbgOptimizing MSVC如果我们使用msvc编译,并且打开优化选项(/Ox),代码将会更短: Listing 17.11:Optimizing MSVC 229操纵特定的bit _a$ = 8 ; size = 4 _f PROC mov eax, DWORD PTR _a$[esp-4] and eax, -513 ; fffffdffH or eax, 16384 ; 00004000H ret 0 _f ENDPNon-optimizing GCC我们来看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 endpOptimizing GCCMSVC未优化的代码有些冗余。 现在我们来看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部分。 230操纵特定的bit8086 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)更合理。Optimizing GCC and regparm编译时候开启-O3并且设置regram=3生成的代码会更短。Listing 19.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)。19.2.2 ARM + Optimizing Keil + ARM modeListing 19.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 LRBIC是“逻辑and“类似于x86下的AND。ORR是”逻辑or“类似于x86下的OR。19.2.3 ARM + Optimizing Keil + thumb modeListing 19.16: Optimizing Keil + thumb mode 231操纵特定的bit 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,采用移位使代码更简洁。19.2.4 ARM + Optimizing Xcode (LLVM) + ARM modeListing 19.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)生成的代码相似。19.2.5 ARM: more about the BIC instruction19.2.6 ARM64: Optimizing GCC (Linaro) 4.919.2.7 ARM64: Non-optimizing GCC (Linaro) 4.919.2.8 MIPS19.3 ShiftsC/C++的移位操作通过<>实现。19.4 设定并请除特定的bit19.4.1 关于异或的一点 232操纵特定的bit19.4.2 x8619.4.3 MIPS19.4.4 ARMOptimizing Keil 6/2013 (ARM mode)Optimizing Keil 6/2013 (Thumb mode)Optimizing GCC 4.6.3 (Raspberry Pi, ARM mode)19.5 计数bit 来置1这里有一个例子函数,计算输入变量有多少个位被置为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<wYear, t->wMonth, t->wDay, t->wHour, t->wMinute, t->wSecond); free (t); return; };让我们用优化/Ox编译一下它,看看我们得到什么东西清单21.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 244结构体所以,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 #include 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; };我们得到: 245结构体 $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同样,我们可以看到编译结果和之前一样。个人重申一次,你不应该在写代码的时候用这么晦涩的方法来表达它。21.3 Unix:结构体tm21.3.1 linux在Linux下,我们看看time.h中的tm结构体是什么样子的: 246结构体 #include #include 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下编译得到:清单21.6:GCC 4.4.1 247结构体 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并没有为本地栈上变量建立本地变量名。但是因为我们已经学了汇编了,我们也不需要在这么简单的例子里面如此依赖它。 248结构体请也注意一下lea edx, [eax+76ch],这个指令把eax的值加上0x76c,但是并不修改任何标记位。请也参考LEA的相关章节(B.6.2节)GDB为了表现出结构体只是一个个的变量连续排列的东西,让我们重新测试一下这个例子,我们看看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 #include void main() { int tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, tm_wd ay, 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’ fro m 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 argu ment is of type ’int *’ 249结构体但是至少,它会生成这段代码:清单18.9 GCC 4.7.3 250结构体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 retnmain endp 251结构体这个代码和我们之前看到的一样,依然无法分辨出源代码是用了结构体还是只是数组而已。当然这样也是可以运行的,但是实际操作中还是不建议用这种晦涩的方法。因为通常,编译器会在栈上按照声明顺序分配变量空间,但是并不能保证每次都是这样。还有,其他编译器可能会警告tm_year,tm_mon, tm_mday, tm_hour, tm_min变量而不是tm_sec使用时未初始化。事实上,计算机并不知道调用localtime_r()的时候他们会被自动填充上。我选择了这个例子来解释是因为他们都是int类型的,而SYSTEMTIME的所有成员是16位的WORD,如果把它们作为本地变量来声明的话,他们会按照32位的边界值来对齐,因此什么都用不了了(因为由于数据对齐,此时GetSystemTime()会把它们错误的填充起来)。请继续读下一节的内容:“结构体的成员封装”。所以,结构体只是把一组变量封装到一个位置上,数据是一个接一个的。我可以说结构体是一个语法糖,因为它只是用来让编译器把一组变量保存在一个地方。但是,我不是编程方面的专家,所以更有可能的是,我可能会误读这个术语。还有,在早期(1972年以前)的时候,C是不支持结构体的。21.3.2 ARMARM+优化Keil+thumb模式同样的例子: 清单21.10: 优化Keil+thumb模式 252结构体 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}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 253结构体 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清单21.11: ARM+优化Xcode(LLVM)+thumb-2模式21.3.3 MIPS 254结构体21.3.4 将结构体作为一组值21.3.5 讲结构体作为一个32位的数组21.3.6 讲结构体作为bit的数组21.4 结构体的成员封装结构体做的一个重要的事情就是封装了成员,让我们看看简单的例子: #include 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字节)。x86编译后得到: 255结构体 _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 256结构体现在,结构体只用了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_S UPPORTED) #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处理结构体大小的方法。OllyDbg + fields are packed by defaultOllyDbg + fields aligning on 1 byte boundary21.4.2 ARMARM+优化Keil+thumb模式清单18.16 257结构体 .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个字节。ARM+优化XCode(LLVM)+thumb-2模式清单18.17: 优化的Xcode (LLVM)+thumb-2模式 258结构体 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节)差不多,但是它不是对内存操作的,而是对一个寄存器操作的,至于剩余的——都一样。21.4.3 MIPS21.5 嵌套结构如果一个结构体里定义了另一个结构体会怎么样? #include 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); }; 259结构体在这个例子里,我们把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(因此这里其实是定义个了一个指针),这个情况下状况则会大为不同。21.5.1 OllyDbg21.6 结构体中的位21.6.1 CPUID 的例子C/C++中允许给结构体的每一个成员都定义一个准确的位域。如果我们想要节省空间的话,这个对我们来说将是非常有用的。比如,对BOOL来说,1位就足矣了。但是当然,如果我们想要速度的话,必然会浪费点空间。 让我们以CPUID指令为例,这个指令返回当前CPU的信息和特性。 如果EAX在指令执行之前就设置为了1,CPUID将会返回这些内容到EAX中。 260结构体MSVC 2010有CPUID的宏,但是GCC 4.4.1没有,所以,我们就手动的利用它的内联汇编器为GCC写一个吧。 261结构体 #include #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 #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让我们在MSVC 2008用/Ox编译一下: 262结构体清单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 263结构体 xor eax, eax pop ebx add esp, 16 ; 00000010H ret 0 _main ENDPSHR指令将EAX寄存器的值右移位,移出去的值必须被忽略,例如我们会忽略右边的位。 AND指令将清除左边不需要的位,换句话说,它处理过后EAX将只留下我们需要的值。MSVC + OllyDbgGCC让我们在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 264结构体 shr eax, 0Ch and eax, 3 mov [esp+8], eax mov dword ptr [esp+4], offset aProcessor_type ; "processor_t ype=%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_mo del_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的计算联合到一块去,而不是把它们分开计算。21.6.2 将浮点数当作结构体看待我们已经在FPU(15章)中注意到了float和double两个类型都是有符号的,他们分为符号、有效数字和指数部分。但是我们能直接用上这些位嘛?让我们试一试float。 265结构体 #include #include #include #include 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 266结构体 _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:2 3 mov ecx, DWORD PTR _t$[ebp] and ecx, -2139095041 ; 807fffffH - drop exponent or ecx, eax ; add original value without exponent with new c alculated 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 267结构体 ; 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 ex ponent 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 endpf()函数基本可以理解,但是有趣的是,GCC可以在编译阶段就通过我们这堆大杂烩一样的代码计算出f(1.234)的值,从而会把他当作参数直接给printf()。21.7 练习 268结构体 269联合体第二十二章联合体22.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来访问它。因此,这只是一个小技巧,而且是很脏的技巧。 270联合体 #include #include #include 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; };22.1.1 x86清单19.1: MSVC 2010 (/Ox) 271联合体 $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 ENDGCC也生成了非常相似的代码。 272联合体22.1.2 MIPS22.1.3 ARM (ARM mode)22.2计算器的精度22.2.1 x8622.2.2 ARM6422.2.3 MIPS22.2.4 Conclusion22.3 快速开方计算 273指向函数的指针第二十三章指向函数的指针函数指针是指向函数的指针,和其他指针一样,只是该指针指向函数代码段的开始地址。函数指针经常用作回调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 *)我们稍作修改: 274指向函数的指针 /* ex3 Sorting ints with qsort */ #include #include 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,-1000 00}; int i; /* Sort the array */ qsort(numbers,10,sizeof(int),comp) ; for (i=0;i<9;i++) printf("Number = %d",numbers[ i ]) ; return 0; }23.1 MSVCMSVC2010 /Ox选项编译:Listing 20.1: Optimizing MSVC 2010: /Ox /GS- /MD 275指向函数的指针 __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 276指向函数的指针 .text:7816CBF0 ; void __cdecl qsort(void *, unsigned int, unsign ed 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()调用了错误的函数指针,可能造成程序崩溃,并且这个错误很难被发现。第二个原因是即使回调函数类型完全正确,使用错误的参数调用函数可能会导致更严重的问题。进程崩溃不是最大的问题,最大的问题是崩溃的原因—编译器很难发现这种潜在的问题。23.1.1 MSVC + OllyDbg我们在OD中加载我们的例子,并在comp()函数下断点。我们可以看到第一次comp()调用时是如何比较的:fig.20.1.OD代码窗口显示了比较的值。我们还可以看到SP指向的RA地址在qsort()函数空间里(实际上位于MSVCR100.DLL)。按F8直到函数返回到qsort()函数:fig20.2.这里比较函数被调用。 277指向函数的指针第二次调用comp()—当前比较的值不相同:fig203。Figure 20.1: OllyDbg: first call of comp()Figure 20.2: OllyDbg: the code in qsort() right a_er comp() callFigure 20.3: OllyDbg: second call of comp()23.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断点中断是的寄存器地址: 278指向函数的指针 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得到: 279指向函数的指针 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:ccIDA加载.idc脚本:fig20.4。IDA给出了函数名字(PtFuncCompare)—IDA认为该函数指针被传递给qsort()。可以看到a和b指向数组不同的位置,并且相差4-32bit的字节数。 280指向函数的指针0x401010 和 0x401012之间的指令从没有被执行:事实上comp()从来不返回0,因为没有相等的元素。Figure 20.4: tracer and IDA. N.B.: some values are cutted at right23.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 _qsortcomp() 函数: 281指向函数的指针 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 endpqsort()的实现在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] ...23.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. 282指向函数的指针License GPLv3+: GNU GPL version 3 or later 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:...Reading symbols from /home/dennis/polygon/a.out...done.(gdb) b 17_1.c:11Breakpoint 1 at 0x804845f: file 17_1.c, line 11.(gdb) runStarting program: /home/dennis/polygon/./a.outBreakpoint 1, comp (_a=0xbffff0f8, _b=_b@entry=0xbffff0fc) at 17_1.c:1111 if (*a==*b)(gdb) p *a$1 = 1892(gdb) p *b$2 = 45(gdb) cContinuing.Breakpoint 1, comp (_a=0xbffff104, _b=_b@entry=0xbffff108) at 17_1.c:1111 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 ,arg=arg@entry=0x0) at msort.c:297#8 0xb7e42dcf in __GI_qsort (b=0xbffff0f8, n=10, s=4, cmp=0x804844d ) at msort.c:307#9 0x0804850d in main (argc=1, argv=0xbffff1c4) at 17_1.c:26 283指向函数的指针 (gdb)23.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 This is free software: you are free to change and redistribute i t. 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: ... 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 : push ebp 0x0804844e : mov ebp,esp 0x08048450 : sub esp,0x10 0x08048453 : mov eax,DWORD PTR [ebp+0x8] 0x08048456 : mov DWORD PTR [ebp-0x8],eax 0x08048459 : mov eax,DWORD PTR [ebp+0xc] 0x0804845c : mov DWORD PTR [ebp-0x4],eax 0x0804845f : mov eax,DWORD PTR [ebp-0x8] 0x08048462 : mov edx,DWORD PTR [eax] 0x08048464 : mov eax,DWORD PTR [ebp-0x4] 0x08048467 : mov eax,DWORD PTR [eax] 0x08048469 : cmp edx,eax 0x0804846b : jne 0x8048474 0x0804846d : mov eax,0x0 0x08048472 : jmp 0x804848e 0x08048474 : mov eax,DWORD PTR [ebp-0x8] 0x08048477 : mov edx,DWORD PTR [eax] 0x08048479 : mov eax,DWORD PTR [ebp-0x4] 0x0804847c : mov eax,DWORD PTR [eax] 0x0804847e : cmp edx,eax 0x08048480 : jge 0x8048489 0x08048482 : mov eax,0xffffffff 284指向函数的指针0x08048487 : jmp 0x804848e 0x08048489 : mov eax,0x10x0804848e : leave0x0804848f : retEnd of assembler dump.(gdb) b *0x08048469Breakpoint 1 at 0x8048469(gdb) runStarting program: /home/dennis/polygon/./a.outBreakpoint 1, 0x08048469 in comp ()(gdb) info registerseax 0x2d 45ecx 0xbffff0f8 -1073745672edx 0x764 1892ebx 0xb7fc0000 -1208221696esp 0xbfffeeb8 0xbfffeeb8ebp 0xbfffeec8 0xbfffeec8esi 0xbffff0fc -1073745668edi 0xbffff010 -1073745904eip 0x8048469 0x8048469 eflags 0x286 [ PF SF IF ]cs 0x73 115ss 0x7b 123ds 0x7b 123es 0x7b 123fs 0x0 0gs 0x33 51(gdb) cContinuing.Breakpoint 1, 0x08048469 in comp ()(gdb) info registerseax 0xff7 4087ecx 0xbffff104 -1073745660edx 0xffffff9e -98ebx 0xb7fc0000 -1208221696esp 0xbfffee58 0xbfffee58ebp 0xbfffee68 0xbfffee68esi 0xbffff108 -1073745656edi 0xbffff010 -1073745904eip 0x8048469 0x8048469 eflags 0x282 [ SF IF ]cs 0x73 115ss 0x7b 123ds 0x7b 123es 0x7b 123fs 0x0 0gs 0x33 51(gdb) cContinuing.Breakpoint 1, 0x08048469 in comp ()(gdb) info registers 285指向函数的指针eax 0xffffff9e -98ecx 0xbffff100 -1073745664edx 0xc8 200ebx 0xb7fc0000 -1208221696esp 0xbfffeeb8 0xbfffeeb8ebp 0xbfffeec8 0xbfffeec8esi 0xbffff104 -1073745660edi 0xbffff010 -1073745904eip 0x8048469 0x8048469 eflags 0x286 [ PF SF IF ]cs 0x73 115ss 0x7b 123ds 0x7b 123es 0x7b 123fs 0x0 0gs 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 ,arg=arg@entry=0x0) at msort.c:297#8 0xb7e42dcf in __GI_qsort (b=0xbffff0f8, n=10, s=4, cmp=0x804844d ) at msort.c:307#9 0x0804850d in main () 286在32位环境中的64位值第二十四章在32位环境中的64位值在32位环境中的通用寄存器是32位的,所以64位值转化为一对32位值。24.1 返回64位的值24.1.1 x8624.1.2 ARM24.1.3 MIPS24.2参数的传递,加法,减法 #include 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; };24.2.1 x86代码 21.1: MSVC 2012 /Ox /Ob1 287在32位环境中的64位值 _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 288在32位环境中的64位值 _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 ; 75939f 79H mov DWORD PTR [esp+12], 5461 ; 000015 55H mov DWORD PTR [esp], 1942892530 ; 73ce2f f2H mov DWORD PTR [esp+4], 2874 ; 00000b 3aH call _f1 mov DWORD PTR [esp+4], eax mov DWORD PTR [esp+8], edx mov DWORD PTR [esp], OFFSET FLAT:LC0 ; "%lld1 2" 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] retGCC代码也是如此。24.2.2 ARM24.2.3 MIPS21.2 乘法,除法 289在32位环境中的64位值 #include 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; };24.3.1 x86代码 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 290在32位环境中的64位值乘法和除法是更为复杂的操作,一般来说,编译器会嵌入库函数的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 retGCC的做法几乎一样,但是乘法代码内联在函数中,可认为这样更有效。GCC有一些不同的库函数:参见附录D24.3.2 ARM 291在32位环境中的64位值24.3.3 MIPS21.3 右位移 #include uint64_t f6 (uint64_t a) { return a>>7; };24.4.1 x86代码 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填充。24.4.2 ARM24.4.3 MIPS24.5从32位值转化为64位值 292在32位环境中的64位值24.5.1 x8624.5.2 ARM24.5.3 MIPS #include 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 293在32位环境中的64位值 _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 294在32位环境中的64位值 _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 ; 00 003039H mov DWORD PTR [esp+8], 1972608889 ; 75 939f79H mov DWORD PTR [esp+12], 5461 ; 00 001555H mov DWORD PTR [esp], 1942892530 ; 73 ce2ff2H mov DWORD PTR [esp+4], 2874 ; 00 000b3aH call _f7 add esp, 28 retGCC生成的汇编代码跟MSVC一样,但是在函数中内联乘法代码。 更多:32位值在16位环境中(30.4) 295SIMD第二十五章SIMDSIMD是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/25.1 Vectorization向量化3,例如循环用两个数组生成一个数组。循环体从输入数组中取值,处理后存储到另一个数组。重要的一点是操作了每一个元素。向量化—同时处理多个元素。 296SIMD向量化并不是新的技术:本书的作者在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 DwordIntegers and Store Low Result)和PMULHW(Multiply Packed Signed Integersand Store High Result),一次可以得到4个64位结果。循环次数从1024变成1024/4,当然更快。25.1.1 Addition example一些简单的情况下某些编译器可以自动向量化,Intel C++5.函数如下: int f (int sz, int *ar1, int *ar2, int *ar3) { for (int i=0; ib) return a; return b; };27.3.1 x64清单 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 OrderedDouble-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 321使用SIMD来处理浮点数但是,GCC 4.4.6生成了更多的优化代码,并且使用了MAXSD(“Return MaximumScalar 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 ENDP27.3.2 x8627.4 Calculating machine epsilon: x64 and SIMD27.5 回顾伪随机书生成器27.6 总结只有低一半的XMM寄存器会被使用,一组IEEE754格式的数字也会被存在这里。显然,所有的指令都有SD后缀(标量双精度数),这些操作数是可以用于IEEE754浮点数的,他们存在XMM寄存器的低64位中。 比FPU更简单的是,显然SIMD扩展并不像FPU以前那么混乱,栈寄存器模型也没使用。 如果你像试着将例子中的double替换成float的话,它们还是会使用同样的指令,但是后缀是SS(标量单精度数),例如MOVSS,COMISS,ADDSS等等。 标量(Scalar)代表着SIMD寄存器会包含仅仅一个值,而不是所有的。可以在所有类型的值中生效的指令都被“封装”成同一个名字。 322关于ARM的特殊细节第二十八章关于ARM的具体细节 323关于MIPS的特殊细节第二十九章关于MIPS的具体细节 324Part II 重要的基础知识Part II 重要的基础知识 325有符号数的表示第三十章有符号数的表示 326字节序第三十一章字节顺序 327内存第三十二章内存 328CPU第三十三章CPU 329哈希函数第三十四章哈希函数 330Part III 更高级些的例子Part III 更高级些的例子 331温度转换温度转换另一个在初学者的编程书中常见的例子是温度转换程序,例如将华氏度转为摄氏度,或者反过来。我也添加了一个简单的错误处理: 1)我们应该检查用户是否输入了正确的数字2)我们应该检查摄氏度是否低于-273゜C,因为这比绝对零度还低,学校物理课上的东西应该都还记得。 exit()函数将立即终止程序,而不会回到调用者函数。35.1 整数值 #include #include 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); };35.1.1 MSVC 2012 x86清单35.1: MSVC 2012 x86 $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 332温度转换 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关于这个我们可以说的是: printf()的地址先被载入了ESI寄存器中,所以printf()调用的序列会被CALL ESI 处理,这是一个非常著名的编译器技术,当代码中存在多个序列调用同一个函 数的时候,并且/或者有空闲的寄存器可以用上的时候,编译器就会这么做。 333温度转换 我们知道ADD EAX,-32指令会把EAX中的数据减去32。 EAX = EAX + (-32)等 同于 EAX = EAX - 32,因此编译器决定用ADD而不是用SUB,也许这样性能 比较高吧。 LEA指令在值应当乘以5的时候用到了: lea ecx, DWORD PTR [eax+eax4]。 是的,i + i 4是等同于i*5的,而且LEA比IMUL运行的要快。 还有,SHL EAX,2/ ADD EAX,EAX指令对也可以替换这句,而且有些编译器就是会这么优 化。 用乘法做除法的技巧也会在这儿用上。 虽然我们没有指定,但是main()函数依然会返回0。C99规范告诉我们[15章, 5.1.2.2.3] main()将在没有return时也会照常返回0。 这个规则仅仅对main()函 数有效。 虽然MSVC并不支持C99,但是这么看说不好他还是做到了一部分 呢?35.1.2 MSVC 2012 x64 /Ox生成的代码几乎一样,但是我发现每个exit()调用之后都有INT 3。 xor ecx, ecx call QWORD PTR __imp_exit int 3INT 3是一个调试器断点。 可以知道的是exit()是永远不会return的函数之一。所以如果他“返回”了,那么估计发生了什么奇怪的事情,也是时候启动调试器了。35.2 浮点数值清单35.1: MSVC 2010 334温度转换 #include #include 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指令...清单35.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 335温度转换 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指令:清单35.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 336温度转换 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。这个没问题,因为编译器并不一定会按照源代码里面的顺序产生代码。 337斐波那契数列第三十六章斐波那契数列另一个在编程教材中普遍使用的例子是,一个用来生成斐波那契数列的递归函数。这个序列非常简单:每个数字都是前面两个数字的和。打头的两个数字都是1或者是0,1,1。该序列起始是这样的:0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181...36.1 例一这个实现起来比较简单。下面这个程序产生直到21的序列。 #include void fib (int a, int b, int limit) { printf ("%d\n", a+b); if (a+b > limit) return; fib (b, a+b, limit); }; int main() { printf ("0\n1\n1\n"); fib (1, 1, 20); };Listing 36.1: MSVC 2010 x86 338斐波那契数列 _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 _limit$ = 16 ; size = 4 _fib PROC push ebp mov ebp, esp mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] push eax push OFFSET $SG2643 call DWORD PTR __imp__printf add esp, 8 mov ecx, DWORD PTR _a$[ebp] add ecx, DWORD PTR _b$[ebp] cmp ecx, DWORD PTR _limit$[ebp] jle SHORT $LN1@fib jmp SHORT $LN2@fib $LN1@fib: mov edx, DWORD PTR _limit$[ebp] push edx mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] push eax mov ecx, DWORD PTR _b$[ebp] push ecx call _fib add esp, 12 $LN2@fib: pop ebp ret 0 _fib ENDP _main PROC push ebp mov ebp, esp push OFFSET $SG2647 call DWORD PTR __imp__printf add esp, 4 push 20 push 1 push 1 call _fib add esp, 12 xor eax, eax pop ebp ret 0 _main ENDP我们将用这个来说明一下栈帧。让我们在OllyDbg中加载这个例子,并且跟踪到最后一次对 f() 函数的调用: 339斐波那契数列图 36.1: OllyDbg: 最后一次对 f() 的调用让我们来更加仔细地研究一下栈。本书的作者向其中加了一些注释(在这个例子中,就是把OllyDbg中的多个条目copy到剪切板中(Ctrl-C)): 340斐波那契数列 0035F940 00FD1039 RETURN to fib.00FD1039 from fib.00FD1000 0035F944 00000008 1st argument: a 0035F948 0000000D 2nd argument: b 0035F94C 00000014 3rd argument: limit 0035F950 /0035F964 saved EBP register 0035F954 |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000 0035F958 |00000005 1st argument: a 0035F95C |00000008 2nd argument: b 0035F960 |00000014 3rd argument: limit 0035F964 ]0035F978 saved EBP register 0035F968 |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000 0035F96C |00000003 1st argument: a 0035F970 |00000005 2nd argument: b 0035F974 |00000014 3rd argument: limit 0035F978 ]0035F98C saved EBP register 0035F97C |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000 0035F980 |00000002 1st argument: a 0035F984 |00000003 2nd argument: b 0035F988 |00000014 3rd argument: limit 0035F98C ]0035F9A0 saved EBP register 0035F990 |00FD1039 RETURN to fib.00FD1039 from fib.00FD1000 0035F994 |00000001 1st argument: a 0035F998 |00000002 2nd argument: b 0035F99C |00000014 3rd argument: limit 0035F9A0 ]0035F9B4 saved EBP register 0035F9A4 |00FD105C RETURN to fib.00FD105C from fib.00FD1000 0035F9A8 |00000001 1st argument: a \ 0035F9AC |00000001 2nd argument: b | prepared in main( ) for f1() 0035F9B0 |00000014 3rd argument: limit / 0035F9B4 ]0035F9F8 saved EBP register 0035F9B8 |00FD11D0 RETURN to fib.00FD11D0 from fib.00FD1040 0035F9BC |00000001 main() 1st argument: argc \ 0035F9C0 |006812C8 main() 2nd argument: argv | prepared in CRT f or main() 0035F9C4 |00682940 main() 3rd argument: envp /该函数是递归的,因此看起来就像个“三明治”。我们能够看出参数limit总是相同的(0x14或20),但是参数a和b在每次调用时都是不同的。其中也有RA(ReturnAddress,返回地址)和保存的EBP值。OllyDbg可以决定基于EBP的帧,所以就画出了这些中括号(])。每个中括号中的值构成了栈帧,换句话说,每一个函数都使用栈来作为暂存空间。我们也可以说每一个函数都不能访问超出其帧边界的栈元素(不包括函数参数),虽然这在技术上是有可能的。上一句话通常是正确的,除非函数中有了bug。每个保存的EBP值为前一栈帧的地址:这就是有些调试器可以很容易地划分在帧中的栈和dump每个函数参数的原因。正如我们在这里所见,每一个函数都为下一个函数调用准备好了参数。在最后有用于 main() 函数的三个参数。argc值为1(是的,我们确实没有用命令行参数来运行程序)。 341斐波那契数列这样很容易导致栈溢出:只是删除(或注释)掉limit检测,程序就会抛出0xC00000FD异常而崩溃(stack overflow)。36.2 例二我构造的函数有些冗余,所以就让我们来添加一个局部变量next并用它代替所有的"a+b": #include void fib (int a, int b, int limit) { int next=a+b; printf ("%d\n", next); if (next > limit) return; fib (b, next, limit); }; int main() { printf ("0\n1\n1\n"); fib (1, 1, 20); };以下的输出是MSVC非优化编译的输出,所以next变量在局部栈中分配空间。 342斐波那契数列 _next$ = -4 ; size = 4 _a$ = 8 ; size = 4 _b$ = 12 ; size = 4 _limit$ = 16 ; size = 4 _fib PROC push ebp mov ebp, esp push ecx mov eax, DWORD PTR _a$[ebp] add eax, DWORD PTR _b$[ebp] mov DWORD PTR _next$[ebp], eax mov ecx, DWORD PTR _next$[ebp] push ecx push OFFSET $SG2751 ; '%d' call DWORD PTR __imp__printf add esp, 8 mov edx, DWORD PTR _next$[ebp] cmp edx, DWORD PTR _limit$[ebp] jle SHORT $LN1@fib jmp SHORT $LN2@fib $LN1@fib: mov eax, DWORD PTR _limit$[ebp] push eax mov ecx, DWORD PTR _next$[ebp] push ecx mov edx, DWORD PTR _b$[ebp] push edx call _fib add esp, 12 $LN2@fib: mov esp, ebp pop ebp ret 0 _fib ENDP _main PROC push ebp mov ebp, esp push OFFSET $SG2753 ; "0\n1\n1\n" call DWORD PTR __imp__printf add esp, 4 push 20 push 1 push 1 call _fib add esp, 12 xor eax, eax pop ebp ret 0 _main ENDP 343斐波那契数列让我再一次加载OllyDbg:图 36.2: OllyDbg: 最后一次对 f() 调用现在next变量就出现在每一个帧中。让我们来更加仔细地研究一下栈。作者也向其中加了他的注释: 344斐波那契数列 0029FC14 00E0103A RETURN to fib2.00E0103A from fib2.00E01000 0029FC18 00000008 1st argument: a 0029FC1C 0000000D 2nd argument: b 0029FC20 00000014 3rd argument: limit 0029FC24 0000000D "next" variable 0029FC28 /0029FC40 saved EBP register 0029FC2C |00E0103A RETURN to fib2.00E0103A from fib2.00E01000 0029FC30 |00000005 1st argument: a 0029FC34 |00000008 2nd argument: b 0029FC38 |00000014 3rd argument: limit 0029FC3C |00000008 "next" variable 0029FC40 ]0029FC58 saved EBP register 0029FC44 |00E0103A RETURN to fib2.00E0103A from fib2.00E01000 0029FC48 |00000003 1st argument: a 0029FC4C |00000005 2nd argument: b 0029FC50 |00000014 3rd argument: limit 0029FC54 |00000005 "next" variable 0029FC58 ]0029FC70 saved EBP register 0029FC5C |00E0103A RETURN to fib2.00E0103A from fib2.00E01000 0029FC60 |00000002 1st argument: a 0029FC64 |00000003 2nd argument: b 0029FC68 |00000014 3rd argument: limit 0029FC6C |00000003 "next" variable 0029FC70 ]0029FC88 saved EBP register 0029FC74 |00E0103A RETURN to fib2.00E0103A from fib2.00E01000 0029FC78 |00000001 1st argument: a \ 0029FC7C |00000002 2nd argument: b | prepared in f1() for next f1() 0029FC80 |00000014 3rd argument: limit / 0029FC84 |00000002 "next" variable 0029FC88 ]0029FC9C saved EBP register 0029FC8C |00E0106C RETURN to fib2.00E0106C from fib2.00E01000 0029FC90 |00000001 1st argument: a \ 0029FC94 |00000001 2nd argument: b | prepared in main( ) for f1() 0029FC98 |00000014 3rd argument: limit / 0029FC9C ]0029FCE0 saved EBP register 0029FCA0 |00E011E0 RETURN to fib2.00E011E0 from fib2.00E01050 0029FCA4 |00000001 main() 1st argument: argc \ 0029FCA8 |000812C8 main() 2nd argument: argv | prepared in CRT f or main() 0029FCAC |00082940 main() 3rd argument: envp /在这里我们可以看出:next的值在每次函数调用时都被计算一遍,然后将其作为参数b传递给下一个函数。36.3 总结 345斐波那契数列递归函数看起来很nice,但是因为它们对栈的笨重用法在技术上可能会降低性能。所以在写有关性能的关键代码时应该要避免使用递归。例如,本书的作者曾经写过一个在二叉树中搜寻特定节点的函数。使用递归函数看起来很优雅,但是因为在每次函数调用的开头和结尾会花费额外的时间,它就比使用迭代(不用递归)的情况慢好几倍。By the way,这是一些函数式PL(Programming language,编程语言,LISP,Python, Lua等)编译器(其中大量使用递归)使用tail call的原因。 346CRC32的计算实例第三十七章CRC32哈希散列计算例子这是非常流行的CRC32哈希散列计算。 /* By Bob Jenkins, (c) 2006, Public Domain */ #include #include #include 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, 347CRC32的计算实例 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 0xedb883 20 */ 348CRC32的计算实例 void build_table() { ub4 i, j; for (i=0; i>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> 8) ^ crctab[(hash & 0xff) ^ k[i]]; return hash; } /* To use, try "gcc -O crc.c -o crc; crc >8; bits 24-31 taken "from nowhere" will be cleared shr eax, 8 ; EAX=EAX^crctab[EDI*4] - choose EDI-th element from crctab[] ta ble xor eax, DWORD PTR _crctab[edi*4] inc ecx ; i++ cmp ecx, edx ; i>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 endpGCC在循环开始的时候通过填入NOP和lea esi,esi+0来按8字节对齐。更多信息请阅读npad小结(64)。 351网址的计算实例#网址的计算实例 352循环:几个迭代器#循环:几个迭代器 353Duff’s device#Duff’s device 354除以9#除以9 355将字符串转化为数字(atoi())#将字符串转化为数字(atoi()) 356内联函数第四十三章内联函数内联代码是指当编译的时候,将函数体直接嵌入正确位置,而不是在这个位置放上函数声明。 #include 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的优化方案,我们会看到:清单43.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 357内联函数这里的除法由乘法完成。 是的,我们的小函数被放到了printf()调用之前。为什么?因为这比直接执行函数之前的“调用/返回”过程速度更快。 在过去,这样的函数在函数声明的时候必须被标记为“内联”。在现代,这样的函数会自动被编译器识别。 另外一个普通的自动优化的例子是内联字符串函数,比如strcpy(),strcmp()等43.1 字符串和内存函数43.1.1 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()的例子。 358内联函数清单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, 0FFFFFFFFh43.1.2 strlen()43.1.3 strcpy()43.1.4 memset()Example#1Example#243.1.5 memcpy()Short blocksLong blocks43.1.6 memcmp()43.1.7 IDA script 359内联函数我写了一个小的用于搜索和归纳的IDA脚本,这样的脚本经常能在内联代码中看到:IDA_scripts. 360C99 的约束C99的限制这个例子说明了为什么某些情况下FORTRAN的速度比C/C++要快 void f1 (int* x, int* y, int* sum, int* product, int* sum_produc t, int* update_me, size_t s) { for (int i=0; icolor=color; }; void print_color() { printf ("color=%d ", color); }; }; class box : public object { private: int width, height, depth; public: 399C++ 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 400C++ ??_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, 0 0H ; ‘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?$CF d?0?5radius@ call _printf add esp, 12 ; 0000000cH ret 0 ?dump@sphere@@QAEXXZ ENDP ; sphere::dump所以,这就是他的内存暑促后:(基类对象) 401C++继承的对象 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()方 402C++法对于输入对象类型来说是不可知的,如果你创建一个继承类,例如继承了box类编译器会自动在depth域之后加上新域,而把box的类域固定在一个固定的位置。 因此,box::dump()方法会在访问color/width/height/depths的时候顺利工作,因为地址的固定,它会很容易的知道偏移。 GCC生成的代码基本一样,只有一个不一样的就是this的传递,就像之前说的一样,它是作为第一个参数传递的,而不是通过ECX传递的。51..13 封装封装是一个把数据装在类的private域里面的动作,这样会让它们只能从类的内部被访问到,而从外面访问不到。 但是,生成的代码里面是否有什么东西指示一个变量是private呢? 没有,让我们看看简单的例子: #include 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()代码: 403C++ ?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, 0 0H 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: ’b ox::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( o); ptr_to_object[1]=123; }; 404C++这个函数的代码非常简单,剋说函数指示把指针指向这些int,然后把123写入第二个int: ?hack_oop_encapsulation@@YAXPAVbox@@@Z PROC ; hack_oop_encapsula tion mov eax, DWORD PTR _o$[esp-4] mov DWORD PTR [eax+4], 123 ; 0000007bH ret 0 ?hack_oop_encapsulation@@YAXPAVbox@@@Z ENDP ; hack_oop_encapsula tion看看它是怎么工作的: 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的值。51.1.4 多重继承多重继承是一个类的创建,这个类会从2个或多个类里面继承函数和成员。 看一个简单的例子: #include class box { public: int width, height, depth; box() { }; box(int width, int height, int depth) { this->width=width; this->height=height; this->depth=depth; 405C++ }; void dump() { printf ("this is box. width=%d, height=%d, depth=%d", wi dth, 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(); 406C++ sb.dump(); printf ("%d", sb.get_weight()); return 0; };让我们在MSVC 2008中用/Ox和/Ob0选项来编译,然后看看box::dump()、solid_object::dump()和solid_box::dump()的函数代码: 407C++ ?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: 408C++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_densi ty, COMDAT ; _this$ = ecx mov eax, DWORD PTR [ecx] ret 0 ?get_density@solid_object@@QAEHXZ ENDP ; solid_object::get_densi ty但是solid_box::get_weight()的代码更有趣: 409C++ ?get_weight@solid_box@@QAEHXZ PROC ; solid_box::get_weight, COMD AT ; _this$ = ecx push esi mov esi, ecx push edi lea ecx, DWORD PTR [esi+12] call ?get_density@solid_object@@QAEHXZ ; solid_object::get_densi ty 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_weightGet_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类对象。 因此,我们可以说,类的一个对象,是从多个其他类继承阿日来,在内存中代表着组合起来的类,因为它有所有继承来的域。每个继承的方法都会又一个指向对应结构部分的指针来处理。51.1.5 虚拟方法还有一个简单的例子: #include 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) 410C++ { this->color=color; this->width=width; this->height=height; this->depth=depth; }; void dump() { printf ("this is box. color=%d, width=%d, height=%d, dep th=%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()的函数代码: 411C++ _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()调用。 看看类构造函数的代码: 412C++ ??_R0?AVbox@@@8 DD FLAT:??_7type_info@@6B@ ; box ‘RTTI Type Desc riptor’ DD 00H DB ’.?AVbox@@’, 00H ??_R1A@?0A@EA@box@@8 DD FLAT:??_R0?AVbox@@@8 ; box::‘RTTI Base C lass 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 413C++我们可以看到一些轻微的内存布局的变化:第一个域是一个指向box::vftable(这个名字由MSVC编译器生成)的指针。 在这个函数表里我们看到了一个指向box::RTTI Complete Object Locator的连接,而且还有一个指向box::dump()函数的。所以这就是被命名的虚函数表和RTTI。虚函数表可以包含所有虚函数体的地址,RTTI表包含类型的信息。另外一提,RTTI表是c++调用dynamic_cast和typeid的结果的枚举表。你可以看到这里函数名是用明文表记的。因此,一个基对象可以调用虚函数object::dump(),然后,会从这个对象的结构里调用这个继承类的函数。枚举这些函数表需要消耗额外的CPU时间,所以可以认为虚函数比普通调用要慢一些。 在GCC生成的代码里,RTTI表的构造有些轻微的不同。51.2 输出流51.3 References51.4 STL51.4 STLInternalsMSVCGCC一个更加复杂的例子std::string 作为全局变量51.4.2 std::listGCCMSVCC++11 std::forward_list51.4.3 std::vector 414C++51.4.4 std::map and std::setMSVCGCCRebalancing demo (GCC) 415负的数组引索#负的数组引索 416Windoes 16-bit第五十三章16位Windows16位windows程序现在很少见了,但是在旧式计算机或者入侵软件狗的时候(58章),我有时候还会遇到这个问题。 16位的windows版本最高到3.11,95(*注:作者笔误写成了Win96)/98/ME也支持16位代码,他们同时也是一个Windows NT家族的32位版本。64位版本的Windows NT家族完全不支持16位程序。 代码类似于MS-DOS代码。 执行文件并不是MZ式或者PE文件,而是NE式(所谓的“NewExecutable”,新执行程序)。 所有的例子都由OpenWatcom 1.9编译器编译,使用这些参数: Wcl.exe -i=C:/WATCOM/h/win/ -s -os -bt=windows example.c53.1 例子#1 #include 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到现在为止,看起来都很简单。53.2 例子#2 417Windoes 16-bit #include 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段。53.3 例子#3 418Windoes 16-bit #include int PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow) { int result=MessageBox (NULL, "hello, world", "caption", MB_Y ESNOCANCEL); 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: 419Windoes 16-bit 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就是前一节的扩展而已。53.4 例子#4 #include 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 420Windoes 16-bit 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 421Windoes 16-bit 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 endp32位的值(long数据类型代表32位,int代表16位数据)在16位模式下(MSDOS和win16)都会按对传递,就像64位数据在32位环境下使用的方式一样(21章)。 422Windoes 16-bitSub_B2在这里是一个编译器生成的库函数,他的作用是“long乘法”,例如两个32位类型想成,其他的编译器函数列在了附录E, D.中。 ADD/ADC指令对用来相加两个值:ADD将设置/清空CF进位标识,ADC将会使用它。 SUB/SBB将会做减法,SUB会设置/清空CF标识位,SBB将会使用它。 32位值按照DX:AX寄存器对返回。常数同样在WinMain()中按照值对的方式传递。 Int类型的123常量首先被转为32位的值,使用的是CWD指令。53.5 例子#5 423Windoes 16-bit #include 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 424Windoes 16-bit 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 ; -------------------------------------------------------------- 425Windoes 16-bit ------------- 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" 426Windoes 16-bit 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编译器针对每个内存模型都有有特别的库:它们会因为数据和代码的不同的指针模型而不同。53.6 例子#6 427Windoes 16-bit #include #include #include 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; }; 428Windoes 16-bit 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 endpUNIX时间是32位的,所以它返回在DX:AX寄存器对中,而且将他们存储到两个本地16位变量中。然后一个指向值对的指针会被当作参数传给localtime()函数。Localtime()函数有一个struct tm,它将通过C库分配内存,所以只有指向它的指针返回了。顺便一提,这也意味着在它的结果被使用之前,函数不能被再次调用。 对time()和localtime()两个函数来说,Watcom调用转换将会在这里:前四个参数使用 429Windoes 16-bitAX、DX、BX、CX传递,剩余的通过栈来传递。使用这个转换的函数也会在名字最后使用下划线来标记。 Sprintf()并不使用PASCAL调用转换,也不会使用watcom转换,所以参数将使用寻常的cdecl方式传递(47.1节)。53.6.1 全局变量这里用同样的例子,但是变量是全局变量: #include #include #include 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; }; 430Windoes 16-bit 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 endpT不会被使用,但是编译器还是用代码存储了这个值。因为他并不确定,也许这个值会在某个地方被用到。 431Part IV JAVAPart IV JAVA 432Java第五十四章JAVA54.1介绍大家都知道,java有很多的反编译器(或是产生JVM字节码) 原因是JVM字节码比其他的X86低级代码更容易进行反编译。 多很多相关数据类型的信息。 JVM(java虚拟机)内存模型更严格和概括。 java编译器没有做任何的优化工作(JVM JIT不是实时),所以,类文件中的 字节代码的通常更清晰易读。JVM字节码知识什么时候有用呢? 文件的快速粗糙的打补丁任务,类文件不需要重新编译反编译的结果。 分析混淆代码 创建你自己的混淆器。 创建编译器代码生成器(后端)目标。我们从一段简短的代码开始,除非特殊声明,我们用的都是JDK1.7反编译类文件使用的命令,随处可见:javap -c -verbase.在这本书中提供的很多的例子,都用到了这个。54.2 返回一个值可能最简单的java函数就是返回一些值,oh,并且我们必须注意,一边情况下,在java中没有孤立存在的函数,他们是“方法”(method),每个方法都是被关联到某些类,所以方法不会被定义在类外面, 但是我还是叫他们“函数” (function),我这么用。 public class ret { public static int main(String[] args) { return 0; } }编译它。 433Java 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: sipush1234 3: ireturnsipush(shot integer)如栈值是1234,slot的名字以为着一个16bytes值将会入栈。sipush(短整型) 1234数值确认时候16-bit值。 434Java 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操作码 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位双精度浮点整数常量区)布尔型 435Java 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位短整型也是一样。 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 字符型? 436Java 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: ireturnbipush 的意思"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也许会问,位什么费事用两个16位整型当32位用?为什么char数据类型和短整型类型还使用char.答案很简单,为了数据类型的控制和代码的可读性。char也许本质上short相同,但是我们快速的掌握它的占位符,16位的UTF字符,并且不像其他的integer值符。使用 short,为各位展现一下变量的范围被限制在16位。在需要的地方使用boolean型也是一个很好的主意。代替C样式的int也是为了相同的目的。在java中integer的64位数据类型。 437Java 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: lreturn64位数也被在存储在常量区,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: dreturndreturn 代表 "return double" 438Java最后,单精度浮点数: 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"那么函数还能返回什么呢? 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.3 简单的计算函数让我们继续看简单的计算函数。 439Java 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: ireturniload_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 常量区 440Java ... #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: ireturniload_0加载第一个函数参数(a),iload_2 第二个参数(b)下面两条指令执行后,栈的情况如下: +---+ TOS ->| b | +---+ | a | +---+iadds 增加两个值,返回结果在栈顶。 +--------+ TOS ->| result | +--------+让我们把这个例子扩展成长整型数据类型。 441Java 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)入栈。 442Java +---------+ TOS ->| c | +---------+ | product | +---------+现在iadd指令可以相加两个值。54.4 JVM内存模型X86和其他低级环境系统使用栈传递参数和存储本地变量,JVM稍微有些不同。主要体现在: 本地变量数组(LVA)被用于存储到来函数的参数和本地变量。iload_0指令是从其中加载值,istore存储值在其中,首先,函数参数到达:开始从0或者1(如果0参被this指针用。),那么本地局部变量被分配。每个槽子的大小都是32位,因此long和double数据类型都占两个槽。操作数栈(或只是"栈"),被用于在其他函数调用时,计算和传递参数。不像低级X86的环境,它不能去访问栈,而又不明确的使用pushes和pops指令,进行出入栈操作。54.5 简单的函数调用mathrandom()返回一个伪随机数,函数范围在「0.0...1.0)之间,但对我们来说,由于一些原因,我们常常需要设计一个函数返回数值范围在「0.0...0.5) public class HalfRandom { public static double f() { return Math.random()/2; } }54.8 常量区 443Java ... #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: dreturnjava本地变量数组 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 常量区常量区的ldc行偏移3,指向"hello,world!"字符串,并且将其入栈,在java里它被成为饮用,其实它就是指针,或是地址。 444Java ... #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放的指针,因此,内部的printmethod取得两个参数,输入1指向对象的this指针,2)"hello,world"字符串的地址,确实,println()在被初始化系统的调用,对象之外,为了方便,javap使用工具把所有的信息都写入到注释中。54.6 调用beep()函数这可能是最简单的,不使用参数的调用两个函数。 445Java 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.7 线性同余伪随机数生成器我们来试一个简单的伪随机函数生成器,我已经在这本书中用过一次了。(在500页20行) 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的输出中,发现类的构造呢? 446Java 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: returniload_1 取得输入值并将其入栈。但为什么不用iload_0?因为这个函数可能使用类的字段属性,因此这个变量被作为参数0传递给了函数,rand_state字段属性,在类中占用2个参数槽子。现在my_rand(): 447Java 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.8 条件跳转让我们进入条件跳转 448Java public class abs { public static int abs(int a) { if (ab) 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 449Javaif_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更复杂的例子。。 450Java public class cond { public static void f(int i) { if (i<100) System.out.print("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 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 451Javaif_icmpge出栈两个值,并且比较它们,如果第的二个值大于第一个,发生跳转到行偏移14,if_icmpne和if_icmple做的工作类似,但是使用不同的判断条件。在行偏移43的ifne指令,它的名字不是很恰当,我要愿意把它命名为ifnz如果栈定的值不是0跳转,但是这是怎么做的,总跳转到行偏移54,如果输入的值不是另,如果是0,执行流程进入行偏移46,“==”字符串被打印。N.BJVM没有无符号数据类型,所以,比较指令的操作数,只有还有符号整数值。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); } }这是main()函数的代码。 452Java 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参数在栈中的被传给其他函数,返回值在栈顶。54.10位。所有位操作工作,与其他的一些ISA(指令集架构)类似: 453Java public static int set (int a, int b) { return a | 1<td_planarconfi g == 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 lzw_nbits 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.000000ST(0)存放了第一个参数,[EBX]存放了第二个参数。FDIV(FSTP)之后的指令在内存中写入了结果: .text:3011E91B DD 1E fstp qword ptr [esi]如果我们设置一个断点,就可以看到结果: 507找到真正的指令 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.000000Excel在这个单元中显示666,我们也可以确信的确找到了正确的位置。 508找到真正的指令如果我们尝试使用同样的Excel版本,但是是64位的,会发现只有12个FDIV指令,我们的目标指令在第三个。 tracer.exe -l:excel.exe bpx=excel.exe!BASE+0x1B7FCC,set(st0,666)看起来似乎许多浮点数和双精度类型的除法操作都被编译器用SSE指令比如DIVSD(DIVSD总共出现了268次)替换了。 509可疑代码的模式第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.lst61.2 Hand-written assembly code现代编译器不会emit LOOP和RCL指令。另一方面,这些指令对于直接用汇编语言编程的程序员来说很熟悉。如果你发现了这些指令,可以猜测这部分代码极有可能是手工编写的。这样的代码在这个指令列表中用(M)标记:A.6节。同时函数prologue/epilogue通常不会以手工编写的汇编的形式呈现。通常情况下,手工编写的代码中参数传递给函数没有固定的系统。Windows 2003 内核(ntoskrnl.exe 文件)的例子: 510可疑代码的模式 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.2\base\ntos\ke\i386\cpu.asm文件中很容易找到。 511在追踪时使用Magic numbers第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]] [69A EB08h]=0 0x150bf6f (_kziaia+0x1d), e= 1 [FS: MOV EAX, [2Ch]] 0x150bf75 (_kziaia+0x23), e= 1 [MOV ECX, [EAX+EDX*4]] [EA X+EDX*4]=0xf1ac360 0x150bf78 (_kziaia+0x26), e= 1 [MOV [EBP-4], ECX] ECX=0xf 1ac360对于网络包中也同样适用。很重要的一点是,幻数必须独特保证没有在该程序中出现过。除了tracer,heavydebug模式下的DosBox(MS-DOS仿真器)也能将每条指令执行后寄存器状态写入到一个文本文件中,因此,这种技术对于DOS程序也是很有用的。 512其他东西第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节 513其他东西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"共享软件如此受欢迎的原因吧。63.4.2 Blink-comparator文件或内存快照的比对让我们想起了blink-comparator:一种曾被天文学家使用的设备,用于发现天体移动。blink-comparator允许在两个不同时间摄影快照间切换,便于天文学家发现差别。顺便说一句,冥王星就是在1930年用blink-comparator发现的。 514Part VI 操作系统的特性Part VI 操作系统的特性 515参数传递方法(调用规则)第六十四章传递参数的方法64.1 cdcel这种传递参数的方法在C/C++语言里面比较流行。如下的代码片段所示,调用者反序地把参数压到栈中:最后一个参数,倒数第二个参数,第一个参数。调用者还必须在函数返回之后把栈指针(ESP)还原为初始状态。Listing 64.1: cdecl push arg3 push arg2 push arg1 call function add esp, 12 ; returns ESP64.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 修饰符。 516参数传递方法(调用规则) 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, 864.2.1 可变参数的函数printf()系列的函数大概是C/C++里面唯一一系列具有可变参数的函数了,在这些函数的帮助下很容易理清cdecl和stdcall两种调用方式之间的重要区别。让我们先假设编译器知道每个调用printf()函数的参数的个数,无论如何,当我们调用printf()的时候,它已经存在于编译好的MSVCRT.DLL之中(我们讨论的是Windows),并没有任何关于传递多少个参数的信息,剩下的办法就是通过它的格式字符串获取得到参数个数。因此,如果printf()函数是一个stdcall调用方式的函数,它必须通过格式字符串计算参数个数用于恢复栈指针(ESP),这是一种相当危险的情况,程序员的一个错别字就可以导致程序崩溃。因此此类函数使用cdecl调用方式远比使用stdcall调用方式适合。64.3 fastcall 517参数传递方法(调用规则)这是一种将部分参数通过寄存器传入,其余参数通过栈方式传入的方法。它的执行效率在一些旧时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 518参数传递方法(调用规则) _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寄存器来传递。 519参数传递方法(调用规则)在GCC里面,this指针是通过第一个参数进行传递的。因此很明显,在所有成员函数里面都会多出一个额外的参数。相关例子请查看(51.1.1)。64.5 x86-6464.5.1 Windows x64在Win64里面传递函数参数的方法类似fastcall调用约定。前四个参数通过RCX,RDX,R8和R9寄存器传参,其余参数通过栈进行传递。调用者还必须预留32个字节或者4个64位的空间,让被调用者可以保存前四个参数。短函数可能直接使用通过寄存器传过来的值,但更大的可能是保存那些值后在进一步使用。调用者还必须负责还原栈指针。这个调用约定也用于Windows x86-64位系统上的DLL(而不是Win32的stdcall)。例子 #include 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 520参数传递方法(调用规则) $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()的反汇编代码一开始就把参数保存到“预留”的栈空间之中,这样做的目的是编译器并不能保证有足够的寄存器可以使用,如果不这样做的话这四个寄存器将被参数占用到函数执行结束。最后,预留栈空间是调用者的职责。 521参数传递方法(调用规则)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) 522参数传递方法(调用规则)this指针的传递(C/C++)this指针通过RCX传递,成员函数的第一个参数通过RDX传递,更多例子请看(51.1.1)。64.5.2 Linux x64Linux 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。 523参数传递方法(调用规则)64.6 返回float和double类型的值除了Win64之外,其它返回float和double类型的值都是通过FPU里面的ST(0)寄存器返回的。 在Win64里面,返回float和double类型的值是通过XMM0寄存器返回。64.7 修改参数有时候,C/C++程序员(虽然不仅仅是这些人)可能会问,如果他们碰巧修改了参数会怎样?答案非常简单,这些参数是保存在栈里面的,修改参数的时候是修改这个栈里面的内容,调用者并没有在被调用函数退出之后再使用它们(至少在我的实践中没有遇到这种情况)。 #include 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++并不提供任何方式可以访问它们。 524参数传递方法(调用规则)64.8 使用指针的函数参数...更有意思的是,有可能在程序中,取一个函数参数的指针并将其传递给另外一个函数。 #include // 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 s tack 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 525参数传递方法(调用规则) $SG2994 DB '%d', 0aH, 00H a$ = 48 f PROC mov DWORD PTR [rsp+8], ecx ; save input value in Sha dow Space sub rsp, 40 lea rcx, QWORD PTR a$[rsp] ; get address of value an d pass it to modify_a() call modify_a mov edx, DWORD PTR a$[rsp] ; reload value from Shado w Space and pass it to printf() lea rcx, OFFSET FLAT:$SG2994 ; '%d' call printf add rsp, 40 ret 0 f ENDPGCC同样将传入的参数存储在本地栈空间: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 l ocal stack lea rdi, [rsp+12] ; take an address of the v alue and pass it to modify_a() call modify_a mov edx, DWORD PTR [rsp+12] ; reload value from the loca l stack and pass it to printf() mov esi, OFFSET FLAT:.LC0 ; '%d' mov edi, 1 xor eax, eax call __printf_chk add rsp, 24 retARM64的GCC也做了同样的事情,但这个空间称为寄存器保护区: 526参数传递方法(调用规则) f: stp x29, x30, [sp, -32]! add x29, sp, 0 ; setup FP add x1, x29, 32 ; calculate address of variable in Regis ter Save Area str w0, [x1,-4]! ; store input value there mov x0, x1 ; pass address of variable to the mod ify_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)。 527本地线程储存区第六十五章线程局部存储TLS是每个线程特有的数据区域,每个线程可以把自己需要的数据存储在这里。一个著名的例子是C标准的全局变量errno。多个线程可以同时使用errno获取返回的错误码,如果是全局变量它是无法在多线程环境下正常工作的。因此errno必须保存在TLS。C++11标准里面新添加了一个thread_local修饰符,标明每个线程都属于自己版本的变量。它可以被初始化并位于TLS中。Listing 65.1: C++11 #include #include 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。 528本地线程储存区 #include #include #include // 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 529本地线程储存区 _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:2 Ch ; 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:2 Ch ; 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 ENDSrand_state现在处于TLS段,而且这个变量每个线程都拥有属于自己版本。它是这么访问的:从FS:2Ch加载TIB(Thread Information Block)的地址,然后添加一个额外的索引(如果需要的话),接着计算出在TLS段的地址。最后可以通过ECX寄存器来访问rand_state变量,它指向每个线程特定的数据区域。FS:这是每个逆向工程师都很熟悉的选择子了。它专门用于指向TIB,因此访问线程特定数据可以很快完成。 530本地线程储存区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设置一些固定的值以避免程序员忘记初始化。 531本地线程储存区 #include #include #include // 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。 532本地线程储存区 线程B开始运行,然后分配给它一个TLS,并把1234拷贝到rand_state,这时 候可以观察到两个线程使用同一个变量,但它们的值是不一样的。TLS callbacks如果我们想给TLS赋一个变量值呢?比方说:程序员忘记调用my_srand()函数来初始化PRNG,但是随机数生成器在开始的时候必须使用一个真正的随机数值而不是1234。这种情况下则可以使用TLS callbaks。下面的代码的可移植性很差,原因你应该明白。我们定义了一个函数(tls_callback()),它在进程/线程开始执行前调用,该函数使用GetTickCount()函数的返回值来初始化PRNG。 #include #include #include // 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 Ge tTickCount()) printf ("%d\n", my_rand()); }; 533本地线程储存区用IDA看一下:Listing 65.4: Optimizing MSVC 2013 .text:00401020 TlsCallback_0 proc near ; DATA XREF: .rdata:TlsCa llbacks .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 300000hTLS 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 534本地线程储存区 .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 535系统调用第六十六章系统调用(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 536系统调用 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.oLinux所有的系统调用在这里可以查看:http://go.yurichev.com/17319。在Linux中可以使用strace(71章)对系统调用进行跟踪或者拦截。66.2 WindowsWindows系统使用int 0x2e中断或x86下特有的指令SYSENTER调用用系统调用服务。Windows所有的系统调用在这里可以查看:http://go.yurichev.com/17320。扩展阅读:“Windows Syscall Shellcode” by Piotr Bania 537Linux第六十七章Linux67.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(位置无关代码),它的目的是让这段代码即使放在内存中任何随机位置都能正确地执行。这也是为什么不能使用绝对地址的原因。 538LinuxPIC(位置无关代码)对于早期的操作系统和现在那些没有虚拟内存支持的嵌入式系统来说至关重要(所有进程都放在同一个连续的内存块)。此外,它还用于*NIX系统的共享库。这样共享库只需要加载一次到内存之后就可以让所有需要的进程使用。而且这些进程可以把同一个共享库映射到各自不同的内存地址上。这也是为什么共享库不使用绝对地址也能够正常地工作的原因。让我们做一个简单的实验: #include 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 539Linux .text:00000440 public __x86_get_pc_thunk_bx .text:00000440 __x86_get_pc_thunk_bx proc near ; CODE XREF: _ini t_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] ; "return ing %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\n”和global_variable的指针。__x86_get_pc_thunk_bx()函数自身调用后在EBX返回一个指针(这里是0x57C)。这是一种获取程序计数器(EIP)的简单方法。0x1A84常量是这个函数开始处到 Global Offset Table Procedure Linkage Table(GOT PLT) 它们之间的距离差。IDA会把这些偏移处理成更容易理解后再显示出来,所以实际上的代码是: 540Linux .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\n"字符串的地址时须减去0x1A30偏移量。顺便说一下,AMD64的指令支持使用RIP用于相对寻址,这使得它可以产生出更简洁的PIC代码。让我们用相同的GCC编译器编译相同的C代码,但使用x64平台。IDA会简化了反汇编代码,造成我们无法看到使用RIP相对寻址的细节,所以我在这里使用了objdump来查看反汇编代码: 0000000000000720 : 720: 48 8b 05 b9 08 20 00 mov rax,QWORD PTR [rip+0x2008b9] # 200fe0 727: 53 push rbx 728: 89 fb mov ebx,edi 72a: 48 8d 35 20 00 00 00 lea rsi,[rip+0x20] #751 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 741: 89 d8 mov eax,ebx 743: 5b pop rbx 744: c3 ret0x2008b9是0x720处指令地址到global_variable地址的差,0x20是0x72a处指令地址到"returning %d\n"字符串地址的差。你可能会看到,频繁重新计算地址会导致执行效率变差(虽然在x64会更好)。所以如果你比较关心性能的话最好还是使用静态链接。67.1.1 WindowsWindows的DLL并没有使用PIC机制。如果Windows加载器需加载DLL到另外一个基地址,它会在内存中(在固定的位置)对DLL "打补丁" 来将所有地址都调整为正确的。这意味着多个Windows进程不能在不同进程内存块的不同地址共享一份DLL,因为每个实例加载在内存后只固定在这些地址工作。 541Linux67.2 LD_PRELOAD hack in LinuxLinux允许让我们自己的动态链接库加载在其它动态链接库之前,甚至是系统库(如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 #include #include #include 542Linux #include #include #include 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 descri ptor else opened_fd=0; return fd; }; int close(int fd) { 543Linux 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, 0x7fff ffff)+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环境变量一直指向我们的动态链接库文件名,其它程序在启动的时候也会加载我们的动态链接库。更多的例子请看: Verysimpleinterceptionofthestrcmp()(YongHuang) KevinPulo—FunwithLD_PRELOAD.Alotofexamplesandideas. File functions interception for compression/decompression files on fly (zlibc). 544Windows-NT第六十八章Windows Nt68.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 545Windows-NT 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 546Windows-NT 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 funct ion 401044 mov ecx, [eax] mov ecx, [ecx] mov [ebp+var_24], ecx push eax push ecx call __XcptFilter pop ecx pop ecx 547Windows-NT $LN24: retn $LN14: ; DATA XREF: .rdata:stru_4092D0 mov esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for func tion 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 int main() { MessageBox (NULL, "hello, world", "caption", MB_OK); }; 548Windows-NT让我们用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 PEPE是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 术语 549Windows-NT 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 (图形程序) 550Windows-NT68.2.4 OS versionPE文件还规定了可以加载它的最小Windows版本号。有一个表保存了PE的版本号和相应的Windows开发代号。举个例子,MSVC 2005编译的.exe文件运行在Windows NT4(version 4.00)。但MSVC 2008不是(生成文件的版本是5.00,至少运行于Windows 2000)。MSVC 2012生成的.exe文件默认是6.00版本,最低平台要求至少是WindowsVista。但可以通过更改编译选项,强制编译器支持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(readabledata)。其它流行的名字还有: .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文件用于这个目的)。 551Windows-NT这是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),并添加新的基地址。 552Windows-NT如果模块加载到原来的基地址,那么不做任何事情。所有的全局变量都可以这样处理。重定位表可能有各种类型,但是在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),但这不是一个强制规定。因为术语混乱,导入表是一个比较令人困惑的地方。让我们尝试一下整理这些信息。 553Windows-NTFigure 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。加载器至少有两种方法重写地址: 代码会有诸如调用__imp_CreateFileA的指令,因为导入函数的地址在某种意 义上是一个全局变量,当模块加载到不同的基地址时,call指令的地址被添加 到重定位表中。 但是,显然这种方法可能会扩大重定位表。因为有可能从这个 554Windows-NT 模块大量调用导入的函数。而且,重定位表太大的话会减慢模块的加载速度。 每个导入函数给它分配一条jmp指令,使用jmp指令加上重定位表的地址跳转到 导入函数。这些入口点被称之为“thunks”,所有调用导入函数仅需要调用相对 应的“thunk”,这种情况下不需要额外的重定位操作,因为这些CALL都使用相 对地址,不需要额外的调整操作。这两种方法可以组合使用。可能的话,链接器给那些被调用太多次的函数创建一个“thunk”,然而默认情况下不是这样。顺便说一下,FirstThunk指向的函数地址数组不必要位于IAT section。举个例子,我曾经写的PE_add_import工具可以给.exe文件添加一个导入函数。在早些时候,这个工具可以让你的函数调用其它DLL文件的函数。我的工具添加了类似下面的代码: MOV EAX, [yourdll.dll!function] JMP EAXFirstThunk指向第一条指令,换句话说,当加载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 PortableExecutable File Format”,建议你的程序安装在最终用户的计算机后不久做捆绑。PE文件的打包器/加密器也可以压缩/加密导入表。在这种情况下,Windows的加载器当然不会加载所有需要的DLL。因此打包器/加密器只能通过LoadLibrary()和GetProcAddress()来获取所需函数。安装在Windows系统中的标准DLL文件,IAT往往是位于PE文件的开头。据说,这是一种优化。加载时.exe文件不是全部加载到内存,它是“映射”和加载部分需要被访问到的内存。可能微软的开发者认为这样加载比较快。68.2.8 Resources资源在PE文件只是一组图标,图片,文本字符串,对话框描述。因为把它们从主代码分离了出来,所以多国语言程序很容易实现,只需要根据操作系统设置的语言去选择文本或图片的语言。 555Windows-NT作为一个副作用,通过使用诸如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 Format68.3 Windows SEH68.3.1 让我们先忘了MSVC在Windows,SEH(Structured Exception Handling(结构化异常处理))是异常处理的一种机制。然而,它是语言无关的,不管是 C++ 或者其它OOP语言。我们可以看到SEH(从 C++ 和MSVC扩展)是独立实现的。 556Windows-NT每个运行的进程都有一个SEH处理链,TIB有它最后的处理程序的地址。当异常发生时(除零,错误的地址访问,用户通过调用RaiseException()函数引发异常),操作系统在TIB找到最后的处理程序并调用它,获取异常时CPU的状态信息(如寄存器的值等等)。处理程序当前的异常能否修复,如果能,则修复该异常。如果不能,它通知操作系统无法处理它并由操作系统调用异常处理链中的下一个处理程序,直到处理程序能够处理的异常被发现。在异常处理链的结尾处有一个标准的处理程序,它显示一个对话框用于通知用户进程崩溃,然后把一些崩溃时CPU的状态信息,收集起来并将其发送给微软开发商。Figure 68.2: Windows XPFigure 68.3: Windows XP 557Windows-NTFigure 68.4: Windows 7Figure 68.5: Windows 8.1早些时候,这个处理程序被称为Dr.Watson。顺便说一句,有些开发人员会在自己的处理程序发送程序崩溃的信息。通过SetUnhandledExceptionFilter()函数注册异常处理程序,如果操作系统没有任何其它方式处理异常,则调用它。一个例子是Oracle RDBMS,它保存了CPU所有可能有用的信息和内存状态的巨大转储文件。让我们写一个自己的primitive exception handler: #include #include DWORD new_value=1234; EXCEPTION_DISPOSITION __cdecl except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, 558Windows-NT void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext ) { unsigned i; printf ("%s\n", __FUNCTION__); printf ("ExceptionRecord->ExceptionCode=0x%p\n", ExceptionRe cord->ExceptionCode); printf ("ExceptionRecord->ExceptionFlags=0x%p\n", ExceptionR ecord->ExceptionFlags); printf ("ExceptionRecord->ExceptionAddress=0x%p\n", Exceptio nRecord->ExceptionAddress); if (ExceptionRecord->ExceptionCode==0xE1223344) { printf ("That's for us\n"); // yes, we "handled" the exception return ExceptionContinueExecution; } else if (ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VI OLATION) { printf ("ContextRecord->Eax=0x%08X\n", ContextRecord->Ea x); // 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; 559Windows-NT 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 stac k } 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)。我们的处理程序安装后,我们调用RaiseException()。这是一个用户异常。处理程序检查异常代码,如果异常代码是0xE1223344,它返回ExceptionContinueExecution。这意味着处理程序修复了CPU的状态(通常是EIP/ESP寄存器),操作系统可以恢复运行。如果你稍微修改一下代码,处理程序返回ExceptionContinueSearch,那么操作系统将调用下一个处理程序,如果没有找到处理程序(因为没人捕获该异常),你会看到标准的Windows进程崩溃对话框。系统异常和用户异常之间的区别是什么?这里有系统的: 560Windows-NT as defined in WinBase.h as defined in ntstatus.h numeric EXCEPTION_ACCESS_VIOLATION STATUS_ACCESS_VIOLATION EXCEPTION_DATATYPE_MISALIGNMENT STATUS_DATATYPE_MISALIGNMEN EXCEPTION_BREAKPOINT STATUS_BREAKPOINT EXCEPTION_SINGLE_STEP STATUS_SINGLE_STEP EXCEPTION_ARRAY_BOUNDS_EXCEEDED STATUS_ARRAY_BOUNDS_EXCEED EXCEPTION_FLT_DENORMAL_OPERAND STATUS_FLOAT_DENORMAL_OPER EXCEPTION_FLT_DIVIDE_BY_ZERO STATUS_FLOAT_DIVIDE_BY_ZERO EXCEPTION_FLT_INEXACT_RESULT STATUS_FLOAT_INEXACT_RESULT EXCEPTION_FLT_INVALID_OPERATION STATUS_FLOAT_INVALID_OPERATIO EXCEPTION_FLT_OVERFLOW STATUS_FLOAT_OVERFLOW EXCEPTION_FLT_STACK_CHECK STATUS_FLOAT_STACK_CHECK EXCEPTION_FLT_UNDERFLOW STATUS_FLOAT_UNDERFLOW EXCEPTION_INT_DIVIDE_BY_ZERO STATUS_INTEGER_DIVIDE_BY_ZER EXCEPTION_INT_OVERFLOW STATUS_INTEGER_OVERFLOW EXCEPTION_PRIV_INSTRUCTION STATUS_PRIVILEGED_INSTRUCTIO EXCEPTION_IN_PAGE_ERROR STATUS_IN_PAGE_ERROR EXCEPTION_ILLEGAL_INSTRUCTION STATUS_ILLEGAL_INSTRUCTION EXCEPTION_NONCONTINUABLE_EXCEPTION STATUS_NONCONTINUABLE_EXCE EXCEPTION_STACK_OVERFLOW STATUS_STACK_OVERFLOW EXCEPTION_INVALID_DISPOSITION STATUS_INVALID_DISPOSITION EXCEPTION_GUARD_PAGE STATUS_GUARD_PAGE_VIOLATION EXCEPTION_INVALID_HANDLE STATUS_INVALID_HANDLE EXCEPTION_POSSIBLE_DEADLOCK STATUS_POSSIBLE_DEADLOCK CONTROL_C_EXIT STATUS_CONTROL_C_EXIT这些异常码的定义规则是: 31 29 28 27 ~ 16 15 ~ 0 S U 0 Facility code Error code 561Windows-NTS是一个基本代码: 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++ 的异常没有任何关联。 562Windows-NT __try { ... } __except(filter code) { handler code }“Finally”块也许能代替handler code: __try { ... } __finally { ... }filte code是一个表达式,告诉handler code是否对应引发的异常。如果你的filtecode太大而无法使用一个表达式,可以定义一个单独的filte函数。在Windows内核有很多这样的结构,下面是几个例子(WRK(Windows ResearchKernel)):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 563Windows-NT 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 specia l job of extracting the "real" I/O error when Mm raises STATUS_IN_PAG E_ERROR beneath us. Arguments: ExceptionPointer - A pointer to the exception record that co ntains 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->Exceptio nCode; if ( (*ExceptionCode == STATUS_IN_PAGE_ERROR) && (ExceptionPointer->ExceptionRecord->NumberParameters >= 3) ) { *ExceptionCode = (NTSTATUS) ExceptionPointer->ExceptionR ecord->ExceptionInformation[2]; } ASSERT( !NT_SUCCESS(*ExceptionCode) ); return EXCEPTION_EXECUTE_HANDLER; } 564Windows-NT在内部,SEH是操作系统支持的异常扩展。但是处理函数是_except_handler3(对于SEH3)或_except_handler4(对于SEH4)。 这个处理函数的代码是与MSVC相关的,它位于它的库或在msvcr*.dll文件。其他的Win32编译器可以提供与之完全不同的机制。SEH3SEH3有一个_except_handler3处理函数,而且扩展了_EXCEPTION_REGISTRATION表,并添加了一个指向scope table和previous trylevel变量。SEH4扩展了scope table缓冲溢出保护。scope table是一个表,包含了指向filter和handler code的块和每个try/except嵌套。再者,操作系统只关心prev/handle字段。_except_handler3函数的工作是读取其他字段和scope table,并决定由哪些处理程序来执行。_except_handler3函数的源代码是闭源的。然而,Sanos操作系统的win32兼容性层重新实现相同的功能。其它类似的实现有Wine和ReactOS。如果filter指针为NULL,handler指针则指向finally代码块。执行期间,栈中的previous try level变量发生变化,所以_except_handler3可以获取当前嵌套级的信息,才知道要使用scope table哪一表项。SEH3: 一个try/except块例子 565Windows-NT #include #include #include 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_SEAR CH) { 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 566Windows-NT 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, c an''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 567Windows-NT在这里我们可以看到SEH帧是如果在栈中构建出来的,scope table位于CONSTsegment-事实上,这些字段是不被改变的。一件有趣的事情是如何改变previous trylevel变量。它的初始化值是0xFFFFFFFF(-1)。当进入try语句块的时候,变量赋值为0。当try语句块结束的时候,写回-1。我们还能看到filter和handler code的地址。因此,我们可以很容易在函数里看到try/except是如何构造的。由于函数序言的SEH安装代码被多个函数共享,有时候编译器会在函数序言插入调用SEH_prolog()函数,这就完成了这个任务。该SEH回收代码是SEH_epilog()函数。让我们尝试用tracer运行这个例子: tracer.exe -l:2.exe --dump-sehListing 68.7: tracer.exe output EXCEPTION_ACCESS_VIOLATION at 2.exe!main+0x44 (0x401054) Excepti onInformation[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!_e xcept_handler SEH3 frame. previous trylevel=0 scopetable entry[0]. previous try level=-1, filter=0x401070 (2.e xe!main+0x60) handler=0x401088 (2.exe!main+0x78) * SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x401204 (2.exe!_e xcept_handler3) SEH3 frame. previous trylevel=0 scopetable entry[0]. previous try level=-1, filter=0x401531 (2.e xe!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 (n tdll.dll!___safe_se_handler_table+0x20) handler=0x771f90eb (ntdl l.dll!_TppTerminateProcess@4+0x43) * SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdl l.dll!_FinalExceptionHandler@16)我们看到,SEH链包含4个handler。前面两个是我们的例子。两个?但是我们只有一个?是的,一个是CRT的_mainCRTStartup()函数设置的。并至少作为FPU异常的处理。它的源码可以在MSVC的安装目录找到:crt/src/winxfltr.c。 568Windows-NT第三个是ntdll.dll的SEH4,第四个handler也位于ntdll.dll,跟MSVC没什么关系,它有一个自描述函数名。正如你所见,在一个链中有三种类型的处理函数:一个跟MSVC(最后一个)没什么关系和两个与MSVC关联的:SEH3和SEH4。SEH3: 两个try/except块例子 569Windows-NT #include #include #include 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(), GetExcep tionInformation())) { // the filter_user_exceptions() function answering to th e question // "is this exception belongs to this block?" // if yes, do the follow: printf("user exception caught\n"); } }现在有两个try块,所以scope table现在有两个元素,每个块占用一个。Previoustry level随着try块的进入或退出而改变。 570Windows-NTListing 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 571Windows-NT 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 entere d. set previous try level to 0 mov DWORD PTR __$SEHRec$[ebp+20], 1 ; inner try block entere d. 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 cra sh' 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] 572Windows-NT push OFFSET FLAT:$SG74621 ; 'access violation, can''t recove r' 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 exite d, 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 exite d. 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()函数设置一个断点,可以看到另一个SEHhandler如何被添加。同样,我们还可以看到scope table包含两个元素。 tracer.exe -l:3.exe bpx=3.exe!printf --dump-seh 573Windows-NTListing 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!_e xcept_handler3) SEH3 frame. previous trylevel=1 scopetable entry[0]. previous try level=-1, filter=0x401120 (3.e xe!main+0xb0) handler=0x40113b (3.exe!main+0xcb) scopetable entry[1]. previous try level=0, filter=0x4010e8 (3.ex e!main+0x78) handler=0x401100 (3.exe!main+0x90) * SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x4012e0 (3.exe!_e xcept_handler3) SEH3 frame. previous trylevel=0 scopetable entry[0]. previous try level=-1, filter=0x40160d (3.e xe!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 (n tdll.dll!___safe_se_handler_table+0x20) handler=0x771f90eb (ntdl l.dll!_TppTerminateProcess@4+0x43) * SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdl l.dll!_FinalExceptionHandler@16)SEH4在缓冲区攻击期间(18.2章),scope table的地址可以被重写。所以从MSVC2005开始,SEH3升级到SEH4后有了缓冲区溢出保护。现在scope table指针与一个security cookie(一个随机值)做异或运算。scope table扩展了包含两个指向security cookie指针的头部。每个元素都有另一个栈内偏移值:栈帧的地址(EBP)与security_cookie异或。该值将在异常处理过程中读取并检查其正确性。栈中的security cookie每次都是随机的,所以远程攻击者无法预测到它。SEH4的previous try level初始化值是-2而不是-1。 574Windows-NT这里有两个使用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 575Windows-NT _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_coo kie lea eax, DWORD PTR __$SEHRec$[ebp+8] ; pointer to VC_EXCE PTION_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 576Windows-NT ; 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 ENDPListing 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 577Windows-NT _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 point er to scope table xor eax, ebp ; ebp ^ secur ity_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 b lock, 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 578Windows-NT 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 b lock, setting previous try level back to 0 $LN2@main: mov DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both block s, 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 block s, 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 579Windows-NT 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 580Windows-NT正如你所认为的,每个函数序言在设置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 re cover' call printf npad 1 ; align next label $LN8@main: 581Windows-NT 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 ENDSListing 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 582Windows-NT 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 re cover' 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 583Windows-NT 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 584Windows-NT 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 ExceptionHandling”. In: MSDN magazine (). URL: http://go.yurichev.com/17293.Igor Skochinsky. Compiler Internals: Exceptions and RTTI. Also available ashttp://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 585Windows-NT typedef struct _RTL_CRITICAL_SECTION { PRTL_CRITICAL_SECTION_DEBUG DebugInfo; // // The following three fields control entering and exiting t he critical // section for the resource // LONG LockCount; LONG RecursionCount; HANDLE OwningThread; // from the thread's ClientId->UniqueTh read HANDLE LockSemaphore; ULONG_PTR SpinCount; // force size on 64-bit systems when pa cked } 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 586Windows-NT在这段代码中最重要的指令是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 ... skippedXADD指令功能是:交换并相加。这种情况下,LockCount加1并把结果保存到EBX寄存器,同时把1赋值给LockCount。这个操作是原子的,因为它使用了LOCK前缀,这意味着系统会阻塞其它CPU或CPU核心同时访问这块内存。LOCK前缀是非常重要的:如果两个线程,每个都工作在不同的CPU或CPU核心,它们都能够进入critical section并修改内存数据,这种行为将导致不确定的后果。 587Windows-NT 588Part VII 工具Part VII 工具 589反汇编器第六十九章反汇编器69.1 IDA较老的可下载的免费版本:http://go.yurichev.com/17031短热键列表(第977页) 590调试器第七十章调试器70.1 OllyDbg非常流行的win32用户态调试器 http://go.yurichev.com/17032 短热键列表(第977页)70.2 GDBGDB在逆向工程师中并不非常流行,但用起来非常舒适。部分命令(第978页)70.3 tracer我用tracer代替调试器。我最终不再使用调试器是因为我所需要的只是在代码执行的时候找到函数的参数,或者寄存器在某点的状态。每次加载调试器的时间太长,因此我编写了一个小工具tracer。它有控制台接口,运行在命令行下,允许我们给函数下断,查看寄存器状态,修改值等等。但出于学习的目的更建议在调试器中手动跟踪代码,观察寄存器状态是怎么变化的(比如经典的SoftICE,Ollydbg,Windbg寄存器值发生变化会高亮),手动修改标志位,数据然后观察效果。 591系统调用的追踪第七十一章系统调用跟踪71.0.1 stace/dtruss显示当前进程的系统调用(第697页)。比如: # strace df -h ... access("/etc/ld.so.nohwcap", F_OK) = -1 ENOENT (No such fil e 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_DENYWR ITE, 3, 0) = 0xb75b3000Mac OS X 的dtruss也有这个功能。Cygwin也有strace,但如果我理解正确的话,它只为cygwin环境编译exe文件工作。 592反编译器第七十二章反编译器只有一个已知的,公开的,高质量的反编译C代码的反编译器:Hex-Rayshttp://go.yurichev.com/17033 593其他工具第七十三章其他工具 Microsoft Visual Studio Express1:Visual Studio精简版,方便做简单的实验。 部分有用的选项(第978页) Hiew:适用于二进制文件小型修改 binary grep:大量文件中搜索常量(或者任何有序字节)的小工具,也可以用于不 可执行文件:GitHub 594Part IX 逆向文件格式的例子Part IX 逆向文件格式的例子 595基本的异或加密第八十四章简单异或加密84.1Norton Guide这款工具在MS-DOS时代很受欢迎,作为超文本参考手册程序常驻在系统中。 Norton Guide的数据库文件扩展名是.ng,内容看上去是加密的:为什么我说内容是加密的而不是压缩的呢?可以看到,0x1A字节(看起来是“→”)经常出现,而在压缩文件中不会有这种情况,所以这是个加密文件。同时我们也发现大段只包含拉丁字母的部分,看上去就像未知语言的字符串。0x1A字节出现得频率很高,我们可以尝试解密这个文件,先假设它是用最简单的异或加密。如果我们用0x1A和Hiew中的每个字节异或,我们就能看见熟悉的英文字符串: 596基本的异或加密与单个固定字节异或是最简单的可能的加密方法,有时可能会碰到。现在我们理解了为什么0x1A出现的频率如此高了:因为文件包含了大量0字节,加密之后替换成了0x1A。但是常量可能会不同。在这个例子中,我们可以尝试0到255之间的每一个常量,在解密文件中寻找熟悉的内容,256就不行了。更多关于Norton Guide文件格式内容: http://go.yurichev.com/1731784.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 597基本的异或加密我所做是加载文件,获取它的熵,解密保存之后再次获取它的熵(竟然是一样的!)。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字节密钥“加密”的结果: 598基本的异或加密容易发现有四个字符重复出现。事实上,PE文件头有许多0字节填充区,这也是密钥能被看出来的原因。下面是十六进制形式PE头的开头:下面是“加密”后: 599基本的异或加密容易发现密钥是这四个字节: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 600Millenium 的存档文件第八十五章Millenium 游戏存档文件"Millenium Return to Earth"是一款很老的DOS游戏(1991),你可以挖掘矿产资源,修建船只,在其他星球上装备它们等等。就像其他游戏一样,你可以将游戏状态存入一个文件中。咱们来看看能不能找到点什么。下面这是游戏中的一处矿井。有些星球的矿井工作更快,也有工作慢的。资源的设置也不同。来看看现在底下埋的是什么样的资源:我保存了游戏状态。存档文件大小为9538字节。我在游戏中等了"几天",现在我们可以从矿井中得到更多的资源: 601Millenium 的存档文件我再次保存了游戏状态。下面我们使用简单的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 ... 602Millenium 的存档文件这里的输出并不完整,有许多不同之处,我截取了结果来展示最有趣的部分。在第一个状态中,我有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):然后我加载这个已改变的文件到游戏中,查看矿井的数据:这就对了。 603Millenium 的存档文件现在我们尝试让这个游戏更快结束,把值设为最大:0xFFFF就是65536,现在我们拥有了许多资源:我在游戏中跳过了"几天",结果有些资源变少了: 604Millenium 的存档文件溢出发生了。游戏开发者可能没考虑到会出现这么多的资源的情况,没有设置溢出检查,但游戏中的矿井仍然在工作,资源在增加,所以导致了溢出。我想我不应该那么贪婪。可能大量的数值都保存在了这个文件中。这就是非常简单的游戏欺骗方法。高分文件可以通过这样打补丁轻松得到。更多关于文件和内存快照的比对:63.4 第681页 605Oracle RDBMS SYM-files第八十六章Oracle RDBMS: .SYM-files当一个Oracle RDBMS进程出于某种原因崩溃时,会将许多信息写入日志文件,包括栈回溯,就像这样: ----- Call Stack Trace ----- calling call entry argument values in hex location type point (? means du bious value) ------------------- -------- -------------------- ---------- ------------------ _kqvrow() 00000000 _opifch2()+2729 CALLptr 00000000 23D4B91 4 E47F264 1F19AE2 EB1C8A8 1 _kpoal8()+2832 CALLrel _opifch2() 89 5 EB1 CC74 _opiodr()+1248 CALLreg 00000000 5E 1C EB1F 0A0 _ttcpip()+1051 CALLreg 00000000 5E 1C EB1F 0A0 0 _opitsk()+1404 CALL??? 00000000 C96C040 5E EB1F0A0 0 EB1ED30 EB1F1CC 53E5 2E 0 EB1F1F8 _opiino()+980 CALLrel _opitsk() 0 0 _opiodr()+1248 CALLreg 00000000 3C 4 EB 1FBF4 _opidrv()+1201 CALLrel _opiodr() 3C 4 E B1FBF4 0 _sou2o()+55 CALLrel _opidrv() 3C 4 EB1FBF4 _opimai_real()+124 CALLrel _opimai_real() 2 EB1 FC2C _OracleThreadStart@ CALLrel _opimai() 2 EB1FF6 C 7C88A7F4 EB1FC34 0 4()+830 EB1FD04 77E6481C CALLreg 00000000 E41FF9C 0 0 E41FF9C 0 EB1FFC4 00000000 CALL??? 00000000当然Oracle RDBMS 可执行文件肯定拥有某种调试信息,或者带有符号信息(或者类似信息)的映射文件。 606Oracle RDBMS SYM-filesWindowsNT Oracle RDBMS 的符号信息包含在具有.SYM扩展名的文件中,具有专门的格式。(平文文件挺好的,但是需要额外解析,因此获取速度更慢)咱们来看看能不能理解它的格式。我选了最短的orawtc8.sym文件,来自于Oracle8.1.7 版本orawtc8.dll 文件。下面是Hiew加载后的效果:通过与其他.SYN文件的对比,我们可以快速发现"OSYM"总是头(和尾),因此这可能就是文件的标志。同时也可以看出,文件格式是:OSYM+一些二进制数据+以0为界定符的字符串+OSYM。这些字符串显然是函数和全局变量名。我标记了OSYM标志和字符串: 607Oracle RDBMS SYM-files咱们来看看。我在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 |OSY MB...........| 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位的值在这里明显不够。 608Oracle RDBMS SYM-files我检查了其他.SYM文件,证实了我的猜想:OSYM符号后面的32位值总表示文件字符串的数量。这对于所有的二进制文件来说几乎是通用的:文件头包含标志和文件其他信息。现在我们来进一步调查二进制块是什么。再次使用Hiew,我把从块头8个字节(32位计数值后面)开始一直到字符串块结尾的内容放入单独的文件中。在Hiew中看看这个二进制块:有一个明显的规律。我用红线划分了这个块:Hiew,就像其他的十六进制编辑器一样,每行显示16个字节。所以规律很容易看出来:每行有4个32位的值。这个规律容易看出来的原因是其中的一些值(地址0x104之前)总是具有0x1000xxxx的格式,以0x10和0字节开始。其他值(从地址0x108开始)都是0x0000xxxx的格式,总是以两个0字节开始。我们把这个块当作32位值的数组dump出来: 609Oracle RDBMS SYM-files $ 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文件时发现基址并不相同,不过没关系,第一个函数是: 610Oracle RDBMS SYM-files .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"。第二个函数是: 611Oracle RDBMS SYM-files .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 ; hM odule .text:603510BB call ds:GetProcAddress ..."ax_unreg"字符串也是字符串块的第二个字符串!第二个函数的开始地址是0x60351080,二进制块的第二个值是10001080.因此就是这个地址,但对于DLL加上了默认基地址现在可以快速的检查然后确定数组开始的66个值(也就是数组前一半)只是DLL中的函数地址,包括一些标签等等。那么另一半是什么呢?剩余的66个值都是以0x0000开始的,看上去范围是[0...0x3FB8]。并且他们看上去不像位域:序列的数量在增长。最后一个十六进制数字看上去是随机的。因此不像是地址(如果它是4字节,8字节,16字节则可除尽)我们问问自己吧:Oracle RDBMS的开发者还会在文件中保存什么呢?随便猜猜:可能是文本字符串(函数名)的地址。可以迅速验证这一点,是的,每个数字代表的就是字符串在这个块中第一个字符的位置。就是这样,完成了!我写了一个工具将这些.SYM文件转换到IDA脚本中,然后我可以加载.IDC脚本,设置函数名: 612Oracle RDBMS SYM-files #include #include #include #include #include #include #include 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 \n\n"); printf ("static main() {\n"); for (i=0; i

逆向工程入门指南
1.11.21.2.11.2.21.2.31.2.41.2.51.2.61.2.71.2.81.2.91.2.101.2
1.3.41.3.51.41.4.11.4.21.4.31.4.41.4.51.4.61.4.71.4.81.4.91.
1.7.51.81.8.11.8.21.8.31.8.41.8.51.91.9.11.9.21.9.31.9.41.10
还剩 629页未读,点此继续全文在线阅读

免费下载逆向工程入门指南到电脑,使用更方便!

本文推荐: 逆向工程入门指南.pdf全文阅读下载  关键词: 逆向工程   工程   入门   入门指南  
学文库温馨提示:文档由用户自行上传分享,文档预览可能有差异,下载后仅供学习交流,未经上传用户书面授权,请勿作他用。 文档下载资源交流QQ群:317981604

逆向工程入门指南pdf目录

< / 633>

QQ|小黑屋|网站声明|网站地图|学文库 ( 冀ICP备06006432号 )

GMT+8, 2020-8-8 05:27 , Processed in 1.029305 second(s), 5 queries , Gzip On, Redis On.

Powered by 学文库 1.0

Copyright © 2019-2020, 学文库

返回顶部