版权信息

书名：逆向工程权威指南

作者：[乌克兰] Dennis Yurichev 丹尼斯

排版：昷一

出版社：人民邮电出版社

出版时间：2017-04-01

ISBN：9787115434456

— · 版权所有 侵权必究 · —

内容提要

逆向工程是一种分析目标系统的过程，旨在识别系统的各组件以及组件间关系，以便能够通过其他形式或在较高的抽象层次上，重建系统的表征。

本书专注于软件的逆向工程，是写给初学者的一本权威指南。全书共分为上、下两册，十二个部分，共102章，涉及X86/X64、ARM/ARM-64、MIPS、Java/JVM等重要内容，详细解析了Oracle RDBMS、Itanium、软件狗、LD_PRELOAD、栈溢出、ELF、Win32 PE文件格式、x86-64（critical sections、syscalls、线程本地存储TLS、地址无关代码（PIC）、以配置文件为导向的优化、C++ STL、OpenMP、SHE等众多技术问题，堪称是逆向工程技术百科全书。除了详细讲解，本书还给出了很多习题来帮助读者巩固所学的知识，附录部分给出了习题的解答。

本书适合对逆向工程技术、操作系统底层技术、程序分析技术感兴趣的读者阅读，也适合专业的程序开发人员参考。

序　从逆向视角透视代码大厦的那些砖石

相对于本书的中文名字《逆向工程权威指南》，我更喜欢它的英文原名《Reverse Engineering for Beginners》，可以直译为“逆向工程入门者必读”。在国外的程序员圈中，这本书被简称为《RE4B》，作者的Blog式连载已经引发了持续关注和好评。

本书的重要特色在于其清晰的章节结构，涵盖了函数、语句、结构体、数组等这些构成代码大厦的基本元素，如显微镜般逐一对比了这些元素在X86、ARM和MIPS体系架构下的“切片”影像。本书几乎每个章节都按照X86、ARM、MIPS三个体系架构分别展开，可以让读者深入对比理解在三种体系架构下，同样的指令、函数及数据结构呈现的异同和特点。无论对分析工程师，还是编码工程师来说，本书都是能更深入理解代码大厦原材料的工作指南。这是基础性、系统性的文献工作，也是令人耳目一新的结构安排。对于在中国传统高校计算机教育中，通过简单的X86 ASM来学习和了解体系结构的工程师来说，这也是尽快打通ARM、MIPS知识背景的“全栈”指南。对于那些从各种破解教程来切入逆向领域的安全爱好者来说，这更像是一份“正餐”。对于作者来说，我相信这个写作过程是充满激情的，但也充满了艰难和痛苦。对于Dennis这样的逆向工程专家来说，这本书的写作过程，更多的不是在探索未知的世界，也不是对高级技巧和经验的总结，而是要对看起来相对枯燥的单元式资料进行整理，将一些对其已经是常识的东西转化为系统而书面化（并易于读者理解）的语言。

逆向工程一直被宣传为一种充满乐趣和带有神秘主义的技能，而逆向分析者的工作看起来更有乐趣的原因，似乎就是在指令奔涌中逆流而上，绕开种种限制和保护，找到代码中的“宝藏”——错误、漏洞或者是被加密算法层层掩盖的数据结构。这个由媒体所打造的形象，也为部分逆向爱好者所自矜。而这也导致逆向领域的书籍和教程，更多地围绕着一些高阶的技巧展开，更多地讲解如何绕过加密和保护，却忘记了逆向工程的初衷是要洞察系统及软件的整体结构和机理。由于之前的逆向领域相关出版物中缺少“代码大全”式的基础读本，本书的完成填补了这一空白，然而，这不仅需要作者有高超的逆向研究能力，也要有正向系统的思维和视野。

本书的译者之一Archer提出想邀请安天的工程师们一同完成本书的翻译时，我曾经觉得他是不是“疯了”。本书当时并无定稿的英文版本，只是Dennis持续发布的Blog式的连载。尽管网络连载已经引发了好评如潮，但其精彩篇章彼时还如散落在海底的珍珠，尚未串成珠链。以Dennis天马行空的写作风格和追求完美主义的性格，他必然会在未完成全部章节的期间，不断地增加体系结构的新热点（如ARM64）和修补既有章节的内容，甚至会改动原有的样例代码，导致本书原版定稿遥遥无期。我觉得，与其说正在养病的Archer接手翻译这本书是一份艰难的任务，倒不如说，Dennis写完本书根本就是一份不可完成的任务。

因此当接近千页的样书摆在我面前时，我被深深震撼了，那种感觉和我之前依靠自己极为蹩脚的英语扫过的英文电子稿不同，我能想象Archer是如何一边咳嗽着、一边码字或者校对代码；以及安天CERT的同事们是如何在样本分析任务饱和的情况下挤出时间来一点点推动翻译进展的。当我在视频例会上向安天各地的部门负责人展示样书时，我能看到每个人的赞叹和兴奋，那种兴奋不啻于我们自己发布了一份最新的长篇分析报告。

二进制分析，是安全分析工程师，包括有志于投身安全领域的编码工程师的基本功底之一。硅谷一些安全人士都曾提及一个有趣的说法——华人的系统安全天赋。中国安全厂商和安全工作者，在系统安全领域和分析工作中，正在不断取得新的进步。在国际网络安全企业中，华人承担关键系统安全研究和分析工作的占比也很高，国际高校的一些华人研究者在新兴领域展开系统安全研究，同样取得了很大的国际影响。遗憾的是，Web和应用开发的兴起，一定程度上让更多年轻安全从业人员的目光转向Web和应用安全，当前新的安全从业者整体的二进制分析能力正在下降，存在着基本功缺失的问题，非常需要系统性的补课。

二进制分析能力，是对安全工程师的基础能力要求。但要对抗APT等新兴威胁，仅仅有基本功是不够的，还需要在更开阔的架构视野上，具备从攻击链、体系化等角度逆向分析研判攻击行为的全程、攻击对手的全貌的能力。

当年受到一些破解教程的误导，一些接触逆向工程的年轻人把破解序列号等当成逆向工程的目的，从而导致浅尝辄止，没有真正理解逆向工程的博大精深。即使是安天部分资深的分析工程师，也往往更乐于寻找解析攻击者的个性技巧和那些有“难度”的分析细节，并常以此感到自豪，而不能站到对攻击作业过程完整复盘和对攻击者画像的角度来看待问题，而后者才是一类高阶的逆向工程。

我们是从分析“震网”所遇到的系列挫折中，从对Flame蠕虫马拉松式的、收效甚微的模块分析中，认识到了传统分析工程师所面临的这种困境的。在“震网”分析中，由国际知名厂商的架构师和分析工程师所组成的混编分析团队，就能比安天完全由软硬件安全分析工程师组成的分析小组，取得更多的分析成果和进展。因为安全厂商和APT攻击之间，不是简单的查杀反制和单点的分析对抗，而是体系化对抗。从认识到这个问题开始，安天的分析团队一方面继续强化二进制分析的基本功底；另一方面自我批判那种“视野从入口点开始”的狭隘分析视角，避免我们自己从透析攻击者载荷的高级技巧中找到太多“快感”，而忘记那些更重要的全局性因素。

过去三年间，安天发布了针对APT-TOCS、白象、方程式等组织相关的多篇分析报告，也分析了类似乌克兰国家电网遭遇攻击停电等与关键基础设施有关的安全事件。在乌克兰停电事件的分析中，我们已经初步走出了入口点视野，而尝试在更大的时空视角上还原事件。

而即使从微观的分析来看，高阶恶意代码也已经从一个附着在宿主上的代码片段或单体文件的木马，进化为一个庞大的工程体系。对于高级恶意代码来说，不仅有前端的持久化载荷和大量按需投放的“原子化”模块，其后更有一个庞大的支撑工程体系。对于前端，我们还能够依靠代码逆向来分析，而对于后面这个大到无形的“魅影”，我们更多的只能依靠分析恶意代码的指令体系、有限的网络感知捕获、少得可怜的可公开检索信息，以及作为信息安全动物的逻辑和本能来进行猜测。我们所要完成的工作，不仅仅是要逐一进行载荷模块分析，而是要对整个攻击面和攻击链的深入复盘。

2017年1月25日，安天分析小组更新了针对方程式攻击组织的第四篇分析报告《方程式组织EQUATION DRUG平台解析》，基于对方程式组织大量恶意代码模块的分析，我们初步完成了一份有价值的工作，那就是方程式组织的主机作业模块积木拼图。

我的同事在《方程式组织EQUATION DRUG平台解析》中写道：

“这些庞杂的模块展开了一组拼图碎片，每一张图上都有有意义的图案，但如果逐一跟进，这些图案就会组成一个巨大的迷宫。迷宫之所以让人迷惑，不在于其处处是死路，而在于其看起来处处有出口，但所有的砖块都不能给进入者以足够的提示。此时，最大的期待，不只是有一只笔，可以在走过的地方做出标记，而是插上双翼凌空飞起，俯瞰迷宫的全貌，当然这是一种提升分析方法论的自我期待。我辈当下虽身无双翼，但或可练就一点灵犀。”

所有高阶的分析工作，都是以最基础的代码分析为起点的。无论是面对APT攻击的深度分析，还是对高级黑产行为的系统挖掘，仅有基本分析固然是远远不够的，但没有最基本的分析是万万不能的。从未来安全工程师所需要的能力来看，熟读本书并不能使你走出迷宫。但不掌握本书相关的基础能力，不能去透视代码迷宫的砖石，就连走入迷宫的资格都没有。也正因为此，本书是安天工程师人手一本的必备手册和必读之物。

肖新光

安天创始人，首席技术架构师，反病毒老兵

前言

“逆向工程”一词用在软件工程领域的具体含义历来模糊不清。逆向工程是一种分析目标系统的过程，旨在识别系统的各组件以及组件之间的关系，以便通过其他形式或在较高的抽象层次上，重建系统的表征。按照目标系统的分类划分，人们常说的“逆向工程”大体可分为：

（1）软件逆向工程。研究编译后的可执行程序。

（2）建模逆向工程。扫描3D结构并进行后续数据处理，以便重现原物。

（3）重建DBMS结构。

本书仅涉及上述第一项，即软件逆向工程的知识范畴。

重点议题

x86/x64、ARM/ARM64、MIPS、Java/JVM。

涉及话题

本书中涉及如下一些话题：

Oracle RDBMS（第81章）、Itanium（IA64，第93章）、加密狗（第78章）、LD_PRELOAD（67.2节）、栈溢出、ELF、Win32 PE文件格式（68.2节）、x86-64（26.1节）、critical sections（68.4节）、syscalls（第66章），线程本地存储TLS，地址无关代码PIC（67.1节）、以配置文件为导向的优化（95.1节）、C++ STL（51.4节）、OpenMP（第92章），SEH（68.3节）。

作者简介

姓名：Dennis Yurichev

特长：逆向工程及计算机编程

联系方式：E-mail dennis(a)yurichev.com

Skype dennis.yurichev

译者简介

安天安全研究与应急处理中心（安天CERT），是承担安天安全威胁应急处理、恶意代码分析、APT攻击分析取证等方面工作的综合研究和服务的部门，由资深安全工程师团队组成。安天CERT以“第一时间启动，同时应对两线严重威胁”为自身能力建设目标，在红色代码Ⅱ、口令蠕虫、震荡波、冲击波等重大安全疫情的响应中，提供了先发预警、深度分析和解决方案；并在2010年后针对震网、毒曲、白象、方程式等APT攻击组织和行动，进行了深入的跟踪分析。分析成果有效推动了安天核心引擎和产品能力的成长，获得了主管部门和用户的好评。

致谢

感谢耐心回答我提问的Andrey “herm1t” Baranovich和Slava “Avid” Kazakov。

感谢帮助我勘误的Stanislav “Beaver” Bobrytskyy、Alexander Lysenko、Shell Rocket、Zhu Ruijin和Changmin Heo。

感谢Andrew Zubinski、Arnaud Patard (rtp on #debian-arm IRC)和Aliaksandr Autayeu的鼎力支持。

感谢本书的中文版翻译Archer和安天安全研究与应急处理中心。

感谢本书的韩语版翻译Byungho Min。

感谢校对人员Alexander “Lstar” Chernenkiy、Vladimir Botov、Andrei Brazhuk、Mark “Logxen” Cooper、 Yuan Jochen Kang、Mal Malakov、Lewis Porter和Jarle Thorsen。

特别感谢承担最多校对工作的，同时也是补救最多纰漏的朋友Vasil Kolev。

感谢封面设计Andy Nechaevsky。

感谢github.com的朋友，谢谢他们所发的各种资料以及勘误。

本书使用了LaTeX的多种工具。在此，我向它们的作者表示敬意。

捐赠

希望鼓励作者继续创作的读者，通过下述网站进行捐赠：

Dennis Yurichevdonate.html

为了表示感谢，每位捐赠者都会获得题名。此外，捐赠者感兴趣的内容将会被优先更新或补充。

捐赠人名录

25 * anonymous, 2 * Oleg Vygovsky (50+100 UAH), Daniel Bilar ($50), James Truscott ($4.5), Luis Rocha ($63), Joris van de Vis ($127), Richard S Shultz ($20), Jang Minchang ($20), Shade Atlas (5 AUD), Yao Xiao ($10), PawelSzczur (40 CHF), Justin Simms ($20), Shawn the R0ck ($27), Ki Chan Ahn ($50), Triop AB (100 SEK), AngeAlbertini (10+50 EUR), Sergey Lukianov (300 RUR), LudvigGislason (200 SEK), Gérard Labadie (40 EUR), Sergey Volchkov (10 AUD), VankayalaVigneswararao ($50), Philippe Teuwen ($4), Martin Haeberli ($10), Victor Cazacov (5 EUR), Tobias Sturzenegger (10 CHF), Sonny Thai ($15), BaynaAlZaabi ($75), Redfive B.V. (25 EUR), JoonaOskariHeikkilä (5 EUR), Marshall Bishop ($50), Nicolas Werner (12 EUR), Jeremy Brown ($100), Alexandre Borges ($25), Vladimir Dikovski (50 EUR), Jiarui Hong (100.00 SEK), Jim_Di (500 RUR), Tan Vincent ($30), Sri HarshaKandrakota (10 AUD), Pillay Harish (10 SGD), TimurValiev (230 RUR), Carlos Garcia Prado (10 EUR), Salikov Alexander (500 RUR), Oliver Whitehouse (30 GBP), Katy Moe ($14), Maxim Dyakonov ($3), Sebastian Aguilera (20 EUR), Hans-Martin Münch (15 EUR), JarleThorsen (100 NOK), VitalyOsipov ($100)。

FAQ

Q：学习汇编语言有何用武之地？

A：除去开发操作系统的研发人员之外，现在几乎没有什么人还要用汇编语言编写程序了。目前，编译程序优化汇编指令的水平已经超越编程人员的脑算水平[1]，而且CPU越来越复杂——即使一个人具备丰富的汇编语言的知识，也不代表他有多么了解计算机硬件。但是不可否认的是，汇编语言的知识至少有两大用处：首先，它有助于安全人员进行安全研究、分析恶意软件；其次，它还有助于帮助编程人员调试程序。本书旨在帮助人们理解汇编语言，而不是要指导读者用汇编语言进行编程。所以，作者组织了大量的源代码和对应的汇编指令，供读者研究。

Q：这本书太厚了，有没有精简版？

A：精简版可从网上下载：http://beginners.re/#lite

。

Q：逆向工程方面的就业情况如何？

A：在reddit等著名网站里，很多著名公司一直在招聘熟悉汇编语言和逆向工程的IT专家，甚至专门招聘逆向工程领域的安全专家。有兴趣的读者可以访问以下网址：

http://www.reddit.com/r/ReverseEngineering/

http://www.reddit.com/r/netsec/comments/221xxu/rnetsecs_q2_2014_information_security_hiring

Q：我想要提些问题……

A：作者的邮件地址是：dennis(a)yurichev.com。

读者还可以在我们的网站forum.yurichev.com参与互动。

读者点评

“构思精巧……而且免费……确实不错”——Daniel Bilar, Siege Technologies, LLC。

“……了不起的免费读物！”——Pete Finnigan, Oracle RDBMS security guru。

“……引人入胜，值得一读!”——Michael Sikorski,《Practical Malware Analysis: The Hands-On Guide to DissectingMalicious Software》的作者。

“……谨向这本出色的教材致以个人的敬意!”——Herbert Bos，阿姆斯特丹自由大学教授，《Modern Operating Systems（4th Edition）》作者之一。

“……令人惊讶、难以置信”。——Luis Rocha, CISSP / ISSAP，技术经理，Verizon业务部网络及信息安全中心。

“感谢如此辛劳的作者、感谢如此精彩的书。”——Joris van de Vis, SAP 集成平台及安全专员。

“……部分内容可圈可点。”——Mike Stay，教员，联邦执法训练中心，Georgia, US。

“这本书超赞！我的数名学生都在学习这本书，我计划把它当作研究生教材。”——Sergey Bratus，计算机科学系研究助理教授，（美）达特茅斯学院。

“Dennis Yurichev发表了一本逆向工程方面的宝典（还免费）！”TanelPoder, Oracle RDBMS性能调控专家。

“这本书可谓初学者的百科全书……”——Archer，中文译者，IT安全研究员。

[1]　可查阅参考文献[Fog13b]。

第一部分
指令讲解

在最初接触C/C++时，我就对程序编译后的汇编指令十分着迷。按照从易到难的顺序，我循序渐进地研究了C/C++语言编译器生成汇编指令的模式。经过日积月累的努力，现在我不仅可以直接阅读x86程序的汇编代码，而且能够在脑海里将其还原成原始的C/C++语句。我相信这是学习逆向工程的有效方法。为了能够帮助他人进行相关研究，我把个人经验整理成册，以待与读者分享。

本书包含大量x86/x64和ARM框架的范例。如果读者熟悉其中某一种框架，可以跳过相关的篇幅。

第1章 CPU简介

CPU是执行程序机器码的硬件单元。简要地说，其相关概念主要有以下几项。

指令码：CPU受理的底层命令。典型的底层命令有：将数据在寄存器间转移、操作内存、计算运算等指令。每类CPU都有自己的指令集架构（Instruction Set Architecture，ISA）。

机器码：发送给CPU的程序代码。一条指令通常被封装为若干字节。

汇编语言：为了让程序员少长白头发而创造出来的、易读易记的代码，它有很多类似宏的扩展功能。

CPU寄存器：每种CPU都有其固定的通用寄存器（GPR）。x86 CPU里一般有8个GPR，x64里往往有16个GPR，而ARM里则通常有16个GPR。您可以认为CPU寄存器是一种存储单元，它能够无差别地存储所有类型的临时变量。假如您使用一种高级的编程语言，且仅会使用到8个32位变量，那么光CPU自带的寄存器就能完成不少任务了！

那么，机器码和编程语言（PL）的区别在哪里？CPU可不像人类那样，能够理解C/C++、Java、Python这类较为贴近人类语言的高级编程语言。CPU更适合接近硬件底层的具体指令。在不久的将来，或许会出现直接执行高级编程语言的CPU，不过那种尚未问世的科幻CPU必定比现在的CPU复杂。人脑和计算机各有所长，如果人类直接使用贴近硬件底层的汇编语言编写程序，其难度也很高——因为那样很容易出现大量的人为失误。可见，我们需要用一种程序把高级的编程语言转换为CPU能受理的底层汇编语言，而这种程序就是人们常说的编译器/Compiler。

1.1　指令集架构

在x86的指令集架构（ISA）里，各opcode（汇编指令对应的机器码）的长度不尽相同。出于兼容性的考虑，后来问世的64位CPU指令集架构也没有大刀阔斧地摒弃原有指令集架构。很多面向早期16位8086 CPU的指令，不仅被x86的指令集继承，而且被当前最新的CPU指令集继续沿用。

ARM属于RISC[1] CPU，它的指令集在设计之初就力图保持各opcode的长度一致。在过去，这一特性的确表现出了自身的优越性。最初的时候，所有ARM指令的机器码都被封装在4个字节里[2]。人们把这种运行模式叫作“ARM模式”。

不久，他们就发现这种模式并不划算。在实际的应用程序中，绝大多数的CPU指令[3]很少用满那4个字节。所以他们又推出了一种把每条指令封装在2个字节的“Thumb”模式的指令集架构。人们把采用这种指令集编码的指令叫作“Thumb模式”指令。然而Thumb指令集并不能够封装所有的ARM指令，它本身存在指令上的局限。当然，在同一个程序里可以同时存在ARM模式和Thumb模式这两种指令。

之后，ARM的缔造者们决定扩充Thumb指令集。他们自ARM v7平台开始推出了Thumb-2指令集。Thumb-2指令基本都可封装在2个字节的机器码之中，2个字节封装不下的指令则由4字节封装。现在，多数人依然错误地认为“Thumb-2指令集是ARM指令集和Thumb指令集的复合体”。实际上，它是一种充分利用处理器性能、足以与ARM模式媲美的独立的运行模式。在扩展了Thumb模式的指令集之后，Thumb-2现在与ARM模式不相上下。由于Xcode编译器默认采用Thumb-2指令集编译，所以现在主流的iPod/iPhone/iPad应用程序都采用了Thumb-2指令集。

64位的ARM处理器接踵而至。这种CPU的指令集架构再次使用固定长度的4字节opcode，所以不再支持Thumb模式的指令。相应地，64位ARM工作于自己的指令集。受到指令集架构的影响，ARM指令集分为3类：ARM模式指令集、Thumb模式指令集（包括Thumb-2）和ARM64的指令集。虽然这些指令集之间有着千丝万缕的联系，需要强调的是：不同的指令集分别属于不同的指令集架构；一个指令集绝非另一个指令集的变种。相应地，本书会以3种指令集、重复演示同一程序的指令片段，充分介绍ARM应用程序的特点。

除了ARM 处理器之外，还有许多处理器都采用了精简指令集。这些处理器多数都使用了固定长度的32位opcode。例如MIPS、PowerPC和Alpha AXP处理器就是如此。

[1]　Reduced instruction computing /精简指令集。

[2]　这种固定长度的指令集，特别便于计算前后指令的地址。有关特性将在13.2.2节进行介绍。

[3]　即MOV/PUSH/CALL/Jcc等指令。

第2章 最简函数

返回预定常量的函数，已经算得上是最简单的函数了。

本章围绕下列函数进行演示：

指令清单2.1　C/C++ 代码

int f()
{
         return 123;
}

2.1　x86

在开启优化功能之后，GCC编译器产生的汇编指令，如下所示。

指令清单2.2　Optimizing GCC/MSVC（汇编输出）

f:
        mov   eax, 123
        ret

MSVC编译的程序和上述指令完全一致。

这个函数仅由两条指令构成：第一条指令把数值123存放在EAX寄存器里；根据函数调用约定[1]，后面一条指令会把EAX的值当作返回值传递给调用者函数，而调用者函数（caller）会从EAX寄存器里取值，把它当作返回结果。

2.2　ARM

ARM模式是什么情况？

指令清单2.3　Optimizing Keil 6/2013 (ARM模式)

f PROC
         MOV    r0,#0x7b ; 123
         BX     lr
         ENDP

ARM程序使用R0寄存器传递函数返回值，所以指令把数值123赋值给R0。

ARM程序使用LR寄存器（Link Register）存储函数结束之后的返回地址（RA/ Return Address）。x86程序使用“栈”结构存储上述返回地址。可见，BX LR指令的作用是跳转到返回地址，即返回到调用者函数，然后继续执行调用体caller的后续指令。

如您所见，x86和ARM指令集的MOV指令确实和对应单词“move”没有什么瓜葛。它的作用是复制（copy），而非移动（move）。

2.3　MIPS

在MIPS指令里，寄存器有两种命名方式。一种是以数字命名（$0～$31），另一种则是以伪名称（pseudoname）命名（$V0～VA0，依此类推）。在GCC编译器生成的汇编指令中，寄存器都采用数字方式命名。

指令清单2.4　Optimizing GCC 4.4.5（汇编输出）

  j       $31
  li      $2,123          # 0x7b

然而IDA则会显示寄存器的伪名称。

指令清单2.5　Optimizing GCC 4.4.5（IDA）

        jr      $ra
        li      $v0, 0x7B

根据伪名称和寄存器数字编号的关系可知，存储函数返回值的寄存器都是$2（即$V0）。此处LI指令是英文词组“Load Immediate（加载立即数）”的缩写。

其中，J和JR指令都属于跳转指令，它们把执行流递交给调用者函数，跳转到$31即$RA寄存器中的地址。这个寄存器相当于的ARM平台的LR寄存器。

此外，为什么赋值指令LI和转移指令J/JR的位置反过来了？这属于RISC精简指令集的特性之一——分支（转移）指令延迟槽 (Branch delay slot)的现象。简单地说，不管分支（转移）发生与否，位于分支指令后面的一条指令（在延时槽里的指令），总是被先于分支指令提交。这是RISC精简指令集的一种特例，我们不必在此处深究。总之，转移指令后面的这条赋值指令实际上是在转移指令之前运行的。

MIPS指令集与寄存器名称

习惯上，MIPS领域中的寄存器名称和指令名称都使用小写字母书写。但是为了在排版风格上与其他指令集架构的程序保持一致，本书采用大写字母进行排版。

[1]　Calling Convention，又称为函数的调用协定、调用规范。

第3章 Hello，world！

现在，我们开始演示《C语言编程》一书[1]中著名的程序：

#include <stdio.h>

int main()
{
    printf("hello, world\n");
    return 0;
}

3.1　x86

3.1.1　MSVC

接下来我们将通过下述指令，使用MSVC 2010编译下面这个程序。

cl 1.cpp /Fa1.asm

其中/Fa选项将使编译器生成汇编指令清单文件（assembly listing file），并指定汇编列表文件的文件名称是1.asm。

上述命令生成的1.asm内容如下。

指令清单3.1　MSVC 2010

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

MSVC生成的汇编清单文件都采用了Intel语体。汇编语言存在两种主流语体，即Intel语体和AT&T语体。本书将在3.1.3节中讨论它们之间的区别。

在生成1.asm之后，编译器会生成1.obj再将之链接为可执行文件1.exe。

在hello world这个例子中，文件分为两个代码段，即CONST和_TEXT段，它们分别代表数据段和代码段。在本例中，C/C++程序为字符串常量“hello，world”分配了一个指针（const char[]），只是在代码中这个指针的名称并不明显（参照下列Bjarne Stroustrup. The C++ Programming Language, 4th Edition. 2013的第176页，7.3.2节）。

接下来，编译器进行了自己的处理，并在内部把字符串常量命名为$SG3830。

因此，上述程序的源代码等效于：

#include <stdio.h>

const char *$SG3830[]="hello, world\n";

int main()
{
    printf($SG3830);
    return 0;
}

在回顾1.asm文件时，我们会发现编译器在字符串常量的尾部添加了十六进制的数字0，即00h。依据C/C++字符串的标准规范，编译器要为这个字符串常量添加结束标志（即数值为零的单个字节）。有关标准请参照本书的57.1.1节。

在代码段_TEXT只有1个函数，即主函数main()。在汇编指令清单里，主函数的函数体有标志性的函数序言（function prologue）和函数尾声（function epilogue）。实际上所有的函数都有这样的序言和尾声。在函数的序言标志之后，我们能够看到调用printf()函数的指令: CALL _printf。

通过PUSH指令，程序把字符串的指针推送入栈。这样，printf()函数就可以调用栈里的指针，即字符串“hello, world!”的地址。

在printf()函数结束以后，程序的控制流会返回到main()函数之中。此时，字符串地址（即指针）仍残留在数据栈之中。这个时候就需要调整栈指针（ESP寄存器里的值）来释放这个指针。

下一条语句是“add ESP，4”，把ESP寄存器（栈指针/Stack Pointer）里的数值加4。

为什么要加上“4”？这是因为x86平台的内存地址使用32位（即4字节）数据描述。同理，在x64系统上释放这个指针时，ESP就要加上8。

因此，这条指令可以理解为“POP某寄存器”。只是本例的指令直接舍弃了栈里的数据而POP指令还要把寄存器里的值存储到既定寄存器[2]。

某些编译器（如Intel C++编辑器）不会使用ADD指令来释放数据栈，它们可能会用POP ECX指令。例如，Oracle RDBMS（由Intel C++编译器编译）就会用POP ECX指令，而不会用ADD指令。虽然POP ECX命令确实会修改ECX寄存器的值，但是它也同样释放了栈空间。

Intel C++编译器使用POP ECX指令的另外一个理由就是，POP ECX对应的OPCODE（1字节）比ADD ESP的OPCODE（3字节）要短。

指令清单3.2　Oracle RDBMS 10.2 Linux (摘自app.o)

.text:0800029A      push    ebx
.text:0800029B      call    qksfroChild
.text:080002A0      pop     ecx

本书将在讨论操作系统的部分详细介绍数据栈。

在上述C/C++程序里，printf()函数结束之后，main()函数会返回0（函数正常退出的返回码）。即main()函数的运算结果是0。

这个返回值是由指令“XOR EAX, EAX”计算出来的。

顾名思义，XOR就是“异或” [3]。编译器通常采用异或运算指令，而不会使用“MOV EAX，0”指令。主要是因为异或运算的opcode较短（2字节:5字节）。

也有一些编译器会使用“SUB EAX,EAX”指令把EAX寄存器置零，其中SUB代表减法运算。总之，main()函数的最后一项任务是使EAX的值为零。

汇编列表中最后的操作指令是RET，将控制权交给调用程序。通常它起到的作用就是将控制权交给操作系统，这部分功能由C/C++的CRT[4]实现。

3.1.2　GCC

接下来，我们使用GCC 4.4.1编译器编译这个hello world程序。

gcc 1.c -o 1

我们使用反汇编工具IDA（Interactive Disassembler）查看main()函数的具体情况。IDA所输出的汇编指令的格式，与MSVC生成的汇编指令的格式相同，它们都采用Intel语体显示汇编指令。

此外，如果要让GCC编译器生成Intel语体的汇编列表文件，可以使用GCC的选项“-S -masm=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\n"
             mov     [esp+10h+var_10], eax
             call    _printf
             mov     eax, 0
             leave
             retn
main         endp

GCC生成的汇编指令，与MSVC生成的结果基本相同。它首先把“hello, world”字符串在数据段的地址（指针）存储到EAX寄存器里，然后再把它存储在数据栈里。

其中值得注意的还有开场部分的“AND ESP, 0FFFFFFF0h”指令。它令栈地址（ESP的值）向16字节边界对齐（成为16的整数倍），属于初始化的指令。如果地址位没有对齐，那么CPU可能需要访问两次内存才能获得栈内数据。虽然在8字节边界处对齐就可以满足32位x86 CPU和64位x64 CPU的要求，但是主流编译器的编译规则规定“程序访问的地址必须向16字节对齐（被16整除）”。人们还是为了提高指令的执行效率而特意拟定了这条编译规范。[5]

“SUB ESP，10h”将在栈中分配0x10 bytes，即16字节。我们在后文看到，程序只会用到4字节空间。但是因为编译器对栈地址（ESP）进行了16字节对齐，所以每次都会分配16字节的空间。

而后，程序将字符串地址（指针的值）直接写入到数据栈。此处，GCC使用的是MOV指令；而MSVC生成的是PUSH指令。其中var_10是局部变量，用来向后面的printf()函数传递参数。

随即，程序调用printf()函数。

GCC和MSVC不同，除非人工指定优化选项，否则它会生成与源代码直接对应的“MOV EAX, 0”指令。但是，我们已经知道MOV指令的opcode肯定要比XOR指令的opcode长。

最后一条LEAVE指令，等效于“MOV ESP, EBP”和“POP EBP”两条指令。可见，这个指令调整了数据栈指针ESP，并将EBP的数值恢复到调用这个函数之前的初始状态。毕竟，程序段在开始部分就对EBP和EBP进行了操作（MOVEBP, ESP/AND ESP, ...），所以函数要在退出之前恢复这些寄存器的值。

3.1.3　GCC:AT&T语体

AT&T语体同样是汇编语言的显示风格。这种语体在UNIX之中较为常见。

接下来，我们使用GCC4.7.3编译如下所示的源程序。

指令清单3.4　使用GCC 4.7.3 编译源程序

gcc –S 1_1.c

上述指令将会得到下述文件。

指令清单3.5　GCC 4.7.3生成的汇编指令

        .file   "1_1.c"
        .section       .rodata
.LC0:
        .string  "hello, world\n"
        .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

在上述代码里，由小数点开头的指令就是宏。这种形式的汇编语体大量使用汇编宏，可读性很差。为了便于演示，我们将其中字符串以外的宏忽略不计（也可以启用GCC的编译选项-fno-asynchronous-unwind-tables，直接预处理为没有cfi宏的汇编指令），将会得到如下指令。

指令清单3.6　GCC 4.7.3生成的指令

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

在继续解读这个代码之前，我们先介绍一下Intel语体和AT&T语体的区别。

运算表达式（operands，即运算单元）的书写顺序相反。
Intel 格式：<指令><目标><源>。
AT&T 格式：<指令><源><目标>。
如果您认为Intel语体的指令使用等号（＝）赋值，那么您可以认为AT&T语法结构使用右箭头（→）进行赋值。应当说明的是，这两种格式里，部分C标准函数的运算单元的书写格式确实是相同的，例如memcpy()、strcpy()。

AT&T语体中，在寄存器名称之前使用百分号（%）标记，在立即数之前使用美元符号（$）标记。AT&T语体使用圆括号，而Intel语体则使用方括号。

AT&T语体里，每个运算操作符都需要声明操作数据的类型：

q－quad（64位）

l指代32位long型数据。

w指代16位word型数据。

b指代8位byte型数据。

其他区别请参考Sun公司发布的《x86 Assembly Language Reference Manual》。

现在再来阅读hello world的AT&T语体指令，就会发现它和IDA里看到的指令没有实质区别。有些人可能注意到，用于数据对齐的0FFFFFFF0h在这里变成了十进制的$-16——把它们按照32byte型数据进行书写后，就会发现两者完全一致。

此外，在退出main()时,处理EAX寄存器的指令是MOV指令而不是XOR指令。MOV的作用是给寄存器赋值（load）。某些硬件框架的指令集里有更为直观的“LOAD”“STORE”之类的指令。

3.2　x86-64

3.2.1　MSVC-x86-64

若用64位MSVC编译上述程序，则会得到下述指令。

指令清单3.7　MSVC 2012 x64

$SG2989  DB      'hello, world', 0AH 00H

main     PROC
         sub     rsp, 40
         lea     rcx, OFFSET FLAT:$SG2989
         call    printf
         xor     eax, eax
         add     rsp, 40
         ret     0
main     ENDP

在x86-64框架的CPU里，所有的物理寄存器都被扩展为64位寄存器。程序可通过R-字头的名称直接调用整个64位寄存器。为了尽可能充分地利用寄存器、减少访问内存数据的次数，编译器会充分利用寄存器传递函数参数（请参见64.3节的fastcall约定）。也就是说，编译器会优先使用寄存器传递部分参数，再利用内存（数据栈）传递其余的参数。Win64的程序还会使用RCX、RDX、R8、R9这4个寄存器来存放函数参数。我们稍后就会看到这种情况：printf()使用RCX寄存器传递参数，而没有像32位程序那样使用栈传递数据。

在x86-64硬件平台上，寄存器和指针都是64位的，存储于R-字头的寄存器里。但是出于兼容性的考虑，64位寄存器的低32位，也要能够担当32位寄存器的角色，才能运行32位程序。

在64位x86兼容的CPU中，RAX/EAX/AX/AL的对应关系如下。

7th（字节号）

6th

5th

4th

3rd

2nd

1st

0th

RAXx64

EAX

AX

AH

AL

main()函数的返回值是整数类型的零，但是出于兼容性和可移植性的考虑，C语言的编译器仍将使用32位的零。换而言之，即使是64位的应用程序，在程序结束时EAX的值是零，而RAX的值不一定会是零。

此时，数据栈的对应空间里仍留有40字节的数据。这部分数据空间有个专用的名词，即阴影空间（shadow space）。本书将在8.2.1节里更详细地介绍它。

3.2.2　GCC－x86-64

我们使用64位Linux的GCC编译器编译上述程序，可得到如下所示的指令。

指令清单3.8　GCC 4.4.6 x64

.string "hello, world\n"
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系统中的应用程序，会优先使用RDI、RSI、RDX、RCX、R8、R9这6个寄存器传递函数所需的头6个参数，然后使用数据栈传递其余的参数。[6]

因此，64位的GCC编译器使用EDI寄存器（寄存器的32位）存储字符串指针。EDI不过是RDI寄存器中地址位较低的32位地址部分。为何GCC不直接使用整个RDI寄存器？

需要注意的是，64位汇编指令MOV在写入R-寄存器的低32位地址位的时候，即对E-寄存器进行写操作的时候，会同时清除R-寄存器中的高32位地址位[7]。所以, “MOV EAX, 011223344h”能够对RAX寄存器进行正确的赋值操作，因为该指令会清除（置零）高地址位的内容。

如果打开GCC生成的obj文件，我们就能看见全部的opcode。[8]

指令清单3.9　GCC 4.4.6 x64

.text:00000000004004D0                   main  proc near
.text:00000000004004D0 48 83 EC 08       sub     rsp, 8
.text:00000000004004D4 BF E8 05 40 00    mov     edi, offset format ; "hello, world\n"
.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 进行了写操作，这部分代码的opcode占用了5个字节；相比之下，对RDI进行写操作的opcode则会占用7个字节。显然，出于空间方面的考虑，GCC进行了相应的优化处理。此外，因为32位地址（指针）能够描述的地址不超过4GB，我们可据此判断这个程序的数据段地址不会超过4GB。

在调用printf()之前，程序清空了EAX寄存器，这是x86-64框架的系统规范决定的。在系统与应用程序接口的规范中，EAX寄存器用来保存用过的向量寄存器(vector registers)。[9]

3.3　GCC的其他特性

只要C语言代码里使用了字符串型常量（可参照3.1.1节的范例），编译器就会把这个字符串常量置于常量字段，以保证其内容不会发生变化。不过GCC有个有趣的特征：它可能会把字符串拆出来单独使用。

我们来看下面这段程序：

#include <stdio.h>
void f1()
{
         printf ("world\n");
}

void f2()
{
         printf ("hello world\n");
}

int main()
{
         f1();
         f2();
}

多数的C/C++编译器（包括MSVC编译器）会分配出两个直接对应的字符串，不过GCC 4.8.1的编译结果则更为可圈可点。

指令清单3.10　在IDA中观察GCC 4.8.1 的汇编指令

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”的时候，这两个词的指针地址实际上是前后相邻的。在调用puts()函数进行输出时，函数本身不知道它所输出的字符串分为两个部分。实际上我们在汇编指令清单中可以看到，这两个字符串没有被“切实”分开。

在f1()函数调用puts()函数时，它输出字符串“world”和外加结束符（数值为零的1个字节），因为puts()函数并不知道字符串可以和前面的字符串连起来形成新的字符串。

GCC编译器会充分这种技术来节省内存。

3.4　ARM

根据我个人的经验，本书将通过以下几个主流的ARM编译器进行演示。

2013年6月版本的Keil编译器。

Apple Xcode 4.6.3 IDE (含LLVM-GCC 4.2编译器) 。[10]

面向 ARM64的GCC 4.9 (Linaro)，其32位的Windows程序可由下述网址下载：http://www. linaro.org/ projects/armv8/。

除非特别标注，否则本书中的ARM程序都是32位ARM程序。在介绍64位的ARM程序时，本书会称其为ARM64程序。

3.4.1　Keil　6/2013——未启用优化功能的ARM模式

请使用下述指令，用Keil编译器把hello world程序编译为ARM指令集架构的汇编程序：

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

虽然armcc编译器生成的汇编指令清单同样采用了Intel语体，但是程序所使用的宏却极具ARM处理器的特色[11]。眼见为实，我们一起用IDA来看看它们的本来面目吧。

指令清单3.11　使用IDA观察Non-optimizing Keil 6/2013 (ARM模式)

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

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

在本节的例子里，每条指令都占用4个字节。正如您所见到，我们确实要把源程序编译为ARM模式指令集的应用程序，而不是把它编译为以Thumb模式的应用程序。

现在回顾上面的代码，第一句“STMFD　 SP!, {R4,LR}”[12]相当于x86的PUSH指令。它把R4寄存器和LR(Link Register)寄存器的数值放到数据栈中。此处，本文的措辞是“相当于”，而非“完全是”。这是因为ARM模式的指令集里没有PUSH指令，只有Thumb模式里的指令集里才有“PUSH/POP”指令。在IDA中可以清楚地看到这种差别，所以本书推荐使用IDA分析上述程序。

这条指令首先将SP[13]递减，在栈中分配一个新的空间以便存储R4和LR的值。

STMFD指令能够一次存储多个寄存器的值，Thumb模式的PUSH指令也可以这样使用。实际上x86指令集中并没有这样方便的指令。STMFD指令可看作是增强版本的PUSH指令，它不仅能够存储SP的值，也能够存储任何寄存器的值。换句话说，STMFD可用来在指定的内存空间存储多个寄存器的值。

接下来的指令是“ADR R0, aHelloWorld”。它首先对PC[14]进行取值操作，然后把“hello, world”字符串的偏移量（可能为负值）与PC的值相加，将其结果存储到R0之中。有些读者可能不明白此处PC寄存器的作用。严谨地说，编译器通常帮助PC把某些指令强制变为“位置无关代码/position-independent code”。在（多数）操作系统把程序加载在内存里的时候，OS分配给程序代码的内存地址是不固定的；但是程序内部既定指令和数据常量之间的偏移量是固定的（由二进制程序文件决定）。这种情况下，要在程序内部进行指令寻址（例如跳转等情况），就需要借助PC指针[15]。ADR将当前指令的地址与字符串指针地址的差值（偏移量）传递给R0。程序借助PC指针可找到字符串指针的偏移地址，从而使操作系统确定字符串常量在内存里的绝对地址。

“BL　__2printf”[16]调用printf()函数。BL实施的具体操作实际上是：

将下一条指令的地址，即地址0xC处“MOV R0, #0”的地址，写入LR寄存器。

然后将printf()函数的地址写入PC寄存器，以引导系统执行该函数。

当printf()完成工作之后，计算机必须知道返回地址，即它应当从哪里开始继续执行下一条指令。所以，每次使用BL指令调用其他函数之前，都要把BL指令的下一个指令的地址存储到LR寄存器。

这便是CISC（复杂指令集）处理器与RISC（精简指令集）处理器在工作模式上的区别。在拥有复杂指令集的x86体系里，操作系统可以利用栈存储返回地址。

顺便说一下，ARM模式跳转指令的寻址能力确实存在局限性。单条ARM模式的指令必须是32位/4字节，所以BL指令无法调用32位绝对地址或32位相对地址（容纳不下），它只能编入24位的偏移量。不过，既然每条指令的opcode必须是4字节，则指令地址必须在4n处，即偏移地址的最后两位必定为零，可在opcode里省略。在处理ARM模式的转移指令时，处理器将指令中的opcode的低24位左移2位，形成26位偏移量，再进行跳转。由此可知，转移指令B/BL的跳转指令的目标地址，大约在当前位置的±32MB区间之内[17]。

下一条指令“MOV R0，＃0”将R0寄存器置零。Hello World的C代码中，主函数返回零。该指令把返回值写在R0寄存器中。

最后到了“LDMFD SP!, {R4,PC}”这一条指令[18]。它与STMFD成对出现，做的工作相反。它将栈中的数值取出，依次赋值给R4和PC，并且会调整栈指针SP。可以说这条指令与POP指令很相似。main()函数的第一条指令就是STMFD指令，它将R4寄存器和LR寄存器存储在栈中。main()函数在结尾处使用LDMFD指令，其作用是把栈里存储的PC的值和R4寄存器的值恢复回来。

前面提到过，程序在调用其他函数之前，必须把返回地址保存在LR寄存器里。因为在调用printf()函数之后LR寄存器的值会发生改变，所以主函数的第一条指令就要负责保存LR寄存器的值。在被调用的函数结束后，LR寄存器中存储的值会被赋值给PC，以便程序返回调用者函数继续运行。当C/C++的主函数main()结束之后，程序的控制权将返回给OS loader，或者CRT中的某个点，或者作用相似的其他地址。

数据段中的DCB是汇编语言中定义ASCII字符数组/字节数组的指令，相当于x86汇编中的DB指令。

3.4.2　Thumb模式下、未开启优化选项的Keil

现在以Thumb模式编译前面的源代码：

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

我们会在IDA中看到如下指令。

指令清单3.12　使用IDA观察Non-optimizing Keil 6/2013 (Thumb模式)

.text:00000000              main
.text:00000000 10 B5          PUSH    {R4,LR}
.text:00000002 C0 A0          ADR     R0, aHelloWorld;"hello, world"
.text:00000004 06 F0 2E F9    BL      _2printf
.text:00000008 00 20          MOVS    R0, #0
.text:0000000A 10 BD          POP     {R4, PC}

.text:00000304 68 65 6C 6C  +aHelloWorld  DCB "hello, world",0    ; DATA XREF: main+2

Thumb 模式程序的每条指令，都对应着2个字节/16位的opcode，这是Thumb模式程序的特征。但是Thumb模式的跳转指令BL“看上去”占用了4个字节的 opcode，实际上它是由2条指令组成的。单条16位opcode传递的信息太有限，不足以向被调用函数传递PC和偏移量信息。所以，上面BL指令分为2条16位opcode。第一条16位指令可以传递偏移量的高10位，第二条指令可以传递偏移量的低11位。而Thumb模式的opcode都是固定的2 个字节长，目标地址位最后一个位必定是0（Thumb模式的opcode的启始地址位必须是2n），因而会被省略。在执行Thumb模式的转移指令时，处理器会将目标地址左移1位，形成22位的偏移量。即Thumb的BL跳转指令将无法跳到奇数地址，而且跳转指令仅仅能偏移到到当前地址 ±2MB（22位有符号整数的取值区间）附近的范围之内。

程序主函数的其他指令，PUSH和POP工作方式与STMFD/LDMFD相似。虽然表面上看不出来，但是实际上它们也会调整SP指针。ADR指令与前文的作用相同。而MOVS 指令负责把返回值（R0寄存器）置零。

3.4.3　ARM模式下、开启优化选项的Xcode

如果不启用优化选项，Xcode 4.6.3 将会产生大量的冗余代码，所以不妨开启优化选项，让其生成最优的代码。请指定编译选项-O3，使用Xcode编译Hello world程序。这将会得到如下所示的汇编代码。

指令清单3.13　Optimizing Xcode 4.6.3 (LLVM) (ARM模式)

__text:000028C4             _helloworld
__text:000028C4 80 40 2D E9   STMFD         SP!, {R7, LR}
__text:000028C8 86 06 01 E3   MOV           R0, #0x1686
__text:000028CC 0D 70 A0 E1   MOV           R7, SP
__text:000028D0 00 00 40 E3   MOVT          R0, #0
__text:000028D4 00 00 8F E0   ADD           R0, PC, R0
__text:000028D8 C3 05 00 EB   BL            _puts
__text:000028DC 00 00 A0 E3   MOV           R0, #0
__text:000028E0 80 80 BD E8   LDMFD         SP!, {R7, PC}

__cstring:00003F62 48 65 6C 6C+aHelloWorld_0  DCB "Hello World!", 0

我们就不再重复介绍STMFD/LDMFD指令了。

第一个MOV指令将字符串“Hello World！”的偏移量，0x1686 赋值到R0寄存器。

根据Apple ABI 函数接口规范[19]，R7寄存器担当帧指针（frame pointer）寄存器。

“MOVT R0, #0”将0写到R0寄存器的高16位地址。在ARM模式里，常规的MOV指令只能操作寄存器的低16位地址，而单条ARM指令最多是32位/4字节。当然，寄存器之间传递数据没有这种限制。所以，对寄存器的高位（第16位到第31位）进行赋值操作的MOVT指令应运而生。然而此处的这条MOVT指令可有可无，因为在执行上一条指令“MOV R0, #0x1686”时，R0寄存器的高16位本来就会被清零。这或许就是编译器智能方面的缺陷吧。

“ADD R0，PC，R0”将PC和R0进行求和，计算得出字符串的绝对地址。前文介绍过了“位置无关代码”，我们知道程序运行之后的启始地址并不固定。此处，程序对这个地址进行了必要的修正。

然后，程序通过BL指令调用puts()函数，而没有像前文那样调用printf()函数。这种差异来自于GCC编译器[20]，编译器将第一个printf()函数替换为puts()函数（这两个函数的作用几乎相同）。

所谓“几乎”就意味着它们还存在差别事实上，如printf()函数支持“%”开头的控制符，而puts()函数则不支持这类格式化字符串。如果参数里有这类控制符，那么这两个函数的输出结果还会不同。

为什么GCC编译器会做这种替换？大概是由于这种情况下puts()的效率更高吧。由于puts()函数不处理控制符（%）、只是把各个字符输出到stdout设备上，所以puts()函数的运行速度更快[21]。

后面的“MOV R0, #0”指令将R0寄存器置零。

3.4.4　Thumb-2模式下、开启优化选项的Xcode（LLVM）

默认情况下，Xcode 4.6.3 会启用优化模式，并以Thumb-2模式编译源程序。

指令清单3.14　Optimizing Xcode 4.6.3 (LLVM) (Thumb-2模式)

   __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   6F 20+aHelloWorld   DCB "Hello word!",0xA,0

上文提到过，thumb模式的BLX和BL指令以2个16位指令的形式成对出现的。在Thumb-2模式下，BL和BLX指令对应的伪opcode有明显的32位指令特征，其对应的opcode都以0xFx或者0xEx开头。

在显示Thumb和Thumb-2模式程序的opcode时，IDA会以两个字节为单位对调。在显示ARM 模式的指令时，IDA以字节为单位、依次逆序显示其opcode。这是字节序的排版差异。

简要地说，在IDA显示ARM平台的指令时，其显示顺序为：

ARM及ARM64模式的指令，opcode以4-3-2-1的顺序显示。

Thumb模式的指令，opcode以2-1的顺序显示。

Thumb-2模式的16位指令对，其opocde以2-1-4-3的顺序显示。

在IDA中，我们可观察到上述MOVW 、MOVT.W、BLX指令都以0xFx开头。

之后的“MOVW R0，#0x13D8”将立即数写到R0寄存器的低16位地址，同时清除寄存器的高16位。

“MOVT.W R0, #0”的作用与前面一个例子中Thumb模式的MOVT的作用相同，只不过此处是Thumb-2的指令。

在这两个例子中，最显著的区别是Thumb-2模式“BLX”指令。此处的BLX与Thumb模式的BL指令有着根本的区别。它不仅将puts()函数的返回地址RA存入了LR寄存器，将控制权交给了puts()函数，而且还把处理器从Thumb/Thumb-2模式调整为ARM模式；它同时也负责在函数退出时把处理器的运行模式进行还原。总之，它同时实现了模式转换和控制权交接的功能，相当于执行了下面的ARM模式的指令：

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

聪明的读者可能会问，此处为什么不直接调用puts()函数？

直接调用的空间开销更大。

几乎所有的程序都会用到动态链接库，详细说来Windows的程序基本上都会用到DLL文件、Linux程序差不多都会用到.SO文件、MacOSX系统的程序多数也会用到.dylib文件。常用的库函数通常都放在动态链接库里。本例用到的标准C函数——puts()函数也不例外。

可执行的二进制文件（Windows的PE可执行文件，ELF或Mach-O）都有一个输入表段（import section）。输入表段声明了该程序需要通过外部模块加载的符号链接（函数名称和全局变量），并且含有外部模块名称等信息。

在操作系统执行二进制文件的时候，它的加载程序（OS loader）会依据这个表段加载程序所需要的模块。在它加载该程序主模块的时候，对导入的符号链接进行枚举，逐一分配符号链接的地址。

在本例中，_imp_puts是操作系统加载程序（OS loader）为hello world程序提供的外部函数地址，属于32位变量。程序只需要使用LDR指令取出这个变量，并且将它赋值给PC寄存器，就可以调用puts()函数。

可见，一次性地给每个符号链接分配独立的内存地址，可以大幅度地减少OS loader在加载方面的耗时。

前文已经指出，如果只能靠单条指令、而不借助内存的读取操作，CPU就无法把32位数值（指针或立即数）赋值给寄存器。所以，可以建立一个以ARM模式运行的独立函数，让它专门处理动态链接库的接口问题。此后Thumb模式的代码就可以跳转到这个处理接口功能的单指令专用函数。这种专用函数称为（运行模式的）形实转换函数（thunk function）。

前面有一个ARM模式的编译例子，它就使用BL指令实现相同功能的形实转换函数。但是那个程序使用的指令是BL而不是BLX，可见处理器并没有切换运行模式。

形实转换函数（thunk function）的由来

形实转换函数，是“形参与实参互相转换的函数”的缩写。它不仅是缩写词，而且是外来词。这一专用名词的出处可参见：http://www.catb.org/jargon/html/T/thunk.html

。

P. Z. Ingerman在1961年首次提出了thunk的概念，这个概念沿用至今：在编译过程中，为满足当时的过程（函数）调用约定，当形参为表达式时，编译器都会产生thunk，把返回值的地址传递给形参。

微软和IBM都对“thunk”一词有定义，将从16位到32位和从32位到16位的转变叫作“thunk”。

3.4.5　ARM64

GCC

使用GCC 4.8.1将上述代码编译为ARM64程序，可得到如下所示的代码。

指令清单3.15　Non-optimizing GCC 4.8.1 + objdump

 1  0000000000400590 <main>:
 2    400590:        a9bf7bfd     stp     x29, x30, [sp,#-16]!
 3    400594:        910003fd     mov     x29, sp
 4    400598:        90000000     adrp    x0, 400000 <_init-0x3b8>
 5    40059c:        91192000     add     x0, x0, #0x648
 6    4005a0:        97ffffa0     bl      400420 <puts@plt>
 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 6f210000  ........Hello!..

一方面，ARM64的CPU只可能运行于ARM模式、不可运行于Thumb 或 Thumb-2模式，所以它必须使用32位的指令。另一方面，64位平台的寄存器数量也翻了一翻，拥有了32个X-字头寄存器（请参见附录B.4.1）。当然，程序还可以通过W-字头的名称直接访问寄存器的低32位空间。

上述程序的STP（Store Pair）指令把两个寄存器（即X29,X30）的值存储到栈里。虽然这个指令实际上可以把这对数值存储到内存中的任意地址，但是由于该指令明确了SP寄存器，所以它就是通过栈来存储这对数值。ARM64平台的寄存器都是64位寄存器，每个寄存器可存储8字节数据。所以程序要分配16字节的空间来存储两个寄存器的值。

这条指令中的感叹号标志，意味着其标注的运算会被优先执行。即，该指令先把SP的值减去16，在此之后再把两个寄存器的值写在栈里。这属于“预索引/pre-index”指令。此外还有“延迟索引/post-index”指令与之对应。有关两者的区别，请参见本书28.2节。

以更为易懂的x86指令来解读的话，这条指令相当于PUSH X29和PUSH X30两条指令。在ARM64平台上，X29寄存器是帧指针FP，X30起着LR的作用，所以这两个寄存器在函数的序言和尾声处成对出现。

第二条指令把SP的值复制给X29，即FP。这用来设置函数的栈帧。

ADRP和ADD指令相互配合，把“Hello！”字符串的指针传递给X0寄存器，继而充当函数参数传递给被调用函数。受到指令方面的限制，ARM无法通过单条指令就把一个较大的立即数赋值给寄存器（可参见本书的28.3.1节）。所以，编译器要组合使用数条指令进行立即数赋值。第一条ADRP把4KB页面的地址传递给X0，而后第二条ADD进行加法运算并给出最终的指针地址。详细解释请参见本书28.4节。

0x400000 + 0x648 = 0x400648。这个数是位于.rodata数据段的C字符串“Hello！”的地址。

接下来，程序使用BL指令调用puts()函数。这部分内容的解读可参见3.4.3节。

MOV指令用来给W0寄存器置零。W0是X0寄存器的低32位，如下图所示。

高32位

低32位

X0

W0

main()函数通过X0寄存器来传递函数返回值0。程序后续的指令依次制备这个返回值。为什么这里把返回值存储到X0寄存器的低32位，即W0寄存器？这种情况和x86-64平台相似：出于兼容性和向下兼容的考虑，ARM64平台的int型数据仍然是32位数据。对于32位的int型数据来说，X0寄存器的低32位足够大了。

为了进行演示，我对源代码进行了小幅度的修改，使main()返回64位值。

指令清单3.16　main()返回uint64_t 型数据

#include <stdio.h>
#include <stdint.h>

uint64_t main()
{
         printf ("Hello!\n");
         return 0;
}

返回值虽然相同，但是对应的MOV指令发生了变化。

指令清单3.17　Non-optimizing GCC 4.8.1 + objdump

  4005a4:    d2800000    mov     x0, #0x0           // #0

在此之后，LDP (Load Pair)指令还原X29和X30寄存器的值。此处的这条指令没有感叹号标记，这意味着它将率先进行赋值操作，而后再把SP的值与16进行求和运算。这属于延时索引（post-index）指令。

RET指令是ARM64平台的特色指令。虽然它的作用与BX LR相同，但是它实际上是按照寄存器的名称进行跳转的（默认使用X30寄存器指向的地址），通过底层指令提示CPU此处为函数的返回指令、不属于普通转移指令的返回过程。RET指令经过了面向硬件的优化处理，它的执行效率较高。

开启优化功能之后，GCC生成的代码完全一样。本文不在对它进行介绍。

3.5　MIPS

3.5.1　全局指针Global pointer

全局指针是MIPS软件系统的一个重要概念。我们已经知道，每条MIPS指令都是32位的指令，所以单条指令无法容纳32位地址（指针）。这种情况下MIPS就得传递一对指令才能使用一个完整的指针。在前文的例子中，GCC在生成文本字符串的地址时，就采用了类似的技术。

从另一方面来说，单条指令确实可以容纳一组由寄存器的符号、有符号的16位偏移量（有符号数）。因此任何一条指令都可以构成的表达式，访问某个取值范围为“寄存器−32768”～“寄存器＋32767”之间的地址（总共69KB）。为了简化静态数据的访问操作，MIPS平台特地为此保留了一个专用的寄存器，并且把常用数据分配到了一个大小为64KB的内存数据空间里。这种专用的寄存器就叫作“全局指针”寄存器。它的值是一个指针，指向64KB（静态）数据空间的正中间。而这64KB空间通常用于存储全局变量，以及printf()这类由外部导入的的外部函数地址。GCC的开发团队认为：获取函数地址这类的操作，应当由单条指令完成；双指令取址的运行效率不可接受。

在ELF格式的文件中，这个64KB的静态数据位于.sbss 和.sdata之中。“.sbss”是small BSS（Block Started by Symbol）的缩写，用于存储非初始化的数据。“.sdata”是small data的缩写，用于存储有初始化数值的数据。

根据这种数据布局编程人员可以自行决定把需要快速访问的数据放在.sdata、还是.sbss数据段中。

有多年工作经验的人员可能会把全局指针和MS-DOS内存（参见本书第94章）、或者MS-DOS的XMS/EMS内存管理器联系起来。这些内存管理方式都把数据的内存存储空间划分为数个64KB区间。

全局指针并不是MIPS平台的专有概念。至少PowerPC平台也使用了这一概念。

3.5.2　Optimizing　GCC

下面这段代码显示了“全局指针”的特色。

指令清单3.18　Optimizing GCC 4.4.5 (汇编输出)

 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 register:
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

主函数序言启动部分的指令初始化了全局指针寄存器GP寄存器的值，并且把它指向64KB数据段的正中央。同时，程序把RA寄存器的值存储于本地数据栈。它同样使用puts()函数替代了printf()函数。而puts()函数的地址，则通过LW（Load Word）指令加载到了$25寄存器。此后，字符串的高16位地址和低16位地址分别由LUI(Load Upper Immediate)和ADDIU(Add Immediate Unsigned Word)两条指令加载到$4寄存器。LUI中的Upper一词说明它将数据存储于寄存器的高16位。与此相对应，ADDIU则把操作符地址处的低16位进行了求和运算。ADDIU指令位于JALR指令之后，但是会先于后者运行[22]。$4寄存器其实就是$A0寄存器，在调用函数时传递第一个参数[23]。

JALR (Jump and Link Register)指令跳转到$25寄存器中的地址，即puts()函数的启动地址，并且把下一条LW指令的地址存储于RA寄存器。可见，MIPS系统调用函数的方法与ARM系统相似。需要注意的是，由于分支延迟槽效应，存储于RA寄存器的值并非是已经运行过的、“下一条”指令的地址，而是更后面那条（延迟槽之后的）指令的地址。所以，在执行这条JALR指令的时候，写入RA寄存器的值是PC+8，即ADDIU后面的那条LW指令的地址。

第19行的LW (Load Word)指令，用于把本地栈中的RA值恢复回来。请注意，这条指令并不位于被调用函数的函数尾声。

第22行的MOVE指令把$0（$ZERO）的值复制给$2($V0)。MIPS有一个常量寄存器，它里面的值是常量0。很明显，因为MIPS的研发人员认为0是计算机编程里用得最多的常量，所以他们开创了一种使用$0寄存器提供数值0的机制。这个例子演示了另外一个值得注意的现象：在MIPS系统之中，没有在寄存器之间复制数值的（硬件）指令。确切地说，MOVE DST, SRC是通过加法指令ADD DST,SRC, $ZERO变相实现的，即DST=SRC+0，这两种操作等效。由此可见，MIPS研发人员希望尽可能地复用opcode，从而精简opcode的总数。然而这并不代表每次运行MOVE指令时CPU都会进行实际意义上的加法运算。CPU能够对这类伪指令进行优化处理，在运行它们的时候并不会用到ALU（Arithmetic logic unit）。

第24行的J指令会跳转到RA所指向的地址，完成从被调用函数返回调用者函数的操作。还是由于分支延迟槽效应，其后的ADDIU指令会先于J指令运行，构成函数尾声。

我们再来看看IDA生成的指令清单，熟悉一下各寄存器的伪名称。

代码清单3.19　Opimizing GCC4.4.5(IDA)

 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_gp >> 16)
 9 .text:00000004             addiu      $sp, -0x20
10 .text:00000008             la         $gp, (__gnu_local_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_10($sp)
16 ; load the address of the puts() function from the GP to $t9:
17 .text:00000014             lw         $t9, (puts & 0xFFFF)($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 register:
21 .text:0000001C             jalr       $t9
22 .text:00000020             la         $a0, ($LC0 & 0xFFFF) # "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, 0x20

第15行的指令使用局部栈保存GP的值。令人感到匪夷所思的是，GCC的汇编输出里看不到这条指令，或许这是GCC自身的问题[24]。严格地说，此时有必要保存GP的值。毕竟每个函数都有着自己的64KB数据窗口。

程序中保存puts()函数地址的寄存器叫作$T9寄存器。这类T-开头的寄存器叫作“临时”寄存器，用于保存代码里的临时值。调用者函数负责保存这些寄存器的数值（caller-saved），因为它有可能会被被调用的函数重写。

3.5.3　Non-optimizing GCC

代码清单3.20　Non-optimizing GCC 4.4.5 (汇编输出)

 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

未经优化处理的GCC输出要详细得多。此处，我们可以观察到程序把FP当作栈帧的指针来用，而且它还有3个NOP（空操作）指令。在这3个空操作指令中，第二个、第三个指令都位于分支跳转指令之后。

笔者个人认为（虽然目前无法肯定），由于这些地方都存在分支延迟槽，所以GCC编译器会在分支语句之后都添加NOP指令。不过，在启用它的优化选项之后，GCC可能就会删除这些NOP指令。所以，此处仍然存在这些NOP指令。

使用IDA程序观察下面这段代码。

指令清单3.21　Non-optimizing 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
10 .text:00000004             sw         $ra, 0x20+var_4($sp)
11 .text:00000008             sw         $fp, 0x20+var_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+var_10($sp)
17 ; load the address of the text string:
18 .text:0000001C             lui        $v0, (aHelloWorld >> 16) # "Hello, world!"
19 .text:00000020             addiu      $a0, $v0, (aHelloWorld & 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+var_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+var_4($sp)
36 ; restore the FP:
37 .text:00000048             lw         $fp, 0x20+var_8($sp)
38 .text:0000004C             addiu      $sp, 0x20
39 ; jump to the RA:
40 .text:00000050             jr         $ra
41 .text:00000054             or         $at, $zero ; NOP

在程序的第15行出现了一个比较有意思的现象——IDA识别出了LUI/ADDIU指令对，把它们显示为单条的伪指令LA(Load address)。那条伪指令占用了8个字节！这种伪指令（即“宏”）并非真正的MIPS指令。通过这种名称替换，IDA帮助我们对指令的作用望文思义。

NOP的显示方法也构成了它的另外一种特点。因为IDA并不会自动地把实际指令匹配为NOP指令，所以位于第22行、第26行、第41行的指令都是“OR $AT, $ZERO”。表面上看，它将保留寄存器$AT的值与0进行或运算。但是从本质上讲，这就是发送给CPU的NOP指令。MIPS和其他的一些硬件平台的指令集都没有单独的NOP指令。

3.5.4　栈帧

本例使用寄存器来传递文本字符串的地址。但是它同时设置了局部栈，这是为什么呢？由于程序在调用printf()函数的时候由于程序必须保存RA寄存器的值和GP的值，故而此处出现了数据栈。如果此函数是叶函数，它有可能不会出现函数的序言和尾声，有关内容请参见本书的2.3节。

3.5.5　Optimizing　GCC:　GDB的分析方法

指令清单3.22　GDB的操作流程

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 <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "mips-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /root/hw...(no debugging symbols found)...done.
(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 <main+0>:     lui     gp,0x42
0x00400644 <main+4>:     addiu   sp,sp,-32
0x00400648 <main+8>:     addiu   gp,gp,-30624
0x0040064C <main+12>:    sw      ra,28(sp)
0x00400650 <main+16>:    sw      gp,16(sp)
0x00400654 <main+20>:    lw      t9,-32716(gp)
0x00400658 <main+24>:    lui     a0,0x40
0x0040065c <main+28>:    jalr    t9
0x00400660 <main+32>:    addiu   a0,a0,2080
0x00400664 <main+36>:    lw      ra,28(sp)
0x00400668 <main+40>:    move    v0,zero
0x0040066c <main+44>:    jr      ra
0x00400670 <main+48>:    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.6　总结

x64和x86指令的主要区别体现在指针上，前者使用64位指针而后者使用32位指针。近年来，内存的价格在不断降低，而CPU的计算能力也在不断增强，当计算机的内存增加到一定程度时，32位指针就无法满足寻址的需要了，所以指针也随之演变为64位指针。

3.7　练习题

3.7.1　题目1

请描述下述32位函数的功能。

main:
     push 0xFFFFFFFF
     call MessageBeep
     xor  eax,eax
     retn

3.7.2　题目2

请描述Linux函数的功能，这里使用了AT&T汇编语言语法。

main:
     pushq   %rbp
     movq    %rsp, %rbp
     mov1    %2, %edi
     call    sleep
     popq    %rbp
     ret

[1]　Brian W. Kernighan. The C Programming Language. Ed. by Dennis M. Ritchie. 2nd. Prentice Hall Professional Tech- nical Reference, 1988. ISBN: 0131103709。

[2]　但是CPU 标志位会发生变化。

[3]　参见http://en.wikipedia.org/wiki/Exclusive_or。

[4]　C runtime library:sec:CRT，参见本书68.1节。

[5]　参考Wikipedia：Data structure alignment http://en.wikipedia.org/wiki/Data_structure_ alignment。

[6]　参考Mit13。

[7]　参考Int13。

[8]　可通过菜单“Options Number of opcode bytes”启用有关选项。

[9]　请参考Mit13。

[10]　Apple公司的Xcode 4.6.3使用的前段编译器是开源的GCC程序，代码生成程序（code generator）使用的是LLVM。

[11]　例如，ARM模式的指令集里没有PUSH/POP指令。

[12]　STMFD是Storage Multiple Full Descending 的缩写。

[13]　stack pointer，栈指针。x86/x64框架中的SP是SP/ESP/RSP，而ARM框架的SP就是SP。

[14]　Program Counter，中文叫做指令指针或程序计数器。x86/x64里的PC叫作IP/EIP/RIP，ARM里它就叫PC。

[15]　本书介绍操作系统的部分有更详细的说明。在不同框架的汇编语言中，PC很少会是当前指令的指针地址＋1，这和CPU的流水/pipeline模式有关。如需完整的官方介绍，请参阅http://www.arm. com/pdfs/comparison-arm7-arm9-v1.pdf。

[16]　BL是Branch with Link的缩写，相当于x86的call指令。

[17]　这是二进制里26位有符号整型数据（26 bits signed int）的数值范围。

[18]　LDMFD是Load Multiple Full Descending的缩写。

[19]　参照参考文献App10。

[20]　Xcode 4.6.3是基于GCC的编译器。

[21]　请参考http://www.ciselant.de/projects/gcc_printf/gcc_printf.html。

[22]　请参考前文介绍的分支延迟槽（Branch delay slot）效应。

[23]　有关MIPS各寄存器的用途，请参见附录C.1。

[24]　很明显，对于GCC的用户来说，查看汇编指令的功能不是那么重要。所以，GCC输出的汇编指令之中仍然可能存在一些（在生成汇编指令的阶段）未被修正的错误。

第4章 函数序言和函数尾声

函数序言（function prologue）是函数在启动的时候运行的一系列指令。其汇编指令大致如下：

push   ebp
mov    ebp, esp
sub    esp, X

这些指令的功能是：在栈里保存EBP寄存器的内容、将ESP的值复制到EBP寄存器，然后修改栈的高度，以便为本函数的局部变量申请存储空间。

在函数执行期间，EBP寄存器不受函数运行的影响它是函数访问局部变量和函数参数的基准值。虽然我们也可使用ESP寄存器存储局部变量和运行参数，但是ESP寄存器的值总是会发生变化，使用起来并不方便。

函数在退出时，要做启动过程的反操作，释放栈中申请的内存，还原EBP寄存器的值，将代码控制权还原给调用者函数(callee)。

mov    esp, ebp
pop    ebp
ret    0

借助函数序言和函数尾声的有关特征，我们可以在汇编语言里识别各个函数。

递归调用

函数序言和尾声都会调整数据栈受硬件IO性能影响，所有递归函数的性能都不太理想。

详细内容请参见本书的36.3节。

第5章 栈

栈是计算机科学里最重要且最基础的数据结构之一。[1]

从技术上说，栈就是CPU寄存器里的某个指针所指向的一片内存区域。这里所说的“某个指针”通常位于x86/x64平台的ESP寄存器/RSP寄存器，以及ARM平台的SP寄存器。

操作栈的最常见的指令是PUSH和POP，在x86和ARM Thumb模式的指令集里都有这两条指令。PUSH指令会对ESP/RSP/SP寄存器的值进行减法运算，使之减去4（32位）或8（64位），然后将操作数写到上述寄存器里的指针所指向的内存中。

POP指令是PUSH指令的逆操作：它先从栈指针（Stack Pointer，上面三个寄存器之一）指向的内存中读取数据，用以备用（通常是写到其他寄存器里），然后再将栈指针的数值加上4或8。

在分配栈的空间之后，栈指针，即Stack Pointer所指向的地址是栈的底部。PUSH将减少栈指针的数值，而POP会增加它的数值。栈的“底”实际上使用的是整个栈的最低地址，即是整个栈的启始内存地址。虽然听起来很奇怪，但是实际上确实如此。

虽然x86/x64的栈已经很难理解了，但是ARM的栈却更为复杂。ARM的栈分为递增栈和递减栈。递减栈（descending stack）的首地址是栈的最高地址，栈向低地址增长，栈指针的值随栈的增长而减少，如STMFD/LDMFD、STMED/LDMED等指令都是递减栈的操作指令。而ARM的递增栈（ascending stack）的首地址则占用栈的最低地址，栈向高地址增长，栈指针的值随栈的增长而增加，如STMFA/LMDFA、STMEA/LDMEA等指令都是递增栈的操作指令。[2]

5.1　为什么栈会逆增长

多数人想象中的“增长”，是栈从低地址位向高地址位增长，似乎这样才符合自然规律。然而研究过栈的人知道，多数的栈是逆增长的，它会从高地址向低地址增长。

这多数还得归功于历史原因。当计算机尚未小型化的时候，它还有数个房间那么大。在那个时候，内存就分为两个部分，即“堆/heap”和“栈/stack”。当然，在程序执行过程中，堆和栈到底会增长到什么地步并不好说，所以人们干脆把它们分开：

有兴趣的读者可以查阅参考文献RT74，其中有这样一段话：

程序镜像（进程）在逻辑上分为 3 个段。从虚拟地址空间的 0 地址位开始，第一个段是文本段（也称为代码段）。文本段在执行过程中不可写，即使一个程序被执行多次，它也必须共享 1 份文本段。在程序虚拟空间中，文本段 8k bytes 边界之上，是不共享的、可写的数据段。程序可以通过调用系统函数调整其数据段的大小。栈启始于虚拟地址空间的最高地址，它应随着硬件栈指针的变化而自动地向下增长。

这就好比用同一个笔记本给两门课程做笔记：第一门的笔记可以按照第一页往最后一页的顺序写；然而在做第二门的笔记时，笔记本要反过来用，也就是要按照从最后一页往第一页的顺序写笔记。至于笔记本什么时候会用完，那就要看笔记本有多厚了。

5.2　栈的用途

5.2.1　保存函数结束时的返回地址

x86

当程序使用call指令调用其他函数时，call指令结束后的返回地址将被保存在栈里；在call所调用的函数结束之后，程序将执行无条件跳转指令，跳转到这个返回地址。

CALL指令等价于“PUSH 返回地址”和“JMP 函数地址”的指令对。

被调用函数里的RET指令，会从栈中读取返回地址，然后跳转到这个地址，就相当于“POP 返回地址”＋“JMP 返回地址”指令。

栈是有限的，溢出它很容易。直接使用无限递归，栈就会满：

void f()
{
          f();
}

如果使用MSVC 2008编译上面的问题程序，将会得到报告：

c:\tmp6>cl ss.cpp /Fass.asm
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

ss.cpp
c:\tmp6\ss.cpp(4) : warning C4717:’f’ : recursive on all control paths. Function will cause runtime stack overflow

但是它还是会老老实实地生成汇编文件：

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

有趣的是，如果打开优化选项“/Ox”，生成的程序反而不会出现栈溢出的问题，而且还会运行得很“好”：

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

无论是否开启优化选项，GCC 4.4.1生成的代码都和MSVC生成的代码相似，只是GCC不会发布任何警告。

ARM

ARM程序也使用栈保存返回地址，只是略有不同。在3.4节中，我们看到“Hello, World!”程序的返回地址保存在LR (link register)寄存器里。但是，如果程序还会继续调用其他函数，就需要在调用函数之前保存LR寄存器的值。通常，函数会在启动过程中（序言处）保存LR寄存器的值。我们通常在函数序言处看到“PUSH R4-R7，LR”，并在尾声处看到“POP R4-R7，PC”。这些指令会对函数自身将要用到的寄存器进行保护，把它们的值存放在栈中——当然，这其中也包括LR寄存器。

如果一个函数不调用其他函数，它就像树上的枝杈末端的叶子那样。这种函数就叫作“叶函数（leaf function）”[3]。叶函数的特点是，它不必保存LR寄存器的值。如果叶函数的代码短到用不到几个寄存器，那么它也可能根本不会使用数据栈。所以，调用叶函数的时候确实可能不会涉及栈操作。这种情况下，因为这种代码不在外部内存RAM进行与栈有关的操作，所以它的运行速度有可能超过x86系统[4]。在没有分配栈或者不可能用栈的时候，这类函数就会显现出“寸有所长”的优势。

本书介绍了很多的叶函数。请参见8.3.2节、8.3.3节、15.2节、15.4节、17.3节、19.5.4节、19.17节、19.33节中演示的程序。

5.2.2　参数传递

在x86平台的程序中，最常用的参数传递约定是cdecl[5]。以cdecl方式处理参数，其上下文大体是这个样子：

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

被调用方函数（Callee functions）通过栈指针获取其所需的参数。

在运行f()函数之前，传递给它的参数将以以下格式存储在内存里。

ESP

返回地址

ESP+4
arg1, 它在IDA里记为arg_0

ESP+8
arg2, 它在IDA里记为arg_4

ESP+0xC
arg3, 它在IDA里记为arg_8

……
……

本书第64章将会详细介绍有关的调用约定（Calling conventions）。需要注意的是，程序员可以使用栈来传递参数，也可以不使用栈传递参数。参数处理方面并没有相关的硬性规定。

例如，程序员可以在堆（heap）中分配内存并用之传递参数。在堆中放入参数之后，可以利用EAX寄存器为函数传递参数。这种做法确实行得通。[6]只是在x86系统和ARM系统上，使用栈处理参数已经成为了约定俗成的习惯，而且它的确十分方便。

另外，被调用方函数并不知晓外部向它传递了多少个参数。如果函数可处理的参数数量可变，它就需要说明符（多数以%号开头）进行格式化说明、明确参数信息。拿我们常见的printf()函数来说：

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

这个命令不仅会让printf()显示1234，而且还会让它显示数据栈内1234之后的两个地址的随机数。

由此可知，声明main()函数的方法并不是那么重要。我们可以将之声明为main(),main(int argc, char *argv[])或main(int argc, char *argv[], char *envp[])。

实际上CRT中调用main()的指令大体上是下面这个样子的。

push envp
push argv
push argc
call main
…

即使我们没有在程序里声明main()函数使用哪些参数，程序还可以照常运行；参数依旧保存在栈里，只是不会被主函数调用罢了。如果将main()函数声明为main(int argc, char*argv[])，程序就能够访问到前两个参数，但仍然无法使用第三个参数。除此以外，也可以声明为main( int argc)，主函数同样可以运行。

5.2.3　存储局部变量

通过向栈底调整栈指针（stack pointer）的方法，函数即可在数据栈里分配出一片可用于存储局部变量的内存空间。可见，无论函数声明了多少个局部变量，都不影响它分配栈空间的速度。

虽然您的确可以在栈以外的任何地方存储局部变量，但是用数据栈来存储局部变量已经是一种约定俗成的习惯了。

5.2.4　x86:alloca()函数

alloca()函数很有特点。[7]

大体来说，alloca()函数直接使用栈来分配内存，除此之外，它与malloc()函数没有显著的区别。

函数尾声的代码会还原ESP的值，把数据栈还原为函数启动之前的状态，直接抛弃由alloca()函数分配的内存。所以，程序不需要特地使用free()函数来释放由这个函数申请的内存。

alloca() 函数的实现方法至关重要。

简要地说，这个函数将以所需数据空间的大小为幅度、向栈底调整ESP的值，此时ESP就成为了新的数据空间的指针。我们一起做个试验：

#ifdef __GNUC__
#include <alloca.h> // GCC
#else
#include <malloc.h> // MSVC
#endif
#include <stdio.h>

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()将输出结果输出到 stdout（也就是终端terminal或console一类的输出设备上），而snprintf()则将结果输出到buf数组（人工设定的缓冲区）、我们需要通过puts()函数才能将buf的内容输出到stdout。当然，printf()函数就足以完成_snprint()和puts()两个函数的功能。本文意在演示缓冲区的用法，所以刻意把它拆分为2个函数。

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()函数是编译器固有函数（参见本书第90章），并非常规函数的缘故，这个程序没有使用栈，而是使用EAX寄存器来传递alloca()函数唯一的参数。在调用alloca()函数之后，ESP将指向600字节大小的内存区域，用以存储数组buf。

GCC Intel语体

在编译上述代码时，GCC 4.4.1 同样不会调用外部函数。

指令清单5.2　GCC 4.7.3

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

GCC+ AT&T语体

接下来让我们看看AT&T语体的指令。

指令清单5.3　GCC 4.7.3

.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)”对应Intel语体的“mov DWORD PTR [esp+20], 3”指令。在以“寄存器＋偏移量”的方式寻址时，AT&T语体将这个寻址表达式显示为“偏移量（%寄存器）”。

5.2.5　（Windows）SEH结构化异常处理

如果程序里存在SEH记录，那么相应记录会保存在栈里。

本书将在68.3节里进行更详细的介绍。

5.2.6　缓冲区溢出保护

本书将在18.2节里进行更详细的介绍。

5.3　典型的栈的内存存储格式

在32位系统中，在程序调用函数之后、执行它的第一条指令之前，栈在内存中的存储格式一般如下表所示。

… | ……
ESP-0xC | 第2个局部变量，在IDA里记为var_8
ESP-8 | 第1个局部变量，在IDA里记为var_4
ESP-4 | 保存的EBP值
ESP | 返回地址
ESP+4 | arg1, 在IDA里记为arg_0
ESP+8 | arg2, 在IDA里记为arg_4
ESP+0xC | arg3, 在IDA里记为arg_8
… | ……

5.4　栈的噪音

本书会经常使用“噪音”、“脏数据”这些词汇。它们怎么产生的呢？待函数退出以后，原有栈空间里的局部变量不会被自动清除。它们就成为了栈的“噪音”、“脏数据”。我们来看下面这段代码。

#include <stdio.h>

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();
};

使用MSVC 2010编译后可得到如下所示的代码。

指令清单5.4　Non-optimizing MSVC 2010

$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

编译器会给出提示：

c:\Polygon\c>cl st.c /Fast.asm /MD
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 16.00.40219.01 for 80x86
Copyright (C) Microsoft Corporation.  All rights reserved.

st.c
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'c' used
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable 'b' used
c:\polygon\c\st.c(11) : warning C4700: uninitialized local variable '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()的任何变量赋值，但是变量自己有自己的值。f2()函数的值就是栈里残存的脏数据。

我们使用OllyDbg打开这个程序，如图5.1所示。

图5.1　使用OllyDbg查看f1()函数的数据栈

在f1()函数给变量a、b、c赋值后，数值存储于0x1FF860开始的连续地址里。

然后，在执行f2()时，情况如图5.2所示。

图5.2　使用OllyDbg查看f2()函数的数据栈

f2()函数的三个变量的地址，和f1()函数的三个变量的地址相同。因为没有对这个空间进行重新赋值，所以那三个变量会因为地址相同的原因获得前三个变量的值。

在这个特例里，第二个函数在第一个函数之后执行，而第二个函数变量的地址和SP的值又与第一个函数的情况相同。所以，相同地址的变量获得的值相同。

总而言之，在运行第二个函数时，栈中的所有值（即内存中的单元）受前一个函数的影响，而获得了前一个函数的变量的值。严格地说，这些地址的值不是随机值，而是可预测的伪随机值。

有没有办法清除脏数据呢？我们可以在每个函数执行之前清除其开辟的栈空间的数据。不过，即使这是一种技术上可行的方法，但是因为这种方法开销太大、而且必要性很低，所以没有人这样做。

5.5　练习题

5.5.1　题目1

如果使用MSVC编译、运行下列程序，将会打印出3个整数。这些数值来自哪里？如果使用MSVC的优化选项“/Ox”，程序又会在屏幕上输出什么？为什么GCC的情况完全不同？

#include <stdio.h>

int main()
{
         printf ("%d, %d, %d\n");

         return 0;
};

答案请参见G1.1。

5.5.2　题目2

请问描述下述程序的功能。

经MSVC 2010（启用/Ox选项）编译而得的代码如下。

指令清单5.5　Optimizing MSVC 2010

$SG3103 DB      '%d', 0aH, 00H

_main   PROC
        push    0
        call    DWORD PTR __imp___time64
        push    edx
        push    eax
        push    OFFSET $SG3103 ; ’%d’
        call    DWORD PTR __imp__printf
        add     esp, 16
        xor     eax, eax
        ret     0
_main   ENDP

指令清单5.6　经Keil 6/2013（启用优化选项）编译而得的ARM模式代码

main PROC
         PUSH    {r4,lr}
         MOV     r0,#0
         BL      time
         MOV     r1,r0
         ADR     r0,|L0.32|
         BL      __2printf
         MOV     r0,#0
         POP     {r4,pc}
         ENDP
|L0.32|
         DCB    "%d\n",0

指令清单5.7　经Keil 6/2013（启用优化选项）编译而得的Thumb模式代码

main PROC
         PUSH   {r4,lr}
         MOVS   r0,#0
         BL     time
         MOVS   r1,r0
         ADR    r0,|L0.20|
         BL     __2printf
         MOVS   r0,#0
         POP    {r4,pc}
         ENDP

|L0.20|
         DCB    "%d\n",0

指令清单5.8　经GCC 4.9（启用优化选项）编译而得的ARM64模式代码

main:
        stp     x29, x30, [sp, -16]!
        mov     x0, 0
        add     x29, sp, 0
        bl      time
        mov     x1, x0
        ldp     x29, x30, [sp], 16
        adrp    x0, .LC0
        add     x0, x0, :lo12:.LC0
        b       printf
.LC0:
        .string "%d\n"

指令清单5.9　经GCC 4.4.5（启用优化选项）编译而得的MIPS指令（IDA）

main:

var_10        = -0x10
var_4         = -4

              lui     $gp, (__gnu_local_gp >> 16)
              addiu   $sp, -0x20
              la      $gp, (__gnu_local_gp & 0xFFFF)
              sw      $ra, 0x20+var_4($sp)
              sw      $gp, 0x20+var_10($sp)
              lw      $t9, (time & 0xFFFF)($gp)
              or      $at, $zero
              jalr    $t9
              move    $a0, $zero
              lw      $gp, 0x20+var_10($sp)
              lui     $a0, ($LC0 >> 16)  # "%d\n"
              lw      $t9, (printf & 0xFFFF)($gp)
              lw      $ra, 0x20+var_4($sp)
              la      $a0, ($LC0 & 0xFFFF)  # "%d\n"
              move    $a1, $v0
              jr      $t9
              addiu   $sp, 0x20

$LC0:         .ascii "%d\n"<0>        # DATA XREF: main+28

[1]　请参见http://en.wikipedia.org/wiki/Call_stack。

[2]　这些指令所对应的英文全称，分别是Store Multiple Full Descending、Load Multiple Full Descending、Store Multiple Empty Descending、Load Multiple Empty Descending 、Store Multiple Full Ascending 、Load Multiple Full Ascending 、Store Multiple Empty Ascending、 Load Multiple Empty Ascending。其中的Full/Empty的区别在于：如果指针指向的地址是最新操作的值，那么它就是Full stack；而指针指向的地址没有值、是下一个值将写到的地址，那么这个栈就是Empty stack。

[3]　参照http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.faqs/ka13785.html。部分文献又称叶函数为“末端函数”

[4]　多年以前，在PDP-11和VAX平台上，调用函数的指令效率很低；在运行程序的时候，差不多近半的耗时都花在互相调用的指令上。以至于当时普遍认为、“大规模调用功能较少的函数”的程序就是垃圾程序。

[5]　此外还有stdcall、fastcall、thiscall等。Windows上很多程序使用stdcall。

[6]　Donald Knuth在《The Art of Computer Programming》 一书的14.1节专门介绍了子程序。读者能够发现他提到了一种方法，通过JMP跳转到子程序并在子程序的入口处即刻提取所需参数。Knuth的这种方法在System/360上非常实用。

[7]　在MSVC编译环境里，它的实现方式可在目录C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\crt\src\intel下的alloca16.asm和chkstk.asm中找到。

第6章 printf()函数与参数传递

现在我们对Hello, world! 程序稍做修改，演示它的参数传递的过程。

#include <stdio.h>

int main()
{
printf("a=%d; b=%d; c=%d", 1, 2, 3);
return 0;
};

6.1　x86

6.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

这与最初的Hello World程序相差不多。我们看到printf()函数的参数以逆序存入栈里，第一个参数在最后入栈。

在32位环境下，32位地址指针和int类型数据都占据32 位/4字节空间。所以，我们这里的四个参数总共占用4×4=16（字节）的存储空间。

在调用函数之后，“ADD ESP, X”指令修正ESP寄存器中的栈指针。通常情况下，我们可以通过call之后的这条指令判断参数的数量：变量总数＝X÷4。

这种判断方法仅适用于调用约定为cdecl的程序。本书将在第64章详细介绍各种函数约定[1]。

如果某个程序连续地调用多个函数，且调用函数的指令之间不夹杂其他指令，那么编译器可能把释放参数存储空间的“ADD ESP,X”指令进行合并，一次性地释放所有空间。例如：

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

如下是一个规定中的例子。

指令清单6.1　x86

.text:100113E7　push 3
.text:100113E9　call sub_100018B0 ; takes one argument (3)
.text:100113EE　call sub_100019D0 ; takes no arguments at all
.text:100113F3　call sub_10006A90 ; takes no arguments at all
.text:100113F8　push 1
.text:100113FA　call sub_100018B0 ; takes one argument (1)
.text:100113FF　add　esp, 8　　   ; drops two arguments from stack at once

使用OllyDbg调试MSVC编译出的程序

现在我们在OllyDbg中加载这个范例。OllyDbg是非常受欢迎的user-land（用户空间）win32 debugger。在使用MSVC 2012编译这个样本程序的时候，启用/MD选项，可使可执行程序与MSVCR*.DLL建立动态链接。这样，我们就能在debugger里清楚地观察到程序从标准库里调用函数的过程。

在OllyDbg里调用可执行文件，将第一个断点设为ntdll.dll，然后按F9键执行。然后把第二个断点设置在CRT-code里。我们应该能够找到main()主函数。

因为MSVC将main()函数分配到代码段的开始处，所以只要向下滚屏就能够在底部找到main()函数体。如图6.1所示。

图6.1　使用OllyDbg：查看main()函数的启动部分

单击PUSH EBP指令，按F2设置断点，再按F9键运行。这么做是为了跳过CRT-code，因为我们的目的不是分析CRT代码。

而后按6次F8键，就是说跳过6条指令。如图6.2所示。

图6.2　使用OllyDbg：定位到调用printf()之前的指令

此时，指令指针PC指向CALL printf指令。与其他主流的调试器相同，OllyDbg调试器会加亮显示所有发生过变化的寄存器。每按一次F8键，EIP的值都会发生变化，所以这个寄存器一直被高亮显示。在这个例子中，ESP同样会被OllyDbg高亮显示，因为在这几步调试中ESP寄存器的值也发生了变化。

栈里的数值在哪呢？我们看一下debugger的右下角，如图6.3所示。

图6.3　使用OllyDbg：观察PUSH指令造成的栈值变化（红色方块内）

此处的内容分为3列：栈地址列、数值列及OllyDbg的注释列。OllyDbg能够识别printf()这样的指令字符串，按照其指令的形式把它所引用的三个值进行了整理。

我们还可以使用鼠标的右键单击printf()的格式化字符串、单击下拉菜单中的“Follow in dump”，这样屏幕左下方将会显示出格式化字符串的输出结果。调试器左下方的这个区域用于显示内存中的部分数据。我们同样可以编辑这些值。如果修改格式化字符串，那么程序的输出结果就会发生变化。在这个例子中，我们还用不上这样的功能，但是我们可以练练手，从而进一步熟悉调试器。

按下F8键，单步执行（Step Over）。

我们将会看到图6.4所示的输出结果。

图6.4　printf() 函数的输出结果

寄存器和栈状态的变化过程如图6.5所示。

图6.5　使用OllyDbg：观察printf()运行后的数据栈

现在，EAX寄存器的值是0xD（即13）。这个值是printf()函数所打印的字符总数，所以这个值没有问题。EIP寄存器的值发生了变化，实际上它是在执行CALL printf指令后的PC。与此同时，ECX和EDX寄存器的值也发生了变化；显然printf()函数在运行过程中会使用这两个寄存器。

这里最重要的现象是ESP的值和栈里的参数都没有发生变化。我们可以观察到数据栈里的、传递给printf()函数的字符串和其他3个参数的值原封未动。这是cdecl调用约定的特征：被调用方函数不负责恢复ESP的状态；调用方函数（caller function）负责还原参数所用的栈空间。

继续按F8键，执行“ADD ESP, 10”指令，如图6.6所示。

图6.6　使用OllyDbg：观察“ADD ESP，10”指令运行之后的状态

ESP寄存器的值有变化，但是栈中的数据还在那里。因为程序没有把原有栈的数据进行置零（也没有必要清除这些值），所以保留在栈指针SP之上的（原有地址）参数值就成为了噪音（noise）或者脏数据（garbage），失去了使用的价值。另外，清除全部噪音的操作十分耗时，程序员完全没有必要刻意地去这么做。

GCC

现在我们使用GCC 4.4.1 编译这个程序，并使用IDA打开可执行文件：

main　　　proc near

var_10   = dword ptr -10h
var_C    = dword ptr -0Ch
var_8    = dword ptr -8
var_4    = dword ptr -4

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

与MSVC生成的程序相比，GCC生成的程序仅在参数入栈的方式上有所区别。在这个例子中，GCC没有使用PUSH/POP指令，而是直接对栈进行了操作。

GCC和GDB

GDB就是GNU debugger。

在Linux下，我们通过下述指令编译示例程序。其中，“−g”选项表示在可执行文件中生成debug信息。

$ gcc 1.c –g –o 1

接下来对可执行文件进行调试。

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

指令清单6.2　在printf()函数开始之前设置调试断点

(gdb) b printf
Breakpoint 1 at 0x80482f0

然后运行程序。我们的程序里没有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.

继而令GDB显示栈里的10个数据，其中左边第一列是栈地址。

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

第一个元素就是返回地址RA(0x0804844a)。为了验证这点，我们让GDB显示出这个地址开始的指令：

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

ret之后的XCHG指令与NOP指令等效。x86平台并没有专用的NOP指令，实际上，多数采用RISC的CPU的指令集里也没有专用的NOP指令。

栈中的第二个数据（0x080484f0）是字符串格式的内存地址（指针）：

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

紧接其后的三个数据，是传递给printf()函数的余下的三个参数。栈中其余的数据，可能是脏数据，也可能是其他函数的数据或局部变量等数据。没有必要进行深究。

此后，我们执行“finish”指令，令调试器“继续执行余下的指令、直到函数结束为止”。在我们的例子里，这条指令将引导GDB执行printf()函数，到它退出为止。

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

GDB 显示：在printf()退出时，EAX寄存器的值为13。这个值是函数打印的字符的总数，其结果与OllyDbg的结果相同。

GDB显示出了程序的源代码“return 0；”，它是1.c文件里第六行的指令。实际上1.c文件就在当前目录里，GDB在源文件里找到了汇编指令对应的源代码。GDB又是如何知道当前的指令对应源代码的哪一行呢？这就要归功于编译器所生成的调试信息了。如果启用了保存调试信息的选项，那么编译器在编译程序的时候，会生成源代码的行号与对应的指令地址之间的对应关系表，把它一并保存在可执行文件里。

我们一起检查下EAX寄存器里到底是不是13：

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

使用下述指令反汇编当前的指令。图中箭头指向的是接下来将要运行的指令。

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

默认情况下，GDB会以AT&T语体显示汇编指令。我们可以通过下述指令，令GDB采用Intel语体：

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

单步调试。此处，GDB用大括号表示这里是函数的结束部分。

(gdb) step
7　　　　};

在执行过“MOV EAX, 0”指令之后，EAX会被置零。我们对此进行验证：

(gdb) info registers

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

6.1.2　x64：传递9个参数

为了便于演示，我们再次修改代码，将参数的数量增加到9个。其中8个参数是int整型变量，另有1个用于控制输出格式的格式化字符串。

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

MSVC

前文介绍过，Win64使用RCX、RDX、R8、R9寄存器传递前4个参数，使用栈来传递其余的参数。我们将在本例中观察到这个现象。在下面的例子里，编译器使用了MOV指令对栈地址进行直接操作而没有使用PUSH指令。

指令清单6.3　MSVC 2012 x64

$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

在64位系统中，整型数据只占用4字节空间。那么，为什么编译器给整型数据分配了8个字节？其实，即使数据的存储空间不足64位，编译器还是会给它分配8字节的存储空间。这不仅是为了方便系统对每个参数进行内存寻址，而且编译器都会进行地址对齐。所以，64位系统为所有类型的数据都保留8字节空间。同理，32位系统也为所有类型的数据都保留4字节空间。

GCC

x86-64的＊NIX系统采用与Win64类似的方法传递参数。它优先使用 RDI、RSI、RDX、RCX、R8、R9寄存器传递前六个参数，然后利用栈传递其余的参数。在生成汇编代码时，GCC把字符串指针存储到了EDI寄存器、而非完整的RDI寄存器。在前面的3.2.2节中，64位GCC生成的汇编代码里也出现过这个现象。

在3.2.2节的那个例子里，程序在调用printf()函数之前清空了EAX寄存器。本例中存在相同的操作。

指令清单6.4　Optimizing GCC 4.4.6 x64

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

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

GCC + GDB

在使用GDB调试它之前，我们首先要编译源代码：

$ gcc -g 2.c -o 2

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

接下来，我们在printf()运行之前设置断点，然后运行这个程序：

(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\n") at printf.c:29
29　printf.c: No such file or directory

RSI/RDX/RCX/R8/R9寄存器的值就是传递给函数的参数。RIP寄存器的值应当是printf()函数的首地址。我们可以在GDB里查看各寄存器的值：

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

指令清单6.5　检查格式化字符串

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

然后使用x/g指令显示栈中的数值。指令中的g代表giant words，即以64位words型数据的格式显示各数据。

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

64位系统的栈与32位系统的栈没有太大的区别。栈里的第一个值是返回地址RA。紧接其后的，是三个保存在栈里的参数6、7、8。应该注意到数值“8”的高32位地址位没有被清零，与之对应的存储空间的值为“0x00007fff00000008”。因为int类型只占用（低）32位，所以这种存储并不会产生问题。另外，我们可以认为这个地方的高32位数据是随机的脏数据。

此后，我们通过以下指令查看printf()函数运行之后的、返回地址开始的有关指令。GDB将会显示main()函数的全部指令。

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

接下来，通过“finish”指令运行printf()之后的程序。余下的指令会清空EAX寄存器，可以注意到那时EAX已经为零。现在，RIP指针指向LEAVE指令，就是main()函数的倒数第二条指令。

(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 () at2.c:6
6　　　　return0;
Value returned is $1=39
(gdb) next
7　　　　};
(gdb) info registers
rax　　0x0　　 0
rbx　　0x0　　 0
rcx　　0x26　　38
rdx　　0x7ffff7dd59f0  140737351866864
rsi　　0x7fffffd9　　 　2147483609
rdi　　0x0　  　      　0
rbp　　0x7fffffffdf60  0x7fffffffdf60
rsp　　0x7fffffffdf40  0x7fffffffdf40
r8　　 0x7ffff7dd26a0  140737351853728
r9　　 0x7ffff7a60134  140737348239668
r10　　0x7fffffffd5b0  140737488344496
r11　　0x7ffff7a95900  140737348458752
r12　　0x400440  4195392
r13　　0x7fffffffe040  140737488347200
r14　　0x0　　　 0
r15　　0x0　　　 0
rip　　0x40057b 0x40057b <main+78>
...

6.2　ARM

6.2.1　ARM模式下传递3个参数

ARM系统在传递参数时，通常会进行拆分：把前4个参数传递给R0～R3寄存器，然后利用栈传递其余的参数。在获取（组装）参数时，遵循的函数调用约定是fastcall约定或win64约定。有关这两种约定的的详细介绍，请参照64.3节和64.5.1节。

32位ARM系统

非经优化的Keil＋ARM模式

指令清单6.6　非经优化的Keil 6/2013（ARM模式）

.text:00000000 main
.text:00000000 10 40 2D E9 STMFD　SP!, {R4,LR}
.text:00000004 03 30 A0 E3 MOV　　 R3, #3
.text:00000008 02 20 A0 E3 MOV　　 R2, #2
.text:0000000C 01 10 A0 E3 MOV　　 R1, #1
.text:00000010 08 00 8F E2 ADR　　 R0, aADBDCD;　　　　"a=%d; b=%d; c=%d"
.text:00000014 06 00 00 EB BL　　　__2printf
.text:00000018 00 00 A0 E3 MOV　　 R0, #0　　 ; return 0
.text:0000001C 10 80 BD E8 LDMFD　 SP!, {R4,PC}

可见，R0～R3寄存器依次负责传递参数。其中，R0寄存器用来传递格式化字符串，R1寄存器传递1，R2寄存器传递2，R3寄存器传递3。

0x18处的指令将R0寄存器置零，对应着源代码中的“return 0”。

这段汇编指令中规中矩。

即使开启了优化选项，Keil 6/2013生成的汇编指令也完全相同。

开启了优化选项的Keil 6/2013 (Thumb模式)

指令清单6.7　开启了优化选项的Keil 6/2013 (Thumb模式)

.text:00000000 main
.text:00000000 10 B5　　　　PUSH　 {R4, LR}
.text:00000002 03 23　　　　MOVS　 R3, #3
.text:00000004 02 22　　　　MOVS　 R2, #2
.text:00000006 01 21　　　　MOVS　 R1, #1
.text:00000008 02 A0　　　　ADR　　R0, aADBDCD;"a=%d; b=%d; c=%d"
.text:0000000A 00 F0 0D F8 BL　　 __2printf
.text:0000000E 00 20　　　　MOVS　 R0, #0
.text:00000010 10 BD　　　　POP　　{R4,PC}

这段代码和前面那段ARM程序没有太多差别。

开启优化选项的Keil 6/2013 (ARM模式) + 无返回值

我们对源代码略做修改，删除“return 0”的语句：

#include <stdio.h>

void main()
{
printf("a=%d; b=%d; c=%d", 1, 2, 3);
};

相应的汇编指令就会出现显著的差别：

.text:00000014 main
.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\n"
.text:00000024 CB 18 00 EA　 B　　　__2printf

在其余优化选项（-O3）之后，把源代码编译为ARM模式的代码。这次，程序的最后一条指令变成了B指令，不再使用之前的BL指令。另外，与前面那个没有启用优化选项的例子相比，本例并没有出现保存R0和LR寄存器的函数序言或函数尾声。这形成了另一个显著的差异。B指令仅仅将程序跳转到另一个地址，不会根据LR寄存器的值进行返回。大体上说，它和x86平台的JMP指令非常相似。为什么编译器会如此处理呢？实际上，这些指令与前面（未启用优化选项）的运行结果相同。主要原因有两个：

（1）栈和SP（Stack Pointer）都没有发生变化。

（2）调用printf()函数是程序的最后一条指令；调用之后程序再无其他操作。

即使未启用优化选项，在完成printf()函数的作业之后，程序只是要返回到LR寄存器里存储的返回地址而已。LR的值并没有因为调用printf()函数而发生变化，而且程序也没有调用printf()函数之外的函数。因为没有指令会修改LR的值，所以程序不必保存LR的状态。另外，在调用这个函数之后，程序也没有其他操作。故而编译器进行了相应的优化。

当程序最后的语句是调用另外一个函数时，编译器通常都会进行这种优化。

本书13.1.1节的“指令清单13.2”再次出现了这种优化技术。

ARM64

非经优化的GCC (Linaro) 4.9

指令清单6.8　非经优化的GCC (Linaro) 4.9

.LC1:
.string "a=%d; b=%d; c=%d"
f2:
; save FP and LR in stack frame:
stp　　　x29, x30, [sp, -16]!
; set stack frame (FP=SP):
add　　　x29, sp, 0
adrp　　 x0, .LC1
add　　　x0, x0, :lo12:.LC1
mov　　　w1, 1
mov　　　w2, 2
mov　　　w3, 3
bl　　　 printf
mov　　　w0, 0
; restore FP and LR
ldp　　　x29, x30, [sp], 16
ret

第一条STP(Store Pair)指令把FP(X29)和LR(X30)的值推送入栈。第二条“ADD X29, SP, 0”指令构成栈帧，它只是把SP的值复制给X29。

后面的“ADRP/ADD”指令对构建了字符串的指针。

在传递给printf()的格式化字符串里，%d是32位int整型数据。所以，程序使用了寄存器的32位存储后面的数据1、2、3。

即使启用了GCC (Linaro) 4.9的优化选项，它生成的指令也与此相同。

6.2.2　ARM模式下传递8个参数

我们对6.1.2节的例子稍做修改：

#include <stdio.h>

int main()
{
　　　　 printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3, 4, 5, 6, 7, 8);
　　　　 return 0;
};

Optimizing Keil 6/2013：ARM模式

.text:00000028　　 main
.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

程序分为以下几个部分。

函数序言：
第一条指令“STR LR, [SP,#var_4]!”将LR寄存器的值推送入栈。在后面调用printf()函数的时候，程序会修改这个寄存器的值。指令中的感叹号表示这属于预索引（pre-index）指令。具体说来，它会首先将SP的值减去4，然后再把LR的值保存在SP所指向的地址。这条指令与x86平台的PUSH指令十分类似。有关PUSH指令的详细介绍，请参见本书的28.2节。
第二条指令“SUB SP, SP, #0x14”将修改栈指针SP，以便在栈内分配0x14（即20）字节的存储空间。在后续的操作中，程序会传递5个32位参数，所以此时需要分配5×4=20字节的存储空间。而函数所需的前4个参数则是由寄存器负责传递。

使用栈传递5、6、7、8：
参数5、6、7、8分别被存储到R0、R1、R2、R3寄存器，然后通过“ADD R12, SP, #0x18 + var_14”指令把栈的指针地址写到R12寄存器里，以供后续指令进行入栈操作。var_14是IDA创建的汇编宏，其数值等于−0x14。这种“var_?”形式的宏出现在与栈有关的操作指令上时，用于标注栈中的局部变量。最终，R12寄存器里将会放入SP＋4。
后面的“STMIA R12, {R0-R3}”指令把R0～R3寄存器中的内容写到R12寄存器的值所表示的内存地址上。STMIA是“Store Multiple Increment After” 的缩写。顾名思义，每写入一个寄存器的值，R12的值就会加4。

通过栈传递数值4：
MOV指令向R0寄存器里写入数值“4”。然后，“STR R0, [SP,#0x18+var_18]”指令把R0寄存器的值也存储于栈中。由于var_18就是−0x18，所以偏移量的值最终为0。可见，R0寄存器里的值将会写到SP所指向的内存地址。

通过寄存器传递1、2、3：
传递给printf()函数的前三个参数a、b、c（分别为1、2、3）通过R1、R2、R3寄存器传递给printf()函数。在此之前，其他的5个数字都已经推送入栈。在传递格式化字符串之后，被调用的printf()函数就可以调用它所需的全部参数了。

调用printf()函数。

函数尾声：
“ADD SP, SP, #0x14”指令把栈指针SP还原为调用printf()之前的状态，起到释放栈空间的作用。当然，栈内原有的数据不会被清除或者置零，仍然存在相应地址上。在执行其他函数时，这些数据将被复写。
在程序的最后，“LDR PC, [SP+4+var_4],#4”从栈中提取LR寄存器的值，把它传递给PC寄存器。接下来程序将会跳转到那里。这将结束整个程序。这条指令没有出现感叹号，属于“延迟索引/post-index”指令。也就是说，它先把[SP+4+var_4]（即［SP］）传递给PC，然后再做SP=SP−4的运算。为什么IDA以这种风格显示这条指令？IDA以此充分展现栈的内存存储布局，var_4是局部栈里保管LR的数据空间。这条指令与x86平台的POP指令十分类似。

Optimizing Keil 6/2013: Thumb模式

.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模式的代码与ARM模式的代码十分相似，但是参数入栈的顺序不同。即，Thumb模式会在第一批次将8推送入栈、第二批次将7、6、5推送入栈，而在第三批次将4推送入栈。

Optimizing Xcode 4.6.3 (LLVM)：ARM模式

__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指针的值，然后将数据推送入栈；而不是先入栈，再调整SP指针。[2]

虽然这些指令表面看来杂乱无章，但是似乎这就是xcode编译出来的程序的一种特点。例如，在地址0x2918、0x2920、0x2928处，R0寄存器的相关操作似乎可以在一处集中处理。不过，这种分散布局是编译器针对并行计算而进行的优化。通常，处理器会尝试着并行处理那些相邻的指令。以“MOVT R0, #0”和“ADD R0, PC, R0”为例——这两条指令都是操作R0寄存器的指令，若集中在一起就无法并行计算。另一方面，“MOVT R0, #0”和“MOV R2, #4”之间不存在这种资源冲突，可以同时执行。想必是出于这种设计，编译器才会尽可能地进行这种处理吧。

Optimizing Xcode 4.6.3 (LLVM)：Thumb-2模式

__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        STR.W     R9, [SP,#0x1C+var_C]
__text:00002BD2 01 F0 0A EA  BLX       _print5
__text:00002BD6 05 B0        ADD       SP, SP, #0x14
__text:00002BD8 80 BD        POP       {R7,PC}

与ARM模式编译出的代码相比，这段代码存在着明显的Thumb指令的特征。除此之外，ARM模式和Thumb模式编译出的代码并无实际区别。

ARM64

Non-optimizing GCC (Linaro) 4.9

指令清单6.9　Non-optimizing GCC (Linaro) 4.9

.LC2:
　　　　 .string "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"
f3:
; grab more space in stack:
        sub　　　sp, sp, #32
; save FP and LR in stack frame:
        stp　　　x29, x30, [sp,16]
; set stack frame (FP=SP):
        add　　　x29, sp, 16
        adrp　　 x0, .LC2 ; "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n"
        add　　　x0, x0, :lo12:.LC2
        mov　　　w1, 8　　 ; 9th argument
        str　　　w1, [sp]　; store 9th argument in the stack
        mov　　　w1, 1
        mov　　　w2, 2
        mov　　　w3, 3
        mov　　　w4, 4
        mov　　　w5, 5
        mov　　　w6, 6
        mov　　　w7, 7
        bl　　　 printf
        sub　　　sp, x29, #16
; restore FP and LR
        ldp　　　x29, x30, [sp,16]
        add　　　sp, sp, 32
        ret

X-或W-寄存器传递函数的前8个参数［参见ARM13c］。字符串指针使用64位寄存器，所以它使用整个X0寄存器。所有的其他参数都属于32位整型数据，可由寄存器的低32位/即W-寄存器传递。程序使用栈来传递第九个参数（数值8）。CPU的寄存器总数有限，所以寄存器往往不足以传递全部参数。

启用优化选项之后，GCC （linaro）4.9生成的代码与此相同。

6.3　MIPS

6.3.1　传递3个参数

Optimizing GCC 4.4.5

在MIPS平台上编译“Hello, world!”程序，编译器不会使用puts()函数替代printf()函数，而且它会使用$5～$7寄存器（即$A0～$A2）传递前3个参数。

这3个寄存器都是“A-”开头的寄存器，因为它们就是负责传递参数（arguments）的寄存器。

指令清单6.10　Optimizing GCC 4.4.5 (汇编输出)

$LC0:
　　　　 .ascii　 "a=%d; b=%d; c=%d\000"
main:
; function prologue:
　　　　 lui　　　$28,%hi(__gnu_local_gp)
　　　　 addiu　　$sp,$sp,-32
　　　　 addiu　　$28,$28,%lo(__gnu_local_gp)
　　　　 sw　　　 $31,28($sp)
; load address of printf():
　　　　 lw　　　 $25,%call16(printf)($28)
; load address of the text string and set 1st argument of printf():
　　　　 lui　　　$4,%hi($LC0)
　　　　 addiu　　$4,$4,%lo($LC0)
; set 2nd argument of printf():
　　　　 li　　　 $5,1　# 0x1
; set 3rd argument of printf():
　　　　 li　　　 $6,2　# 0x2
; call printf():
　　　　 jalr　　 $25
; set 4th argument of printf() (branch delay slot):
　　　　 li　　　 $7,3　# 0x3

; function epilogue:
　　　　 lw　　　 $31,28($sp)
; set return value to 0:
　　　　 move　　 $2,$0
; return
　　　　 j　　　　$31
　　　　 addiu　　$sp,$sp,32 ; branch delay slot

指令清单6.11　Optimizing GCC 4.4.5 (IDA)

.text:00000000　main:
.text:00000000
.text:00000000 var_10　　  　= -0x10
.text:00000000 var_4　　　   = -4
.text:00000000
; function prologue:
.text:00000000　　 lui　　$gp, (__gnu_local_gp >> 16)
.text:00000004　　 addiu  $sp, -0x20
.text:00000008　　 la　　 $gp, (__gnu_local_gp & 0xFFFF)
.text:0000000C　　 sw　　 $ra, 0x20+var_4($sp)
.text:00000010　　 sw　　 $gp, 0x20+var_10($sp)
; load address of printf():
.text:00000014　　 lw　　 $t9, (printf & 0xFFFF)($gp)
; load address of the text string and set 1st argument of printf():
.text:00000018　　 la　　 $a0, $LC0　　　# "a=%d; b=%d; c=%d"
; set 2nd argument of printf():
.text:00000020　　 li　　 $a1, 1
; set 3rd argument of printf():
.text:00000024　　 li　　 $a2, 2
; call printf():
.text:00000028　　 jalr　$t9
; set 4th argument of printf() (branch delay slot):
.text:0000002C　　 li　　$a3, 3
; function epilogue:
.text:00000030　　 lw　　$ra, 0x20+var_4($sp)
; set return value to 0:
.text:00000034　　 move　$v0, $zero
; return
.text:00000038　　 jr　　$ra
.text:0000003C　　 addiu　#sp, 0x20 ; branch delay slot

IDA没有显示0x1C的指令。实际上0x18是“LUI”和“ADDIU”两条指令，IDA把它们显示为单条的伪指令，占用了8个字节。

Non-optimizing GCC 4.4.5

如果不启用编译器的优化选项，那么GCC输出的指令会详细得多。

指令清单6.12　Non-optimizing GCC 4.4.5（汇编输出）

$LC0:
　　　　 .ascii　"a=%d; b=%d; c=%d\000"
main:
; function prologue:
　　　　 addiu    $sp,$sp,-32
　　　　 sw       $31,28($sp)
　　　　 sw       $fp,24($sp)
　　　　 move     $fp,$sp
　　　　 lui      $28,%hi(__gnu_local_gp)
　　　　 addiu    $28,$28,%lo(__gnu_local_gp)
; load address of the text string
　　　　 lui      $2,%hi($LC0)
　　　　 addiu    $2,$2,%lo($LC0)
; set 1st argument of printf():
　　　　 move     $4,$2
; set 2nd argument of printf():
　　　　 li       $5,1# 0x1
; set 3rd argument of printf():
　　　　 li       $6,2# 0x2
; set 4th argument of printf():
　　　　 li       $7,3# 0x3
; get address of printf():
　　　　 lw       $2,%call16(printf)($28)
　　　　 nop
; call printf():
　　　　 move     $25,$2
　　　　 jalr     $25
　　　　 nop

; function epilogue:
　　　　 lw       $28,16($fp)
; set return value to 0:
　　　　 move     $2,$0
　　　　 move     $sp,$fp
　　　　 lw       $31,28($sp)
　　　　 lw       $fp,24($sp)
　　　　 addiu    $sp,$sp,32
; return
　　　　 j        #31
　　　　 nop

指令清单6.13　Non-optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_10      = -0x10
.text:00000000 var_8       = -8
.text:00000000 var_4       = -4
.text:00000000
; function prologue:
.text:00000000             addiu     $sp, -0x20
.text:00000004             sw        $ra, 0x20+var_4($sp)
.text:00000008             sw        $fp, 0x20+var_8($sp)
.text:0000000C             move      $fp, $sp
.text:00000010             la        $gp, __gnu_local_gp
.text:00000018             sw        $gp, 0x20+var_10($sp)
; load address of the text string:
.text:0000001C             la        $v0, aADBDCD　# "a=%d; b=%d; c=%d"
; set 1st argument of printf():
.text:00000024             move      $a0, $v0
; set 2nd argument of printf():
.text:00000028             li        $a1, 1
; set 3rd argument of printf():
.text:0000002C             li        $a2, 2
; set 4th argument of printf():
.text:00000030             li        $a3, 3
; get address of printf():
.text:00000034             lw        $v0, (printf & 0xFFFF)($gp)
.text:00000038             or        $at, $zero
; call printf():
.text:0000003C             move      $t9, $v0
.text:00000040             jalr      $t9
.text:00000044             or        $at, $zero ; NOP
; function epilogue:
.text:00000048             lw        $gp, 0x20+var_10($fp)
; set return value to 0:
.text:0000004C             move      $v0, $zero
.text:00000050             move      $sp, $fp
.text:00000054             lw        $ra, 0x20+var_4($sp)
.text:00000058             lw        $fp, 0x20+var_8($sp)
.text:0000005C             addiu     $sp, 0x20
; return
.text:00000060             jr        $ra
.text:00000064             or        $at, $zero ; NOP

6.3.2　传递9个参数

我们再次使用6.1.2节中的例子，演示9个参数的传递。

#include <stdio.h>

int main()
{
　　　　 printf("a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\n", 1, 2, 3, 4, 5, 6, 7, 8);
　　　　 return 0;
};

Optimizing GCC 4.4.5

在传递多个参数时，MIPS会使用$A0～$A3传递前4个参数，使用栈传递其余的参数。这种平台主要采用一种名为“O32”的函数调用约定。实际上大多数MIPS系统都采用这种约定。如果采用了其他的函数调用约定，例如N32约定，寄存器的用途则会有不同的设定。

下面指令中的“SW”是“Store Word”的缩写，用以把寄存器的值写入内存。MIPS的指令集很小，没有把数据直接写入内存地址的那类指令。当需要进行这种操作时，就不得不组合使用LI/SW指令。

指令清单6.14　Optimizing GCC 4.4.5 (汇编输出)

$LC0:
　　　　 .ascii　"a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\012\000"
main:
; function prologue:
　　　　 lui      $28,%hi(__gnu_local_gp)
　　　　 addiu    $sp,$sp,-56
　　　　 addiu    $28,$28,%lo(__gnu_local_gp)
　　　　 sw       $31,52($sp)
; pass 5th argument in stack:
　　　　 li       $2,4　　　# 0x4
　　　　 sw       $2,16($sp)
; pass 6th argument in stack:
　　　　 li       $2,5　　　# 0x5
　　　　 sw       $2,20($sp)
; pass 7th argument in stack:
　　　　 li       $2,6　　　# 0x6
　　　　 sw       $2,24($sp)
; pass 8th argument in stack:
　　　　 li       $2,7　　　# 0x7
　　　　 lw       $25,%call16(printf)($28)
　　　　 sw       $2,28($sp)
; pass 1st argument in $a0:
　　　　 lui      $4,%hi($LC0)
; pass 9th argument in stack:
　　　　 li       $2,8　　　# 0x8
　　　　 sw       $2,32($sp)
　　　　 addiu    $4,$4,%lo($LC0)
; pass 2nd argument in $a1:
　　　　 li       $5,1　　　# 0x1
; pass 3rd argument in $a2:
　　　　 li       $6,2　　　# 0x2
; call printf():
　　　　 jalr     $25
; pass 4th argument in $a3 (branch delay slot):
　　　　 li       $7,3　　　# 0x3

; function epilogue:
　　　　 lw       $31,52($sp)
; set return value to 0:
　　　　 move     $2,$0
; return
　　　　 j        $31
　　　　 addiu    $sp,$sp,56 ; branch delay slot

指令清单6.15　Optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_28     = -0x28
.text:00000000 var_24     = -0x24
.text:00000000 var_20     = -0x20
.text:00000000 var_1C     = -0x1C
.text:00000000 var_18     = -0x18
.text:00000000 var_10     = -0x10
.text:00000000 var_4      = -4
.text:00000000
; function prologue:
.text:00000000             lui       $gp, (__gnu_local_gp >> 16)
.text:00000004             addiu     $sp, -0x38
.text:00000008             la        $gp, (__gnu_local_gp & 0xFFFF)
.text:0000000C             sw        $ra, 0x38+var_4($sp)
.text:00000010             sw        $gp, 0x38+var_10($sp)
; pass 5th argument in stack:
.text:00000014             li        $v0, 4
.text:00000018             sw        $v0, 0x38+var_28($sp)
; pass 6th argument in stack:
.text:0000001C             li        $v0, 5
.text:00000020             sw        $v0, 0x38+var_24($sp)
; pass 7th argument in stack:
.text:00000024             li        $v0, 6
.text:00000028             sw        $v0, 0x38+var_20($sp)
; pass 8th argument in stack:
.text:0000002C             li        $v0, 7
.text:00000030             lw        $t9, (printf & 0xFFFF)($gp)
.text:00000034             sw        $v0, 0x38+var_1C($sp)
; prepare 1st argument in $a0:
.text:00000038             lui       $a0, ($LC0 >> 16) # "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"…
; pass 9th argument in stack:
.text:0000003C             li        $v0, 8
.text:00000040             sw        $v0, 0x38+var_18($sp)
; pass 1st argument in $a1:
.text:00000044             la        $a0, ($LC0 & 0xFFFF) # "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%"...
; pass 2nd argument in $a1:
.text:00000048             li        $a1, 1
; pass 3rd argument in $a2:
.text:0000004C             li        $a2, 2
; call printf():
.text:00000050             jalr      $t9
; pass 4th argument in $a3 (branch delay slot):
.text:00000054             li        $a3, 3
; function epilogue:
.text:00000058             lw        $ra, 0x38+var_4($sp)
; set return value to 0:
.text:0000005C             move      $v0, $zero
; return
.text:00000060             jr        $ra
.text:00000064             addiu     $sp, 0x38 ; branch delay slot

Non-optimizing GCC 4.4.5

关闭优化选项后，GCC会生成较为详细的指令。

指令清单6.16　Non-optimizing GCC 4.4.5（汇编输出）

$LC0:
　　　　 .ascii　"a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%d; h=%d\012\000"
main:
; function prologue:
　　　　 addiu    $sp,$sp,-56
　　　　 sw       $31,52($sp)
　　　　 sw       $fp,48($sp)
　　　　 move     $fp,$sp
　　　　 lui      $28,%hi(__gnu_local_gp)
　　　　 addiu    $28,$28,%lo(__gnu_local_gp)
　　　　 lui      $2,%hi($LC0)
　　　　 addiu    $2,$2,%lo($LC0)
; pass 5th argument in stack:
　　　　 li       $3,4　　　　 # 0x4
　　　　 sw       $3,16($sp)
; pass 6th argument in stack:
　　　　 li       $3,5　　　　 # 0x5
　　　　 sw       $3,20($sp)
; pass 7th argument in stack:
　　　　 li       $3,6　　　　 # 0x6
　　　　 sw       $3,24($sp)
; pass 8th argument in stack:
　　　　 li       $3,7　　　　 # 0x7
　　　　 sw       $3,28($sp)
; pass 9th argument in stack:
　　　　 li       $3,8　　　　 # 0x8
　　　　 sw       $3,32($sp)
; pass 1st argument in $a0:
　　　　 move     $4,$2
; pass 2nd argument in $a1:
　　　　 li       $5,1　　　　 # 0x1
; pass 3rd argument in $a2:
　　　　 li       $6,2　　　　 # 0x2
; pass 4th argument in $a3:
　　　　 li       $7,3　　　　 # 0x3
; call printf():
　　　　 lw       $2,%call16(printf)($28)
　　　　 nop
　　　　 move     $25,$2
　　　　 jalr     $25
　　　　 nop
; function epilogue:
　　　　 lw       $28,40($fp)
; set return value to 0:
　　　　 move     $2,$0
　　　　 move     $sp,$fp
　　　　 lw       $31,52($sp)
　　　　 lw       $fp,48($sp)
　　　　 addiu    $sp,$sp,56
; return
　　　　 j        $31
　　　　 nop

指令清单6.17　Non-optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_28      = -0x28
.text:00000000 var_24      = -0x24
.text:00000000 var_20      = -0x20
.text:00000000 var_1C      = -0x1C
.text:00000000 var_18      = -0x18
.text:00000000 var_10      = -0x10
.text:00000000 var_8       = -8
.text:00000000 var_4       = -4
.text:00000000
;　function prologue:
.text:00000000             addiu $sp, -0x38
.text:00000004             sw    $ra, 0x38+var_4($sp)
.text:00000008             sw    $fp, 0x38+var_8($sp)
.text:0000000C             move  $fp, $sp
.text:00000010             la    $gp, __gnu_local_gp
.text:00000018             sw    $gp, 0x38+var_10($sp)
.text:0000001C             la    $v0, aADBDCDDDEDFDGD # "a=%d; b=%d; c=%d; d=%d; e=%d; f=%d; g=%" ...
; pass 5th argument in stack:
.text:00000024             li    $v1, 4
.text:00000028             sw    $v1, 0x38+var_28($sp)
; pass 6th argument in stack:
.text:0000002C             li    $v1, 5
.text:00000030             sw    $v1, 0x38+var_24($sp)
; pass 7th argument in stack:
.text:00000034             li    $v1, 6
.text:00000038             sw    $v1, 0x38+var_20($sp)
; pass 8th argument in stack:
.text:0000003C             li    $v1, 7
.text:00000040             sw    $v1, 0x38+var_1C($sp)
; pass 9th argument in stack:
.text:00000044             li    $v1, 8
.text:00000048             sw    $v1, 0x38+var_18($sp)
; pass 1st argument in $a0:
.text:0000004C             move  $a0, $v0
; pass 2nd argument in $a1:
.text:00000050             li    $a1, 1
; pass 3rd argument in $a2:
.text:00000054             li    $a2, 2
; pass 4th argument in $a3:
.text:00000058             li    $a3, 3
; call printf():
.text:0000005C             lw    $v0, (printf & 0xFFFF)($gp)
.text:00000060             or    $at, $zero
.text:00000064             move  $t9, $v0
.text:00000068             jalr  $t9
.text:0000006C             or    $at, $zero ; NOP
; function epilogue:
.text:00000070             lw    $gp, 0x38+var_10($fp)
; set return value to 0:
.text:00000074             move  $v0, $zero
.text:00000078             move  $sp, $fp
.text:0000007C             lw    $ra, 0x38+var_4($sp)
.text:00000080             lw    $fp, 0x38+var_8($sp)
.text:00000084             addiu $sp, 0x38
; return
.text:00000088             jr    $ra
.text:0000008C             or    $at, $zero ; NOP

6.4　总结

调用函数的时候，程序的传递过程大体如下。

指令清单6.18　x86

...
PUSH 3rd argument
PUSH 2nd argument
PUSH 1st argument
CALL function
; modify stack pointer (if needed)

指令清单6.19　x64 (MSVC)

MOV RCX, 1st argument
MOV RDX, 2nd argument
MOV R8, 3rd argument
MOV R9, 4th argument
...
PUSH 5th, 6th argument, etc (if needed)
CALL function
; modify stack pointer (if needed)

指令清单6.20　x64 (GCC)

MOV RDI, 1st argument
MOV RSI, 2nd argument
MOV RDX, 3rd argument
MOV RCX, 4th argument
MOV R8, 5th argument
MOV R9, 6th argument
...
PUSH 7th, 8th argument, etc (if needed)
CALL function
; modify stack pointer (if needed)

指令清单6.21　ARM

MOV R0, 1st argument
MOV R1, 2nd argument
MOV R2, 3rd argument
MOV R3, 4th argument
; pass 5th, 6th argument, etc, in stack (if needed)
BL function
; modify stack pointer (if needed)

指令清单6.22　ARM64

MOV X0, 1st argument
MOV X1, 2nd argument
MOV X2, 3rd argument
MOV X3, 4th argument
MOV X4, 5th argument
MOV X5, 6th argument
MOV X6, 7th argument
MOV X7, 8th argument
; pass 9th, 10th argument, etc, in stack (if needed)
BL CALL function
; modify stack pointer (if needed)

指令清单6.23　MIPS（O32 调用约定）

LI $4, 1st argument ; AKA $A0
LI $5, 2nd argument ; AKA $A1
LI $6, 3rd argument ; AKA $A2
LI $7, 4th argument ; AKA $A3
; pass 5th, 6th argument, etc, in stack (if needed)
LW temp_reg, address of function
JALR temp_reg

6.5　其他

在x86、x64、ARM和MIPS平台上，程序向函数传递参数的方法各不相同。这种差异性表明，函数间传递参数的方法与CPU的关系并不是那么密切。如果付诸努力，人们甚至可以开发出一种不依赖数据栈即可传递参数的超级编译器。

为了方便记忆，在采用O32调用约定时，MIPS平台的4号～7号寄存器也叫作$A0～$A7寄存器。实际上，编程人员完全可以使用$ZERO之外的其他寄存器传递数据，也可以采取其他的函数调用约定。

CPU完全不在乎程序使用何种调用约定。

汇编语言的编程新手可能采取五花八门的方法传递参数。虽然他们基本上都是利用寄存器传递参数，但是传递参数的顺序往往不那么讲究，甚至可能通过全局变量传递参数。不过，这样的程序也能正常运行。

[1]　calling convention，又有“调用规范”“调用协定”等译法。

[2]　指针指向的地址必须有数据，这就是full stack的涵义，也是它们与empty stack的区别。

第7章 scanf()

本章演示scanf()函数。

7.1　演示案例

#include<stdio.h>

intmain()
{
        int x;
        printf ("Enter X:\n");

        scanf ("%d", &x);

        printf ("You entered %d...\n", x);

        return 0;
};

好吧，我承认时下的程序如果大量使用scanf()就不会有什么前途。本文仅用这个函数演示整形数据的指针的传递过程。

7.1.1　指针简介

在计算机科学里，“指针”属于最基础的概念。如果直接向函数传递大型数组、结构体或数据对象，程序的开销就会很大。毫无疑问，使用指针将会降低开销。不过指针的作用不只如此：如果不使用指针，而是由调用方函数直接传递数组或结构体这种大型数据（同时还要返回这些数据），那么参数的传递过程将会复杂得出奇。所以，调用方函数只负责传递数组或结构体的地址，让被调用方函数处理地址里的数据，无疑是最简单的做法。

在C/C++的概念中，指针就是描述某个内存地址的数据。

x86系统使用体系32位数字（4字节数据）描述指针；x64系统则使用64位数字（8字节数据）。从数据空间来看，64位系统的指针比32位系统的指针大了一倍。当人们逐渐从x86平台开发过渡到x86-64平台开发的时候，不少人因为难以适应而满腹牢骚。

程序人员确实可在所有的程序里仅仅使用无类型指针这一种指针。例如，在使用C语言memcpy()函数、在内存中复制数据的时候，程序员完全不必知道操作数的数据类型，直接使用2个void*指针复制数据。这种情况下，目标地址里数据的数据类型并不重要，重要的是存储数据的空间大小。

指针还广泛应用于多个返回值的传递处理。本书的第10章会详细介绍这部分内容。scanf()函数就可以返回多个值。它不仅能够分析调用方法传递了多少个参数，而且还能读取各个参数的值。

在C/C++的编译过程里，编译器只会在类型检查的阶段才会检查指针的类型。在编译器生成的汇编程序里，没有指针类型的任何信息。

7.1.2　x86

MSVC

使用MSVC 2010编译上述源代码，可得到下述汇编指令：

CONST     SEGMENT
$SG3831     DB     'Enter X:', 0aH, 00H
$SG3832     DB     '%d', 00H
$SG3833     DB     'You entered %d...', 0aH, 00H
CONST     ENDS
PUBLIC     _main
EXTRN      _scanf:PROC
EXTRN      _printf:PROC
; Function compile flags: /Odtp
_TEXT     SEGMENT
_x$=-4                ; size = 4
_main     PROC
     push   ebp
     mov    ebp, esp
     push   ecx
     push   OFFSET $SG3831 ; 'Enter X: '
     call   _printf
     add    esp, 4
     lea    eax, DWORD PTR _x$[ebp]
     push   eax
     push   OFFSET $SG3832 ; '%d'
     call   _scanf
     add    esp, 8
     mov    ecx, DWORD PTR _x$[ebp]
     push   ecx
     push   OFFSET $SG3833 ; 'You entered %d...'
     call   _printf
     add    esp, 8

     ; return 0
     xor    eax, eax
     mov    esp, ebp
     pop    ebp
     ret   0
_main   ENDP
_TEXT   ENDS

变量x是局部变量。

C/C++标准要求：函数体内部应当可以访问局部变量，且函数外部应该访问不到函数内部的局部变量。演变至今，人们不约而同地利用数据栈来存储局部变量。虽然分配局部变量的方法不只这一种，但是所有面向x86平台的编译器都约定俗成般地采用这种方法存储局部变量。

在函数序言处有一条“PUSH ECX”指令。因为函数尾声处没有对应的“POP ECX”指令，所以它的作用不是保存ECX的值。

实际上，这条指令在栈内分配了4字节的空间、用来存储局部变量x。

汇编宏_x$ （其值为−4）用于访问局部变量x，而EBP寄存器用来存储栈当前帧的指针。

在函数运行的期间，EBP一直指向当前的栈帧（stack frame）。这样，函数即可通过EBP+offset的方式访问本地变量、以及外部传入的函数参数。

ESP也可以用来访问本地变量，获取函数所需的运行参数。不过ESP的值经常发生变化，用起来并不方便。函数在启动之初就会利用EBP寄存器保存ESP寄存器的值。这就是为了确保在函数运行期间保证EBP寄存器存储的原始ESP值固定不变。

在32位系统里，典型的栈帧（stack frame）结构如下表所示。

…… | ……
EBP-8 | 局部变量＃2，IDA标记为var_8
EBP-4 | 局部变量＃1，IDA标记为var_4
EBP | EBP的值
EBP+4 | 返回地址Return address
EBP+8 | 函数参数＃1，IDA标记为arg_0
EBP+0xC | 函数参数＃2，IDA标记为arg_4
EBP+0x10 | 函数参数＃3，IDA标记为arg_8
…… | ……

本例中的scanf()有两个参数。

第一个参数是一个指针，它指向含有“%d”的格式化字符串。第二个参数是局部变量x的地址。

首先，“lea eax, DWORD PTR _x$[ebp]”指令将变量x的地址放入EAX寄存器。“lea”是“load effective address”的缩写，能够将源操作数（第二个参数）给出的有效地址（offset）传送到目的寄存器（第一个参数）之中。[1]

此处，LEA将EBP寄存器的值与宏_x$求和，然后使用EAX寄存器存储这个计算结果；也就是等同于“ lea eax, [ebp−4]”。

就是说，程序会将EBP寄存器的值减去4，并把这个运算结果存储于EAX寄存器。把EAX寄存器的值推送入栈之后，程序才开始调用scanf()函数。

在调用printf()函数之前，程序传给它第一个参数，即格式化字符串“You entered %d...\n”的指针。

printf()函数所需的第二个参数由“mov ecx,[ebp−4]”指令间接取值。传递给ecx的值是ebp−4所指向的地址的值（即变量x的值），而不是ebp−4所表达的地址。

此后的指令把ECX推送入栈，然后启动printf()函数。

7.1.3　MSVC＋OllyDbg

现在使用OllyDbg调试上述例子。加载程序之后，一直按F8键单步执行，等待程序退出ntdll.dll、进入我们程序的主文件。然后向上翻滚滚轴，查找main()主函数。在main()里面点中第一条指令“PUSH EBP”，并在此处按下F2键设置断点。接着按F9键，运行断点之前的指令。

我们一起来跟随调试器查看变量x的计算指令，如图7.1所示。

图7.1　OllyDbg：局部变量x的赋值过程

在这个界面里，我们在寄存器的区域内用右键单击EAX寄存器，然后选择“Follow in stack”。如此一来，OllyDbg就会在栈窗口里显示栈地址和栈内数据，以便我们清楚地观察栈里的局部变量。图中红箭头所示的就是栈里的数据。其中，在地址0x6E494714处的数据就是脏数据。在下一时刻，PUSH指令会把数据存储到栈里的下一个地址。接下来，在程序执行完scanf()函数之前，我们一直按F8键。在执行scanf()函数的时候，我们要在运行程序的终端窗口里输入数据，例如123，如图7.2所示。

图7.2　控制台窗口

scanf()函数的执行之后的情形如图7.3所示。

图7.3　OllyDbg：运行scanf()之后

EAX寄存器里存有函数的返回值1。这表示它成功地读取了1个值。我们可以在栈里找到局部变量的地址，其数值为0x7B（即数字123）。

这个值将通过栈传递给ECX寄存器，然后再次通过栈传递给printf()函数，如图7.4所示。

图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 ; "Enter 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 entered %d...\n"
             mov     [esp+20h+var_1C], edx
             mov     [esp+20h+var_20], eax
             call    _printf
             mov     eax, 0
             leave
             retn
main         endp

GCC把printf()替换为puts()，这和3.4.3节中的现象相同。

此处，GCC通过MOV指令实现入栈操作，这点和MSVC相同。

其他

这个例子充分证明：在编译过程中，编译器会遵循C/C++源代码的表达式顺序和模块化结构。在C/C++源代码中，只要两个相邻语句之间没有其他的表达式，那么在生成的机器码中对应的指令之间就不会有其他的指令，而且其执行顺序也与源代码各语句的书写顺序相符。

7.1.4　x64

在编译面向x64平台的可执行程序时，由于这个程序的参数较少，编译器会直接使用寄存器传递参数。除此之外，编译过程和x86的编译过程没有太大的区别。

MSVC

指令清单7.1　MSVC 2012 x64

_DATA   SEGMENT
$SG1289 DB       'Enter X: ', 0aH, 00H
$SG1291 DB       '%d', 00H
$SG1292 DB       'You entered %d... ', 0aH, 00H
_DATA   ENDS

_TEXT   SEGMENT
x$ = 32
main    PROC
$LN3:
        sub      rsp, 56
        lea      rcx, OFFSET FLAT:$SG1289 ; 'Enter X: '
        call     printf
        lea      rdx, QWORD PTR x$[rsp]
        lea      rcx, OFFSET FLAT:$SG1291 ; '%d'
        call     scanf
        mov      edx, DWORD PTR x$[rsp]
        lea      rcx, OFFSET FLAT:$SG1292 ; 'You entered %d... '
        call     printf

        ; return 0
        xor      eax, eax
        add      rsp, 56
        ret      0
main    ENDP
_TEXT   ENDS

GCC

使用GCC 4.6（启用优化选项－O3）编译上述程序，可得到如下所示的汇编指令。

指令清单7.2　Optimizing GCC 4.4.6 x64

.LC0:
        .string "Enter X:"
.LC1:
        .string "%d"
.LC2:
        .string "You entered %d...\n"

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...\n"
        xor     eax, eax
        call    printf

        ;return 0
        xor     eax, eax
        add     rsp, 24
        ret

7.1.5　ARM

Optimizing Keil 6/2013 (Thumb模式)

.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:\n"
.text:00000046 06 F0 D3 F8                   BL        __2printf
.text:0000004A 69 46                         MOV       R1, SP
.text:0000004C AA A0                         ADR       R0, aD          ; "%d"
.text:0000004E 06 F0 CD F8                   BL        __0scanf
.text:00000052 00 99                         LDR       R1, [SP,#8+var_8]
.text:00000054 A9 A0                         ADR       R0,aYouEnteredD___ ; "You entered%d…\n"
.text:00000056 06 F0 CB F8                   BL        __2printf
.text:0000005A 00 20                         MOVS      R0, #0
.text:0000005C 08 BD                         POP       {R3,PC}

scanf()函数同样要借助指针传递返回值。在本例里，编译器给它分配了一个整型变量的指针。整型数据占用4个字节的存储空间。但是返回数据的内存地址，可以直接放在CPU的寄存器中。在生成的汇编代码里，变量x存在于数据栈中，被IDA标记为var_8，然而，此时程序完全可以直接使用。栈指针SP指向的存储空间，没有必要像上述代码那样机械式地调整SP分配栈空间。此后，程序把栈指针SP（x的地址）存入R1寄存器，再把格式化字符串的偏移量存入R0寄存器，如此一来，scanf()函数就获得了它所需要的所有参数。在此之后，程序使用LDR指令把栈中的返回值复制到R1寄存器、以供printf()调用。

即使使用Xcode LLVM 程序以ARM模式编译这段程序，最终生成的汇编代码也没有实质性的区别，所以本书不再演示。

ARM64

指令清单7.3　Non-optimizing GCC 4.9.1 ARM64

 1 .LC0:
 2          .string "Enter X:"
 3 .LC1:
 4          .string "%d"
 5 .LC2:
 6          .string "You entered %d...\n"
 7 scanf_main:
 8 ; subtract 32 from SP, then save FP and LR in stack frame:
 9          stp     x29, x30, [sp, -32]!
10 ; set stack frame (FP=SP)
11          add     x29, sp, 0
12 ; load pointer to the "Enter X:" string:
13          adrp    x0, .LC0
14          add x0, x0, :lo12:.LC0
15 ; X0=pointer to the "Enter X:" string
16 ; print it:
17          bl      puts
18 ; load pointer to the "%d" string:
19          adrp    x0, .LC1
20          add     x0, x0, :lo12:.LC1
21 ; find a space in stack frame for "x" variable (X1=FP+28):
22          add     x1, x29, 28
23 ; X1=address of "x" variable
24 ; pass the address to scanf() and call it:
25          bl      __isoc99_scanf
26 ; load 32-bit value from the variable in stack frame:
27          ldr     w1, [x29,28]
28 ; W1=x
29 ; load pointer to the "You entered %d...\n" string
30 ; printf() will take text string from X0 and "x" variable from X1 (or W1)
31          adrp    x0, .LC2
32          add     x0, x0, :lo12:.LC2
33          bl      printf
34 ; return 0
35          mov     w0, 0
36 ; restore FP and LR, then add 32 to SP:
37          ldp     x29, x30, [sp], 32
38          ret

第22行用来分配局部变量x的存储空间。当scanf()函数获取这个地址之后，它就把用户输入的数据传递给这个内存地址。输入的数据应当是32位整型数据。第27行的指令把输入数据的值传递给printf()函数。

7.1.6　MIPS

MIPS编译器同样使用数据栈存储局部变量x。然后程序就可以通过$sp+24调用这个变量。scanf()函数获取地址指针之后，通过LW（Load Word）指令把输入变量存储到这个地址、以传递给printf()函数。

指令清单7.4　Optimizing GCC 4.4.5 (assembly output)

$LC0:
          .ascii  "Enter X:\000"
$LC1:
          .ascii  "%d\000"
$LC2:
          .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:
          lw      $5,24($sp)
          lw      $25,%call16(printf)($28)
          lui     $4,%hi($LC2)
          jalr    $25
          addiu   $4,$4,%lo($LC2) ; branch delay slot

; function epilogue:
          lw      $31,36($sp)
; set return value to 0:
          move    $2,$0
; return:
          j       $31
          addiu   $sp,$sp,40 ; branch delay slot

IDA将本程序的栈结构显示如下。

指令清单7.5　Optimizing GCC 4.4.5 (IDA)

.text:00000000 main:
.text:00000000
.text:00000000 var_18     = -0x18
.text:00000000 var_10     = -0x10
.text:00000000 var_4      = -4
.text:00000000
; function prologue:
.text:00000000            lui         $gp, (__gnu_local_gp >> 16)
.text:00000004            addiu       $sp, -0x28
.text:00000008            la          $gp, (__gnu_local_gp & 0xFFFF)
.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)  # "Enter 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 & 0xFFFF)($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)($gp)
.text:00000048            lui         $a0, ($LC2 >> 16) # "You entered %d...\n"
.text:0000004C            jalr        $t9
.text:00000050            la          $a0, ($LC2 & 0xFFFF) # "You entered %d...\n" ; brach 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 slot

7.2　全局变量

在本章前文的那个程序里，如果x不是局部变量而是全局变量，那会是什么情况？一旦x变量成为了全局变量，函数内部的指令、以及整个程序中的任何部分都可以访问到它。虽然优秀的程序不应当使用全局变量，但是我们应当了解它的技术特点。

#include <stdio.h>

//now x is global variable
int x;

int main()
{
          printf ("Enter X:\n");

          scanf ("%d", &x);

          printf ("You entered %d...\n", 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

与前文不同的是，x变量的存储空间是数据段（_data域），反而没有使用数据栈。因此整个程序的所有指令都可以直接访问全局变量x。在可执行文件中，未经初始化的变量不会占用任何存储空间。

在某些指令在变量访问这种未初始化的全局变量的时候，操作系统会分配一段数值为零的地址给它。这是操作系统VM（虚拟内存）的管理模式所决定的。

如果对上述源代码稍做改动，加上变量初始化的指令：

int x=10; //设置默认值

那么对应的代码会变为：

_DATA   SEGMENT
_x      DD      0aH
...

上述指令将初始化x。其中DD代表DWORD，表示x是32位的数据。

若在IDA里打开对x进行初始化的可执行文件，我们将会看到在数据段的开头部分看到初始化变量x。紧随其后的空间用于存储本例中的字符串。

用IDA打开7.2节里那个不初始化变量x的例子，那么将会看到下述内容。

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

这段代码里有很多带“？”标记的变量，这是未初始化的x变量的标记。这意味着在程序加载到内存之后，操作系统将为这些变量分配空间、并填入数字零[2]。但是在可执行文件里，这些未初始化的变量不占用内存空间。为了方便使用巨型数组之类的大型数据，人们刻意做了这种设定。

7.2.2　MSVC：x86+OllyDbg

我们可以在OllyDbg观察程序的数据段里的变量。如图7.5所示。

图7.5　OllyDbug：运行scanf()函数之后的状态

全局变量x出现在数据段里。在调试器执行完PUSH指令之后，变量x的指针推即被推送入栈，我们就可在栈里右键单击x的地址并选择“Follow in dump”，并在左侧的内存窗口观察它。

在控制台输入123之后，栈里的数据将会变成0x7B（左下窗口的高亮部分）。

您有没有想过，为什么第一个字节是0x7B？若考虑到数权，此处应该是00 00 00 7B。可见，这是x86系统里低位优先的“小端字节序/LITTLE-ENDIAN”的典型特征。小端字节序属于“字节（顺）序/endianness”的一种，它的第一个字节是数权最低的字节，数权最高的字节会排列在最后。本书将在第31章将详细介绍字节序。

此后，EAX寄存器将存储这个地址里的32位值，并将之传递给printf()函数。

本例中，变量x的内存地址是0x00C53394。

在OllyDbg里，按下Alt+M组合键可查看这个进程的内存映射（process memory map）。如图7.6所示，这个地址位于程序PE段的.data域。

图7.6　OllyDbg：进程内存映射

7.2.3　GCC：x86

在汇编指令层面，Linux与Windows的编译结果区别不大。它们的区别主要体现在字段（segament）名称和字段属性上：Linux GCC生成的未初始化变量会出现在_bss段，对应的ELF文件描述了这个字段数据的属性。

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

如果给这个变量分配了初始值，比如说10，那么这个变量将会出现在_data段，并且具有下述属性。

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

7.2.4　MSVC：x64

指令清单7.6　MSVC 2012 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

这段x64代码与x86的代码没有明显区别。请注意变量x的传递过程：scanf()函数通过LEA指令获取x变量的指针；而printf()函数则是通过MOV指令获取x变量的值。“DWORD PTR”与机器码无关，它是汇编语言的一部分，用来声明后面的变量为32位的值，以便MOV指令能够正确处理数据类型。

7.2.5　ARM: Optimizing Keil 6/2013 (Thumb模式)

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

因为变量x是全局变量，所以它出现于数据段的“.data”字段里。有的读者可能会问，为什么文本字符串出现在代码段（.text），但是x变量却出现于数据段（.data）？原因在于x是变量。顾名思义，变量的值可变、属于一种频繁变化的可变数据。在ARM系统里，代码段的程序代码可存储于处理器的ROM（Read-Only Memory），而可变变量存储于RAM（Random-Access Memory）中。与x86/x64系统相比，ARM系统的内存寻址能力很有限，可用内存往往十分紧张。在ARM系统存在ROM的情况下，使用RAM 内存存储常量则明显是一种浪费。此外，ARM系统在启动之后RAM里的值都是随机数；想要使用RAM存储常量，还要单独进行初始化赋值才行。

在后续代码段的指令中，程序给变量x分配了个指针（即off_2c）。此后，程序都是通过这个指针对x变量进行的操作。不这样做的话变量x可能被分配到距离程序代码段很远的内存空间，其偏移量有可能超过有关寻址指令的寻址能力。Thumb模式下，ARM系统的LDR指令只能够使用周边1020字节之内的变量；即使在32位ARM模式下，它也只能调用偏移量在±4095字节之内的变量。这个范围就是变量地址（与调用指令之间）的偏移量的局限。为了保证它的地址离代码段足够近、能被代码调用，就需要就近分配x变量的地址。由于在链接阶段（linker）x的地址可能会被随意分配，甚至可能被分配到外部内存的地址，所以编译器必须在前期阶段就把x的地址分配到就近的区域之内。

另外，如果声明某变量为常量/const，Keil编译程序会把这个变量分配到.constdata字段。这可能是为了便于后期linker把这个字段与代码段一起放入ROM。

7.2.6　ARM64

指令清单7.7　Non-optimizing GCC 4.9.1 ARM64

 1         .comm    x,4,4
 2 .LC0:
 3         .string "Enter X:"
 4 .LC1:
 5         .string "%d"
 6 .LC2:
 7         .string "You entered %d...\n"
 8 f5:
 9 ; save FP and LR in stack frame:
10         stp       x29, x30, [sp, -16]!
11 ; set stack frame (FP=SP)
12         add       x29, sp, 0
13 ; load pointer to the "Enter X:" string:
14         adrp      x0, .LC0
15         add       x0, x0, :lo12:.LC0
16         bl        puts
17 ; load pointer to the "%d" string:
18         adrp      x0, .LC1
19         add       x0, x0, :lo12:.LC1
20 ; form address of x global variable:
21         adrp      x1, x
22         add       x1,  x1, :lo12:x
23         bl        __isoc99_scanf
24 ; form address of x global variable again:
25         adrp      x0, x
26         add       x0, x0, :lo12:x
27 ; load value from memory at this address:
28         ldr       w1, [x0]
29 ; load pointer to the "You entered %d...\n" string:
30         adrp      x0, .LC2
31         add       x0, x0, :lo12:.LC2
32         bl        printf
33 ; return 0
34         mov       w0, 0
35 ; restore FP and LR:
36         ldp       x29, x30, [sp], 16
37         ret

在上述代码里变量x被声明为全局变量。程序通过ADRP/ADD 指令对（第21行和第25行）计算它的指针。

7.2.7　MIPS

无未初始值的全局变量

以变量x为全局变量为例。我们把它编译为可执行文件，然后使用IDA加载这个程序。因为程序在声明变量x的时候没有对它进行初始化赋值，所以在IDA中变量x出现在.sbss ELF里（请参见3.5.1节的全局指针）。

指令清单7.8　Optimizing GCC 4.4.5 (IDA)

.text:004006C0 main:
.text:004006C0
.text:004006C0 var_10       = -0x10
.text:004006C0 var_4        = -4
.text:004006C0
; function prologue:
.text:004006C0              lui     $gp, 0x42
.text:004006C4              addiu   $sp, -0x20
.text:004006C8              li      $gp, 0x418940
.text:004006CC              sw      $ra, 0x20+var_4($sp)
.text:004006D0              sw      $gp, 0x20+var_10($sp)
; call puts():
.text:004006D4              la      $t9, puts
.text:004006D8              lui     $a0, 0x40
.text:004006DC              jalr    $t9 ; puts
.text:004006E0              la      $a0, aEnterX  # "Enter X:" ; branch delay slot
; call scanf():
.text:004006E4              lw      $gp, 0x20+var_10($sp)
.text:004006E8              lui     $a0, 0x40
.text:004006EC              la      $t9, __isoc99_scanf
; prepare address of x:
.text:004006F0              la      $a1, x
.text:004006F4              jalr    $t9 ; __isoc99_scanf
.text:004006F8              la      $a0, aD          # "%d" ; branch delay slot
; call printf():
.text:004006FC              lw      $gp, 0x20+var_10($sp)
.text:00400700              lui     $a0, 0x40
; get address of x:
.text:00400704              la      $v0, x
.text:00400708              la      $t9, printf
; load value from "x"  variable and pass it to printf() in $a1:
.text:0040070C              lw      $a1, (x - 0x41099C)($v0)
.text:00400710              jalr    $t9 ; printf
.text:00400714              la      $a0, aYouEnteredD___ # "You entered %d...\n" ; branch delay slot
; function epilogue:
.text:00400718              lw      $ra, 0x20+var_4($sp)
.text:0040071C              move    $v0, $zero
.text:00400720              jr      $ra
.text:00400724              addiu   $sp, 0x20  ; branch delay slot
...

.sbss:0041099C  #  Segment type: Uninitialized
.sbss:0041099C              .sbss
.sbss:0041099C              .globl x
.sbss:0041099C x:           .space 4
.sbss:0041099C

IDA会精简部分指令信息。我们不妨通过objdump观察上述文件确切的汇编指令。

指令清单7.9　Optimizing GCC 4.4.5 (objdump)

 1 004006c0 <main>:
 2 ; function prologue:
 3   4006c0:        3c1c0042         lui       gp,0x42
 4   4006c4:        27bdffe0         addiu     sp,sp,-32
 5   4006c8:        279c8940         addiu     gp,gp,-30400
 6   4006cc:        afbf001c         sw        ra,28(sp)
 7   4006d0:        afbc0010         sw        gp,16(sp)
 8 ; call puts():
 9   4006d4:        8f998034         lw        t9,-32716(gp)
10   4006d8:        3c040040         lui       a0,0x40
11   4006dc:        0320f809         jalr      t9
12   4006e0:        248408f0         addiu     a0,a0,2288 ; branch delay slot
13 ; call scanf():
14   4006e4:        8fbc0010         lw        gp,16(sp)
15   4006e8:        3c040040         lui       a0,0x40
16   4006ec:        8f998038         lw        t9,-32712(gp)
17 ; prepare address of x:
18   4006f0:        8f858044         lw        a1,-32700(gp)
19   4006f4:        0320f809         jalr      t9
20   4006f8:        248408fc         addiu     a0,a0,2300 ; branch delay slot
21 ; call printf():
22   4006fc:        8fbc0010         lw        gp,16(sp)
23   400700:        3c040040         lui       a0,0x40
24 ; get address of x:
25   400704:        8f828044         lw        v0,-32700(gp)
26   400708:        8f99803c         lw        t9,-32708(gp)
27 ; load value from "x" variable and pass it to printf() in $a1:
28   40070c:        8c450000         lw        a1,0(v0)
29   400710:        0320f809         jalr      t9
30   400714:        24840900         addiu     a0,a0,2304 ; branch delay slot
31 ; function epilogue:
32   400718:        8fbf001c         lw        ra,28(sp)
33   40071c:        00001021         move      v0,zero
34   400720:        03e00008         jr        ra
35   400724:        27bd0020         addiu     sp,sp,32 ; branch delay slot
36 ; pack of NOPs used for aligning next function start on 16-byte boundary:
37   400728:        00200825         move      at,at
38   40072c:        00200825         move      at,at

第18行处的指令对全局指针GP和一个负数值的偏移量求和，以此计算变量x在64KB数据缓冲区里的访问地址。此外，三个外部函数（puts()、scanf()、printf()）在64KB数据空间里的全局地址，也是借助GP计算出来的（第9行、第16行、第26行）。GP指向数据空间的正中央。经计算可知，三个函数的地址和变量x的地址都在数据缓冲区的前端。这并不意外，因为这个程序已经很短小了。

此外，值得一提的是函数结尾处的两条NOP指令。它的实际指令是空操作指令“MOVE$AT, $AT”。借助这两条NOP指令，后续函数的起始地址可向16字节边界对齐。

有初始值的全局变量

我们对前文的例子做相应修改，把有关行改为：

int x=10; // default value

则可得如下代码段。

指令清单 7.10　Optimizing GCC 4.4.5 (IDA)

.text:004006A0  main:
.text:004006A0
.text:004006A0  var_10      = -0x10
.text:004006A0  var_8       = -8
.text:004006A0  var_4       = -4
.text:004006A0
.text:004006A0              lui         $gp, 0x42
.text:004006A4              addiu       $sp, -0x20
.text:004006A8              li          $gp, 0x418930
.text:004006AC              sw          $ra, 0x20+var_4($sp)
.text:004006B0              sw          $s0, 0x20+var_8($sp)
.text:004006B4              sw          $gp, 0x20+var_10($sp)
.text:004006B8              la          $t9, puts
.text:004006BC              lui         $a0, 0x40
.text:004006C0              jalr        $t9 ; puts
.text:004006C4              la          $a0, aEnterX     # "Enter X:"
.text:004006C8              lw          $gp, 0x20+var_10($sp)
; prepare high part of x address:
.text:004006CC              lui         $s0, 0x41
.text:004006D0              la          $t9, __isoc99_scanf
.text:004006D4              lui         $a0, 0x40
; add low part of x address:
.text:004006D8              addiu       $a1, $s0, (x - 0x410000)
; now address of x is in $a1.
.text:004006DC              jalr        $t9 ; __isoc99_scanf
.text:004006E0              la          $a0, aD          # "%d"
.text:004006E4              lw          $gp, 0x20+var_10($sp)
; get a word from memory:
.text:004006E8              lw          $a1, x
; value of x is now in $a1.
.text:004006EC              la          $t9, printf
.text:004006F0              lui         $a0, 0x40
.text:004006F4              jalr        $t9 ; printf
.text:004006F8              la          $a0, aYouEnteredD___  # "You entered %d...\n"
.text:004006FC              lw          $ra, 0x20+var_4($sp)
.text:00400700              move        $v0, $zero
.text:00400704              lw          $s0, 0x20+var_8($sp)
.text:00400708              jr          $ra
.text:0040070C              addiu       $sp, 0x20

...

.data:00410920              .globl  x
.data:00410920 x:           .word   0xA

为何它处没有了.sdata段？这可能是受到了GCC选项的影响。不论如何，变量x出现在.data段里。这个段会被加载到常规的通用内存区域，我们可以在此看到变量的处理方法。

MTPS程序必须使用成对指令处理变量的地址。本例使用的是LUI(Load Upper Immediate)和ADDIU (Add Immediate Unsigned Word)指令时。

我们继续借助objdump观察确切地操作指令。

指令清单7.11　Optimizing GCC 4.4.5 (objdump)

004006a0 <main>:
  4006a0:        3c1c0042        lui       gp,0x42
  4006a4:        27bdffe0        addiu     sp,sp,-32
  4006a8:        279c8930        addiu     gp,gp,-30416
  4006ac:        afbf001c        sw        ra,28(sp)
  4006b0:        afb00018        sw        s0,24(sp)
  4006b4:        afbc0010        sw        gp,16(sp)
  4006b8:        8f998034        lw        t9,-32716(gp)
  4006bc:        3c040040        lui       a0,0x40
  4006c0:        0320f809        jalr      t9
  4006c4:        248408d0        addiu     a0,a0,2256
  4006c8:        8fbc0010        lw        gp,16(sp)
; prepare high part of x address:
  4006cc:        3c100041        lui       s0,0x41
  4006d0:        8f998038        lw        t9,-32712(gp)
  4006d4:        3c040040        lui       a0,0x40
; add low part of x address:
  4006d8:        26050920        addiu     a1,s0,2336
; now address of x is in $a1.
  4006dc:        0320f809        jalr      t9
  4006e0:        248408dc        addiu     a0,a0,2268
  4006e4:        8fbc0010        lw        gp,16(sp)
; high part of x address is still in $s0.
; add low part to it and load a word from memory:
  4006e8:        8e050920        lw        a1,2336(s0)
; value of x is now in $a1.
  4006ec:        8f99803c        lw        t9,-32708(gp)
  4006f0:        3c040040        lui       a0,0x40
  4006f4:        0320f809        jalr      t9
  4006f8:        248408e0        addiu     a0,a0,2272
  4006fc:        8fbf001c        lw        ra,28(sp)
  400700:        00001021        move      v0,zero
  400704:        8fb00018        lw        s0,24(sp)
  400708:        03e00008        jr        ra
  40070c:        27bd0020        addiu     sp,sp,32

这个程序使用 LUI和ADDIU指令对生成变量地址。地址的高地址位仍然存储于$S0寄存器，而且单条LW指令（Load Word）即可封装这个偏移量。所以，单条LW指令足以提取变量的值，然后把它交付给printf()函数。

T-字头的寄存器名称是临时数据寄存器的助记符。此外，这段程序还使用到了S-字头的寄存器名称。在调用其他函数之前，调用方函数应当保管好自身S-字头的寄存器的值，避免它们受到被调用方函数的影响，举例来说在0x4006cc处的指令对$S0寄存器赋值，而后程序调用了scanf()函数，接着地址为0x4006e8的指令然继续调用$S0据此我们可以判断。scanf()函数不会变更$S0的值。

7.3　scanf()函数的状态监测

大家都知道scanf()不怎么流行了，但是这并不代表它派不上用场了。在万不得已必须使用这个函数的时候，切记检查函数的退出状态是否正确。例如：

#include <stdio.h>

int main()
{
        int x;
        printf ("Enter X:\n");

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

根据这个函数的功能规范[3]，scanf()函数在退出时会返回成功赋值的变量总数。

就本例子而言，正常情况下：用户输入一个整型数字时函数返回1；如果没有输入的值存在问题（或为EOF/没有输入数据），scanf()则返回0。

为此我们可在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生成的汇编代码如下所示。

          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

当被调用方函数callee（本例中是scanf()函数）使用EAX寄存器向调用方函数caller（本例中是main()函数）传递返回值。

之后，“CMP EAX，1”指令对返回值进行比对，检查其值是否为1。

JNE是条件转移指令其全称是“Jump if Not Equal”。在两值不相同时进行跳转。

就是说，如果EAX寄存器里的值不是1，则程序将跳转到JNE所指明的地址（本例中会跳到$LN1@main）；在将控制权指向这个地址之后，CPU会执行其后的打印指令，显示“What you entered? Huh?”。另一种情况是scanf()成功读取指定数据类型的数据，其返回值就会是1，此时不会发生跳转，而是继续执行JNE以后的指令，显示‘You entered %d…’和变量x的值。

在scanf()函数成功地给变量赋值的情况下，程序会一路执行到JMP（无条件转移）指令。这条指令会跳过第二条调用printf()函数的指令，从“XOR EAX,EAX”指令开始执行，从而完成return 0的操作。

可见，“一般地说”条件判断语句会出现成对的“CMP/Jcc”指令。此处cc是英文“condition code”的缩写。比较两个值的CMP指令会设置处理器的标志位[4]。Jcc指令会检查这些标志位，判断是否进行跳转。

但是上述的说法容易产生误导，实际上CMP指令进行的操作是减法运算。确切地说，不仅是CMP指令所有的“数学/算术计算”指令都会设置标志位。如果将1与1进行比较，1−1=0，ZF标志位（“零”标识位，最终运算结果是0）将被计算指令设定为1。将两个不同的数值进行CMP比较时，ZF标志位的值绝不会是1。JNE指令会依据ZF标志位的状态判断是否需要进行跳转，实际上此两者（Jump if Not Zero）的同义指令。JNE和JNZ的opcode都相同。所以，即使使用减法运算操作指令SUB替换CMP指令，Jcc指令也可以进行正常的跳转。不过在使用SUB指令时，我们还需要分配一个寄存器保存运算结果，而CMP则不需要使用寄存器保存运算结果。

7.3.2　MSVC：x86：IDA

现在来让IDA大显身手。对于多数初学者来说，使用MSVC编译器的/MD选项是个值得推荐的好习惯。这个选项会要求编译器“不要链接（link）标准函数”，而是从MSVCR＊.DLL里导入这些标准函数。总之，使用/MD选项编译出来的代码一目了然，便于我们观察它在哪里、调用了哪些标准函数。

在使用IDA分析程序的时候，应当充分利用它的标记功能。比如说，分析这段程序的时候，我们明白在发生错误的时候会执行JNE跳转。此时就可以用鼠标单击跳转的JNE指令，按下“n”键，把相应的标签（lable）改名为“error”；然后把正常退出的标签重命名为“exit”。这种修改就可大幅度增强代码的可读性。

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

如此一来，这段代码就容易理解了。虽然重命名标签的功能很强大，但是逐一修改每条指令的标签则无疑是画蛇添足。

此外，IDA还有如下一些高级用法。

整理代码：

标记某段代码之后，按下键盘数字键的“－”减号，整段代码将被隐藏，只留下首地址和标签。下面的例子中，我隐藏了两段代码，并对整段代码进行了重命名。

.text:00401000 _text  segment para public 'CODE' use32
.text:00401000        assume cs:_text
.text:00401000        ;org 401000h
.text:00401000 ;  ask for X
.text:00401012 ;  get X
.text:00401024        cmp     eax, 1
.text:00401027        jnz     short error
.text:00401029 ;  print result
.text:0040103B        jmp     short exit
.text:0040103D
.text:0040103D error: ; CODE XREF: _main+27
.text:0040103D        push  offset aWhat ; "What you entered? Huh?\n"
.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将会进入图解的显示方式。其效果如图7.7所示。

图7.7　IDA的图解模式

判断语句会分出两个箭头，一条是红色、一条是绿色。当判断条件表达式的值为真时，程序会走绿色箭头所示的流程；如果判断条件表达式不成立，程序会采用红色箭头所标示的流程。

图解模式下，也可以对各分支节点命名和收缩。图7.8处理了3个模块。

图7.8　IDA图解模式下的收缩操作

这种图解模式非常实用。逆向工程工作经验不多的人，可使用这种方式来大幅度地减少他需要处理的信息量。

7.3.3　MSVC：x86+OllyDbg

我们使用OllyDbg调试刚才的程序，在scanf()异常返回的情况下强制其继续运行余下的指令。

在把本地变量的地址传递给scanf()的时候，变量本身处于未初始化状态，其值应当是随机的噪声数据。如图7.9所示，变量x的值为0x6E494714。

图7.9　使用OllyDbg观察scanf()的变量传递过程

在执行scanf()函数的时候，我们输入非数字的内容，例如 “asdasd”。这时候scanf()会通过EAX寄存器返回0，如图7.10所示。这个零意味着scanf()函数遇到某种错误。

图7.10　使用OllyDbg观察scanf()的异常处理

执行scanf()前后，变量x的值没有发生变化。在上述情况下，scanf()函数仅仅返回0，而没对变量进行赋值。

现在来“hack”一下。右键单击EAX，选中“Set to 1”。

在此之后EAX寄存器存储的值被人为设定为1，程序将完成后续的操作，printf()函数会在控制台里显示数据栈里变量x的值。

我们使用F9键继续运行程序。此后控制台的情况如图7.11所示。

图7.11　控制台窗口

1850296084是十进制值，其十六进制值就是我们刚才看到的0x6E494714。

7.3.4　MSVC：x86+Hiew

下面，我们一起修改可执行文件。所谓的“修改”可执行文件，就是对其打补丁（即人们常说的“patch”）。通过打补丁的方法，我们可以强制程序在所有情况下都进行输出。

首先，我们要启用MSVC的编译选项/MD。这样编译出来的可执行文件将把标准函数链接到MSVCR*.DLL，以方便我们在可执行文件的文本段里找到main()函数。此后，我们使用Hiew工具打开可执行文件，找到.text段开头部分的main()函数（依次使用Enter, F8, F6, Enter, Enter）。

然后会看到图7.12所示的界面。

图7.12　使用Hiew观察main()函数

Hiew 能够识别并显示ASCIIZ，即以null为结束字节的ASCII字符串。它还能识别导入函数的函数名称。

如图7.13所示，将鼠标光标移至.0040127处（即JNZ指令的所在位置，我们使其失效），按F3键，然后输入“9090”。“9090”是两个连续的NOP（No Operation，空操作）的opcode。

图7.13　在Hiew中把JNZ替换为两条NOP指令

然后按F9键（更新），把修改后的可执行文件保存至磁盘。

连续的9090是典型的修改特征，有的人会觉得不甚美观。此外还可以把这个opcode的第二个字节改为零（opcode的两个字节代表jump offset），令jcc指令跳转的偏移量为0，从而继续运行下一条指令。

刚才的修改方法可使转移指令失效。除此以外我们还可以强制程序进行转我们可以把jcc对应的opcode的第一个字节替换为“EB”，不去修改第二字节（offset）。这种修改方法把条件转移指令替换为了无条件跳转指令。经过这样的调整之后，本例中的可执行文件都会无条件地显示错误处理信息“What you entered? Huh?”。

7.3.5　MSVC:x64

本例使用的变量x是int型整数变量。在x64系统里，int型变量还是32位数据。在64位平台上访问寄存器的（低）32位时，计算机就要使用助记符以E-头的寄存器名称。然而在访问x64系统的64位指针时，我们就需要使用R-字头的寄存器名称、处理完整的64位数据。

指令清单7.12　MSVC 2012 x64

_DATA   SEGMENT
$SG2924 DB        'Enter X: ', 0aH, 00H
$SG2926 DB        '%d', 00H
$SG2927 DB        'You entered %d... ', 0aH, 00H
$SG2929 DB        'What you entered? Huh? ', 0aH, 00H
_DATA   ENDS

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

7.3.6　ARM

ARM: Optimizing Keil 6/2013 (Thumb 模式)

指令清单7.13　Optimizing Keil 6/2013 (Thumb模式)

var_8        = -8

             PUSH       {R3,LR}
             ADR        R0, aEnterX           ; "Enter X:\n"
             BL         __2printf
             MOV        R1, SP
             ADR        R0, aD                ; "%d"
             BL         __0scanf
             CMP        R0, #1
             BEQ        loc_1E
             ADR        R0, aWhatYouEntered   ; "What you entered? Huh?\n"
             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...\n"
             BL         __2printf
             B          loc_1A

这里我们见到了未介绍过的CMP和BEQ指令。

ARM系统的CMP与x86系统的同名指令相似。它们都是将两个参数相减，并设置相应的标志位。

BEQ是条件转移指令（Branch if EQual），在CMP操作数相等的情况下进行跳转。如果CMP比较的两个值相同，则ZF标志寄存器的值就会是1，那么BEQ指令就会跳转到它指定的偏移量上去。它与x86的JZ指令作用相同。

其余的指令并不难理解：程序流有两个分支，这两个分支最终收敛于loc_1A处，通过“MOV R0, #0”指令把返回值保存于R0寄存器，然后退出。

ARM64

指令清单7.14　Non-optimizing GCC 4.9.1 ARM64

 1 .LC0:
 2         .string "Enter X:"
 3 .LC1:
 4         .string "%d"
 5 .LC2:
 6         .string "You entered %d...\n"
 7 .LC3:
 8         .string "What you entered? Huh?"
 9 f6:
10 ; save FP and LR in stack frame:
11         stp     x29, x30, [sp, -32]!
12 ; set stack frame (FP=SP)
13         add     x29, sp, 0
14 ; load pointer to the "Enter X:" string:
15         adrp    x0, .LC0
16         add     x0, x0, :lo12:.LC0
17         bl      puts
18 ; load pointer to the "%d" string:
19         adrp    x0, .LC1
20         add     x0, x0, :lo12:.LC1
21 ; calculate address of x variable in the local stack
22         add     x1, x29, 28
23         bl      __isoc99_scanf
24 ; scanf() returned result in W0.
25 ; check it:
26         cmp     w0, 1
27 ; BNE is Branch if Not Equal
28 ; so if W0<>0, jump to L2 will be occurred
29         bne    .L2
30 ; at this moment W0=1, meaning no error
31 ; load x value from the local stack
32         ldr     w1, [x29,28]
33 ; load pointer to the "You entered %d...\n" string:
34         adrp    x0, .LC2
35         add     x0, x0, :lo12:.LC2
36         bl      printf
37 ; skip the code, which print the "What you entered? Huh?" string:
38         b       .L3
39 .L2:
40 ; load pointer to the "What you entered? Huh?" string:
41         adrp    x0, .LC3
42         add     x0, x0, :lo12:.LC3
43         bl      puts
44 .L3:
45 ; return 0
46         mov     w0, 0
47 ; restore FP and LR:
48         ldp     x29, x30, [sp], 32
49         ret

上述程序通过CMP/BNE指令对控制分支语句。

7.3.7　MIPS

指令清单7.15　Optimizing GCC 4.4.5 (IDA)

.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

scanf()函数通过$V0寄存器传递其返回值。地址为0x004006E4的指令负责比较$V0和$V1的值。其中，$V1在0x004006DC处被赋值为1。BEQ的作用是“Branch Equal”（在相等时进行跳转）。如果两个寄存器里的值相等，即成功读取了1个整数，那么程序将会从0x0040070C处继续执行指令。

7.3.8　练习题

JNE/JNZ指令可被修改为JE/JZ指令，而且后者也可被修改为前者。BNE和BEQ之间也有这种关系。不过，在进行这种替代式修改之后，还要对程序的基本模块进行修改。请多进行一些有关练习。

7.4　练习题

7.4.1　题目

这段代码在Linux x86-64上用GCC编译，运行的时候都崩溃了（段错误）。然而，它在Windows环境下用Msvc 2010 x86编译后却能工作，为什么？

#include <string.h>
#include <stdio.h>

void alter_string(char *s)
{
        strcpy (s, "Goodbye!");
        printf ("Result: %s\n", s);
};

int main()
{
        alter_string ("Hello, world!\n");
};

[1]　参见附录A.6.2。

[2]　请参阅ISO07，6.7.8节。

[3]　请参照http://msdn.microsoft.com/en-us/library/9y6s16x1(VS.71).aspx。

[4]　processor flags,参见http://en.wikipedia.org/wiki/FLAGS_register_(computing)。

第8章 参数获取

调用方（caller）函数通过栈向被调用方（callee）函数传递参数。本章介绍被调用方函数获取参数的具体方式。

指令清单8.1　范例

#include <stdio.h>

int f (int a, int b, int c)
{
         return a*b+c;
};

int main()
{
         printf ("%d\n", f(1, 2, 3));
         return 0;
};

8.1　x86

8.1.1　MSVC

使用MSVC 2010 Express编译上述程序，可得到汇编指令如下。

指令清单8.2　MSVC 2010 Express

_TEXT   SEGMENT
_a$ = 8                         ; size = 4
_b$ = 12                        ; size = 4
_c$ = 16                        ; size = 4
_f      PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _a$[ebp]
        imul    eax, DWORD PTR _b$[ebp]
        add     eax, DWORD PTR _c$[ebp]
        pop     ebp
        ret     0
_f      ENDP

_main   PROC
        push    ebp
        mov     ebp, esp
        push    3 ; 3rd argument
        push    2 ; 2nd argument
        push    1 ; 1st argument
        call    _f
        add     esp, 12
        push    eax
        push    OFFSET $SG2463 ; '%d', 0aH, 00H
        call    _printf
        add     esp, 8
       ; return 0
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP

main()函数把3个数字推送入栈，然后调用了f(int, int, int)。被调用方函数f()通过_a$=8一类的汇编宏访问所需参数以及函数自定义的局部变量。只不过从被调用方函数的数据栈的角度来看，外部参考的偏移量是正值，而局部变量的偏移量是负值。可见，当需要访问栈帧（stack frame）以外的数据时，被调用方函数可把汇编宏（例如_a$）与EBP寄存器的值相加，从而求得所需地址。

当变量a的值存入EAX寄存器之后，f()函数通过各参数的地址依次进行乘法和加法运算，运算结果一直存储于EAX寄存器。此后EAX的值就可以直接作为返回值传递给调用方函数。调用方函数main()再把EAX的值当作参数传递给printf()函数。

8.1.2　MSVC+OllyDbg

本节演示OllyDbg的使用方法。当f()函数读取第一个参数时，EBP的值指向栈帧，如图8.1中的红色方块所示。栈帧里的第一个值是EBP的原始状态，第二个值是返回地址RA，第三个值开始的三个值依次为函数的第一个参数、第二个参数和第三个参数。在访问第一个参数（当需要访问第一个参数）时，计算机需要把EBP的值加上8（2个32位words）。

图8.1　使用OllyDbg观察f()函数

OllyDbg能够识别出外部传递的参数。它会对栈里的数据进行标注，添加上诸如“RETURN from”“Arg1”之类的标注信息。

故而，被调用方函数所需的参数并不在自己的栈帧之中，而是在调用方函数的栈帧里。所以，被OllyDbg标注为Arg的数据都存储于其他函数的栈帧。

8.1.3　GCC

我们使用GCC 4.4.1编译上述源程序，然后使用IDA查看它的汇编指令。

指令清单8.3　GCC 4.4.1

           public f
f          proc near

arg_0      = dword ptr  8
arg_4      = dword ptr  0Ch
arg_8      = dword ptr  10h

           push    ebp
           mov     ebp, esp
           mov     eax, [ebp+arg_0] ; 1st argument
           imul    eax, [ebp+arg_4] ; 2nd argument
           add     eax, [ebp+arg_8] ; 3rd argument
           pop     ebp
           retn
f          endp

           public main
main       proc near

var_10     = dword ptr -10h
var_C      = dword ptr -0Ch
var_8      = dword ptr -8

           push    ebp
           mov     ebp, esp
           and     esp, 0FFFFFFF0h
           sub     esp, 10h
           mov     [esp+10h+var_8], 3 ; 3rd argument
           mov     [esp+10h+var_C], 2 ; 2nd argument
           mov     [esp+10h+var_10], 1 ; 1st argument
           call    f
           mov     edx, offset aD  ; "%d\n"
           mov     [esp+10h+var_C], eax
           mov     [esp+10h+var_10], edx
           call    _printf
           mov     eax, 0
           leave
           retn
main       endp

GCC的编译结果和MSVC的编译结果十分相似。

不同之处是两个被调用方函数（f和printf）没有还原栈指针SP。这是因为函数尾声的倒数第二条指令——LEAVE指令（参见附录A.6.2）能够还原栈指针。

8.2　x64

x86-64系统的参数传递过程略有不同。x86-64系统能够使用寄存器传递（前4个或前6个）参数。就这个程序而言，被调用方函数会从寄存器里获取参数，完全不需要访问栈。

8.2.1　MSVC

启用优化选项后，MSVC编译的结果如下。

指令清单8.4　Optimizing MSVC 2012 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指令的效率高，所以它分配了LEA指令。在制备f()函数的第一个和第三个参数时，main()函数同样使用了LEA指令。编译器无疑认为LEA指令向寄存器赋值的速度比常规的MOV指令速度快。

我们再来看看MSVC未开启优化选项时的编译结果。

指令清单8.5　MSVC 2012 x64

f               proc near

; shadow space:
arg_0           = dword ptr  8
arg_8           = dword ptr  10h
arg_10          = dword ptr  18h

                ; ECX - 1st argument
                ; EDX - 2nd argument
                ; R8D - 3rd argument
                mov     [rsp+arg_10], r8d
                mov     [rsp+arg_8], edx
                mov     [rsp+arg_0], ecx
                mov     eax, [rsp+arg_0]
                imul    eax, [rsp+arg_8]
                add     eax, [rsp+arg_10]
                retn
f               endp

main            proc near
                sub     rsp, 28h
                mov     r8d,3 ; 3rd argument
                mov     edx,2 ; 2nd argument
                mov     ecx,1 ; 1st argument
                call    f
                mov     edx, eax
                lea     rcx, $SG2931  ; "%d\n"
                call    printf

                ; return 0
                xor     eax, eax
                add     rsp, 28h
                retn
main            endp

比较意外的是，原本位于寄存器的3个参数都被推送到了栈里。这种现象叫作“阴影空间/shadow space”[1]。每个Win64程序都可以（但非必须）把4个寄存器的值保存到阴影空间里。使用阴影空间有以下两个优点：1）通过栈传递参数，可避免浪费寄存器资源（有时可能会占用4个寄存器）；2）便于调试器debugger在程序中断时找到函数参数（请参阅：https://msdn.microsoft.com/en-us/library/ew5tede7%28v=VS.90%29.aspx

）。

大型函数可能会把输入参数保存在阴影空间里，但是小型函数（如本例）可能就不会使用阴影空间了。

在使用阴影空间时，由调用方函数分配栈空间，由被调用方函数根据需要将寄存器参数转储到它们的阴影空间中。

8.2.2　GCC

在启用优化选项后，GCC生成的代码更为晦涩。

指令清单8.6　Optimizing GCC 4.4.6 x64

f:
        ; EDI - 1st argument
        ; ESI - 2nd argument
        ; EDX - 3rd argument
        imul    esi, edi
        lea     eax, [rdx+rsi]
        ret

main:
        sub     rsp, 8
        mov     edx, 3
        mov     esi, 2
        mov     edi, 1
        call    f
        mov     edi, OFFSET FLAT:.LC0 ; "%d\n"
        mov     esi, eax
        xor     eax, eax  ; number of vector registers passed
        call    printf
        xor     eax, eax
        add     rsp, 8
        ret

不开启优化选项的情况下，GCC生成的代码如下。

指令清单8.7　GCC 4.4.6 x64

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

阴影空间只是微软的概念，System V *NIX［参见参考文献Mit13］里没有这种规范或约定。GCC只有在寄存器数量容纳不下所有参数的情况下，才会使用栈传递参数。

8.2.3　GCC: uint64_t型参数

指令清单8.1的源程序采用的是32位int整型参数，所以以上的汇编指令使用的寄存器都是64位寄存器的低32位（即E-字头寄存器）。如果把参数改为64位数据，那么汇编指令则会略有不同：

#include <stdio.h>
#include <stdint.h>

uint64_t f (uint64_t a, uint64_t b, uint64_t c)
{
         return a*b+c;
};

int main()
{
         printf ("%lld\n", f(0x1122334455667788,
                                      0x1111111122222222,
                                      0x3333333344444444));
         return 0;
};

指令清单8.8　Optimizing GCC 4.4.6 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\n"
           mov      rsi, rax
           xor      eax, eax ; number of vector registers passed
           call     _printf
           xor      eax, eax
           add      rsp, 8
           retn
main       endp

相应的汇编指令使用了整个寄存器（R-字头寄存器）。其他部分基本相同。

8.3　ARM

8.3.1　Non-optimizing Keil 6/2013 (ARM mode)

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

主函数只起到了调用另外2个函数的作用。它把3个参数传递给了f()函数。

前文提到过，在ARM系统里，前4个寄存器（R0～R3）负责传递前4个参数。

在本例中，f()函数通过前3个寄存器（R0～R2）读取参数。

MLA（Multiply Accumulate）指令将前两个操作数（R3和R1里的值）相乘，然后再计算第三个操作数（R2里的值）和这个积的和，并且把最终运算结果存储在零号寄存器R0之中。根据ARM指令的有关规范，返回值就应该存放在R0寄存器里。

MLA是乘法累加指令[2]，能够一次计算乘法和加法运算，属于非常有用的指令。在SIMD技术[3]的FMA指令问世之前，x86平台的指令集里并没有类似的指令。

首条指令“MOV R3, R0”属于冗余指令。即使此处没有这条指令，后面的MLA指令直接使用有关的寄存器也不会出现任何问题。因为我们没有启用优化选项，所以编译器没能对此进行优化。

BL指令把程序的控制流交给LR寄存器里的地址，而且会在必要的时候切换处理器的运行模式（Thumb模式和ARM模式之间进行模式切换）。被调用方函数f()并不知道它会被什么模式的代码调用，不知道调用方函数属于ARM模式的代码还是Thumb模式的代码。所以这种模式切换的功能还是必要的。如果它被Thumb模式的代码调用，BX指令不仅会进行相应的跳转，还会把处理器模式调整为Thumb。如果它被ARM模式的指令[4]调用，则不会进行模式切换。

8.3.2　Optimizing Keil 6/2013 (ARM mode)

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

在启用最大幅度的优化功能（-O3）之后，前面那条MOV指令被优化了，或者说被删除了。MLA直接使用所有寄存器的参数，并且把返回值保存在R0寄存器里。调用方函数继而可从R0寄存器获取返回值。

8.3.3　Optimizing Keil 6/2013 (Thumb mode)

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

因为Thumb模式的指令集里没有MLA指令，所以编译器将它分为两个指令。第一条MULS指令计算R0和R1的积，把运算结果存储在R0寄存器里。第二条ADDS计算R1和R2的和，并且把计算结果存储在R0寄存器里。

8.3.4　ARM64

Optimizing GCC (Linaro) 4.9

ARM64的情况简单一些。MADD指令可以一次进行乘法和加法的混合运算，与前文的MLA指令十分类似。全部3个参数由X-字头寄存器的低32位传递。这是因为这些参数都是32位整型数据。函数的返回值存储在W0寄存器。

指令清单8.9　Optimizing GCC (Linaro) 4.9

f:
        madd      w0, w0, w1, w2
        ret
main:
; save FP and LR to stack frame:
        stp       x29, x30, [sp, -16]!
        mov       w2, 3
        mov       w1, 2
        add       x29, sp, 0
        mov       w0, 1
        bl        f
        mov       w1, w0
        adrp      x0, .LC7
        add       x0, x0, :lo12:.LC7
        bl        printf
; return 0
        mov       w0, 0
; restore FP and LR
        ldp       x29, x30, [sp], 16
        ret
.LC7:
        .string "%d\n"

我们再来看看参数为64位 uint64_t的情况：

#include <stdio.h>
#include <stdint.h>

uint64_t f (uint64_t a, uint64_t b, uint64_t c)
{
        return a*b+c;
};
int main()
{
        printf ("%lld\n", f(0x1122334455667788,
                                      0x1111111122222222,
                                      0x3333333344444444));
        return 0;
};

f:
        madd    x0, x0, x1, x2
        ret
main:
        mov     x1, 13396
        adrp    x0, .LC8
        stp     x29, x30, [sp, -16]!
        movk    x1, 0x27d0, lsl 16
        add     x0, x0, :lo12:.LC8
        movk    x1, 0x122, lsl 32
        add     x29, sp, 0
        movk    x1, 0x58be, lsl 48
        bl      printf
        mov     w0, 0
        ldp     x29, x30, [sp], 16
        ret
.LC8:
        .string "%lld\n"

这两段代码的f()函数部分相同。在采用64位参数之后，程序使用了整个64位 X寄存器。程序通过两条指令才能把长数据类型的64位值存储到寄存器里。本书在28.3.1节会详细介绍64位数据的有关操作。

Non-optimizing GCC (Linaro) 4.9

在没有启用优化选项的情况下，编译器生成的代码稍显冗长：

f:
        sub     sp, sp, #16
        str     w0, [sp,12]
        str     w1, [sp,8]
        str     w2, [sp,4]
        ldr     w1, [sp,12]
        ldr     w0, [sp,8]
        mul     w1, w1, w0
        ldr     w0, [sp,4]
        add     w0, w1, w0
        add     sp, sp, 16
        ret

函数f()把传入的参数保存在数据栈里，以防止后期的指令占用W0～W2寄存器。这可防止后续指令覆盖函数参数，起到保护传入参数的作用。这种技术叫作“寄存器保护区/Register Save Area”［参见ARM13c］。但是，本例的这种被调用方函数可以不这样保存参数。寄存器保护区与8.2.1节介绍的阴影空间十分相似。

在启用优化选项后，GCC 4.9会把这部分寄存器存储指令删除。这是因为优化功能判断出后续指令不会再操作函数参数的相关地址，所以编译器不再另行保存W0～W2中存储的数据。

此外，上述代码使用了MUL/ADD指令对，而没有使用MADD指令。

8.4　MIPS

指令清单8.10　Optimizing GCC 4.4.5

.text:00000000 f:
; $a0=a
; $a1=b
; $a2=c
.text:00000000             mult      $a1, $a0
.text:00000004             mflo      $v0
.text:00000008             jr        $ra
.text:0000000C             addu      $v0, $a2, $v0 ; branch delay slot
; result in $v0 upon  return

.text:00000010 main:
.text:00000010
.text:00000010 var_10      = -0x10
.text:00000010 var_4       =-4
.text:00000010
.text:00000010             lui       $gp, (__gnu_local_gp >> 16)
.text:00000014             addiu     $sp, -0x20
.text:00000018             la        $gp, (__gnu_local_gp & 0xFFFF)
.text:0000001C             sw        $ra, 0x20+var_4($sp)
.text:00000020             sw        $gp, 0x20+var_10($sp)
; set c:
.text:00000024             li        $a2, 3
; set a:
.text:00000028             li        $a0, 1
.text:0000002C             jal       f
; set b:
.text:00000030             li        $a1, 2      ; branch delay slot
; result in $v0 now
.text:00000034             lw        $gp, 0x20+var_10($sp)
.text:00000038             lui       $a0, ($LC0 >> 16)
.text:0000003C             lw        $t9, (printf & 0xFFFF)($gp)
.text:00000040             la        $a0, ($LC0 & 0xFFFF)
.text:00000044             jalr      $t9
; take result of f()       function and pass it as a second argument to printf():
.text:00000048             move      $a1, $v0         ; branch delay slot
.text:0000004C             lw        $ra, 0x20+var_4($sp)
.text:00000050             move      $v0, $zero
.text:00000054             jr        $ra
.text:00000058             addiu     $sp, 0x20        ; branch delay slot

函数所需的前4个参数由4个A-字头寄存器传递。

MIPS平台有两个特殊的寄存器：HI和LO。它们用来存储MULT指令的乘法计算结果——64位的积。只有MFLO和MFHI指令能够访问HI和LO寄存器。其中，MFLO负责访问积的低32位部分。本例中它把积的低32位部分存储到$V0寄存器。

因为本例没有访问积的高32位，所以那半部分被丢弃了。不过我们的程序就是这样设计的：积是32位的整型数据。

最终ADDU（Add Unsigned）指令计算第三个参数与积的和。

在MIPS平台上，ADD和ADDU是两个不同的指令。此二者的区别体现在异常处理的方式上，而符号位的处理方式反而没有区别。ADD指令可以触发溢出处理机制。溢出有时候是必要的[5]，而且被Ada和其他编程语言支持。ADDU不会引发溢出。因为C/C++不支持这种机制，所以本例使用的是ADDU指令而非ADD指令。

此后$V0寄存器存储这32位的运算结果。

main()函数使用到了JAL（Jump and Link）指令。JAL和JALR指令有所区别，前者使用的是相对地址——偏移量，后者则跳转到寄存器存储的绝对地址里。JALR的R代表Register。由于f()函数和main()函数都位于同一个object文件，所以f()函数的相对地址是已知的，可以被计算出来。

[1]　请参阅MSDN https://msdn.microsoft.com/en-us/library/zthk2dkh%28v=vs.80%29.aspx。

[2]　请参见http://en.wikipedia.org/wiki/Multiply%E2%80%93accumulate_operation。

[3]　“单指令流多数据流”的缩写，请参见https://en.wikipedia.org/wiki/FMA_instruction_set。

[4]　ARM12，附录A2.3.2。

[5]　http://blog.regehr.org/archives/1154。

第9章 返回值

在x86系统里，被调用方函数通常通过EAX寄存器返回运算结果。[1]若返回值属于byte或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()函数不会明确返回任何值（没有return指令）。不过在main()函数退出时，EAX寄存器还会存有数据，EAX寄存器保存的数据会被传递给exit()函数、成为后者的输入参数。通常EAX寄存器的值会是被调用方函数残留的确定数据，所以void类型函数的返回值、也就是主函数退出代码往往属于伪随机数（pseudorandom）。

在我们进行相应的演示之前，请注意main()函数返回值是void型：

#include <stdio.h>

void main()
{
          printf ("Hello, world!\n");
};

然后使用Linux系统编译。

3.4.3节处介绍过GCC 4.8.1会使用puts()替换printf()，而且puts()函数会返回它所输出的字符的总数。我们将充分利用这点，观察main()函数的返回值。请注意在main()函数结束时，EAX寄存器的值不会是零；也就是说，此时EAX寄存器存储的值应当是上一个函数——puts()函数的返回值。

指令清单9.1　GCC 4.8.1

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

我们再通过一段bash脚本程序，观察程序的退出状态（返回值）。

指令清单9.2　tst.sh

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

执行上述脚本之后，我们将会看到：

$ ./tst.sh
Hello, world!
14

这个“14”就是puts()函数输出的字符的总数。

9.2　函数返回值不被调用的情况

printf() 函数的返回值为打印的字符的总数，但是很少有程序会使用这个返回值。实际上，确实有调用运算函数、却不使用运算结果的程序：

int f()
{
     // skip first 3 random values
     rand();
     rand();
     rand();
     // and use 4th
     return rand();
};

上述四个rand()函数都会把运算结果存储到EAX寄存器里。但是前三个rand()函数留在EAX寄存器的运算结果都被抛弃了。

9.3　返回值为结构体型数据

我们继续讨论使用EAX寄存器存储函数返回值的案例。函数只能够使用EAX这1个寄存器回传返回值。因为这种局限，过去的C编译器无法编译返回值超过EAX容量（一般来说，就是int型数据）的函数。那个时候，如果要让返回多个返回值，那么只能用函数返回一个值、再通过指针传递其余的返回值。现在的C编译器已经没有这种短板了，return指令甚至可以返回结构体型的数据，只是时下很少有人会这么做。如果函数的返回值是大型结构的数据，那么应由调用方函数（caller）负责分配空间，给结构体分配指针，再把指针作为第一个参数传递给被调用方函数。现在的编译器已经能够替程序员自动完成这种复杂的操作了，其处理方式相当于上述几个步骤，只是编译器隐藏了有关操作。

我们来看：

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};
};

经GCC 4.8.1编译上述程序，可得到如下所示的指令。

指令清单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

可见，调用方函数（caller）创建了数据结构、分配了数据空间，被调用的函数仅向结构体填充数据。其效果等同于返回结构体。这种处理方法并不会影响程序性能。

[1]　请参见MSDN: Return Values (C++): http://msdn.microsoft.com/en-us/library/7572ztz4.aspx。

第10章 指针

指针通常用来帮助函数处理返回值（请参阅第7章的范例）。当函数需要返回多个值时，它通常都是通过指针传递返回值的。

10.1　全局变量

#include <stdio.h>

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

int sum, product;

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

经MSVC 2010（启用优化选项/Ox /Ob）编译上述程序，可得到如下所示的指令。

指令清单10.1　Optimizing MSVC 2010 (/Ob0)

COMM     _product:DWORD
COMM     _sum:DWORD
$SG2803 DB     'sum=%d, product=%d', 0aH, 00H
_x$ = 8                                         ; size = 4
_y$ = 12                                        ; size = 4
_sum$ = 16                                      ; size = 4
_product$ = 20                                  ; size = 4

_f1   PROC
      mov  ecx, DWORD PTR _y$[esp-4]
      mov  eax, DWORD PTR _x$[esp-4]
      lea  edx, DWORD PTR [eax+ecx]
      imul eax, ecx
      mov  ecx, DWORD PTR _product$[esp-4]
      push esi
      mov  esi, DWORD PTR _sum$[esp]
      mov  DWORD PTR [esi], edx
      mov  DWORD PTR [ecx], eax
      pop  esi
      ret  0
_f1   ENDP

_main PROC
      push OFFSET _product
      push OFFSET _sum
      push 456                                  ; 000001c8H
      push 123                                  ; 0000007bH
      call _f1
      mov  eax, DWORD PTR _product
      mov  ecx, DWORD PTR _sum
      push eax
      push ecx
      push OFFSET $SG2803
      call DWORD PTR __imp__printf
      add  esp, 28                              ; 0000001cH
      xor  eax, eax
      ret  0
_main ENDP

如图10.1所示，我们使用OllyDbg调试这个程序。

图10.1　使用OllyDbg观察全局变量的地址传递到f1()函数的过程

首先，全局变量的地址会被传递给f1()函数。我们用右键单击栈中的元素，选中“Follow in dump”，将会在数据段中看到两个变量的实际情况。

在运行第一条指令之前，在BSS（Block Started by Symbol）域内未被初始化赋值的数据会置零，所以这些变量会被清空（ISO可规范，6.7.8节）。变量驻留在数据段。如图10.2所示，我们可以按Alt-M组合键调用memory map进行确认。

图10.2　使用OllyDbg查看内存映射

按下F7键及跟踪调试f1()函数[1]，如图10.3所示。

图10.3　使用OllyDbug跟踪调试f1()

如上图所示，数据栈里会有0x1C8(456)和0x7B(123)两个值，以及这两个全局变量的地址（指针）。

继续跟踪调试程序，待其执行到f1()函数尾部。如图10.4所示，我们会在左下方的窗口看到全局变量的计算结果。

图10.4　使用ollyDbg观察f1()函数执行完毕

而后，全局变量的值将被加载到寄存器中，通过栈传递给printf()函数。如图10.5所示。

图10.5　使用OllyDbg观察printf()函数获取全局变量的地址

10.2　局部变量

下面，我们略微调整下程序，使其通过局部变量完成同样的功能。

指令清单10.2　将和、积变量存储在局部变量里

void main()
{
        int sum, product; // now variables are local in this function

        f1(123, 456, &sum, &product);
        printf ("sum=%d, product=%d\n", sum, product);
};

编译之后，f1()函数的汇编代码与使用全局变量的代码完全相同。而main()函数的汇编代码会有相应的变化。启用/Ox /Ob0的优化选项编译后，会得到如下所示的指令。

指令清单10.3　Optimizing MSVC 2010 (/Ob0)

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

接下来，我们使用OllyDbg来调试这个程序。栈内的两个变量在栈内0x2EF854 和 0x2EF858两个地址上。如图10.6所示，我们观察下它们入栈的过程。

图10.6　使用OllyDbg观察局部变量的入栈过程

在f1()函数启动时，0x2EF854和0x2EF858地址的数据都是随机的脏数据，如图10.7所示。

图10.7　使用OllyDbg追踪f1()的启动过程

f1()函数结束时的情况如图10.8所示。

现在0x2EF854的值是0xDB18，0x2EF8588的值是0x243。它们都是f1()函数的运算结果。

图10.8　使用OllyDbg观察f1()执行完毕时的栈内数据

10.3　总结

借助指针，f1()函数可以返回位于任意地址的任意值。这个例子体现了指针的重要作用。C++的引用（指针）/reference的作用与C pointer（指针）相同。本书的51.3节将详细介绍C++的引用。

[1]　trace，可以进入被调用的函数里继续进行单步调试。如果使用F8键，则可一步就完成call调用的函数。

第11章 GOTO语句

人们普遍认为使用GOTO语句将破坏代码的结构化构造[1]。即使如此，某些情况下我们还必须使用这种语句[2]。

本章围绕以下程序进行说明：

#include <stdio.h>

int main()
{
        printf ("begin\n");
        goto exit;
        printf ("skip me!\n");
exit:
        printf ("end\n");
};

使用MSVC 2012编译上述程序，可得到如下所示的指令。

指令清单11.1　MSVC 2012

$SG2934 DB      'begin', 0aH, 00H
$SG2936 DB      'skip me!', 0aH, 00H
$SG2937 DB      'end', 0aH, 00H

_main   PROC
        push    ebp
        mov     ebp, esp
        push    OFFSET $SG2934 ; 'begin'
        call    _printf
        add     esp, 4
        jmp     SHORT $exit$3
        push    OFFSET $SG2936 ; 'skip me!'
        call    _printf
        add     esp, 4
$exit$3:
        push    OFFSET $SG2937 ; 'end'
        call    _printf
        add     esp, 4
        xor     eax, eax
        pop     ebp
        ret     0
_main   ENDP

源代码中的goto语句直接被编译成了JMP指令。这两个指令的效果完全相同：无条件的转移到程序中的另一个地方继续执行后续命令。

只有在人工干预的情况下，例如使用调试器调整程序、或者对程序打补丁的情况下，程序才会调用第二个printf()函数。

我们用它练习patching技术。首先使用Hiew打开刚才编译的可执行文件，如图11.1所示将光标移动到JMP（0x410）处，按下F3键（编辑），再输入两个零。这样，我们就把这个地址的opcode改为了EB 00如图11.2所示。

图11.1　Hiew

图11.2　Hiew

JMP所在的opcode的第二个字节代表着跳转的相对偏移量。把这个偏移量改为0，就可以让它继续运行后续指令。这样JMP指令就不会跳过第二个printf()函数。

按下F9键（保存文件）并退出Hiew。再次运行这个可执行文件的情况应当如图11.3所示。

图11.3　修改后的程序

当然，把JMP指令替换为两个NOP指令可以达到同样效果。因为NOP的opcode是0x90、只占用一个字节，所以在进行替换时要写上两个NOP指令。

11.1　无用代码Dead Code

在编译术语里，上述程序中第二次调用printf()函数的代码称为“无用代码/dead code”。无用代码永远不会被执行。所以，在启用编译器的优化选项之后，编译器会把这种无用代码删除得干干净净。

指令清单11.2　Optimizing MSVC 2012

$SG2981 DB      'begin', 0aH, 00H
$SG2983 DB      'skip me!', 0aH, 00H
$SG2984 DB      'end', 0aH, 00H

_main   PROC
        push    OFFSET $SG2981 ; 'begin'
        call    _printf
        push    OFFSET $SG2984 ; 'end'
$exit$4:
        call    _printf
        add     esp, 8
        xor     eax, eax
        ret     0
_main   ENDP

然而编译器却没能删除字符串“skip me!”。

11.2　练习题

请结合自己的编译器和调试器进行有关练习。

[1]　请参阅Dij68。

[2]　请参阅Knu74，以及Yur13。

第12章 条件转移指令

12.1　数值比较

本章会围绕以下程序进行演示：

#include <stdio.h>

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

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

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

12.1.1　x86

x86 + MSVC

在关闭优化选项时，使用MSVC编译上述源程序，可得到f_signed()函数。

指令清单12.1　Non-optimizing MSVC 2010

_a$ = 8
_b$ = 12
_f_signed PROC
    push   ebp
    mov    ebp, esp
    mov    eax, DWORD PTR _a$[ebp]
    cmp    eax, DWORD PTR _b$[ebp]
    jle    SHORT $LN3@f_signed
    push   OFFSET $SG737            ; 'a>b'
    call   _printf
    add    esp, 4

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

第一个条件转移指令是JLE，即“Jump if Less or Equal”。如果上一条CMP指令的第一个操作表达式小于或等于（不大于）第二个表达式，JLE将跳转到指令所标明的地址；如果不满足上述条件，则运行下一条指令，就本例而言程序将会调用printf()函数。第二个条件转移指令是JNE，“Jump if Not Equal”，如果上一条CMP的两个操作符不相等，则进行相应跳转。

第三个条件转移指令是JGE，即“Jump if Greater or Equal”。如果CMP的第一个表达式大于或等于第二个表达式（不小于），则进行跳转。这段程序里，如果三个跳转的判断条件都不满足，将不会调用printf()函数；不过，除非进行特殊干预，否则这种情况应该不会发生。

现在我们观察f_unsigned()函数的汇编指令。f_unsigned()函数和f_signed()函数大体相同。它们的区别集中体现在条件转移指令上：f_unsinged()函数的使用的条件转移指令是JBE和JAE，而f_signed()函数使用的条件转移指令则是JLE和JGE。

使用GCC编译上述程序，可得到f_unsigned()的汇编指令如下。

指令清单12.2　GCC

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

GCC编译的结果与MSVC编译的结果基本相同。

经GCC编译后，f_unsigned()函数使用的条件转移指令是JBE（Jump if Below or Equal，相当于JLE）和JAE（Jump if Above or Equal，相当于JGE）。JA/JAE/JB/JBE与JG/JGE/JL/JLE的区别，在于它们检查的标志位不同：前者检查借/进位标志位CF（1意味着小于）和零标志位ZF（1意味着相等），后者检查“SF XOR OF”（1意味着异号）和ZF。从指令参数的角度看，前者适用于unsigned（无符号）类型数据的（CMP）运算，而后者的适用于signed（有符号）类型数据的运算。

本书第30章会介绍signed类型数据。可见，根据条件转移的指令，我们可以直接判断CMP所比较的变量的数据类型。

接下来，我们一起研究main()函数的汇编代码。

指令清单12.3　main()

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

x86 + MSVC + OllyDbg

我们可以通过OllyDbg直观地观察到指令对标志寄存器的影响。我们先用OllyDbg观察f_unsigned()函数比较无符号数的过程。f_unsigned()函数使用了CMP指令，分三次比较了两个相同的unsigned类型数据。因为参数相同，所以CMP设置的标志位必定相同。

如图12.1所示，在运行到第一个条件转移指令时，C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0。OllyDbg会使用标志位的首字母作为该标志位的简称。

图12.1　使用OllyDbg：观察f_unsigned()的第一个条件转移指令

OllyDbg在左下窗口进行提示，JBE条件跳转指令的条件已经达成，下一步会进行相应跳转。这种预测准确无误，JBE的触发条件是（CF=1或ZF=1）。条件表达式为真时，JBE确实会进行跳转。

如图12.2所示，在运行到第二个条件转移指令——JNZ指令时，ZF=0。所以OllyDbg能够判断程序会进行相应跳转。

图12.2　使用OllyDbg观察f_unsigned()函数的第二个条件转移指令

如图12.3所示，运行到第三个条件转移指令——JNB指令的时候，借/进位标志CF＝0，条件表达式会为假，所以不会发生跳转，程序将执行第三个printf()指令。

图12.3　使用OllyDbg观察f_unsigned()函数的第三个条件转移指令

现在来调试下示例程序里的f_signed()函数，它的参数为signed型数据。

在运行f_signed()函数时，标志位的状态和刚才一样。即，运行CMP指令之后，C=1, P=1, A=1, Z=0, S=1, T=0, D=0, O=0。

第一个条件转移指令——JLE指令将会被触发，如图12.4所示。

图12.4　使用OllyDbg观察f_signed()函数的第一个条件转移指令

参照［Int13］，触发JLE的条件是ZF=1或SF≠OF。本例满足SF≠OF的条件。

由于ZF＝0，第二个条件转移指令——JNZ指令会被触发，如图12.5所示。

图12.5　使用OllyDbg观察f_signed()函数的第二个条件转移指令

而第三个条件转移指令——JGE指令不会被触发。触发JGE的条件是SF=OF，而当前情形不满足这个条件，如图12.6所示。

图12.6　使用OllyDbg观察f_signed()函数的第三个条件转移指令

x86 + MSVC +Hiew

我们还可以用Hiew给可执行文件打补丁，强制f_unsigned()函数永远打印“a==b”、忽略它的输入参数，如图2.7所示。

图12.7　使用Hiew打开f_unsigned()函数

本文将分3次修改上述可执行程序，分别完成下述三个任务：

强制触发第一个条件转移指令。

强制屏蔽第二个条件转移指令。

强制触发第三个条件转移指令。

我们可以直接修改程序，令程序流永远转向第二个printf()函数并打印“a==b”。

故而需要修改三条指令（3个字节）：

把第一个条件转移指令改为JMP，并保留原始的转移偏移量（jump offset）。

第二个条件转移指令的触发条件不一定成立。无论触发条件是否成立，我们都要它跳转到下一条指令。所以，我们把转移偏移量设置为零。对于条件转移语句来说，跳转的目标地址是下一条地址与转移偏移量的和。把转移偏移量设置为零之后，程序会继续执行下一条指令。

第三个条件转移指令的修改方法和第一个条件转移指令的修改方法相同。我们只需把将条件转移指令换成JMP（无条件转移指令）即可。

修改之后的f_unsigned()函数如图12.8所示。

图12.8　经过修改之后的f_unsigned()函数

三个条件转移指令全部都要修改。如果少修改了一个指令，它就可能会多次调用printf()函数，与我们的预期——只调用一次printf()函数的任务目标相悖。

Non-optimizing GCC

如果关闭了GCC的优化选项，那么它编译出来的程序和MSVC编译出来的程序没什么区别，只不过就是把printf()函数替换为了puts()函数[1]。

Optimizing GCC

聪明的您一定会问，既然CMP比较的是相同的值，比较之后的标志位的状态也相同，那么何必要对同样的参数进行多次比较呢？或许MSVC真的不能再智能一些了；但是启用优化选项后，GCC 4.8.1确实能够进行这种深度优化。

指令清单12.4　GCC 4.8.1 f_signed()

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

很明显，它使用jmp指令替代了臃肿的“CALL ……puts …… RETN”指令。本书将在13.1.1节里详细解说这种编译技术。

我们不得不说在x86的系统中，这种程序比较少见。MSVC 2012做不到GCC那种程度的深度优化。另一方面，汇编语言的编程人员确实可能学会Jcc指令的连用技巧。所以，如果您遇到了这样精简的程序，而且还能够判断出它不是GCC编译出来的程序，那么您基本上可以判断它是手写出来的汇编程序。

即使开启了同样的优化选项，f_unsigned()函数对应的指令也没有那么精致。

指令清单12.5　GCC 4.8.1 f_unsigned()

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

程序中只有两条CMP指令，至少它优化去了一个CMP指令。可见，GCC 4.8.1的优化算法还有改进的空间。

12.1.2　ARM

32位ARM程序

Optimizing Keil 6/2013 (ARM mode)

指令清单12.6　Optimizing Keil 6/2013 (ARM mode)

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

ARM模式的多数指令都存在着相应的条件执行指令。这些派生出来的条件执行指令仅会在特定标志位为1的情况下执行。换句话说，只有当前面存在比较数值的指令时，后面才可能会出现这种派生出来的条件执行指令。

举例来讲，加法指令ADD指令实际上是ADDAL指令。“AL”就是always的缩写，即ADDAL总会被无条件执行。在32位的ARM指令中，条件判断表达式被封装在条件执行指令的前（最高）4位——条件字段（condition field）里。即使是无条件转移指令B指令，其前4位还是条件字段。从指令构成上说，B指令仍然属于条件转移指令，只不过它的条件字段是AL而已。顾名思义，AL的作用就是忽略标志寄存器、永远执行这条指令。

ADRGT指令中的GT代表greater than（大于）。该指令依据先前CMP指令的比较结果，而判断是否执行寻址指令。当且仅当CMP比较的第一个值大于第二个值的时候，ADRGT指令才会执行寻址（ADR）指令。

后面的BLGT指令有异曲同工之妙。仅在相同条件下，即当且仅当CMP比较的第一个值大于第二个值的时候，BLGT指令才会执行BL指令。在这个条件成立的时候，前面的ADRGT指令已经把字符串“a>b /n”的地址赋值给R0寄存器，成为了printf()的参数，而BLGT负责调用printf()。可见，当且仅当在R0的值（变量a）大于R4的值（变量b）的情况下，计算机才会运行后面那组带有-GT后缀的指令。很显然，这是一组相互关联的指令。

后面的ADREQ和BLEQ指令，都在最近一个CMP的操作数相等的情况下才会讲行ADR和BL指令的操作。程序之中连续两次出现“CMP R5, R4”指令，这是因为夹在其间的printf()函数可能会影响标志位。

LDMGEFD是“Great or Equal（大于或等于）”的情况下进行LDMFD (Load Multiple Full Descending) 操作的指令。

依此类推，“LDMGEFD SP!, {R4-R6,PC}”指令起到函数尾声的作用，不过它只会在“a>=b”的时候才会结束本函数。

如果上述条件不成立，即“a<b”的时候，会执行下一条指令“LDMFD SP!, {R4-R6,LR}”。这同样起到函数尾声的作用。该指令将恢复R4～R6寄存器、LR寄存器的值，而不恢复PC寄存器的值，且不会退出当前函数。

函数最后的两条指令，分别向printf()函数传递参数（字符串“a<b\n”），并且调用printf()函数。本书的6.2.1节已经介绍过：当调用方函数调用（跳转到）printf()函数之后，调用方函数可以伴随printf()函数退出而退出。所以本节不再进行有关解释。

f_unsigned()函数与f_signed()函数的功能十分类似。不同之处是它用到了ADRHI、BLHI和LDMSFD指令。指令尾部的-HI代表Unsigned Higher，CS代表Carry Set (greater than or equal)。因为参数的数据类型有所变化，所以这两个函数的具体指令有所区别。

这个程序的main()函数的汇编指令如下。

指令清单12.7　main()函数

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

可见，ARM模式的程序可以完全不依赖条件转移指令。

这样做有什么优点呢？依赖精简指令集（RISC）的ARM处理器采用流水线技术（pipeline）。简单地说，这种处理器在跳转指令方面的性能不怎么优越，所以它们的分支预测处理器（branch predictor unites）决定了整体的性能。对于采用流水线技术的处理器来说，运行其上的程序跳转次数越少（无论是条件转移还是无条件转移），程序的性能就越高。条件执行指令[2]，会受益于其跳跃次数最少的优点，体现出最高的效率。详细介绍请参阅本书的33.1节

x86指令集里只有CMOVcc指令，没有其他的条件执行指令了。CMOVcc指令是仅在特定标志位为1（通常由CMP指令设置）的情况下才会执行MOV操作的条件执行指令。

Optimizing Keil 6/2013 (Thumb mode)

指令清单12.8　Optimizing Keil 6/2013 (Thumb mode)

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

在ARM系统的Thumb模式指令集里，只有B指令才有派生出来的条件执行指令。所以Thumb模式下的汇编指令看上去更为贴近x86指令。

上述指令里，条件转移指令有BLE（Less than or Equal）、BNE（Not Equal）、BGE（Greater than or Equal）。这些指令都可以望文生义。

f_unsigned函数十分雷同，只是里面出现了新的条件转移指令BLS（Unsigned Lower or Same）和BCS（Carry Set(Greater than or equal)）。

64位ARM程序

Optimizing GCC(Linaro)4.9

指令清单12.9　f_signed()

f_signed:
; W0=a, W1=b
        cmp     w0, w1
        bgt     .L19     ; Branch if Greater Than (a>b)
        beq     .L20     ; Branch if Equal (a==b)
        bge     .L15     ; Branch if Greater than or Equal (a>=b) (impossible here)
        ; a<b
        adrp    x0, .LC11        ; "a<b"
        add     x0, x0, :lo12:.LC11
        b       puts
.L19:
        adrp    x0, .LC9        ; "a>b"
        add     x0, x0, :lo12:.LC9
        b       puts
.L15:   ; impossible here
        ret
.L20:
        adrp    x0, .LC10       ; "a==b"
        add     x0, x0, :lo12:.LC10
        b       puts

指令清单12.10　f_unsigned()

f_unsigned:
        stp      x29, x30, [sp, -48]!
; W0=a, W1=b
        cmp      w0, w1
        add      x29, sp, 0
        str      x19, [sp,16]
        mov      w19, w0
        bhi      .L25          ; Branch if HIgher (a>b)
        cmp      w19, w1
        beq      .L26          ; Branch if Equal (a==b)
.L23:
        bcc      .L27          ; Branch if Carry Clear (if less than) (a<b)
; function epilogue, impossible to be here
        ldr      x19, [sp,16]
        ldp      x29, x30, [sp], 48
        ret
.L27:
        ldr      x19, [sp,16]
        adrp     x0, .LC11     ; "a<b"
        ldp      x29, x30, [sp], 48
        add      x0, x0, :lo12:.LC11
        b        puts
.L25:
        adrp     x0, .LC9      ; "a>b"
        str      x1, [x29,40]
        add      x0, x0, :lo12:.LC9
        bl       puts
        ldr      x1, [x29,40]
        cmp      w19, w1
        bne      .L23    ; Branch if Not Equal
.L26:
        ldr      x19, [sp,16]
        adrp     x0, .LC10       ; "a==b"
        ldp      x29, x30, [sp], 48
        add      x0, x0, :lo12:.LC10
        b        puts

我在程序之中添加了注释。很明显，虽然有些条件表达式不可能成立，但是编译器不能自行判断出这种问题。所以程序留有一些永远不会执行的无效代码。

练习题

上述代码中存在无效代码。请在不添加新指令的情况下删除多余多指令。

12.1.3　MIPS

MIPS处理器没有标志位寄存器，这是它最显著的特征之一。这种设计旨在降低数据相关性的分析难度。

x86的指令集中有SETcc指令，MIPS平台也有类似的指令：SLT（Set on Less Than/操作对象为有符号数）和SLTU（无符号数）。这两个指令会在条件表达式为真的时候设置目的寄存器为1，否则设置其为零。

随即可用BEQ（Branch on Equal）或BEN（Branch on Not Equal）指令检查上述寄存器的值，判断是否进行跳转。总之，在MIPS平台上组合使用这两种指令，可完成条件转移指令的比较和转移操作。

我们来看范本程序里处理有符号数的相应函数。

指令清单12.11　Non-optimizing GCC 4.4.5 (IDA)

.text:00000000 f_signed:                                  # CODE XREF: main+18
.text:00000000
.text:00000000 var_10           = -0x10
.text:00000000 var_8            = -8
.text:00000000 var_4            = -4
.text:00000000 arg_0            =  0
.text:00000000 arg_4            =  4
.text:00000000
.text:00000000                  addiu     $sp, -0x20
.text:00000004                  sw        $ra, 0x20+var_4($sp)
.text:00000008                  sw        $fp, 0x20+var_8($sp)
.text:0000000C                  move      $fp, $sp
.text:00000010                  la        $gp, __gnu_local_gp
.text:00000018                  sw        $gp, 0x20+var_10($sp)
; store input values into local  stack:
.text:0000001C                  sw        $a0, 0x20+arg_0($fp)
.text:00000020                  sw        $a1, 0x20+arg_4($fp)
; reload them.
.text:00000024                  lw        $v1, 0x20+arg_0($fp)
.text:00000028                  lw        $v0, 0x20+arg_4($fp)
; $v0=b
; $v1=a
.text:0000002C                  or        $at, $zero ; NOP
; this is pseudoinstruction. in fact, "slt $v0,$v0,$v1" is there.
; so $v0 will be set to 1 if $v0<$v1 (b<a) or to 0 if otherwise:
.text:00000030                  slt       $v0, $v1
; jump to loc_5c, if condition is not true.
; this is pseudoinstruction. in fact, "beq $v0,$zero,loc_5c" is there:
.text:00000034                  beqz      $v0, loc_5C
; print "a>b" and finish
.text:00000038                  or        $at, $zero ; branch delay slot, NOP
.text:0000003C                  lui       $v0, (unk_230 >> 16) # "a>b"
.text:00000040                  addiu     $a0, $v0, (unk_230 & 0xFFFF) # "a>b"
.text:00000044                  lw        $v0, (puts & 0xFFFF)($gp)
.text:00000048                  or        $at, $zero ; NOP
.text:0000004C                  move      $t9, $v0
.text:00000050                  jalr      $t9
.text:00000054                  or        $at, $zero ; branch delay slot, NOP
.text:00000058                  lw        $gp, 0x20+var_10($fp)
.text:0000005C
.text:0000005C loc_5C:                                   # CODE XREF: f_signed+34
.text:0000005C                  lw        $v1, 0x20+arg_0($fp)
.text:00000060                  lw        $v0, 0x20+arg_4($fp)
.text:00000064                  or        $at, $zero ; NOP
; check if a==b, jump to loc_90 if its not true':
.text:00000068                  bne       $v1, $v0, loc_90
.text:0000006C                  or        $at, $zero ; branch delay slot, NOP
; condition is true, so print "a==b" and finish:
.text:00000070                  lui       $v0, (aAB >> 16) # "a==b"
.text:00000074                  addiu     $a0, $v0, (aAB & 0xFFFF)  # "a==b"
.text:00000078                  lw        $v0, (puts & 0xFFFF)($gp)
.text:0000007C                  or        $at, $zero ; NOP
.text:00000080                  move      $t9, $v0
.text:00000084                  jalr      $t9
.text:00000088                  or        $at, $zero ; branch delay slot, NOP
.text:0000008C                  lw        $gp, 0x20+var_10($fp)
.text:00000090
.text:00000090 loc_90:                                   # CODE XREF: f_signed+68
.text:00000090                  lw        $v1, 0x20+arg_0($fp)
.text:00000094                  lw        $v0, 0x20+arg_4($fp)
.text:00000098                  or        $at, $zero ; NOP
; check if $v1<$v0 (a<b), set $v0 to 1 if condition is true:
.text:0000009C                  slt       $v0, $v1, $v0
; if condition is not true (i.e., $v0==0), jump to loc_c8:
.text:000000A0                  beqz      $v0, loc_C8
.text:000000A4                  or        $at, $zero ; branch delay slot, NOP
; condition is true, print "a<b" and finish
.text:000000A8                  lui       $v0, (aAB_0 >> 16)  # "a<b"
.text:000000AC                  addiu     $a0, $v0, (aAB_0 & 0xFFFF)  # "a<b"
.text:000000B0                  lw        $v0, (puts & 0xFFFF)($gp)
.text:000000B4                  or        $at, $zero ; NOP
.text:000000B8                  move      $t9, $v0
.text:000000BC                  jalr      $t9
.text:000000C0                  or        $at, $zero ; branch delay slot, NOP
.text:000000C4                  lw        $gp, 0x20+var_10($fp)
.text:000000C8
; all 3 conditions were false, so just finish:
.text:000000C8 loc_C8:                                   # CODE XREF: f_signed+A0
.text:000000C8                  move      $sp, $fp
.text:000000CC                  lw        $ra, 0x20+var_4($sp)
.text:000000D0                  lw        $fp, 0x20+var_8($sp)
.text:000000D4                  addiu     $sp, 0x20
.text:000000D8                  jr        $ra
.text:000000DC                  or        $at, $zero ; branch delay slot, NOP
.text:000000DC  # End of function f_signed

此处有两条指令是IDA的伪指令。“SLT REG0, REG1”的实际指令是“SLT REG0, REG0, REG1”，而BEQZ的实际指令是“BEQ REG, $ZERO, LABEL”。

f_unsigned()函数的汇编指令，只是把f_signed()函数中的SLT指令替换为SLTU（U是unsigned的缩写）。除此之外，处理有符号数和无符号数的两个函数完全相同。

指令清单12.12　Non-optimizing GCC 4.4.5 (IDA)

.text:000000E0 f_unsigned:                               # CODE XREF: main+28
.text:000000E0
.text:000000E0 var_10         = -0x10
.text:000000E0 var_8          = -8
.text:000000E0 var_4          = -4
.text:000000E0 arg_0          =  0
.text:000000E0 arg_4          =  4
.text:000000E0
.text:000000E0                addiu       $sp, -0x20
.text:000000E4                sw          $ra, 0x20+var_4($sp)
.text:000000E8                sw          $fp, 0x20+var_8($sp)
.text:000000EC                move        $fp, $sp
.text:000000F0                la          $gp, __gnu_local_gp
.text:000000F8                sw          $gp, 0x20+var_10($sp)
.text:000000FC                sw          $a0, 0x20+arg_0($fp)
.text:00000100                sw          $a1, 0x20+arg_4($fp)
.text:00000104                lw          $v1, 0x20+arg_0($fp)
.text:00000108                lw          $v0, 0x20+arg_4($fp)
.text:0000010C                or          $at, $zero
.text:00000110                sltu        $v0, $v1
.text:00000114                beqz        $v0, loc_13C
.text:00000118                or          $at, $zero
.text:0000011C                lui         $v0, (unk_230 >> 16)
.text:00000120                addiu       $a0, $v0, (unk_230 & 0xFFFF)
.text:00000124                lw          $v0, (puts & 0xFFFF)($gp)
.text:00000128                or          $at, $zero
.text:0000012C                move        $t9, $v0
.text:00000130                jalr        $t9
.text:00000134                or          $at, $zero
.text:00000138                lw          $gp, 0x20+var_10($fp)
.text:0000013C
.text:0000013C loc_13C:                                 # CODE XREF: f_unsigned+34
.text:0000013C                lw          $v1, 0x20+arg_0($fp)
.text:00000140                lw          $v0, 0x20+arg_4($fp)
.text:00000144                or          $at, $zero
.text:00000148                bne         $v1, $v0, loc_170
.text:0000014C                or          $at, $zero
.text:00000150                lui         $v0, (aAB >> 16)  # "a==b"
.text:00000154                addiu       $a0, $v0, (aAB & 0xFFFF)  # "a==b"
.text:00000158                lw          $v0, (puts & 0xFFFF)($gp)
.text:0000015C                or          $at, $zero
.text:00000160                move        $t9, $v0
.text:00000164                jalr        $t9
.text:00000168                or          $at, $zero
.text:0000016C                lw          $gp, 0x20+var_10($fp)
.text:00000170
.text:00000170 loc_170:                                 # CODE XREF: f_unsigned+68
.text:00000170                lw          $v1, 0x20+arg_0($fp)
.text:00000174                lw          $v0, 0x20+arg_4($fp)
.text:00000178                or          $at, $zero
.text:0000017C                sltu        $v0, $v1, $v0
.text:00000180                beqz        $v0, loc_1A8
.text:00000184                or          $at, $zero
.text:00000188                lui         $v0, (aAB_0 >> 16)  # "a<b"
.text:0000018C                addiu       $a0, $v0, (aAB_0 & 0xFFFF) # "a<b"
.text:00000190                lw          $v0, (puts & 0xFFFF)($gp)
.text:00000194                or          $at, $zero
.text:00000198                move        $t9, $v0
.text:0000019C                jalr        $t9
.text:000001A0                or          $at, $zero
.text:000001A4                lw          $gp, 0x20+var_10($fp)
.text:000001A8
.text:000001A8 loc_1A8:                                 # CODE XREF: f_unsigned+A0
.text:000001A8                move        $sp, $fp
.text:000001AC                lw          $ra, 0x20+var_4($sp)
.text:000001B0                lw          $fp, 0x20+var_8($sp)
.text:000001B4                addiu       $sp, 0x20
.text:000001B8                jr          $ra
.text:000001BC                or          $at, $zero
.text:000001BC  # End of function f_unsigned

12.2　计算绝对值

本节将围绕以下程序进行演示：

int my_abs (int i)
{
         if (i<0)
                 return -i;
         else
                 return i;
};

12.2.1　Optimizing MSVC

上述程序的编译结果如下。

指令清单12.13　Optimizing MSVC 2012 x64

i$ = 8
my_abs   PROC
; ECX = input
         test    ecx, ecx
; check for sign of input value
; skip NEG instruction if sign is positive
         jns     SHORT $LN2@my_abs
; negate value
         neg     ecx
$LN2@my_abs:
; prepare result in EAX:
        mov    eax,   ecx
        ret     0
my_abs  ENDP

GCC 4.9的编译结果几乎相同。

12.2.2　Optimizing Keil 6/2013: Thumb mode

指令清单12.14　Optimizing Keil 6/2013：Thumb mode

my_abs PROC
        CMP       r0,#0
; is input value equal to zero or greater than zero?
; skip RSBS instruction then
        BGE       |L0.6|
; subtract input value from 0:
        RSBS      r0,r0,#0
|L0.6|
        BX        lr
        ENDP

ARM平台没有负数运算指令，所以Keil编译器使用了“零减去数值”的减法运算指令“Reverse Subtract”（减数和被减数位置对调的减法运算），同样达到了替换符号的效果。

12.2.3　Optimizing Keil 6/2013: ARM mode

因为ARM模式的指令集存在条件执行指令，所以开启优化选项后可得到如下指令。

指令清单12.15　Optimizing Keil 6/2013：ARM mode

my_abs PROC
        CMP      r0,#0
; execute "Reverse Subtract" instruction only if input value is less than 0:
        RSBLT    r0,r0,#0
        BX       lr
        ENDP

即使没有使用条件转移指令（请参见33.1节），它也实现相同的功能。

12.2.4　Non-optimizng GCC 4.9 (ARM64)

ARM64的指令集存在求负运算的NEG指令。

指令清单12.16　Optimizing GCC 4.9 (ARM64)

my_abs:
        sub      sp, sp, #16
        str      w0, [sp,12]
        ldr      w0, [sp,12]
; compare input value with contents of WZR register
; (which always holds zero)
        cmp      w0, wzr
        bge      .L2
        ldr      w0, [sp,12]
        neg      w0, w0
        b        .L3
.L2:
        ldr      w0, [sp,12]
.L3:
        add      sp, sp, 16
        ret

12.2.5　MIPS

指令清单12.17　Optimizing GCC 4.4.5 (IDA)

my_abs:
; jump if $a0<0:
                   bltz     $a0, locret_10
; just return input value ($a0) in $v0:
                   move     $v0, $a0
                   jr       $ra
                   or       $at, $zero ; branch delay slot, NOP
locret_10:
; negate input value and store it in $v0:
                   jr       $ra
; this is pseudoinstruction. in fact, this is "subu $v0,$zero,$a0" ($v0=0-$a0)
                   negu     $v0, $a0

这里出现了新指令BLTZ（Branch if Less Than Zero），以及伪指令NEGU。NEGU指令计算零减去操作数的差。SUBU和NEGU指令中的后缀U代表它的操作数是无符号型数据，并且在整数溢出的情况下不会触发异常处理机制。

12.2.6　不使用转移指令

不使用转移指令同样可以计算绝对值。本书的第45章有详细说明。

12.3　条件运算符

C/C++都支持条件运算符：

表达式? 表达式: 表达式

例如：

const char* f (int a)
{
        return a==10 ? "it is ten" : "it is not ten";
};

12.3.1　x86

在编译含有条件运算符的语句时，早期无优化功能的编译器会以编译“if/else”语句的方法进行处理。

指令清单12.18　Non-optimizing MSVC 2008

$SG746   DB       'it is ten', 00H
$SG747   DB       'it is not ten', 00H

tv65 = -4 ; this will be used as a temporary variable
_a$ = 8
_f       PROC
         push     ebp
         mov      ebp, esp
         push     ecx
; compare input value with 10
         cmp      DWORD PTR _a$[ebp], 10
; jump to $LN3@f if not equal
         jne      SHORT $LN3@f
; store pointer to the string into temporary variable:
         mov      DWORD PTR tv65[ebp], OFFSET $SG746 ; 'it is ten'
; jump to exit
         jmp      SHORT $LN4@f
$LN3@f:
; store pointer to the string into temporary variable:
         mov      DWORD PTR tv65[ebp], OFFSET $SG747 ; 'it is not ten'
$LN4@f:
; this is exit. copy pointer to the string from temporary variable to EAX.
         mov      eax, DWORD PTR tv65[ebp]
         mov      esp, ebp
         pop      ebp
         ret      0
_f       ENDP

指令清单12.19　Optimizing MSVC 2008

$SG792  DB       'it is ten', 00H
$SG793  DB       'it is not ten', 00H

_a$ = 8 ; size = 4
_f      PROC
; compare input value with 10
        cmp      DWORD PTR _a$[esp-4], 10
        mov      eax, OFFSET $SG792 ; 'it is ten'
; jump to $LN4@f if equal
        je       SHORT $LN4@f
        mov      eax, OFFSET $SG793 ; 'it is not ten'
$LN4@f:
        ret      0
_f      ENDP

新编译器生成的程序更为简洁。

指令清单12.20　Optimizing MSVC 2012 x64

$SG1355 DB      'it is ten', 00H
$SG1356 DB      'it is not ten', 00H

a$      = 8
f       PROC
; load pointers to the both strings
        lea     rdx, OFFSET FLAT:$SG1355 ; 'it is ten'
        lea     rax, OFFSET FLAT:$SG1356 ; 'it is not ten'
; compare input value with 10
        cmp     ecx, 10
; if equal, copy value from RDX ("it is ten")
; if not, do nothing. pointer to the string "it is not ten" is still in RAX as for now.
        cmove   rax, rdx
        ret     0
f       ENDP

启用优化选项后，GCC 4.8生成的x86指令同样使用了CMOVcc指令。相比之下，在关闭优化功能的情况下，GCC 4.8用条件转移指令编译条件操作符。

12.3.2　ARM

启用优化功能之后，Keil生成的ARM代码会应用条件运行指令ADRcc。

指令清单12.21　Optimizing Keil 6/2013 (ARM mode)

f PROC
; compare input value with 10
        CMP       r0, #0xa
; if comparison result is EQual, copy pointer to the "it is ten" string into R0
        ADREQ     r0,|L0.16| ; "it is ten"
; if comparison result is Not Equal, copy pointer to the "it is not ten" string into R0
        ADRNE     r0,|L0.28| ; "it is not ten"
        BX        lr
        ENDP

|L0.16|
        DCB       "it is ten",0
|L0.28|
        DCB       "it is not ten",0

除非存在人为干预，否则ADREQ和ADRNE指令不可能在同一次调用期间都被执行。

在启用优化功能之后，Keil会给编译出的Thumb模式代码分配条件转移指令。毕竟在Thumb模式的指令之中，没有支持标志位判断的赋值指令。

指令清单12.22　Optimizing Keil 6/2013 (Thumb mode)

f PROC
; compare input value with 10
        CMP       r0,#0xa
; jump to |L0.8| if EQual
        BEQ       |L0.8|
        ADR       r0,|L0.12| ; "it is not ten"
        BX        lr
|L0.8|
        ADR       r0,|L0.28| ; "it is ten"
        BX        lr
        ENDP

|L0.12|
        DCB       "it is not ten",0
|L0.28|
        DCB       "it is ten",0

12.3.3　ARM64

启用优化功能之后，GCC（Linaro）4.9编译出来的ARM64程序同样用条件转移指令实现条件运算符。

指令清单12.23　Optimizing GCC (Linaro) 4.9

f:
        cmp      x0, 10
        beq      .L3          ; branch if equal
        adrp     x0, .LC1     ; "it is ten"
        add      x0, x0, :lo12:.LC1
        ret
.L3:
        adrp     x0, .LC0     ; "it is not ten"
        add      x0, x0, :lo12:.LC0
        ret
.LC0:
        .string  "it is ten"
.LC1:
        .string  "it is not ten"

ARM64同样没有能够判断标志位的条件赋值指令。而32位的ARM指令集[3]，以及x86的CMOVcc指令都可以根据相应标志位进行条件赋值。虽然ARM64存在“条件选择”指令CSEL（Conditional SELect），但是GCC 4.9似乎无法给这种程序分配上这条指令。

12.3.4　MIPS

不幸的是，GCC 4.45在编译MIPS程序方面的智能程度也有待完善。

指令清单12.24　Optimizing GCC 4.4.5 (assembly output)

$LC0:
        .ascii   "it is not ten\000"
$LC1:
        .ascii   "it is ten\000"
f:
        li       $2,10                           # 0xa
; compare $a0 and 10, jump if equal:
        beq      $4,$2,$L2
        nop ; branch delay slot

; leave address of "it is not ten" string in $v0 and return:
        lui      $2,%hi($LC0)
        j        $31
        addiu    $2,$2,%lo($LC0)

$L2:
; leave address of "it is ten" string in $v0 and return:
        lui      $2,%hi($LC1)
        j        $31
        addiu    $2,$2,%lo($LC1)

12.3.5　使用if/else替代条件运算符

const char* f (int a)
{
        if (a==10)
                  return "it is ten";
        else
                  return "it is not ten";
};

启用优化功能之后，GCC 4.8在编译x86程序时能够应用CMOVcc指令。

指令清单12.25　Optimizing GCC 4.8

.LC0:
        .string "it is ten"
.LC1:
        .string "it is not ten"
f:
.LFB0:
; compare input value with 10
        cmp     DWORD PTR [esp+4], 10
        mov     edx, OFFSET FLAT:.LC1 ; "it is not ten"
        mov     eax, OFFSET FLAT:.LC0 ; "it is ten"
; if comparison result is Not Equal, copy EDX value to EAX
; if not, do nothing
        Cmovne  eax, edx
        ret

Optimizing Keil编译的ARM程序，与指令清单12.21相同。

但是启用优化功能的MSVC 2012仍然没有什么起色。

12.3.6　总结

启用优化功能之后，编译器会尽可能地避免使用条件转移指令。本书的33.1节将详细讲解这个问题。

12.4　比较最大值和最小值

12.4.1　32位

int my_max(int a, int b)
{
        if (a>b)
                return a;
        else
                return b;
};

int my_min(int a, int b)
{
        if (a<b)
                return a;
        else
                return b;
};

指令清单12.26　Non-optimizing MSVC 2013

_a$ = 8
_b$ = 12
_my_min PROC
        push     ebp
        mov      ebp, esp
        mov      eax, DWORD PTR _a$[ebp]
; compare A and B:
        cmp      eax, DWORD PTR _b$[ebp]
; jump, if A is greater or equal to B:
        jge      SHORT $LN2@my_min
; reload A to EAX if otherwise and jump to exit
        mov      eax, DWORD PTR _a$[ebp]
        jmp      SHORT $LN3@my_min
        jmp      SHORT $LN3@my_min ; this is redundant JMP
$LN2@my_min:
; return B
        mov      eax, DWORD PTR _b$[ebp]
$LN3@my_min:
        pop      ebp
        ret      0
_my_min ENDP

_a$ = 8
_b$ = 12
_my_max PROC
        push     ebp
        mov      ebp, esp
        mov      eax, DWORD PTR _a$[ebp]
; compare A and B:
        cmp      eax, DWORD PTR _b$[ebp]
; jump if A is less or equal to B:
        jle      SHORT $LN2@my_max
; reload A to EAX if otherwise and jump to exit
        mov      eax, DWORD PTR _a$[ebp]
        jmp      SHORT $LN3@my_max
        jmp      SHORT $LN3@my_max ; this is redundant JMP
$LN2@my_max:
; return B
        mov      eax, DWORD PTR _b$[ebp]
$LN3@my_max:
        pop      ebp
        ret      0
_my_max ENDP

两个函数的唯一区别就是条件转移指令：第一个函数使用的是JGE（Jump if Greater or Equal），而第二个函数使用的是JLE（Jump if Less or Equal）。

上述每个函数里都存在一个多余的JMP指令。这可能是MSVC的问题。

无分支指令的编译方法

Keil编译的Thumb模式程序与x86程序有几分相似。

指令清单12.27　Optimizing Keil 6/2013 (Thumb mode)

my_max PROC
; R0=A
; R1=B
; compare A and B:
        CMP       r0,r1
; branch if A is greater then B:
        BGT       |L0.6|
; otherwise (A<=B) return R1 (B):
        MOVS      r0,r1
|L0.6|
; return
        BX       lr
        ENDP

my_min PROC
; R0=A
; R1=B
; compare A and B:
        CMP       r0,r1
; branch if A is less then B:
        BLT       |L0.14|
; otherwise (A>=B) return R1 (B):
        MOVS      r0,r1
|L0.14|
; return
        BX        lr
        ENDP

两个函数所用的转移指令不同：一个是BGT，而另一个是BLT。

在编译ARM模式程序时，编译器可能会使用条件执行指令（即“有分支”指令）。这种程序会显得更为短小。在编译条件表达式时，Keil编译器使用了MOVcc指令。

指令清单12.28　Optimizing Keil 6/2013 (ARM mode)

my_max PROC
; R0=A
; R1=B
; compare A and B:
        CMP       r0,r1
; return B instead of A by placing B in R0
; this instruction will trigger only if A<=B (hence, LE - Less or Equal)
; if instruction is not triggered (in case of A>B), A is still in R0 register
        MOVLE     r0,r1
        BX        lr
        ENDP

my_min PROC
; R0=A
; R1=B
; compare A and B:
        CMP       r0,r1
; return B instead of A by placing B in R0
; this instruction will trigger only if A>=B (hence, GE - Greater or Equal)
; if instruction is not triggered (in case of A<B), A value is still in R0 register
        MOVGE     r0,r1
        BX        lr
        ENDP

在启用优化功能的情况下，GCC 4.8.1和MSVC 2013都能使用CMOVcc指令。这个指令相当于ARM程序里的MOVcc指令。

指令清单12.29　Optimizing MSVC 2013

my_max:
        mov     edx, DWORD PTR [esp+4]
        mov     eax, DWORD PTR [esp+8]
; EDX=A
; EAX=B
; compare A and B:
        cmp     edx, eax
; if A>=B, load A value into EAX
; the instruction idle if otherwise (if A<B)
        cmovge  eax, edx
        ret
my_min:
        mov     edx, DWORD PTR [esp+4]
        mov     eax, DWORD PTR [esp+8]
; EDX=A
; EAX=B
; compare A and B:
        cmp     edx, eax
; if A<=B, load A value into EAX
; the instruction idle if otherwise (if A>B)
        cmovle  eax, edx
        ret

12.4.2　64位

#include <stdint.h>

int64_t my_max(int64_t a, int64_t b)
{
        if (a>b)
                return a;
        else
                return b;
};

int64_t my_min(int64_t a, int64_t b)
{
        if (a<b)
                return a;
        else
                return b;
};

虽然编译出来的程序里存在不必要的数据交换，但是代码功能一目了然。

指令清单12.30　Non-optimizing GCC 4.9.1 ARM64

my_max:
        sub     sp, sp, #16
        str     x0, [sp,8]
        str     x1, [sp]
        ldr     x1, [sp,8]
        ldr     x0, [sp]
        cmp     x1, x0
        ble     .L2
        ldr     x0, [sp,8]
        b       .L3
.L2:
        ldr     x0, [sp]
.L3:
        add     sp, sp, 16
        ret

my_min:
        sub     sp, sp, #16
        str     x0, [sp,8]
        str     x1, [sp]
        ldr     x1, [sp,8]
        ldr     x0, [sp]
        cmp     x1, x0
        bge     .L5
        ldr     x0, [sp,8]
        b       .L6
.L5:
        ldr     x0, [sp]
.L6:
        add     sp, sp, 16
        ret

无分支指令的编译方法

既然函数参数就在寄存器里，那么就不必通过栈访问它们。

指令清单12.31　Optimizing GCC 4.9.1 x64

my_max:
; RDI=A
; RSI=B
; compare A and B:
        cmp     rdi, rsi
; prepare B in RAX for return:
        mov     rax, rsi
; if A>=B, put A (RDI) in RAX for return.
; this instruction is idle if otherwise (if A<B)
        cmovge  rax, rdi
        ret
my_min:
; RDI=A
; RSI=B
; compare A and B:
        cmp     rdi, rsi
; prepare B in RAX for return:
        mov     rax, rsi
; if A<=B, put A (RDI) in RAX for return.
; this instruction is idle if otherwise (if A>B)
        cmovle  rax, rdi
        ret

MSVC 2013的编译方法几乎一样。

ARM64指令集里有CSEL指令。它相当于ARM指令集中的MOVcc指令，以及x86平台的CMOVcc指令。它只是名字不同：“Conditional SELect”。

指令清单12.32　Optimizing GCC 4.9.1 ARM64

my_max:
; X0=A
; X1=B
; compare A and B:
        cmp     x0, x1
; select X0 (A) to X0 if X0>=X1 or A>=B (Greater or Equal)
; select X1 (B) to X0 if A<B
        csel    x0, x0, x1, ge
        ret
my_min:
; X0=A
; X1=B
; compare A and B:
        cmp     x0, x1
; select X0 (A) to X0 if X0<=X1 or A<=B (Less or Equal)
; select X1 (B) to X0 if A>B
        csel    x0, x0, x1, le
        ret

12.4.3　MIPS

不幸的是，GCC 4.4.5在编译MIPS程序方面的智能化程度有限。

指令清单12.33　Optimizing GCC 4.4.5 (IDA)

my_max:
; set $v1  $a1<$a0,or clear otherwise (if $01>$a0):
                slt      $v1, $a1, $a0
; jump, if $v1 iso (or $a1>$a9):
                beqz     $v1, locret_10
; this is branch delay slot
; prepare $a1 in $v0 in case of branch triggered:
                move     $v0, $a1
; no branch triggered, prepare $a0 in $v0:
                move     $v0, $a0
locret_10:
                jr       $ra
                or       $at, $zero ; branch delay slot, NOP

; the min() function is same, but input operands in SLT instruction are swapped:
my_min
                slt      $v1, $a0, $a1
                beqz     $v1, locret_28
                move     $v0, $a1
                move     $v0, $a0
locret_28:
                jr       $ra
                or       $at, $zero ; branch delay slot, NOP

请注意分支延时槽现象：第一个MOVE指令“先于”BEQZ指令运行，而第二个MOVE指令仅在不发生跳转的情况下才会被执行。

12.5　总结

条件转移指令的构造大体如下。

12.5.1　x86

指令清单12.34　x86

CMP register, register/value
Jcc true ; cc=condition code
false:
... some code to be executed if comparison result is false ...
JMP exit
true:
... some code to be executed if comparison result is true ...
exit:

12.5.2　ARM

指令清单12.35　ARM

CMP register, register/value
Bcc true ; cc=condition code
false:
... some code to be executed if comparison result is false ...
JMP exit
true:
... some code to be executed if comparison result is true ...
exit:

12.5.3　MIPS

指令清单12.36　遇零跳转

BEQZ REG, label
...

指令清单12.37　遇负数跳转

BLTZ REG, label
...

指令清单12.38　值相等的情况下跳转

BEQ REG1, REG2, label
...

指令清单12.39　值不等的情况下跳转

BNE REG1, REG2, label
...

指令清单12.40　第一个值小于第二个值的情况下跳转(signed)

SLT REG1, REG2, REG3
BEQ REG1, label
...

指令清单12.41　第一个值小于第二个值的情况下跳转(unsigned)

SLTU REG1, REG2, REG3
BEQ REG1, label
...

12.5.4　无分支指令（非条件指令）

如果条件语句十分短，那么编译器可能会分配条件执行指令：

编译ARM模式的程序时应用MOVcc指令。

编译ARM64程序时应用CSEL指令。

编译x86程序时应用CMOVcc指令。

ARM

在编译ARM模式的程序时，编译器可能用条件执行指令替代条件转移指令。

指令清单12.42　ARM (ARM mode)

CMP register, register/value
instr1_cc ; some instruction will be executed if condition code is true
instr2_cc ; some other instruction will be executed if other condition code is true
... etc ...

在被执行指令不修改任何标志位的情况下，程序可有任意多条的条件执行指令。

Thumb模式的指令集里有IT指令。它可以把后续四条指令构成一个指令组，并且在条件表达式为真的时候运行这组指令。详细介绍请参见本书的17.7.2节。

指令清单12.43　ARM (Thumb mode)

CMP register, register/value
ITEEE EQ ; set these suffixes: if-then-else-else-else
instr1   ; instruction will be executed if condition is true
instr2   ; instruction will be executed if condition is false
instr3   ; instruction will be executed if condition is false
instr4   ; instruction will be executed if condition is false

12.6　练习题

请使用CSFL指令替代指令清单12.23中所有的条件转移语句。

[1]　请参见本书3.4.3节的解释。

[2]　即predicated instructions，泛指BLGT/ADREQ这类混合条件判定功能的操作指令。

[3]　请参阅ARM13a，p390，C5.5。

第13章 switch()/case/default

13.1　case陈述式较少的情况

本节将围绕这个例子进行讲解：

#include <stdio.h>

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

int main()
{
    f(2); //test
};

13.1.1　x86

Non-optimizing MSVC

使用MSVC 2010编译上述程序，可得到如下指令。

指令清单13.1　MSVC 2010

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

上面这个函数的源程序相当于：

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

如果仅从汇编代码入手，那么我们无法判断上述函数是一个判断表达式较少的switch()语句、还是一组if()语句。确实可以认为，switch()语句是一种旨在简化大量嵌套if()语句而设计的语法糖[1]。

上面的汇编代码把输入参数a代入了临时的局部变量tv64，其余部分的指令都很好理解。[2]

若用GCC 4.4.1编译器编译这个程序，无论是否启用其最大程度优化的选项“-O3”，生成的汇编代码也和MSVC编译出来的代码没有什么区别。

Optimizing MSVC

经指令“c1 1.c /Fa1.asm /Ox”编译上述程序，可得到如下指令。

指令清单13.2　MSVC

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

我们看到，它有以下两处不同。

第一处：程序把变量a存储到EAX寄存器之后，又用EAX的值减去零。似乎这样做并没有什么道理。但是这两条指令可以检查EAX寄存器的值是否是零。如果EAX寄存器的值是零，ZF标志寄存器会被置1（也就是说0−0＝0，这就可以提前设置ZF标志位），并会触发第一条条件转移指令JE，使程序跳转到 $LN4@f，继而在屏幕上打印“Zero”。如果EAX寄存器的值仍然不是零，则不会触发第一条跳转指令、做“EAX＝EAX－1”的运算，若计算结果是零则做相应输出；若此时EAX寄存器的值仍然不是零，就会再做一次这种减法操作和条件判断。

如果三次运算都没能使EAX寄存器的值变为零，那么程序会输出最后一条信息“something unknown”。

第二处：在把字符串指针存储到变量a之后，函数使用JMP指令调用printf()函数。在调用printf()函数的时候，调用方函数而没有使用常规的call指令。这点不难解释：调用方函数把参数推送入栈之后，的确通常通过CALL指令调用其他函数。这种情况下，CALL指令会把返回地址推送入栈、并通过无条件转移的手段启用被调用方函数。就本例而言，在被调用方函数运行的任意时刻，栈的内存存储结构为：

ESP——指向RA。

ESP＋4——指向变量a。

另一方面，在本例程序调用printf()函数之前、之后，除了制各第一个格式化字符串的参数问题以外，栈的存储结构其实没有发生变化。所以，编译器在分配JMP指令之前，把字符串指针存储到相应地址上。

这个程序把函数的第一个参数替换为字符串的指针，然后跳转到printf()函数的地址，就好像程序没有“调用”过f()函数、直接“转移”了printf()函数一般。当printf()函数完成输出的使命以后，它会执行RET返回指令。RET指令会从栈中读取（POP）返回地址RA、并跳转到RA。不过这个RA不是其调用方函数——f()函数内的某个地址，而是调用f()函数的函数即main()函数的某个地址。换而言之，跳转到这个RA地址后，printf()函数会伴随其调用方函数f()一同结束。

除非每个case从句的最后一条指令都是调用printf()函数，否则编译器就做不到这种程度的优化。某种意义上说这与longjmp()函数[3]十分相似。当然，这种优化的目的无非就是提高程序的运行速度。

ARM编译器也有类似的优化，请参见本书的6.2.1节。

OllyDbg

本节讲解使用OllyDbg调试这个程序的具体方法。

OllyDbg可以识别switch()语句的指令结构，而且能够自动地添加批注。最初的时候，EAX的值、即函数的输入参数为2，如图13.1所示。

图13.1　OllyDbg：观察EAX 里存储的函数的第一个（也是唯一一个）参数

EAX的值（2）减去0。当然，EAX里的值还是2。此后ZF标志位被设为0，代表着运算结果不是零，如图13.2所示。

图13.2　OllyDbg：执行第一个SUB指令

执行DEC指令之后，EAX里的值为1。但是1还不是零，ZF标志位还是0，如图13.3所示。

图13.3　OllyDbg：执行第一条DEC指令

再次执行DEC指令，此时EAX里的值终成为是零了。因为运算结果为零，ZF标志位被置位为1，如图13.4所示。

图13.4　OllyDbg：执行第二条DEC指令

OllyDbg提示将会触发条件转移指令，字符串“two”的指针即刻被推送入栈，如图13.5所示。

图13.5　OllyDbg：函数的第一个参数被赋值为字符串指针

请注意：函数的当前参数是2，它位于0x0020FA44处的栈。

MOV指令把地址0x0020FA44的指针放入栈中，然后进行跳转。程序将执行文件MSVCR100.DLL里的printf()函数的第一条指令。为了便于演示，本例在编译程序时使用了/MD开关，如图13.6所示。

图13.6　OllyDbg：文件MSVCR100.DLL中printf()函数的第一条指令

之后，printf()函数从地址0x00FF3010处读取它的唯一参数——字符串地址。然后函数会stdout设备（一般来说，就是屏幕）上在打印字符串。

printf()函数的最后一条指令如图13.7所示：

图13.7　OllyDbg：Printf()函数的最后一条指令

此时，字符串“two”就会被输出到控制台窗口（console）。

接下来，我们使用F7或F8键、单步执行这条返回指令。然而程序没有返回f()函数，而是回到了主函数main()，如图13.8所示。

图13.8　OllyDbg: 返回至main()函数

正如你所看到的那样，程序从printf()函数的内部直接返回到main()函数。这是因为RA寄存器里存储的返回地址确实不是f()函数中的某个地址，而是main()函数里的某个地址。请仔细观察返回地址的上一条指令，即“CALL 0X00FF1000”指令。它同样还是调用函数（即调用f()）的指令。

13.1.2　ARM: Optimizing Keil 6/2013 (ARM mode)

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

我们同样无法根据汇编指令判断源代码使用的是switch()语句还是if()语句。

此外，这段代码还出现了ADRQ指令之类的条件执行指令。第一条ADREQ指令会在R0＝0的情况下，将字符串“zero \n”的地址传给R0。紧接其后都BEQ指令在相同的条件下把控制流转交给loc_170。或许有读者会问，ADREQ之后的BEQ指令还能读取到前面由CMP设置的标志吗？这当然不是问题。只有少数指令才会修改标志位寄存器的值，而一般的条件执行指令不会重设任何标志位。

其余的指令不难理解。程序只在尾部调用了一次printf()函数。前面的6.2.1节讲解过编译器的这种处理技术。最后，3条逻辑分支都会收敛于同一个printf()函数。

最后一条CMP指令是“CMP R0, #2”。它的作用是检查a是否为2。如果条件不成立，程序将通过ADRNE指令把“something unknown \n”的指针赋值给R0寄存器。在此之前，程序已经检查过a是否是0或1；所以运行到这里时，我们已经可以确定变量a不是这两个值。如果R0的值为2，那么ADREQ指令将把“two”的指针传递给R0寄存器。

13.1.3　ARM: Optimizing Keil 6/2013 (Thumb mode)

.text:000000D4              f1:
.text:000000D4 10 B5        PUSH     {R4,LR}
.text:000000D6 00 28        CMP      R0, #0
.text:000000D8 05 D0        BEQ      zero_case
.text:000000DA 01 28        CMP      R0, #1
.text:000000DC 05 D0        BEQ      one_case
.text:000000DE 02 28        CMP      R0, #2
.text:000000E0 05 D0        BEQ      two_case
.text:000000E2 91 A0        ADR      R0, aSomethingUnkno ; "something unknown \n"
.text:000000E4 04 E0        B        default_case

.text:000000E6              zero_case ; CODE XREF: f1+4
.text:000000E6 95 A0        ADR      R0, aZero ; "zero\n"
.text:000000E8 02 E0        B        default_case

.text:000000EA              one_case ; CODE XREF: f1+8
.text:000000EA 96 A0        ADR      R0, aOne ; "one\n"
.text:000000EC 00 E0        B        default_case

.text:000000EE              two_case ; CODE XREF: f1+C
.text:000000EE 97 A0        ADR      R0, aTwo ; "two\n"
.text:000000F0              default_case ; CODE XREF: f1+10
.text:000000F0                                            ; f1+14
.text:000000F0 06 F0 7E F8  BL      __2printf
.text:000000F4 10 BD        POP     {R4,  PC}

前文介绍说过，条件执行指令是ARM模式指令的特色之一。Thumb模式的指令集里没有条件执行指令。所以Thumb模式的指令更为接近x86 CISC（复杂指令集）的指令，理解难度相对较低。

13.1.4　ARM64: Non-optimizing GCC (Linaro) 4.9

.LC12:
        .string "zero"
.LC13:
        .string "one"
.LC14:
        .string "two"
.LC15:
        .string "something unknown"
f12:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0
        str     w0, [x29,28]
        ldr     w0, [x29,28]
        cmp     w0, 1
        beq     .L34
        cmp     w0, 2
        beq     .L35
        cmp     w0, wzr
        bne     .L38              ; jump to default label
        adrp    x0, .LC12         ; "zero"
        add     x0, x0, :lo12:.LC12
        bl      puts
        b       .L32
.L34:
        adrp    x0, .LC13        ; "one"
        add     x0, x0, :lo12:.LC13
        bl      puts
        b       .L32
.L35:
        adrp    x0, .LC14        ; "two"
        add     x0, x0, :lo12:.LC14
        bl      puts
        b       .L32
.L38:
        adrp    x0, .LC15        ; "something unknown"
        add     x0, x0, :lo12:.LC15
        bl      puts
        nop
.L32:
        ldp     x29, x30, [sp], 32
        ret

因为输入变量属于整型数据，所以ARM64编译器没有给它分配64位的X0寄存器而是为它分配了容量相当的X0寄存器。另外，本例用ADRP/ADD指令对向puts()函数传递字符串指针。前文3.4.5节中的“Hello,world！”也是这样传递参数的。

13.1.5　ARM64: Optimizing GCC (Linaro) 4.9

f12:
        cmp     w0, 1
        beq     .L31
        cmp     w0, 2
        beq     .L32
        cbz     w0, .L35
; default case
        adrp    x0, .LC15        ; "something unknown"
        add     x0, x0, :lo12:.LC15
        b       puts
.L35:
        adrp    x0, .LC12        ; "zero"
        add     x0, x0, :lo12:.LC12
        b       puts
.L32:
        adrp    x0, .LC14        ; "two"
        add     x0, x0, :lo12:.LC14
        b       puts
.L31:
        adrp    x0, .LC13        ; "one"
        add     x0, x0, :lo12:.LC13
        b       puts

优化编译的效果显著。CBZ（Compare and Branch on Zero）会在W0的值为零的情况下进行跳转。此外，在调用puts()函数的时候，本例使用的是JMP指令而非常规的call指令调用，这再现了13.1.1节出现过的函数调用方式。

13.1.6　MIPS

指令清单13.3　Optimizing GCC 4.4.5(IDA)

f:
                   lui     $gp, (__gnu_local_gp >> 16)
; is it 1?
                   Li      $v0, 1
                   beq     $a0, $v0, loc_60
                   la      $gp, (__gnu_local_gp & 0xFFFF) ; branch delay slot
; is it 2?
                   Li      $v0, 2
                   beq     $a0, $v0, loc_4C
                   or      $at, $zero ; branch delay slot, NOP
; jump, if not equal to 0:
                   bnez    $a0, loc_38
                   or      $at, $zero ; branch delay slot, NOP
; zero case:
                   lui     $a0, ($LC0 >> 16)  # "zero"
                   lw      $t9, (puts & 0xFFFF)($gp)
                   or      $at, $zero ; load delay slot, NOP
                   jr      $t9 ; branch delay slot, NOP
                   la      $a0, ($LC0 & 0xFFFF)  # "zero" ; branch delay slot
# ----------------------------------------------------------------
loc_38:                                        # CODE XREF: f+1C
                   lui     $a0, ($LC3 >> 16)  # "something unknown"
                   lw      $t9, (puts & 0xFFFF)($gp)
                   or      $at, $zero ; load delay slot, NOP
                   jr      $t9
                   la      $a0, ($LC3 & 0xFFFF)  # "something unknown" ; branch delay slot
# ----------------------------------------------------------------
loc_4C:                                        # CODE XREF: f+14
                   lui     $a0, ($LC2 >> 16)  # "two"
                   lw      $t9, (puts & 0xFFFF)($gp)
                   or      $at, $zero ; load delay slot, NOP
                   jr      $t9
                   la      $a0, ($LC2 & 0xFFFF)  # "two" ; branch delay slot
# ----------------------------------------------------------------
loc_60:                                        # CODE XREF: f+8
                   lui     $a0, ($LC1 >> 16)  # "one"
                   lw      $t9, (puts & 0xFFFF)($gp)
                   or      $at, $zero ; load delay slot, NOP
                   jr      $t9
                   la      $a0, ($LC1 & 0xFFFF)  # "one" ; branch delay slot

在汇编层面，每个case分支的最后一条指令都是调用puts()函数的指令。而且本例的每个case分支都通过跳转指令JR（Jump Register）调用puts()函数，完全没有使用常规的函数调用指令JAL（Jump And Link）。有关详细介绍，请参阅13.1.1节。

另外，参数LW指令之后有一条NOP指令。这种指令组合叫作“加载延迟槽/load delay slot”，是MIPS平台的另一种延迟槽。在LW指令从内存加载数据的时候，下面的那条指令可能和它并发执行。这样一来，LW后面的那条指令就无法使用LW读取的数据了。当今主流的MIPS CPU都针对这一问题进行了优化，在下一条指令LW的数据的情况下能够进行自动处理。虽然现在的MIPS处理器不再存在加载延时槽，但是GCC还是会颇为保守地添加加载延迟槽。总之，我们已经可以忽视这种延迟槽了。

13.1.7　总结

在case分支较少的情况下，switch()语句和if/else语句的编译结果基本相同。指令清单13.1可充分论证这个结论。

13.2　case陈述式较多的情况

在switch()语句存在大量case()分支的情况下，编译器就不能直接套用大量JE/JNE指令了。否则程序代码肯定会非常庞大。

#include <stdio.h>

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

int main()
{
    f(2); // test
};

13.2.1　x86

Non-optimizing MSVC

使用MSVC 2010编译上述程序，可得到如下指令。

指令清单13.4　MSVC 2010

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

这段代码可被分为数个调用printf()函数的指令组，而且每组指令传递给printf()函数的参数还各不相同。这些指令组在内存中拥有各自的起始地址，也就被编译器分配到了不同的符号标签（symbolic label）之后。总的来看，程序通过$LN11@f处的一组数据调派这些符号标签。

函数最初把变量a的值与数字4进行比较。如果a大于4，函数则跳转到$LN1@f处，把字符串“something unknown”的指针传递给printf()函数。

如果变量a小于或等于4，则会计算a乘以4的积，再计算积与$LN11@f的偏移量的和（表查询），并跳转到这个结果所指向的地址上。以变量a等于2的情况来说，2×4=8（由于x86系统的内存地址都是32位数据，所以$LN11@f表中的每个地址都占用4字节）。在计算8与$LN11@f的偏移量的和之后，再跳转到这个和指向的标签——即$LN4@f处。JMP指令最终跳转到$LN4@f的地址。

$LN11@f标签（偏移量）开始的表，叫作“转移表jumptable”，也叫作“转移（输出）表branchtable”。[4]

当a等于2的时候，程序分配给printf()的参数是“two”。实际上，此时的switch语句的分支指令等效于“jmp DWORD PTR $LN11@f[ecx*4]”。它会进行间接取值的操作，把指针“PTR｛表达式｝”所指向的数据读取出来，当作DWORD型数据传递给JMP指令。在这个程序里，表达式的值为$LN11@f+ecx*4。

此处出现的npad指令属于汇编宏，本书第88章会详细介绍它。它的作用是把紧接其后的标签地址向4字节（或16字节）边界对齐。npad的地址对齐功能可提高处理器的IO读写效率，通过一次操作即可完成内存总线、缓冲内存等设备的数据操作。

OllyDbg

接下来使用OllyDbg调试这个程序。我们在EAX＝2的时候进行调试，如图13.9所示。

图13.9　使用OllyDbg查看EAX获取输入值的情况

程序将检验输入值是否大于4。因为2<4，所以不会执行“default”规则的跳转，如图13.10所示。

图13.10　OllyDbgEAX 4,不会触发default规则的跳转

然后就开始处理转移表，如图13.11所示。

图13.11　利用转移表计算目标地址

在用鼠标选择“Follow in Dump”→“Address constant”功能之后，即可在数据窗口看见转移表。表里有5个32位的值[5]。现在ECX寄存器的值是2，所以对应表中的第2个元素（从0开始数）。另外，您还可以使用OllyDbg的“Follow in Dump→Memory address”功能查看JMP指令的目标地址。此时，这个目标地址为0x010B103A。

地址0x010B103A处的指令将会打印字符串“two”，如图13.12所示。

图13.12　使用OllyDbg观察case：label的触发过程

Non-optimizing GCC

GCC 4.4.1编译出的代码如下。

指令清单13.5　GCC 4.4.1

        public   f
f       proc near ; CODE XREF: main+10

var_18 = dword ptr -18h
arg_0  = dword ptr  8

        push   ebp
        mov    ebp, esp
        sub    esp, 18h
        cmp    [ebp+arg_0], 4
        ja     short loc_8048444
        mov    eax, [ebp+arg_0]
        shl    eax, 2
        mov    eax, ds:off_804855C[eax]
        jmp    eax

loc_80483FE: ; DATA XREF: .rodata:off_804855C
        mov    [esp+18h+var_18], offset aZero ; "zero"
        call   _puts
        jmp    short locret_8048450

loc_804840C: ; DATA XREF: .rodata:08048560
        mov    [esp+18h+var_18], offset aOne ; "one"
        call   _puts
        jmp    short locret_8048450

loc_804841A: ; DATA XREF: .rodata:08048564
        mov    [esp+18h+var_18], offset aTwo ; "two"
        call   _puts
        jmp    short locret_8048450

loc_8048428: ; DATA XREF: .rodata:08048568
        mov    [esp+18h+var_18], offset aThree ; "three"
        call   _puts
        jmp    short locret_8048450

loc_8048436: ; DATA XREF: .rodata:0804856C
        mov    [esp+18h+var_18], offset aFour ; "four"
        call   _puts
        jmp    short locret_8048450

loc_8048444: ; CODE XREF: f+A
        mov    [esp+18h+var_18], offset aSomethingUnkno ; "something unknown"
        call   _puts

locret_8048450: ; CODE XREF: f+26
                ; f+34...
        leave
        retn
f       endp

off_804855C dd offset loc_80483FE   ; DATA XREF: f+12
            dd offset loc_804840C
            dd offset loc_804841A
            dd offset loc_8048428
            dd offset loc_8048436

这段代码与MSVC编译出来的代码几乎相同。参数arg_0被左移2位（数学上等同于乘以4，有关指令介绍请参阅16.2.1节），然后在转移表off_804855C的数组中获取相应地址，并将计算结果存储于EAX寄存器。最后通过JMP EAX指令进行跳转。

13.2.2　ARM: Optimizing Keil 6/2013 (ARM mode)

指令清单13.6　Optimizing Keil 6/2013 (ARM mode)

00000174               f2
00000174  05 00 50 E3      CMP     R0, #5            ; switch 5 cases
00000178  00 F1 8F 30      ADDCC   PC, PC, R0,LSL#2  ; switch jump
0000017C  0E 00 00 EA      B       default_case      ; jumptable 00000178 default case

00000180
00000180               loc_180 ; CODE XREF: f2+4
00000180  03 00 00 EA      B        zero_case        ; jumptable 00000178 case 0

00000184
00000184               loc_184 ; CODE XREF: f2+4
00000184  04 00 00 EA      B        one_case         ; jumptable 00000178 case 1

00000188
00000188               loc_188 ; CODE XREF: f2+4
00000188  05 00 00 EA      B        two_case         ; jumptable 00000178 case 2

0000018C
0000018C               loc_18C ; CODE XREF: f2+4
0000018C  06 00 00 EA      B        three_case       ; jumptable 00000178 case 3

00000190
00000190               loc_190 ; CODE XREF: f2+4
00000190  07 00 00 EA      B        four_case        ; jumptable 00000178 case 4

00000194
00000194               zero_case ; CODE XREF: f2+4
00000194                         ; f2:loc_180
00000194  EC 00 8f E2      ADR     R0, aZero        ; jumptable 00000178 case 0
00000198  04 00 00 EA      B       loc_1B8

0000019C
0000019C               one_case  ; CODE XREF: f2+4
0000019C                         ; f2:loc_184
0000019C  EC 00 8F E2      ADR      R0, aOne        ; jumptable 00000178 case 1
000001A0  04 00 00 EA      B        loc_1B8

000001A4
000001A4               two_case  ; CODE XREF: f2+4
000001A4                         ; f2:loc_188
000001A4  01 0C 8F E2      ADR      R0, aTwo        ; jumptable 00000178 case 2
000001A8  02 00 00 EA      B        loc_1B8

000001AC
000001AC               three_case ; CODE XREF: f2+4
000001AC                          ; f2:loc_18C
000001AC  01 0C 8F E2      ADR    R0, aThree      ; jumptable 00000178 case 3
000001B0  00 00 00 EA      B      loc_1B8

000001B4
000001B4               four_case ; CODE XREF: f2+4
000001B4                         ; f2:loc_190
000001B4  01 0C 8F E2      ADR    R0, aFour       ; jumptable 00000178 case 4
000001B8
000001B8               loc_1B8  ; CODE XREF: f2+4
000001B8                        ; f2+2C
000001B8  66 18 10 EA      B       __2printf

000001BC               default_case ; CODE XREF: f2+4
000001BC                            ; f2+8
000001BC D4 00 8F E2       ADR    R0, aSomethingUnkno ; jumptable default case
000001C0 FC FF FF EA       B      loc_1B8

这段代码充分体现了ARM模式下每条汇编指令占用4个字节的特点。

这个程序能够识别出4及4以下的自然数。当输入值是大于4的整数时，程序都会显示“something unknown \n”。

第一条指令是“CMP R0, #5”。它将输入变量与5做比较。

“ADDCC PC, PC, R0, LSL＃2”会在R0寄存器的值小于5的时候进行加法计算，其中CC代表借位标志Carry Clear。如果R0寄存器的值不小于5，（即R0大于或等于5），则会直接跳转到标签default_case 处。

如果R0寄存器的值是5以下的整数，那么将会触发ADDCC，并且进行下列运算：

将R0的值乘以4。LSL是左移操作，左移两位（2bits）就相当于乘以4。

把上述积与PC的值相加，并会把运算结果存储在PC寄存器里。这种调整PC指针的操作，等同于运行B跳转指令。

在执行ADDCC指令的时候，PC寄存器的值会比当前指令的（首）地址提前8个字节。此时ADDCC的地址是0x178，PC的值为0x180。即，PC指向当前指令后面的第二条指令。

这是ARM处理器的pipeline/流水线决定的。当ARM处理器执行某条指令时，处理器正在处理（fetch取指）后面的第二条指令。实际上PC指向后面第二条（正在被fetch/取指的）指令的地址。[6]

如果a＝＝0，“加零”操作使PC寄存器的值不变。所以在PC操作之后，CPU会跳到8个字节之后的loc_180处继续执行后续命令，开始执行ADDCC指令。

如果a＝＝1，则PC＋8+a×4=PC＋16=0x184。程序会跳转到loc_184处。

依此类推，变量a的值每增加1，PC的值就会增加4。这4字节是每个分支语句的唯一一条指令——B指令的opcode的长度。在ADDCC之后，总共有5个B跳转指令。

后面的指令比较容易理解，5条B跳转指令接着完成各自赋值和打印的任务，完成switch()语句的功能。

13.2.3　ARM: Optimizing Keil 6/2013 (Thumb mode)

指令清单13.7　Optimizing Keil 6/2013 (Thumb mode)

000000F6                       EXPORT f2
000000F6                   f2
000000F6 10 B5             PUSH    {R4,LR}
000000F8 03 00             MOVS    R3, R0
000000FA 06 F0 69 F8       BL      __ARM_common_switch8_thumb ; switch 6 cases

000000FE 05                DCB 5
000000FF 04 06 08 0A 0C 10 DCB 4, 6, 8, 0xA, 0xC, 0x10 ; jump table for switch statement
00000105 00                ALIGN 2
00000106
00000106               zero_case ; CODE XREF: f2+4
00000106 8D A0             ADR     R0, aZero ; jump table 000000FA case 0
00000108 06 E0             B       loc_118

0000010A
0000010A               one_case ; CODE XREF: f2+4
0000010A 8E A0             ADR     R0, aOne ; jumptable 000000FA case 1
0000010C 04 E0             B       loc_118

0000010E
0000010E               two_case ; CODE XREF: f2+4
0000010E 8F A0             ADR     R0, aTwo ; jumptable 000000FA case 2
00000110 02 E0             B       loc_118

00000112
00000112               three_case ; CODE XREF: f2+4
00000112 90 A0             ADR     R0, aThree ; jumptable 000000FA case 3
00000114 00 E0             B       loc_118

00000116
00000116               four_case ; CODE XREF: f2+4
00000116 91 A0             ADR     R0, aFour ; jumptable 000000FA case 4
00000118
00000118               loc_118 ; CODE XREF: f2+12
00000118                       ; f2+16
00000118 06 F0 6A F8       BL      __2printf
0000011C 10 BD             POP     {R4,PC}

0000011E
0000011E               default_case ; CODE XREF: f2+4
0000011E 82 A0             ADR     R0, aSomethingUnkno ; jumptable 000000FA default case
00000120 FA E7             B       loc_118

000061D0                   EXPORT __ARM_common_switch8_thumb
000061D0               __ARM_common_switch8_thumb ; CODE XREF: example6_f2+4
000061D0 78 47              BX      PC

000061D2 00 00              ALIGN 4
000061D2               ; End of function __ARM_common_switch8_thumb
000061D2
000061D4               __32__ARM_common_switch8_thumb ; CODE XREF__ARM_common_switch8_thumb
000061D4 01 C0 5E E5           LDRB    R12, [LR,#-1]
000061D8 0C 00 53 E1           CMP     R3, R12
000061DC 0C 30 DE 27           LDRCSB  R3, [LR,R12]
000061E0 03 30 DE 37           LDRCCB  R3, [LR,R3]
000061E4 83 C0 8E E0           ADD     R12, LR, R3,LSL#1
000061E8 1C FF 2F E1           BX      R12
000061E8                    ; End of function __32__ARM_common_switch8_thumb

Thumb和Thumb-2程序的opcode长度并不固定。这一特征更接近x86系统的程序。

它们的程序代码里有一个专门用于存储case从句信息（default以外）的表。这个表负责记录case从句的数量、偏移量和标签，以便程序可以进行准确的寻址。程序通过这个表单进行相应的跳转，继而处理相应的分支case语句。

因为需要操作转移表并进行后续跳转，所以这个程序也使用了专用函数_ARM_common_ switch8_thumb。这个函数的第一条指令是“BX PC”，它将运行模式切换到32位的ARM模式，然后在32位模式下进行操作。然后函数着手表查询和分支转移单操作。具体指令非常复杂，本文在这里只是简单介绍一下，暂时不进行详解。

比较有趣的是，这个函数使用LR寄存器存储表的指针。在调用这个函数之后，LR寄存器存储着“BL __ARM_common_switch8_thumb”的后续指令的地址，也就是表开始的地址。

这个程序出现了每个switch()陈述句都会复用的专用函数，具有显著的结构化特征。可能是编译器为了避免生成重复代码而进行的处理。

IDA能够自动识别出这个函数和相应的转移表。IDA还能给相应的条目加上合理的注释。举例来说，IDA就给本例添加了“jumptable 000000FA case 0”这样的注释。

13.2.4　MIPS

指令清单13.8　Optimizing GCC 4.4.5 (IDA)

f:
                lui     $gp, (__gnu_local_gp >> 16)
; jump to loc_24 if input value is lesser than 5:
                sltiu   $v0, $a0, 5
                bnez    $v0, loc_24
                la      $gp, (__gnu_local_gp & 0xFFFF) ; branch delay slot
; input value is greater or equal to 5.
; print "something unknown" and finish:
                lui     $a0, ($LC5 >> 16)  # "something unknown"
                lw      $t9, (puts & 0xFFFF)($gp)
                or      $at, $zero ; NOP
                jr      $t9
                la      $a0, ($LC5 & 0xFFFF)  # "something unknown" ; branch delay slot

loc_24:                                        # CODE XREF: f+8
; load address of jumptable
; LA is pseudoinstruction, LUI and ADDIU pair are there in fact:
                la      $v0, off_120
; multiply input value by 4:
                sll     $a0, 2
; sum up multiplied value and jumptable address:
                addu    $a0, $v0, $a0
; load element from jumptable:
                lw      $v0, 0($a0)
                or      $at, $zero ; NOP
; jump to the address we got in jumptable:
                jr      $v0
                or      $at, $zero ; branch delay slot, NOP

sub_44:                                        # DATA XREF: .rodata:0000012C
; print "three" and finish
                lui     $a0, ($LC3 >> 16)  # "three"
                lw      $t9, (puts & 0xFFFF)($gp)
                or      $at, $zero ; NOP
                jr      $t9
                la      $a0, ($LC3 & 0xFFFF)  # "three" ; branch delay slot

sub_58:                                        # DATA XREF: .rodata:00000130
; print "four" and finish
                lui     $a0, ($LC4 >> 16)  # "four"
                lw      $t9, (puts & 0xFFFF)($gp)
                or      $at, $zero ; NOP
                jr      $t9
                la      $a0, ($LC4 & 0xFFFF)  # "four" ; branch delay slot
sub_6C:                                        # DATA XREF: .rodata:off_120
; print "zero" and finish
                lui     $a0, ($LC0 >> 16)  # "zero"
                lw      $t9, (puts & 0xFFFF)($gp)
                or      $at, $zero ; NOP
                jr      $t9
                la      $a0, ($LC0 & 0xFFFF)  # "zero" ; branch delay slot

sub_80:                                        # DATA XREF: .rodata:00000124
; print "one" and finish
                lui     $a0, ($LC1 >> 16)  # "one"
                lw      $t9, (puts & 0xFFFF)($gp)
                or      $at, $zero ; NOP
                jr      $t9
                la      $a0, ($LC1 & 0xFFFF)  # "one" ; branch delay slot

sub_94:                                        # DATA XREF: .rodata:00000128
; print "two" and finish
                lui     $a0, ($LC2 >> 16)  # "two"
                lw      $t9, (puts & 0xFFFF)($gp)
                or      $at, $zero ; NOP
                jr      $t9
                la      $a0, ($LC2 & 0xFFFF)  # "two" ; branch delay slot

; may be placed in .rodata section:
off_120:        .word sub_6C
                .word sub_80
                .word sub_94
                .word sub_44
                .word sub_58

上述代码出现了SLTIU（Set on Less Than lmmediate Unsigned）指令。它和SLTU（Set on Less Than Unsigned）的功能基本相同。请注意，这两个指令名称里差了一个“立即数（immediate）”字样。这说明前者需要在指令中指定既定的立即数。

BNEZ是“在非零情况下进行转移/Branche if Not Equal to Zero”的缩写。

上述代码和其他指令集的代码十分相近。SLL（Shift Word Left Logical）是逻辑左移的指令，本例用它进行“乘以4”的运算。毕竟这是一个面向32位MIPS CPU的程序，所有转移表里的所有地址都是32位指针。

13.2.5　总结

switch()的大体框架参见指令清单13.9。

指令清单13.9　x86

MOV REG,input
CMP REG,4 ; maximal number of cases
JA default
SHL REG,3 ; find element in table.shift for 3bits in x64.
MOV REG, jump_table[REG]
JMP REG

case1;
     ; do something
     JMP exit
case2;
     ; do something
     JMP exit
case3;
     ; do something
     JMP exit
case4;
     ; do something
     JMP exit
Case5;
     ; do something
     JMP exit

defaule:

     …

exit:

     …

jump_table dd casel
           dd case2
           dd case3
           dd case4
           dd case5

若不使用上述指令，我们也可以在32位系统上使用指令JMP jump_table[REG*4]/在64位上使用JMP jump_table[REG*8]，实现转移表中的寻址计算。

说到底，转移表只不过是某种指针数组它和18.5节介绍的那种指针数组十分雷同。

13.3　case从句多对一的情况

多个case陈述从句触发同一系列操作的情况并不少见，例如：

#include <stdio.h>

void f (int a)
{
         switch (a)
         {
         case 1:
         case 2:
         case 7:
         case 10:
                  printf ("1, 2, 7, 10\n");
                  break;
         case 3:
         case 4:
         case 5:
         case 6:
                  printf ("3, 4, 5\n");
                  break;
         case 8:
         case 9:
         case 20:
         case 21:
                  printf ("8 9, 21\n");
                  break;
         case 22:
                  printf ("22\n");
                  break;
         default:
                  printf ("default\n");
                  break;
         };
};

int main ()
{
         f(4);
};

如果编译器刻板地按照每种可能的逻辑分支逐一分配对应的指令组，那么程序里将会存在大量的重复指令。一般而言，编译器会通过某种派发机制来降低代码的冗余度。

13.3.1　MSVC

使用MSVC 2010（启用/Ox选项）编译上述程序，可得到如下指令。

指令清单13.10　Optimizing MSVC 2010

 1 $SG2798 DB        '1, 2, 7, 10', 0aH, 00H
 2 $SG2800 DB        '3, 4, 5', 0aH, 00H
 3 $SG2802 DB        '8, 9, 21', 0aH, 00H
 4 $SG2804 DB        '22', 0aH, 00H
 5 $SG2806 DB        'default', 0aH, 00H
 6
 7 _a$ = 8
 8 _f      PROC
 9         mov       eax, DWORD PTR _a$[esp-4]
10         dec       eax
11         cmp       eax, 21
12         ja        SHORT $LN1@f
13         movzx     eax, BYTE PTR $LN10@f[eax]
14         jmp       DWORD PTR $LN11@f[eax*4]
15 $LN5@f:
16         mov       DWORD PTR _a$[esp-4], OFFSET $SG2798 ; '1, 2, 7, 10'
17         jmp       DWORD PTR __imp__printf
18 $LN4@f:
19         mov       DWORD PTR _a$[esp-4], OFFSET $SG2800 ; '3, 4, 5'
20         jmp       DWORD PTR __imp__printf
21 $LN3@f:
22         mov       DWORD PTR _a$[esp-4], OFFSET $SG2802 ; '8, 9, 21'
23         jmp       DWORD PTR __imp__printf
24 $LN2@f:
25         mov       DWORD PTR _a$[esp-4], OFFSET $SG2804 ; '22'
26         jmp       DWORD PTR __imp__printf
27 $LN1@f:
28         mov       DWORD PTR _a$[esp-4], OFFSET $SG2806 ; 'default'
29         jmp       DWORD PTR __imp__printf
30         npad      2 ; align $LN11@f table on 16-byte boundary
31 $LN11@f:
32         DD        $LN5@f ; print '1, 2, 7, 10'
33         DD        $LN4@f ; print '3, 4, 5'
34         DD        $LN3@f ; print '8, 9, 21'
35         DD        $LN2@f ; print '22'
36         DD        $LN1@f ; print 'default'
37 $LN10@f:
38         DB        0 ; a=1
39         DB        0 ; a=2
40         DB        1 ; a=3
41         DB        1 ; a=4
42         DB        1 ; a=5
43         DB        1 ; a=6
44         DB        0 ; a=7
45         DB        2 ; a=8
46         DB        2 ; a=9
47         DB        0 ; a=10
48         DB        4 ; a=11
49         DB        4 ; a=12
50         DB        4 ; a=13
51         DB        4 ; a=14
52         DB        4 ; a=15
53         DB        4 ; a=16
54         DB        4 ; a=17
55         DB        4 ; a=18
56         DB        4 ; a=19
57         DB        2 ; a=20
58         DB        2 ; a=21
59         DB        3 ; a=22
60 _f      ENDP

这个程序用到了两个表：一个是索引表$LN10@f；另一个是转移表$LN11@f。

第13行的movzx指令在索引表里查询输入值。

索引表的返回值又分为0（输入值为1、2、7、10）、1（输入值为3、4、5）、2（输入值为8、9、21）、3（输入值为22）、4（其他值）这5种情况。

程序把索引表的返回值作为关键字，再在第二个转移表里进行查询，以完成相应跳转（第14行指令的作用）。

需要注意的是，输入值为0的情况没有相应的case从句。如果a＝0，则“dec eax”指令会继续进行计算，而$LN10@f表的查询是从1开始的。可见，没有必要为0的特例设置单独的表。

这是一种普遍应用的编译技术。

表面看来，这种双表结构似乎不占优势。为什么它不象前文（请参见13.2.1节）那样采用一个统一的指针结构呢？在这种双表结构中，索引表采用的是byte型数据，所以双表结构比前面那种单表结构更为紧凑。

13.3.2　GCC

在编译这种多对一的switch语句时，GCC会生成统一的转移表。其代码风格和前文13.2.1节的风格相同。

13.3.3　ARM64: Optimizing GCC 4.9.1

因为输入值为零的情况没有对应的处理方法，所以GCC会从输入值为1的特例开始枚举各个分支，以便把转移表压缩得尽可能小。

GCC 4.9.1for ARM64的编译技术更为优越。它能把所有的偏移量信息编码为8位字节型数据、封装在单条指令的opcode里。前文介绍过，ARM64程序的每条指令都对应着4个字节的opcode。在本例这种类型的小型代码中，各分支偏移量的具体数值不会很大。GCC能够充分利用这一现象，构造出单字节指针组成的转移表。

指令清单13.11　Optimizing GCC 4.9.1 ARM64

f14:
; input value in W0
        sub      w0, w0, #1
        cmp      w0, 21
; branch if less or equal (unsigned):
        bls      .L9
.L2:
; print "default":
        adrp     x0, .LC4
        add      x0, x0, :lo12:.LC4
        b        puts
.L9:
; load jumptable address to X1:
        adrp     x1, .L4
        add      x1, x1, :lo12:.L4
; W0=input_value-1
; load byte from the table:
        ldrb     w0, [x1,w0,uxtw]
; load address of the Lrtx label:
        adr      x1, .Lrtx4
; multiply table element by 4 (by shifting 2 bits left) and add (or subtract) to the address of lrtx
        add      x0, x1, w0, sxtb #2
; jump to the calculated address:
        br       x0
; this label is pointing in code (text) segment:
.Lrtx4:
        .section          .rodata
; everything after ".section" statement is allocated in the read-only data (rodata) segment:
.L4:
        .byte   (.L3 - .Lrtx4) / 4  ;case 1
        .byte   (.L3 - .Lrtx4) / 4  ;case 2
        .byte   (.L5 - .Lrtx4) / 4  ;case 3
        .byte   (.L5 - .Lrtx4) / 4  ;case 4
        .byte   (.L5 - .Lrtx4) / 4  ;case 5
        .byte   (.L5 - .Lrtx4) / 4  ;case 6
        .byte   (.L3 - .Lrtx4) / 4  ;case 7
        .byte   (.L6 - .Lrtx4) / 4  ;case 8
        .byte   (.L6 - .Lrtx4) / 4  ;case 9
        .byte   (.L3 - .Lrtx4) / 4  ;case 10
        .byte   (.L2 - .Lrtx4) / 4  ;case 11
        .byte   (.L2 - .Lrtx4) / 4  ;case 12
        .byte   (.L2 - .Lrtx4) / 4  ;case 13
        .byte   (.L2 - .Lrtx4) / 4  ;case 14
        .byte   (.L2 - .Lrtx4) / 4  ;case 15
        .byte   (.L2 - .Lrtx4) / 4  ;case 16
        .byte   (.L2 - .Lrtx4) / 4  ;case 17
        .byte   (.L2 - .Lrtx4) / 4  ;case 18
        .byte   (.L2 - .Lrtx4) / 4  ;case 19
        .byte   (.L6 - .Lrtx4) / 4  ;case 20
        .byte   (.L6 - .Lrtx4) / 4  ;case 21
        .byte   (.L7 - .Lrtx4) / 4  ;case 22
        .text
; everything after ".text" statement is allocated in the code (text) segment:
.L7:
; print "22"
        adrp     x0, .LC3
        add      x0, x0, :lo12:.LC3
        b        puts
.L6:
; print "8, 9, 21"
        adrp     x0, .LC2
        add      x0, x0, :lo12:.LC2
        b        puts
.L5:
; print "3, 4, 5"
        adrp     x0, .LC1
        add      x0, x0, :lo12:.LC1
        b        puts
.L3:
; print "1, 2, 7, 10"
        adrp     x0, .LC0
        add      x0, x0, :lo12:.LC0
        b        puts
.LC0:
        .string "1, 2, 7, 10"
.LC1:
        .string "3, 4, 5"
.LC2:
        .string "8, 9, 21"
.LC3:
        .string "22"
.LC4:
        .string "default"

把上述程序编译为obj文件，然后再使用IDA打开，可看到其转移表如下。

指令清单13.12　jumptable in IDA

.rodata:0000000000000064             AREA .rodata, DATA, READONLY
.rodata:0000000000000064             ; ORG 0x64
.rodata:0000000000000064  $d         DCB    9    ; case 1
.rodata:0000000000000065             DCB    9    ; case 2
.rodata:0000000000000066             DCB    6    ; case 3
.rodata:0000000000000067             DCB    6    ; case 4
.rodata:0000000000000068             DCB    6    ; case 5
.rodata:0000000000000069             DCB    6    ; case 6
.rodata:000000000000006A             DCB    9    ; case 7
.rodata:000000000000006B             DCB    3    ; case 8
.rodata:000000000000006C             DCB    3    ; case 9
.rodata:000000000000006D             DCB    9    ; case 10
.rodata:000000000000006E             DCB 0xF7    ; case 11
.rodata:000000000000006F             DCB 0xF7    ; case 12
.rodata:0000000000000070             DCB 0xF7    ; case 13
.rodata:0000000000000071             DCB 0xF7    ; case 14
.rodata:0000000000000072             DCB 0xF7    ; case 15
.rodata:0000000000000073             DCB 0xF7    ; case 16
.rodata:0000000000000074             DCB 0xF7    ; case 17
.rodata:0000000000000075             DCB 0xF7    ; case 18
.rodata:0000000000000076             DCB 0xF7    ; case 19
.rodata:0000000000000077             DCB    3    ; case 20
.rodata:0000000000000078             DCB    3    ; case 21
.rodata:0000000000000079             DCB    0    ; case 22
.rodata:000000000000007B ; .rodata ends

当输入值为1时，目标偏移量的技术方法是：9乘以4、再加上Lrtx4的偏移量。当输入值为22时，目标偏移量为：0乘以4、结果为0。在转移表Lrtx4之后就是L7的标签的指令了，这部分指令将负责打印数字22。请注意，转移表位于单独的.rodata段。编译器没有把它分配到.text的代码段里。

上述转移表有一个负数0xF7。这个偏移量指向了打印默认字符串（.L2标签）的相关指令。

13.4　Fall-through

Switch()语句还有一种常见的使用方法——fall-through。

 1 #define R 1
 2 #define W 2
 3 #define RW 3
 4
 5 void f(int type)
 6 {
 7          int read=0, write=0;
 8
 9          switch (type)
10          {
11          case RW:
12                   read=1;
13          case W:
14                   write=1;
15                   break;
16          case R:
17                   read=1;
18                   break;
19          default:
20                   break;
21          };
22          printf ("read=%d, write=%d\n", read, write);
23 };

如果type为1（参见第一行可知，这是读取权限R为真的情况），则read的值会被设置为1；如果type为2（W），则write被设置1；如果type为3（RW），则read和write的值都会被设置为1。

无论type的值是RW还是W，程序都会执行第14行的指令。type为RW的陈述语句里没有break指令，从而利用switch语句的fall through效应。

13.4.1　MSVC x86

指令清单13.13　MSVC 2012

$SG1305 DB       'read=%d, write=%d', 0aH, 00H

_write$ = -12    ; size= 4
_read$  = -8     ; size= 4
tv64  = -4       ; size= 4
_type$  = 8      ; size= 4
_f        PROC
          push   ebp
          mov    ebp, esp
          sub    esp, 12
          mov    DWORD PTR _read$[ebp], 0
          mov    DWORD PTR _write$[ebp], 0
          mov    eax, DWORD PTR _type$[ebp]
          mov    DWORD PTR tv64[ebp], eax
          cmp    DWORD PTR tv64[ebp], 1 ; R
          je     SHORT $LN2@f
          cmp    DWORD PTR tv64[ebp], 2 ; W
          je     SHORT $LN3@f
          cmp    DWORD PTR tv64[ebp], 3 ; RW
          je     SHORT $LN4@f
          jmp    SHORT $LN5@f
$LN4@f: ; case RW:
          mov    DWORD PTR _read$[ebp], 1
$LN3@f: ; case W:
          mov    DWORD PTR _write$[ebp], 1
          jmp    SHORT $LN5@f
$LN2@f: ; case R:
          mov    DWORD PTR _read$[ebp], 1
$LN5@f: ; default
          mov    ecx, DWORD PTR _write$[ebp]
          push   ecx
          mov    edx, DWORD PTR _read$[ebp]
          push   edx
          push   OFFSET $SG1305 ; 'read=%d, write=%d'
          call   _printf
          add    esp, 12
          mov    esp, ebp
          pop    ebp
          ret    0
_f        ENDP

上述汇编指令与C语言源代码几乎一一对应。因为在$LN4@f和$LN3@f之间没有转移指令，所以当程序执行了$LN4@f处的“令read的值为1”的指令之后，它还会执行后面那个标签的write赋值指令。这也是“fall through”（滑梯）这个名字的来源：当执行完前面那个陈述语句的指令（read赋值）之后，继续执行下一个陈述语句的指令（write赋值）。如果type的值为W，程序会执行$LN3@f的指令，而不会执行前面那个read赋值指令。

13.4.2　ARM64

指令清单13.14　GCC (Linaro) 4.9

.LC0:
        .string "read=%d, write=%d\n"
f:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     w0, [x29,28]
        str     wzr, [x29,44]   ; set "read" and "write" local variables to zero
        str     wzr, [x29,40]
        ldr     w0, [x29,28]    ; load "type" argument
        cmp     w0, 2           ; type=W?
        beq     .L3
        cmp     w0, 3           ; type=RW?
        beq     .L4
        cmp     w0, 1           ; type=R?
        beq     .L5
        b       .L6             ; otherwise...
.L4: ; case RW
        mov     w0, 1
        str     w0, [x29,44]    ; read=1
.L3: ; case W
        mov     w0, 1
        str     w0, [x29,40]    ; write=1
        b       .L6
.L5: ; case R
        mov     w0, 1
        str     w0, [x29,44]    ; read=1
        nop
.L6: ; default
        adrp    x0, .LC0        ; "read=%d, write=%d\n"
        add     x0, x0, :lo12:.LC0
        ldr     w1, [x29,44]    ; load "read"
        ldr     w2, [x29,40]    ; load "write"
        bl      printf
        ldp     x29, x30, [sp], 48
        ret

Arm64程序的汇编指令与MSVC x86的汇编指令大致相同。编译器同样没有在标签.L4和标签.L3之间分配转移指令，从而形成了Switch()语句的fall-through效应。

13.5　练习题

13.5.1　题目1

13.2节有一段C语言源代码。请改写这个程序，并且在不改变程序功能的前提下，让编译器生成体积更小的可执行程序。

[1]　即syntactic sugar，指代增强代码可读性、降低编程出错率的语法改进措施。

[2]　MSVC编译器在处理栈内的局部变量时，按照其需要，可能给这些内部变量分配以tv开头的宏变量。

[3]　https://en.wikipedia.org/wiki/Setjmp.h。

[4]　这个名称来自于Fortran早期的GOTO算法。虽然现在保留了这个名称，但是已经和那个概念没什么关系了。详情请参见http://en.wikipedia.org/wiki/Branch_table。

[5]　OllyDbg用下画线的格式显示这些值，因为它们也是FIXUPS。本书的68.2.6节会进行详细的解释。

[6]　虽然pipeline三级流水的解释较为直观，但是官方手册《ARM architecture reference manual》第1章第2节否认了这种硬件上的联系，它把PC与指令间opcode的增量关系解释为历史原因。

第14章 循环

14.1　举例说明

14.1.1　x86

x86指令集里有一条专门的LOOP指令。LOOP指令检测ECX寄存器的值是否是0，如果它不是0则将其递减，并将操作权交给操作符所指定的标签处（即跳转）。或许是因为循环指令过于复杂的缘故，我至今尚未见过直接使用LOOP指令将循环语句转译成汇编语句的编译器。所以，如果哪个程序直接使用LOOP指令进行循环控制，那它很可能就是手写的汇编程序。

C/C++语言的循环控制语句主要分为for()、while()、do/while()语句。

我们从for()语句开始演示。

for()语句定义了循环的初始态（计数器的初始值）、循环条件（在变量满足何等条件下继续进行循环），以及循环控制（每次循环后对变量进行什么操作，通常是递增或递减）。当然这种语句也必须声明循环体，即每次循环时要实现什么操作。简而言之，for()语句的使用方法是：

for (初始态；循环条件；循环控制)
{
    循环体；
}

根据for()语句所代表的各种功能，编译器会把for()语句在编译为4个相应的功能体。

我们一起来编译下面的程序：

#include <stdio.h>

void printing_function(int i)
{
        printf ("f(%d)\n", i);
};

int main()
{
        int i;

        for (i=2; i<10; i++)
                printing_function(i);
        return 0;
};

使用MSVC 2010编译上述程序，可得到如下所示的指令。

指令清单14.1　MSVC 2010

_i$ = -4
_main   PROC
    push   ebp
    mov    ebp, esp
    push   ecx
    mov    DWORD PTR _i$[ebp], 2     ; 初始态
    jmp    SHORT $LN3@main
$LN2@main:
    mov    eax, DWORD PTR _i$[ebp]     ; 循环控制语句：
    add    eax, 1                      ; i递增1
    mov    DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp    DWORD PTR _i$[ebp], 10      ; 判断是否满足循环条件
    jge    SHORT $LN1@main             ; 如果i=10 则终止循环语句
    mov    ecx, DWORD PTR _i$[ebp]     ; 循环体: call f(i)
    push   ecx
    call   _printing_function
    add    esp, 4
    jmp    SHORT $LN2@main             ; 跳到循环开始处
$LN1@main:                             ; 循环结束
    xor    eax, eax
    mov    esp, ebp
    pop    ebp
    ret     0
_main    ENDP

上面的汇编代码可以说是中规中矩。

使用GCC 4.4.1（未启用优化选项）编译上述程序，可得到下述汇编指令。

指令清单14.2　GCC 4.4.1

main            proc near

var_20          = dword    ptr -20h
var_4           = dword    ptr –4

                push       ebp
                mov        ebp, esp
                and        esp, 0FFFFFFF0h
                sub        esp, 20h
                mov        [esp+20h+var_4], 2 ; (i) initializing
                jmp        short loc_8048476
loc_8048465:
                mov        eax, [esp+20h+var_4]
                mov        [esp+20h+var_20], eax
                call       printing_function
                add        [esp+20h+var_4], 1 ; (i) increment
loc_8048476:
                cmp        [esp+20h+var_4], 9
                jle        short loc_8048465 ; if i<=9, continue loop
                mov        eax, 0
                leave
                retn
main            endp

在开启优化选项（/Ox）后，MSVC的编译结果如下。

指令清单14.3　Optimizing MSVC

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

在开启优化选项后，ESI寄存器成为了局部变量i的专用寄存器；而在通常情况下，局部变量都应当位于栈。可见，编译器会在局部变量为数不多的情况下进行这样的优化。

进行这种优化的前提条件是：被调用方函数不应当修改局部变量专用寄存器的值。当然，在本例中编译器能够判断函数printing_function ()不会修改ESI寄存器的值。在编译器决定给局部变量分配专用寄存器的时候，它会在函数序言部分保存这些专用寄存器的初始状态，然后在函数尾声里还原这些寄存器的原始值。因此，您可以在本例main()函数的序言和尾声中分别看见PUSH ESI/POP ESI指令。

现在启用GCC 4.4.1的最深程度优化选项-O3，看看生成的汇编指令。

指令清单14.4　Optimizing GCC 4.4.1

main            proc near

var_10          = dword ptr -10h

                push    ebp
                mov     ebp, esp
                and     esp, 0FFFFFFF0h
                sub     esp, 10h
                mov     [esp+10h+var_10], 2
                call    printing_function
                mov     [esp+10h+var_10], 3
                call    printing_function
                mov     [esp+10h+var_10], 4
                call    printing_function
                mov     [esp+10h+var_10], 5
                call    printing_function
                mov     [esp+10h+var_10], 6
                call    printing_function
                mov     [esp+10h+var_10], 7
                call    printing_function
                mov     [esp+10h+var_10], 8
                call    printing_function
                mov     [esp+10h+var_10], 9
                call    printing_function
                xor     eax, eax
                leave
                retn
main            endp

GCC把我们的循环指令给展开（分解）了。

编译器会对选代次数较少的循环进行循环分解（Loop unwinding）对处理。展开循环体以后代码的执行效率会有所提升，但是会增加程序代码的体积。

编译经验表明，展开循环体较大的循环结构并非良策。大型函数的缓存耗费的内存占有量（cache footprint）较大。[1]

我们把变量i的最大值调整到100，看看GCC是否还会分解循环。

指令清单14.5　GCC

                public main
main            proc near

var_20          = dword    ptr -20h

                push       ebp
                mov        ebp, esp
                and        esp, 0FFFFFFF0h
                push       ebx
                mov        ebx, 2 ; i=2
                sub        esp, 1Ch

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

这就与MSVC 2010 /Ox编译出来的代码差不多了。唯一的区别是，GCC使用EBX寄存器充当变量i的专用寄存器。因为GCC能够判断后面的被调用方函数不会修改这个专用寄存器的值，所以它才会这样分配寄存器。在GCC不能进行相应判断，但是还决定给局部变量分配专用寄存器的时候，它就会在使用局部变量的那个函数的序言和尾声部分添加相应指令，利用数据栈保存和恢复专用寄存器的原始值。我们可以在main()函数里看到这种现象：它在函数的序言和尾声部分分别存储和恢复了局部变量专用寄存器ebx的原始值。

14.1.2　x86:OllyDbg

我们启用MSVC 2010的优化选项“/Ox”和“/Ob0”来编译上述程序，然后使用OllyDbg调试生成的可执行文件。

如图14.1所示，OllyDbg能够识别简单的循环语句，并用方括号进行标注。

图14.1　OllyDbg：main()函数的启动代码

我们按F8键进行单步调试，将会看到ESI寄存器的值不断递增。我们运行到ESI的值（变量i）为6的时刻，如图14.2所示。

图14.2　OllyDbg：i＝6时的循环体

当i=9的时候，循环语句会做最后一次迭代。进行了这次迭代之后，i值变为10，不会再触发条件转移指令JL。main()函数结束时的情况如图14.3所示。

图14.3　OllyDbg：ESI＝10之后，循环结束

14.1.3　x86:跟踪调试工具tracer

您可能也注意到了，直接使用OllyDbg这样的调试工具跟踪调试程序并不方便。所以我自己写了个调试工具tracer。[2]

使用IDA打开编译后的可执行文件，找到PUSH ESI（这条指令把参数传递给printing_function()函数），记下其地址0x401026，然后通过下述指令启动tracer程序：

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

上述指令会在BPX设置的断点地址处中断，输出此时各寄存器的状态。

tracer工具会把寄存器的状态输出到tracer.log:

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

从中可以看到ESI寄存器的值从2递增到9。

tracer工具能够追查函数中任意地址处的寄存器状态。它名字中的trace就强调了其独首的追查功能。它能够在任意指令处设置断点，以记录指定寄存器的变化过程。此外，它还可以生成可被IDA调用的.idc脚本文件，并为指令添加注释。例如，我们知道main()函数的地址是0x00401020，就可以执行

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

BPF的意思就是在函数执行前设置断点。

这条指令可以生成两个脚本文件，即loops_2.exe.idc和loops_2.exe_clear.idc。我们使用IDA加载脚本文件loops_2.exe.idc之后的情形如图14.4所示。

图14.4　IDA加载.idc脚本文件

根据Tracer给循环体进行的注释可知：ESI的值首先会从2递增到9。在递增指令结束之后，ESI的值再从3递增到0xA（10）。另外，在main()函数结束的时候，EAX的值会是零。

在生成注释文件的同时，tracer还会生成loops_2.ext.txt。这个文件会统计每条指令被执行的次数，并且标注了各寄存器数值的变化过程。

指令清单14.6　loops_2.exe.txt

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

我们可以使用grep指令搜索特定的关键字。

14.1.4　ARM

Non-optimizing Keil 6/2013 (ARM mode)

main
         STMFD    SP!, {R4,LR}
         MOV      R4, #2
         B        loc_368
loc_35C  ; CODE XREF: main+1C
         MOV      R0, R4
         BL       printing_function
         ADD      R4, R4, #1

loc_368  ; CODE XREF: main+8
         CMP      R4, #0xA
         BLT      loc_35C
         MOV      R0, #0
         LDMFD    SP!, {R4,PC}

上述代码中的R4寄存器用于存储循环计数器（即变量i）。

“MOV R4, #2”给变量i进行初始化赋值。

“MOV R0, R4”和“BL printing_function”指令构成循环体。前者负责向被调用方函数传递参数，后者则直接调用printing_function ()函数。

“ADD R4, R4, #1”指令在每次迭代之后进行i++的运算。

“CMP R4, #0xA”指令会比较变量i和数字10（十六进制的0xA）。下一条指令BLT（Branch Less Than）会在i<10的情况下进行跳转；否则执行“MOV R0，＃0”（处理返回值），并且退出当前函数。

Optimizing Keil 6/2013 (Thumb mode)

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

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

这段代码似乎不需更多解释。

Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

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

实际上，它把printing_function ()函数内部的指令直接代入了循环体：

void printing_function (int i)
{
    printf ("%d\n", i);
};

可见，LLVM不仅把循环控制语句展开为8个顺序执行的循环体，而且还在main()函数的循环体内直接代入了printing_function ()函数内部的指令。当循环体调用的函数不复杂且循环的次数不高时，LLVM编译器会把它们都拿出来进行分解和展开。

ARM64: Optimizing GCC 4.9.1

指令清单14.7　Optimizing GCC 4.9.1

printing_function:
; prepare second argument of printf():
        mov      w1, w0
; load address of the "f(%d)\n" string
        adrp     x0, .LC0
        add      x0, x0, :lo12:.LC0
; just branch here instead of branch with link and return:
        b        printf
main:
; save FP and LR in the local stack:
        stp      x29, x30, [sp, -32]!
; set up stack frame:
        add      x29, sp, 0
; save contents of X19 register in the local stack:
        str      x19, [sp,16]
; we will use W19 register as counter.
; set initial value of 2 to it:
        mov      w19, 2
.L3:
; prepare first argument of printing_function():
        mov      w0, w19
; increment counter register.
        add      w19, w19, 1
; W0 here still holds value of counter value before increment.
        bl       printing_function
; is it end?
        cmp      w19, 10
; no, jump to the loop body begin:
        bne      .L3
; return 0
        mov      w0, 0
; restore contents of X19 register:
        ldr      x19, [sp,16]
; restore FP and LR values:
        ldp      x29, x30, [sp], 32
        ret
.LC0:
        .string "f(%d)\n"

ARM64: Non-optimizing GCC 4.9.1

指令清单14.8　Non-optimizing GCC 4.9.1

printing_function:
; prepare second argument of printf():
        mov      w1, w0
; load address of the "f(%d)\n" string
        adrp     x0, .LC0
        add      x0, x0, :lo12:.LC0
; just branch here instead of branch with link and return:
        b        printf
main:
; save FP and LR in the local stack:
        stp      x29, x30, [sp, -32]!
; set up stack frame:
        add      x29, sp, 0
; save contents of X19 register in the local stack:
        str      x19, [sp,16]
; we will use W19 register as counter.
; set initial value of 2 to it:
        mov      w19, 2
.L3:
; prepare first argument of printing_function():
        mov      w0, w19
; increment counter register.
        add      w19, w19, 1
; W0 here still holds value of counter value before increment.
        bl       printing_function
; is it end?
        cmp      w19, 10
; no, jump to the loop body begin:
        bne      .L3
; return 0
        mov      w0, 0
; restore contents of X19 register:
        ldr      x19, [sp,16]
; restore FP and LR values:
        ldp      x29, x30, [sp], 32
        ret
.LC0:
        .string "f(%d)\n"

14.1.5　MIPS

指令清单14.9　Non-optimizing GCC 4.4.5 (IDA)

main:

; IDA is not aware of variable names in local stack
; We gave them names manually:
i               = -0x10
saved_FP        = -8
saved_RA        = -4

; function prologue:
                addiu    $sp, -0x28
                sw       $ra, 0x28+saved_RA($sp)
                sw       $fp, 0x28+saved_FP($sp)
                move     $fp, $sp
; initialize counter at 2 and store this value in local stack
                li       $v0, 2
                sw       $v0, 0x28+i($fp)
; pseudoinstruction. "BEQ $ZERO, $ZERO, loc_9C" there in fact:
                b        loc_9C
                or       $at, $zero ; branch delay slot, NOP
# ---------------------------------------------------------------------------
loc_80:                                  # CODE XREF: main+48
; load counter value from local stack and call printing_function():
                lw       $a0, 0x28+i($fp)
                jal      printing_function
                or       $at, $zero ; branch delay slot, NOP
; load counter, increment it, store it back:
                lw       $v0, 0x28+i($fp)
                or       $at, $zero ; NOP
                addiu    $v0, 1
                sw       $v0, 0x28+i($fp)
loc_9C:                                  # CODE XREF: main+18
; check counter, is it 10?
                lw       $v0, 0x28+i($fp)
                or       $at, $zero ; NOP
                slti     $v0, 0xA
; if it is less than 10, jump to loc_80 (loop body begin):
                bnez     $v0, loc_80
                or       $at, $zero ; branch delay slot, NOP
; finishing, return 0:
                move     $v0, $zero
; function epilogue:
                move     $sp, $fp
                lw       $ra, 0x28+saved_RA($sp)
                lw       $fp, 0x28+saved_FP($sp)
                addiu    $sp, 0x28
                jr       $ra
                or       $at, $zero ; branch delay slot, NOP

上述代码中的“B”指令属于IDA生成的伪指令。它原本的指令是BEQ。

14.1.6　其他

您可能注意到了，在对循环控制变量i进行初始化赋值之后，程序首先检查变量i是否满足循环条件。在满足循环表达式的情况下，才会运行循环体的指令。这恐怕是有意而为之。如果循环变量的初始值不能满足循环条件，那么整个循环体都不会被执行。例如，下面例子中的循环体就可能不会被执行：

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

如果变量total_entries_to_process等于零，就不应当执行循环体。所以，循环控制语句有必要首先检查控制变量的初始值是否满足循环条件。

在启用优化选项之后，如果编译器能够判断循环语句的初始状态不可能满足循环体的执行条件，那么它可能根本不会生成该循环体的条件检测指令和循环体的对应指令。在启用编译器的优化功能之后，在编译本例的源程序时，Keil、Xcode（LLVM）、MSVC都能做到这种程度的优化。

14.2　内存块复制

目前的计算机多数采取了SIMD和矢量化等技术，在内存中复制数据时，能够在每次迭代中复制4～8个字节。本节将从一个尽可能简单的例子开始演示。

#include <stdio.h>

void my_memcpy (unsigned char* dst, unsigned char* src, size_t cnt)
{
         size_t i;
         for (i=0; i<cnt; i++)
                  dst[i]=src[i];
};

14.2.1　编译结果

指令清单14.10　GCC 4.9 x64 optimized for size (-Os)

my_memcpy:
; RDI = destination address
; RSI = source address
; RDX = size of block

; initialize counter (i) at 0
          xor     eax, eax
.L2:
; all bytes copied? exit then:
          cmp     rax, rdx
          je      .L5
; load byte at RSI+i:
          mov     cl, BYTE PTR [rsi+rax]
; store byte at RDI+i:
          mov     BYTE PTR [rdi+rax], cl
          inc     rax ; i++
          jmp     .L2
.L5:
          ret

指令清单14.11　GCC 4.9 ARM64 optimized for size (-Os)

my_memcpy:
; X0 = destination address
; X1 = source address
; X2 = size of block

; initialize counter (i) at 0
          mov      x3, 0
.L2:
; all bytes copied? exit then:
          cmp      x3, x2
          beq      .L5
; load byte at X1+i:
          ldrb    w4, [x1,x3]
; store byte at X1+i:
          strb    w4, [x0,x3]
          add     x3,x3,1         ; i++
          b       .L2
.L5:
          ret

指令清单14.12　Optimizing Keil 6/2013 (Thumb mode)

my_memcpy PROC
; R0 = destination address
; R1 = source address
; R2 = size of block

        PUSH      {r4, lr}
; initialize counter (i) at 0
        MOVS      r3,#0
; condition checked at the end of function, so jump there:
        B         |L0.12|
|L0.6|
; load byte at R1+i:
        LDRB      r4,[r1,r3]
; store byte at R1+i:
        STRB      r4,[r0,r3]
; i++
        ADDS      r3,r3,#1
|L0.12|
; i<size?
        CMP       r3,r2
; jump to the loop begin if its so:'
        BCC       |L0.6|
        POP       {r4,pc}
        ENDP

14.2.2　编译为ARM模式的程序

在编辑ARM模式的应用程序时，Keil能够充分利用相应指令集的条件执行指令：

指令清单14.13　Optimizing Keil 6/2013 (ARM mode)

my_memcpy PROC
; R0 = destination address
; R1 = source address
; R2 = size of block

; initialize counter (i) at 0
        MOV     r3,#0
|L0.4|
; all bytes copied?
        CMP     r3,r2
; the following block is executed only if "less than" condition,
; i.e., if R2<R3 or i<size.
; load byte at R1+i:
        LDRBCC  r12,[r1,r3]
; store byte at R1+i:
        STRBCC  r12,[r0,r3]
; i++
        ADDCC   r3,r3,#1
; the last instruction of the "conditional block".
; jump to loop begin if i<size
; do nothing otherwise (i.e., if i>=size)
        BCC     |L0.4|
; return
        BX      lr
        ENDP

32位的ARM程序只用了一个转移指令，比thumb程序少了一个转移指令。

14.2.3　MIPS

指令清单14.14　GCC 4.4.5 optimized for size (-Os) (IDA)

my_memcpy:
; jump to loop check part:
                   b         loc_14
; initialize counter (i) at 0
; it will always reside in \$v0:
                   move     $v0, $zero ; branch delay slot

loc_8:                                          # CODE XREF: my_memcpy+1C
; load byte as unsigned at address in $t0 to $v1:
                   lbu      $v1, 0($t0)
; increment counter (i):
                   addiu    $v0, 1
; store byte at $a3
                   sb       $v1, 0($a3)

loc_14:                                        # CODE XREF: my_memcpy
; check if counter (i) in $v0 is still less then 3rd function argument ("cnt" in $a2):
                   sltu     $v1, $v0, $a2
; form address of byte in source block:
                   addu     $t0, $a1, $v0
; $t0 = $a1+$v0 = src+i
; jump to loop body if counter sill less then "cnt":
                   bnez     $v1, loc_8
; form address of byte in destination block (\$a3 = \$a0+\$v0 = dst+i):
                   addu     $a3, $a0, $v0 ; branch delay slot
; finish if BNEZ wasnt triggered:'
                   jr       $ra
                   or       $at, $zero ; branch delay slot, NOP

这里序言介绍的两个指令分别是LBU（Load Byte Unsigned）和SB（Store Byte）。与ARM模式的程序相似，所有的MIPS寄存器都是32位寄存器。在这两个平台上，我们无法像x86平台上那样直接对寄存器的低位进行直接操作。因此在对单个字节进行操作时，我们还是得访问整个32位寄存器。LBU指令加载字节型数据并且会清除寄存器的其他位。另外由于有符号数据存在符号位的问题，所以在处理有符号数的时候还要使用LB（Load Byte）指令把单字节的有符号数据扩展为等值的32位数据、再存储在寄存器里。SB指令的作用是把寄存器里的低8位复制到内存里。

14.2.4　矢量化技术

25.1.2节会详细介绍GCC的优化编译方式基于矢量化技术（Vectorization）的。

14.3　总结

当循环控制变量从2递增到9时，循环语句对应的汇编代码大体如下。

指令清单14.15　x86

    mov [counter], 2 ; initialization
    jmp check
body:
    ; loop body
    ; do something here
    ; use counter variable in local stack
    add [counter], 1 ; increment
check:
    cmp [counter], 9
    jle body

如果没有开启编译器的优化编译功能，那么控制变量的递增操作可能对应着3条汇编指令。

指令清单14.16　x86

    MOV [counter], 2 ; initialization
    JMP check
body:
    ; loop body
    ; do something here
    ; use counter variable in local stack
    MOV REG, [counter] ; increment
    INC REG
    MOV [counter], REG
check:
    CMP [counter], 9
    JLE body

当循环体较为短小时，编译器可能给循环控制变量分配专用的寄存器。

指令清单14.17　x86

    MOV EBX, 2 ; initialization
    JMP check
body:
    ; loop body
    ; do something here
    ; use counter in EBX, but do not modify it!
    INC EBX ; increment
check:
    CMP EBX, 9
    JLE body

编译器还可能调换部分指令的顺序。

指令清单14.18　x86

    MOV [counter], 2 ; initialization
    JMP label_check
label_increment:
    ADD [counter], 1 ; increment
label_check:
    CMP [counter], 10
    JGE exit
    ; loop body
    ; do something here
    ; use counter variable in local stack
    JMP label_increment
exit:

通常情况下，程序应当首先判断循环条件是否满足，然后再执行循环体。但是在编译器确定第一次迭代肯定会发生的情况下，它可能会调换循环体和判断语句的顺序。下面这个程序就是个典型的例子。

指令清单14.19　x86

    MOV REG, 2 ; initialization
body:
    ; loop body
    ; do something here
    ; use counter in REG, but do not modify it!
    INC REG ; increment
    CMP REG, 10
    JL body

编译器不会使用LOOP指令。由LOOP控制的循环控制语句比较少见。如果某段代码带有LOOP指令，那么您应当认为这是手写出来的汇编程序。

指令清单14.20　x86

    ; count from 10 to 1
    MOV ECX, 10
body:
    ; loop body
    ; do something here
    ; use counter in ECX, but do not modify it!
    LOOP body

ARM平台的R4寄存器专门用于存储循环控制变量。

指令清单14.21　ARM

    MOV R4, 2 ; initialization
    B check
body:
    ; loop body
    ; do something here
    ; use counter in R4, but do not modify it!
    ADD R4,R4, #1 ; increment
check:
    CMP R4, #10
    BLT body

14.4　练习题

14.4.1　题目1

为什么现在的编译器不再使用LOOP指令了？

14.4.2　题目2

使用您喜欢的操作系统和编译器编译14.1.1节的样本程序，然后修改这个可执行程序，使循环变量i可以从6递增到20。

14.4.3　题目3

请描述下述代码的功能。

指令清单14.22　由MSVC 2010 （启用/Ox选项）编译而得的代码

$SG2795 DB      '%d', 0aH, 00H

_main   PROC
        push   esi
        push   edi
        mov    edi,  DWORD PTR __imp__printf
        mov    esi, 100
        npad   3; align next label
$LL3@main:
        push   esi
        push   OFFSET $SG2795   ; '%d'
        call   edi
        dec    esi
        add    esp,  8
        test   esi,  esi
        jg     SHORT $LL3@main
        pop    edi
        xor    eax, eax
        pop    esi
        ret0
_main   ENDP

指令清单14.23　Non-optimizing Keil 6/2013 (ARM mode)

main PROC
         PUSH     {r4,lr}
         MOV      r4,#0x64
|L0.8|
         MOV      r1,r4
         ADR      r0,|L0.40|
         BL       __2printf
         SUB      r4,r4,#1
         CMP      r4,#0
         MOVLE    r0,#0
         BGT      |L0.8|
         POP      {r4,pc}
         ENDP

|L0.40|
         DCB      "%d\n",0

指令清单14.24　Non-optimizing Keil 6/2013 (Thumb mode)

main PROC
         PUSH     {r4,lr}
         MOVS     r4,#0x64
|L0.40|
         MOVS     r1,r4
         ADR      r0,|L0.24|
         BL       __2printf
         SUBS     r4,r4,#1
         CMP      r4,#0
         BGT      |L0.4|
         MOVS     r0,#0
         POP      {r4,pc}
         ENDP

         DCW      0x0000
|L0.24|
         DCB      "%d\n",0

指令清单14.25　Optimizing GCC 4.9 (ARM64)

main:
         stp      x29, x30, [sp, -32]!
         add      x29, sp, 0
         stp      x19, x20, [sp,16]
         adrp     x20, .LC0
         mov      w19, 100
         add      x20, x20, :lo12:.LC0
.L2:
         mov      w1, w19
         mov      x0, x20
         bl       printf
         subs     w19, w19, #1
         bne      .L2
         ldp      x19, x20, [sp,16]
         ldp      x29, x30, [sp], 32
         ret
.LC0:
         .string  "%d\n"

指令清单14.26　Optimizing GCC 4.4.5 (MIPS) (IDA)

main:

var_18           = -0x18
var_C            = -0xC
var_8            = -8
var_4            = -4

                 lui       $gp, (__gnu_local_gp >> 16)
                 addiu     $sp, -0x28
                 la        $gp, (__gnu_local_gp & 0xFFFF)
                 sw        $ra, 0x28+var_4($sp)
                 sw        $s1, 0x28+var_8($sp)
                 sw        $s0, 0x28+var_C($sp)
                 sw        $gp, 0x28+var_18($sp)
                 la        $s1, $LC0        # "%d\n"
                 li        $s0, 0x64  # 'd'

loc_28:                                  # CODE XREF: main+40
                 lw        $t9, (printf & 0xFFFF)($gp)
                 move      $a1, $s0
                 move      $a0, $s1
                 jalr      $t9
                 addiu     $s0, -1
                 lw        $gp, 0x28+var_18($sp)
                 bnez      $s0, loc_28
                 or        $at, $zero
                 lw        $ra, 0x28+var_4($sp)
                 lw        $s1, 0x28+var_8($sp)
                 lw        $s0, 0x28+var_C($sp)
                 jr        $ra
                 addiu     $sp, 0x28

$LC0:            .ascii "%d\n"<0>        # DATA XREF: main+1C

14.4.4　题目4

请描述下述代码的功能。

指令清单14.27　Optimizing MSVC 2010

$SG2795 DB      '%d', 0aH, 00H

_main   PROC
        push     esi
        push     edi
        mov      edi, DWORD PTR __imp_printf
        mov      esi, 1
        npad     3; align next label
$LL3@main:
        push     esi
        push     OFFSET $SG2795  ; '%d'
        call     edi
        add      esi, 3
        add      esp, 8
        cmp      esi, 100
        jl       SHORT $LL3@main
        pop      edi
        xor      eax, eax
        pop      esi
        ret      0
_main   ENDP

指令清单14.28　Non-optimizing Keil 6/2013 (ARM mode)

main  PROC
         PUSH    {r4,lr}
         MOV     r4,#1
|L0.8|
         MOV     r1,r4
         ADR     r0,|L0.40|
         BL      __2printf
         ADD     r4,r4,#3
         CMP     r4,#0x64
         MOVGE   r0,#0
         BLT     |L0.8|
         POP     {r4,pc}
         ENDP

|L0.40|
         DCB     "%d\n",0

指令清单14.29　Non-optimizing Keil 6/2013 (Thumb mode)

main  PROC
         PUSH    {r4,lr}
         MOVS    r4,#1
|L0.4|
         MOVS    r1,r4
         ADR     r0,|L0.24|
         BL      __2printf
         ADDS    r4,r4,#3
         CMP     r4,#0x64
         BLT     |L0.4|
         MOVS    r0, #0
         POP     {r4,pc}
         ENDP

         DCW     0x0000
|L0.40|
         DCB     "%d\n",0

指令清单14.30　Optimizing GCC 4.9 (ARM64)

main:
         stp     x29, x30, [sp, -32]!
         add     x29, sp, 0
         stp     x19, x20, [sp,16]
         adrp    x20, .LC0
         mov     w19, 1
         add     x20, x20, :lo12:.LC0
.L2:
         mov     w1, w19
         mov     x0, x20
         add     w19, w19, 3
         bl      printf
         cmp     w19, 100
         bne     .L2
         ldp     x19, x20, [sp,16]
         ldp     x29, x30, [sp], 32
         ret
.LC0:
         .string "%d\n"

指令清单14.31　Optimizing GCC 4.4.5 (MIPS) (IDA)

main:

var_18     = -0x18
var_10     = -0x10
var_C      = -0xC
var_8      = -8
var_4      = -4

           lui       $gp, (__gnu_local_gp >> 16)
           addiu     $sp, -0x28
           la        $gp, (__gnu_local_gp & 0xFFFF)
           sw        $ra, 0x28+var_4($sp)
           sw        $s2, 0x28+var_8($sp)
           sw        $s1, 0x28+var_C($sp)
           sw        $s0, 0x28+var_10($sp)
           sw        $gp, 0x28+var_18($sp)
           la        $s2, $LC0        # "%d\n"
           li        $s0, 1
           li        $s1, 0x64  # 'd'
loc_30:                                 # CODE XREF: main+48
           lw        $t9, (printf & 0xFFFF)($gp)
           move      $a1, $s0
           move      $a0, $s2
           jalr      $t9
           addiu     $s0, 3
           lw        $gp, 0x28+var_18($sp)
           bne       $s0, $s1, loc_30
           or        $at, $zero
           lw        $ra, 0x28+var_4($sp)
           lw        $s2, 0x28+var_8($sp)
           lw        $s1, 0x28+var_C($sp)
           lw        $s0, 0x28+var_10($sp)
           jr        $ra
           addiu     $sp, 0x28

$LC0:      .ascii    "%d\n"<0>           # DATA XREF: main+20

[1]　请参阅Dre07，及Int14, 3.4.1.7节。

[2]　如果您有兴趣，可以到作者的网站上下载。http://yurichev.com/tracer-en.html。

第15章 C语言字符串的函数

15.1　strlen()

本章是循环控制语句的具体应用。通常，strlen()函数由循环语句while()语句实现。参照MSVC标准库对strlen()函数的定义方法，我们讨论下述的程序：

int my_strlen (const char * str)
{
        const char *eos = str;

        while( *eos++ ) ;

        return( eos - str - 1 );
}

int main()
{
        // test
        return my_strlen("hello!");
};

15.1.1　x86

Non-optimizing MSVC

使用MSVC 2010编译上述程序可得到：

_eos$ = -4                            ; size = 4
_str$ = 8                             ; size = 4
_strlen PROC
    push    ebp
    mov     ebp, esp
    push    ecx
    mov     eax, DWORD PTR _str$[ebp] ; 将指针指向字符串
    mov     DWORD PTR _eos$[ebp], eax ; 指向局部变量eos
$LN2@strlen_:
    mov     ecx, DWORD PTR _eos$[ebp] ; ECX=eos

    ; take 8-bit byte from address in ECX and place it as 32-bit value to EDX with sign extension

    movsx   edx, BYTE PTR [ecx]
    mov     eax, DWORD PTR _eos$[ebp] ; EAX=eos
    add     eax, 1                    ; EAX++
    mov     DWORD PTR _eos$[ebp], eax ; EAX还原为EOS
    test    edx, edx                  ; EDX = 0 ?
    je      SHORT $LN1@strlen_        ; yes, then finish loop
    jmp     SHORT $LN2@strlen_        ; continue loop
$LN1@strlen_:
    ; here we calculate the difference between two pointers

    mov    eax, DWORD PTR _eos$[ebp]
    sub    eax, DWORD PTR _str$[ebp]
    sub    eax, 1                     ; subtract 1 and return result
    mov    esp, ebp
    pop    ebp
    ret    0
_strlen_ ENDP

这里出现了两个新指令：MOVSX和TEST。

此处，MOVSX指令从内存中读取8位（单字节）数据，并把它存储到32位寄存器里。MOVSX是MOV with Sign-Extend的缩写。在把小空间数据转换为大空间数据时，存在填充高位数据的问题；本例中，MOVSX将用原始数据的8位数据填充EDX寄存器的低8位；如果原始数据是负数，该指令将使用1填充第8到第31位（高24位），否则使用0填充高24位。

这是为了保证有符号型数据在类型转换后的数值保持不变。

根据C/C++标准，char类型数据是signed（有符号型）数据。现在设想一下把char型数据转换为int型数据（都是有符号型数据）的情况：假如char型数据的原始值是−2（0xFE），直接把整个字节复制到int型数据的最低8位上时，int型数据的值就变成0x000000FE，以有符号型数据的角度看它被转换为254了，而没有保持原始值−2。−2对应的int型数据是0xFFFFFFFE。所以，在把原始数据复制到目标变量之后，还要使用符号标志位填充剩余的数据，而这就是MOVSX的功能。

本书第30章会更为详细地介绍有符号型数据的表示方法。

虽然不太确定寄存器是否有必要分配EDX寄存器专门保存char型数据，看上去它只使用了寄存器的低8位空间（相当于DL）。显然，编译器根据自身的寄存器分配规则进行了相应分配。

您将在后面看到“TEST EDX，EDX”指令。本书会在第19章详细介绍TEST指令。在本例中，它的功能是检查EDX的值是否是零，并设置相应的标志位。

Non-optimizing GCC

使用GCC 4.4.1（未启用优化选项）编译上述程序，可得到：

                 public strlen
strlen           proc near

eos              = dword ptr -4
arg_0            = dword ptr  8

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

GCC编译的结果和MSVC差不多。这里它没有使用MOVSX指令，而是用了MOVZX指令。MOVZX是MOV with Zero-Extent的缩写。在将8位或16位数据转换为32位数据的时候，它直接复制原始数据到目标寄存器的相应低位，并且使用0填充剩余的高位。拆文解字的角度来看，这条指令相当于一步完成了“xor eax, eax”和“mov al，[源8/16位数据]”2条指令。

另一方面，编译器可以生成“mov al, byte ptr [eax] / test al, al”这样的代码，但是这样一来EAX寄存器的高位将会存在随机的噪声。不如说，这就是编译器的短板所在——在它生成汇编代码的时候，它不会照顾汇编代码的（人类）可读性。严格说来，编译器编译出来的代码本来就是给机器运行的，而不是给人阅读的。

本例还出现了未介绍过的SETNZ指令。从前面一条指令开始解释：如果AL的值不是0，则“test al, al”指令会设置标志寄存器ZF=0；而SETNZ（Not Zero）指令会在ZF＝0的时候，设置AL＝1。用白话解说，就是：如果AL不等于0，则跳到loc_80483F0处。编译器转译出来的代码中，有些代码确实没有实际意义，这是因为我们没有开启优化选项。

Optimizing MSVC

使用MSVC（启用优化选项/Ox /Ob0）编译上述程序，可得到如下所示的指令。

指令清单15.1　Optimizing MSVC 2012 /Ob0

_str$ = 8                                     ; size = 4
_strlen PROC
        mov     edx, DWORD PTR _str$[esp-4]   ;用EDX作字符串指针
        mov     eax, edx                      ;  复制到 EAX

$LL2@strlen:
        mov     cl, BYTE PTR [eax]            ; CL = *EAX
        inc     eax                           ; EAX++
        test    cl, cl                        ; CL==0?
        jne     SHORT $LL2@strlen             ; no, continue loop
        sub     eax, edx                      ; 计算指针的变化量
        dec     eax                           ; decrement EAX
        ret     0
_strlen ENDP

优化编译生成的程序短了很多。不必多说，只有在函数较短且局部变量较少的情况下，编译器才会做出这种程度的优化。

inc/dec指令就是递增、递减指令，换句话说它们相当于运算符“＋＋”“−−”。

Optimizing MSVC + OllyDbg

我们使用OllyDbg打开MSVC优化编译后的可执行文件。程序在进行第一次迭代时的情况如图15.1所示。

图15.1　OllyDbg：第一次迭代

OllyDbg识别出了循环结构，并且用方括号把整组循环体指令标示了出来。如需OllyDbg在内存窗口里显示对应的数据，可用右键单击EAX寄存器，然后选择“Follow in Dump”，OllyDbg将自动滚动到对应的地址。在内存数据的显示窗口里，我们可以看到字符串“hello！”。字符串会用数值为零的字节做结尾，在零之后的数据就是随机的噪音数据。如果OllyDbg发现某个寄存器的值是指向这片内存地址（某处）的指针，它就会把对应的字符串提示出来。

然后不停地按F8键，等待循环体进入下一次循环。

如图15.2所示，EAX寄存器的值是个指针，它指向字符串的第二个字符的地址。

图15.2　OllyDbg：第二次迭代

继续按F8键，等待循环语句执行完毕。

如图15.3所示，EAX寄存器里的指针最终指向了数值为零的字符串终止字符。在循环过程中，EDX寄存器的值始终没有发生变化，它一直是字符串首地址的指针。在循环语句结束后，程序即将计算两个指针的差值。

图15.3　OllyDbg：即将计算指针（地址）之间的差值

把这两个寄存器的值（指针）相减，再减去1，就可得到字符串的长度，如图15.4所示。

图15.4　OllyDbg：EAX递减

经计算，指针之间的差值为7。实际上我们的字符串“hello！”只有6个字节，算上结束标志字节才是7个字节。很明显字符串以外的那个数值为零的字节不应当纳入字符串的长度，所以函数最后做了递减运算，把这个字节去掉。

Optimizing GCC

使用GCC 4.4.1编译（启用优化选项“－O3”）上述程序，可得到：

                public strlen
strlen          proc near

arg_0           = dword ptr  8

                push    ebp
                mov     ebp, esp
                mov     ecx, [ebp+arg_0]
                mov     eax, ecx

loc_8048418:
                movzx   edx, byte ptr [eax]
                add     eax, 1
                test    dl, dl
                jnz     short loc_8048418
                not     ecx
                add     eax, ecx
                pop     ebp
                retn
strlen          endp

GCC编译的结果和MSVC差不多，主要区别体现在MOVZX指令上。

即使把这条MOVZX指令替换为“mov dl, byte ptr [eax]”也没问题。

GCC编译器的代码生成器这样做，或许是为了便于“记住”整个寄存器已经分配给了char型变量，以保证寄存器的高地址位（bits）不会含有噪音数据。

接下来将要介绍的是NOT指令。NOT指令对操作数的所有位（bit）都进行非运算。可以说，这条指令和XOR ECX, 0xffffffffh指令等价。“not ecx”的结果与某数相加，相当于某数减去ECX和1。在程序开始的时候，ECX保存了字符串首个字符的地址（指针），EAX寄存器存储的是终止符的地址。对ECX求非、再与eax相加，就是在计算eax−ecx−1的值。这种运算可以得到正确的字符串长度。

其中的数学问题，请参见第30章。

换句话说，在执行完循环语句之后，函数进行了如下操作：

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

它得到的结果与下述运算完全相同：

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

GCC编译器这么编译代码的具体原因不明。我们能够确定的是，即使GCC和MSVC分别选择不同的算法，计算的结果肯定相同。

15.1.2　ARM

32bit ARM

Non-optimizing Xcode 4.6.3 (LLVM) (ARM mode)

指令清单15.2　Non-optimizing Xcode 4.6.3 (LLVM) (ARM mode)

_strlen
eos  =-8
str  =-4

     SUB    SP, SP, #8 ; allocate 8 bytes for local variables
     STR    R0, [SP,#8+str]
     LDR    R0, [SP,#8+str]
     STR    R0, [SP,#8+eos]

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

如果不指定优化选项，LLVM编译器所生成的代码会很啰唆。但是这种冗长的代码有助于我们理解它处理局部变量所用的栈结构。本例只用到了2个局部变量，即eos和str。

在IDA的反编译结果中，我把变量var_8重命名为原始变量名eos，把var_4重命名为str。

程序的前几条指令把输入值传递给变量str和eos。

循环体的起始地址是loc_2CB8。

循环体内的前3条指令（LDR，ADD，STR）把eos的值保存在R0寄存器里，然后将这个值递增（＋1），再把修改后的值直接赋值给栈内的局部变量eos。

接着“LDRSB RO,[R0]” （Load Register Signed Byte）指令从R0所指向的地址处读取1个字节，并将之转换为32位有符号数据（Keil也把Char型数据视为有符号数据）。这条指令类似于x86的MOVSX指令（参见15.1.1节）。既然在C标准里char类型数据是Signed类型数据，编译器就把这个值当作Signed型数据处理。

应当注意的是，虽然x86系统可以把一个32位寄存器拆分为8位寄存器或16位寄存器单独调用，但是ARM系统没有把寄存器拆解出来、分别使用的助记符。确切地说，x86的这种特性是兼容性的要求：为了运行16位8086指令甚至是8位8080指令，它的指令集必须向下兼容。而ARM系统的处理器最初就是32位RISC处理器。所以，在使用ARM系统的寄存器时，只能把它当作完整的32位寄存器使用。

此后，LDRSB指令把字符串中的字符逐字节地传递到R0寄存器。其后的CMP和BEQ指令，会检查这个字节是不是零字节。如果这个字节不是0，则再次进行循环；如果这个字节是0，则结束循环语句。

函数在结束以前计算eos和str的差值，再把这个差值减去1，然后作为函数的返回值、保存在R0寄存器里。

整个函数没有把寄存器推送入栈的操作。按照ARM调用函数的约定，R0～R3寄存器的作用是传递参数的暂存寄存器；函数在退出以后它们已经完成使命了，也不必恢复它们的初始值。在被调用方函数结束之后，再怎么操作它们都行。另外，这个程序没有用到其他的寄存器，也没必要使用栈。所以在函数结束时，唯一需要做的工作就是使用跳转指令（BX）把控制权交给LR寄存器保存的返回地址。

Optimizing Xcode 4.6.3 (LLVM) (Thumb mode)

指令清单15.3　Optimizing Xcode 4.6.3 (LLVM) (Thumb mode)

_strlen
           MOV       R1, R0

loc_2DF6
           LDRB.W    R2, [R1],#1
           CMP       R2, #0
           BNE       loc_2DF6
           MVNS      R0, R0
           ADD       R0, R1
           BX        LR

在进行优化编译的时候，编译器认为寄存器已经足够用了，不必使用栈来保管变量eos和str。在开始循环之前，编译器使用R0寄存器存储变量str、用R1寄存器存储变量eos。

“LDRB.W R2, [R1],#1”从R1所指向的地址里读取1个字节，将其转换为32位有符号数据（signed）之后存储在R2寄存器里。指令末端的立即数＃1将在上述操作之后，把R1寄存器的值加1（递增）。这种指令属于延迟索引寻址（Post-indexed address，又称后变址寻址）指令，便于操作数组。

本书的28.2节会详细介绍延迟索引寻址。

循环体的CMP和BNE指令负责判断循环结束的条件。它们在读取到数值为零的字节之前会保证循环语句处于的迭代状态。

MVNS（MoVe Not）指令相当于x86指令集中的NOT指令，与后面的ADD指令配合完成“eos-str-1”的运算。在15.1.1节里，我们详细介绍过其中的各种细节，这里不再复述。

LLVM编译器与GCC都认为这种代码的效率更高。

Optimizing Keil 6/2013 (ARM mode)

使用Keil（启用优化选项）编译上述程序，可得到如下所示的指令。

指令清单15.4　Optimizing Keil 6/2013 (ARM mode)

_strlen
           MOV   R1, R0

loc_2C8
           LDRB  R2, [R1],#1
           CMP   R2, #0
           SUBEQ R0, R1, R0
           SUBEQ R0, R0, #1
           BNE   loc_2C8
           BX    LR

这段代码与前面的LLVM优化编译生成的Thumb代码相似。区别在于前面的例子在循环结束后才运算str−eos−1，而本例则是在循环体内进行表达式演算。前文介绍过，条件执行指令中的－EQ后缀表示其运行条件为“当前面的CMP比较的两个值相等/EQ时，才会执行该指令”。所以，如果R0寄存器的值是0，则会触发CMP指令之后的两条SUBEQ指令。其运算结果会被保留在R0寄存器，迭代结束之后就自然成为函数的返回值。

ARM64

Optimizing GCC (Linaro) 4.9

my_strlen:
        mov     x1, x0
        ; X1 is now temporary pointer (eos), acting like cursor
.L58:
        ; load byte from X1 to W2, increment X1 (post-index)
        ldrb    w2, [x1],1
        ; Compare and Branch if NonZero: compare W2 with 0, jump to .L58 if it is not
        cbnz    w2, .L58
        ; calculate difference between initial pointer in X0 and current address in X1
        sub     x0, x1, x0
        ; decrement lowest 32-bit of result
        sub     w0, w0, #1
        ret

本例的算法与指令清单15.11的算法相同。它计算首字符和终止符地址的差值，然后再从差里减去1。指令清单中的注释详细介绍了具体的演算方法。不过，我的源程序有问题：my_strlen()的返回值是32位int型数据，但是它应当返回size_t或者另外一种64位数据。理论上讲，在64位平台上，strlen()函数的参数地址可能是4GB以上的内存地址，所以它在64位平台上的返回值应当是64位数据。由于源程序存在这个缺陷，所以最后一条SUB指令只对寄存器的低32位进行操作，但是倒数第二条SUB指令依然是对完整的64位寄存器进行减法运算。虽说程序存在缺陷，但是出于演示的目的，我还是把这个样本保留了下来。

Non-optimizing GCC (Linaro) 4.9

my_strlen:
; function prologue
        sub      sp, sp, #32
; first argument (str) will be stored in [sp,8]
        str      x0, [sp,8]
        ldr      x0, [sp,8]
; copy "str" to "eos" variable
        str      x0, [sp,24]
        nop
.L62:
; eos++
        ldr      x0, [sp,24]  ; load "eos" to X0
        add      x1, x0, 1    ; increment X0
        str      x1, [sp,24]  ; save X0 to "eos"
; load byte from memory at address in X0 to W0
        ldrb     w0, [x0]
; is it zero? (WZR is the 32-bit register always contain zero)
        cmp      w0, wzr
; jump if not zero (Branch Not Equal)
        bne      .L62
; zero byte found. now calculate difference.
; load "eos" to X1
        ldr      x1, [sp,24]
; load "str" to X0
        ldr      x0, [sp,8]
; calculate difference
        sub      x0, x1, x0
; decrement result
        sub      w0, w0, #1
; function epilogue
        add      sp, sp, 32
        ret

关闭优化功能之后，编译器生成的代码就长了许多。在操作变量时，程序频繁地访问内存中的栈。此外，由于源程序的设计缺陷，最后一条SUB指令只对寄存器的低32位数据进行了减法运算。

15.1.3　MIPS

指令清单15.5　Optimizing GCC 4.4.5 (IDA)

my_strlen:
; "eos" variable will always reside in $v1:
                move    $v1, $a0
loc_4:
; load byte at address in "eos" into $a1:
                lb      $a1, 0($v1)
                or      $at, $zero ; load delay slot, NOP
; if loaded byte is not zero, jump to loc_4:
                bnez    $a1, loc_4
; increment "eos" anyway:
                addiu   $v1, 1 ; branch delay slot
; loop finished. invert "str" variable:
                nor     $v0, $zero, $a0
; $v0=-str-1
                jr      $ra
; return value = $v1 + $v0 = eos + ( -str-1 ) = eos - str - 1
                addu    $v0, $v1, $v0 ; branch delay slot

MIPS的指令集里没有求非（NOT）运算指令，但是它有非或NOR（即OR+NOT）指令。在数字电路领域，非或运算电路十分普遍；但是在计算机编程领域，它还比较冷门。借助非或运算指令，求非的NOT运算指令可由“NOR DST,$ZERO,SRC”指令变相实现。

前文介绍过，求非运算相当于“求负、再减1”的运算。与后面的ADDU指令配合，可完成“eos−str−1”的运算，求得正确的字符串长度。

15.2　练习题

15.2.1　题目1

请描述下列代码的功能。

指令清单15.6　Optimizing MSVC 2010

_s$   =  8
_f    PROC
           mov  edx, DWORD PTR _s$[esp-4]
           mov  cl, BYTE PTR [edx]
           xor  eax, eax
           test cl, cl
           je   SHORT $LN2@f
           npad 4    ; align next label
$LL4@f:
           cmp  cl, 32
           jne  SHORT $LN3@f
           inc  eax
$LN3@f:
           mov  cl, BYTE PTR [edx+1]
           inc  edx
           test cl, cl
           jne  SHORT $LL4@f
$LN2@f:
           ret  0
_f         ENDP

指令清单15.7　GCC 4.8.1 -O3

f:
.LFB24:
        push      ebx
        mov       ecx, DWORD PTR [esp+8]
        xor       eax, eax
        movzx     edx, BYTE PTR [ecx]
        test      dl, dl
        je        .L2
.L3:
        cmp       dl, 32
        lea       ebx, [eax+1]
        cmove     eax, ebx
        add       ecx, 1
        movzx     edx, BYTE PTR [ecx]
        test      dl, dl
        jne       .L3
.L2:
        pop       ebx
        ret

指令清单15.8　Optimizing Keil 6/2013 (ARM mode)

f   PROC
        MOV     r1,#0
|L0.4|
        LDRB    r2,[r0,#0]
        CMP     r2,#0
        MOVEQ   r0,r1
        BXEQ    lr
        CMP     r2,#0x20
        ADDEQ   r1,r1,#1
        ADD     r0,r0,#1
        B       |L0.4|
        ENDP

指令清单15.9　Optimizing Keil 6/2013 (Thumb mode)

f PROC
        MOVS    r1,#0
        B       |L0.12|
|L0.4|
        CMP     r2,#0x20
        BNE     |L0.10|
        ADDS    r1,r1,#1
|L0.10|
        ADDS    r0,r0,#1
|L0.12|
        LDRB    r2,[r0,#0]
        CMP     r2,#0
        BNE     |L0.4|
        MOVS    r0,r1
        BX      lr
        ENDP

指令清单15.10　Optimizing GCC 4.9 (ARM64)

f:
        ldrb    w1, [x0]
        cbz     w1, .L4
        mov     w2, 0
.L3:
        cmp     w1, 32
        ldrb    w1, [x0,1]!
        csinc   w2, w2, w2, ne
        cbnz    w1, .L3
.L2:
        mov     w0, w2
        ret
.L4:
        mov     w2, w1
        b       .L2

指令清单15.11　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:
                lb         $v1, 0($a0)
                or         $at, $zero
                beqz       $v1, locret_48
                li         $a1,0x20#''
                b          loc_28
                move       $v0, $zero
loc_18:                                       # CODE XREF: f:loc_28
                lb         $v1, 0($a0)
                or         $at, $zero
                beqz       $v1, locret_40
                or         $at, $zero
loc_28:                                       # CODE XREF: f+10
                                              # f+38
                bne        $v1, $a1, loc_18
                addiu      $a0, 1
                lb         $v1, 0($a0)
                or         $at, $zero
                bnez       $v1, loc_28
                addiu      $v0, 1
locret_40:                                    # CODE XREF: f+20
                jr         $ra
                or         $at, $zero
locret_48:                                    # CODE XREF: f+8
                jr         $ra
                move       $v0, $zero

第16章 数学计算指令的替换

出于性能优化的考虑，编译器可能会将1条数学运算指令替换为其他的1条、甚至是一组等效指令。

例如LEA指令通常替代其他的简单计算指令：请参见附录A.6.2节。

ADD和SUB指令同样可以相互替换。例如，指令清单52.1的第18行就是如此。

16.1　乘法

16.1.1　替换为加法运算

我们通过下述程序进行演示。

指令清单16.1　Optimizing MSVC 2010

unsigned int f(unsigned int a)
{
         return a*8;
};

如果使用MSVC 2010（启用/Ox）进行编译，编译器会把“乘以8”的运算指令拆解为3条加法指令。很明显MSVC认为替换后的程序性能更高。

_TEXT   SEGMENT
_a$ = 8                         ; size = 4
_f      PROC
; File c:\polygon\c\2.c
        mov     eax, DWORD PTR _a$[esp-4]
        add     eax, eax
        add     eax, eax
        add     eax, eax
        ret     0
_f      ENDP
_TEXT   ENDS
END

16.1.2　替换为位移运算

编译器通常会把“乘以2”“除以2”的运算指令处理为位移运算指令：

unsigned int f(unsigned int a)
{
        return a*4;
};

指令清单16.2　Non-optimizing MSVC 2010

_a$ = 8          ; size = 4
_f       PROC
         push    ebp
         mov     ebp, esp
         mov     eax, DWORD PTR _a$[ebp]
         shl     eax, 2
         pop     ebp
         ret     0
_f       ENDP

“乘以4”的运算就是把被乘数左移2位、再把位移产生的空缺位添上0的运算。就好比在心算计算3×100的时候，我们就是在3后面的空缺位添加两个零。

位移指令的示意图如下所示。

右侧产生的空缺位都由零填充。

乘以4的ARM指令如下所示。

指令清单16.3　Non-optimizing Keil 6/2013 (ARM mode)

f  PROC
        LSL     r0,r0,#2
        BX      lr
        ENDP

对应的MIPS指令如下所示。

指令清单16.4　Optimizing GCC 4.4.5 (IDA)

                jr      $ra
                sll     $v0, $a0, 2 ; branch delay slot

其中，SLL是逻辑左移“Shift Left Logical”的缩写。

16.1.3　替换为位移、加减法的混合运算

即使乘数是7或17，乘法运算仍然可以用非乘法运算指令配合位移指令实现。其算术原理也不难推测。

32位

#include <stdint.h>

int f1(int a)
{
         return a*7;
};

int f2(int a)
{
         return a*28;
};

int f3(int a)
{
         return a*17;
};

x86

指令清单16.5　Optimizing MSVC 2012

; a*7
_a$ = 8
_f1      PROC
         mov     ecx, DWORD PTR _a$[esp-4]
; ECX=a
         lea     eax, DWORD PTR [ecx*8]
; EAX=ECX*8
         sub     eax, ecx
; EAX=EAX-ECX=ECX*8-ECX=ECX*7=a*7
         ret     0
_f1     ENDP

; a*28
_a$ = 8
_f2     PROC
         mov     ecx, DWORD PTR _a$[esp-4]
; ECX=a
         lea     eax, DWORD PTR [ecx*8]
; EAX=ECX*8
         sub     eax, ecx
; EAX=EAX-ECX=ECX*8-ECX=ECX*7=a*7
         shl     eax, 2
; EAX=EAX<<2=(a*7)*4=a*28
         ret     0
_f2     ENDP

; a*17
_a$ = 8
_f3     PROC
         mov     eax, DWORD PTR _a$[esp-4]
; EAX=a
         shl     eax, 4
; EAX=EAX<<4=EAX*16=a*16
         add     eax, DWORD PTR _a$[esp-4]
; EAX=EAX+a=a*16+a=a*17
         ret     0
_f3     ENDP

ARM

ARM指令集的位移运算指令有3个操作符，可在位移运算符后再进行一次加法运算。故而Keil生成的代码更为短小。

指令清单16.6　Optimizing Keil 6/2013 (ARM mode)

; a*7
||f1|| PROC
         RSB       r0,r0,r0,LSL #3
; R0=R0<<3-R0=R0*8-R0=a*8-a=a*7
         BX        lr
         ENDP

; a*28
||f2|| PROC
         RSB      r0,r0,r0,LSL #3
; R0=R0<<3-R0=R0*8-R0=a*8-a=a*7
         LSL      r0,r0,#2
; R0=R0<<2=R0*4=a*7*4=a*28
         BX       lr
         ENDP

; a*17
||f3|| PROC
         ADD      r0,r0,r0,LSL #4
; R0=R0+R0<<4=R0+R0*16=R0*17=a*17
         BX       lr
         ENDP

然而Thumb模式的位移指令不支持这种运算。编译器无法优化f2()。

指令清单16.7　Optimizing Keil 6/2013 (Thumb mode)

; a*7
||f1||  PROC
          LSLS      r1,r0,#3
; R1=R0<<3=a<<3=a*8
          SUBS      r0,r1,r0
; R0=R1-R0=a*8-a=a*7
          BX        lr
          ENDP

; a*28
||f2|| PROC
          MOVS      r1,#0x1c ; 28
; R1=28
          MULS      r0,r1,r0
; R0=R1*R0=28*a
          BX        lr
          ENDP

; a*17
||f3|| PROC
          LSLS      r1,r0,#4
; R1=R0<<4=R0*16=a*16
          ADDS      r0,r0,r1
; R0=R0+R1=a+a*16=a*17
          BX        lr
          ENDP

MIPS

指令清单16.8　Optimizing GCC 4.4.5 (IDA)

_f1:
                  sll      $v0, $a0, 3
; $v0 = $a0<<3 = $a0*8
                  jr       $ra
                  subu     $v0, $a0 ; branch delay slot
; $v0 = $v0-$a0 = $a0*8-$a0 = $a0*7

_f2:
                  sll      $v0, $a0, 5
; $v0 = $a0<<5 = $a0*32
                  sll      $a0, 2
; $a0 = $a0<<2 = $a0*4
                  jr       $ra
                  subu     $v0, $a0 ; branch delay slot
; $v0 = $a0*32-$a0*4 = $a0*28

_f3:
                  sll      $v0, $a0, 4
; $v0 = $a0<<4 = $a0*16
                  jr       $ra
                  addu     $v0, $a0 ; branch delay slot
; $v0 = $a0*16+$a0 = $a0*17

64位

#include <stdint.h>

int64_t f1(int64_t a)
{
        return a*7;
};
int64_t f2(int64_t a)
{
        return a*28;
};

int64_t f3(int64_t a)
{
        return a*17;
};

x64

指令清单16.9　Optimizing MSVC 2012

; a*7
f1:
        lea     rax, [0+rdi*8]
; RAX=RDI*8=a*8
        sub     rax, rdi
; RAX=RAX-RDI=a*8-a=a*7
        ret

; a*28
f2:
        lea     rax, [0+rdi*4]
; RAX=RDI*4=a*4
        sal     rdi, 5
; RDI=RDI<<5=RDI*32=a*32
        sub     rdi, rax
; RDI=RDI-RAX=a*32-a*4=a*28
        mov     rax, rdi
        ret

; a*17
f3:
        mov     rax, rdi
        sal     rax, 4
; RAX=RAX<<4=a*16
        add     rax, rdi
; RAX=a*16+a=a*17
        ret

ARM64

由于ARM64模式的位移指令同样可进行加法运算，所以这种程序要短一些。

指令清单16.10　Optimizing GCC (Linaro) 4.9 ARM64

; a*7
f1:
        lsl     x1, x0, 3
; X1=X0<<3=X0*8=a*8
        sub     x0, x1, x0
; X0=X1-X0=a*8-a=a*7
        ret

; a*28
f2:
        lsl     x1, x0, 5
; X1=X0<<5=a*32
        sub     x0, x1, x0, lsl 2
; X0=X1-X0<<2=a*32-a<<2=a*32-a*4=a*28
        ret

; a*17
f3:
        add     x0, x0, x0, lsl 4
; X0=X0+X0<<4=a+a*16=a*17
        ret

16.2　除法运算

16.2.1　替换为位移运算

本节围绕下述源程序进行演示：

unsigned int f(unsigned int a)
{
       return a/4;
};

使用MSVC 2010编译上述程序，可得到如下所示的指令。

指令清单16.11　MSVC 2010

_a$ = 8                                         ; size = 4
_f      PROC
        mov     eax, DWORD PTR _a$[esp-4]
        shr     eax, 2
        ret     0
_f      ENDP

上述代码中的SHR（SHift Right）指令将EAX寄存器中的数值右移2位，使用零填充位移产生的空缺位，并且舍弃那些被右移指令移动出存储空间的比特位。实际上，那些被舍弃的比特位正是除法运算的余数。

SHR指令和SHL指令的运算模式相同，只是位移方向不同。

右移与余数的关系与十进制运算的移动小数点运算相似：当被除数为23、除数为10时，我们将23的小数点左移一位，2为商、3为余数。

毕竟本例是整数运算，商也应当是整数（不会是浮点数），所以我们舍弃余数完全没有问题。

“除以4”运算的ARM程序如下所示。

指令清单16.12　Non-optimizing Keil 6/2013 (ARM mode)

f  PROC
        LSR     r0,r0,#2
        BX      lr
        ENDP

“除以4”运算的MIPS程序如下所示。

指令清单16.13　Optimizing GCC 4.4.5 (IDA)

                jr       $ra
                srl      $v0, $a0, 2 ; branch delay slot

上述程序中的SRL指令是“逻辑右移指令/Shift-Right Logical”。

16.3　练习题

16.3.1　题目1

请分析下列程序的功能。

指令清单16.14　Optimizing MSVC 2010

_a$ = 8
_f      PROC
        mov     ecx, DWORD PTR _a$[esp-4]
        lea     eax, DWORD PTR [ecx*8]
        sub     eax, ecx
        ret     0
_f      ENDP

指令清单16.15　Non-optimizing Keil 6/2013 (ARM mode)

f PROC
        RSB     r0,r0,r0,LSL #3
        BX      lr
        ENDP

指令清单16.16　Non-optimizing Keil 6/2013 (Thumb mode)

f PROC
        LSLS    r1,r0,#3
        SUBS    r0,r1,r0
        BX      lr
        ENDP

指令清单16.17　Optimizing GCC 4.9 (ARM64)

f:
        lsl     w1, w0, 3
        sub     w0, w1, w0
        ret

指令清单16.18　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:
                sll      $v0, $a0, 3
                jr       $ra
                subu     $v0, $a0

第17章 FPU

FPU是专门处理浮点数的运算单元，是CPU的一个组件。

在早期的计算机体系中，FPU位于CPU之外的单独的运算芯片上。

17.1　IEEE 754

IEEE 754 标准规定了计算机程序设计环境中的二进制和十进制的浮点数的交换、算术格式以及方法。符合这种标准的浮点数由符号位、尾数（又称为有效数字、小数位）和指数位构成。

17.2　x86

事先接触过stack machine[1]或Forth语言编程基础[2]的读者，理解本节内容的速度会比较快。

在80486处理器问世之前，FPU（与CPU位于不同的芯片）叫作协作（辅助）处理器。而且那个时候的FPU还不属于主板的标准配置；如果想要在主板上安装FPU，人们还得单独购买它。[3]

80486 DX之后的CPU处理器集成了FPU的功能。

若没有FWAIT指令和opcode以D8～DF开头的所谓的“ESC”字符指令（opcode以D8～DF开头），恐怕很少有人还会想起FPU属于独立运算单元的这段历史。FWAIT指令的作用是让CPU等待FPU运算结束，而ESC字符指令都在FPU上执行。

FPU自带一个由8个80位寄存器构成的循环栈。这些80位寄存器用以存储IEEE 754格式的浮点数据[4]，通常叫作ST(0)～ST(7)寄存器。IDA和OllyDbg都把ST(0)显示为ST。也有不少教科书把ST(0)叫作“栈顶/Stack Top”寄存器。

17.3　ARM、MIPD、x86/x64 SIMD

在ARM和MIPS平台的概念里，FPU寄存器不构成栈结构，仅仅是一组寄存器。x86/64构架的SIMD 扩展（单指令多数据流扩展）也延承了这种理念。

17.4　C/C++

标准C/C++语言支持两种浮点类型数据，即单精度32位浮点数据（float）和双精度64位浮点数据（double）。

GCC编译器还支持long double类型浮点，即80位增强精度的扩展浮点类型数据（extended precision）。不过MSVC编译器不支持这种类型的浮点数据。[5]

虽然单精度浮点（float）型数据和整数（int）型数据在32位系统里都是32位数据，但是它们的数据格式完全不一样。

17.5　举例说明

本节围绕下述例子进行讲解：

#include <stdio.h>

double f (double a, double b)
{
          return a/3.14 + b*4.1;
};

int main()
{
          printf ("%f\n", f(1.2, 3.4));
  };

17.5.1　x86

MSVC

使用MSVC编译上述程序，可得到如下所示的指令。

指令清单17.1　MSVC 2010：f()

CONST    SEGMENT
__real@4010666666666666 DQ 04010666666666666r ; 4.1
CONST    ENDS
CONST    SEGMENT
__real@40091eb851eb851f DQ 040091eb851eb851fr ; 3.14
CONST    ENDS
_TEXT    SEGMENT
_a$ = 8             ; size = 8
_b$ = 16            ; size = 8
_f PROC
   push    ebp
   mov     ebp, esp
   fld     QWORD PTR _a$[ebp]

; current stack state: ST(0) = _a

    fdiv   QWORD PTR __real@40091eb851eb851f

; current stack state: ST(0) = result of _a divided by 3.14

    fld    QWORD PTR _b$[ebp]

; current stack state: ST(0) = _b; ST(1) = result of _a divided by 3.14

    fmul   QWORD PTR __real@4010666666666666

; current stack state:
; ST(0) = result of _b * 4.1;
; ST(1) = result of _a divided by 3.14

    faddp ST(1), ST(0)

; current stack state: ST(0) = result of addition

    pop   ebp
    ret   0
_f  ENDP

FLD指令从栈中读取8个字节，把这个值转换为FPU寄存器所需的80位数据格式，并存入ST(0)寄存器。

FDIV指令把ST(0)寄存器的值用作被除数，把参数__real@40091eb851eb851f（即3.14）的值当作除数，进行除法运算。因为汇编语法不支持含有小数点的浮点立即数，所以程序使用64位IEEE 754格式的16进制数040091eb851eb851fr表示3.14 。

在进行FDIV运算之后，ST(0)寄存器将保存商。

此外，FDIVP也是FPU的除法运算指令。FDIVP在进行ST(1)/ST(0)运算时，先把两个寄存器的值POP出来进行运算，再把商推送入(PUSH)FPU的栈（即ST(0)寄存器）。这相当于Forth语言[6]中的堆栈机[7]。

下一条FLD指令，把变量b的值推送到FPU的栈。

此时，ST(1)寄存器里是上次除法运算的商，ST(0)寄存器里是变量b的值。

接下来的FMUL指令做乘法运算。它用ST(0)寄存器里的值（即变量b），乘以参数__real @4010666666666666（即4.1），并将运算结果（积）存储到ST(0)寄存器。

最后一条运算指令FADDP计算栈内顶部两个值的和。它先把运算结果存储在ST(1)寄存器，再POP ST(0)。所以，运算表达式的运算结果存储在栈顶的ST(0)寄存器里。

根据有关规范，函数必须使用ST(0)寄存器存储浮点运算的返回结果。所以在FADDP指令之后，除了函数尾声的指令之外再无其他指令。

MSVC＋OllyDbg

图17.1标记出了两对32位数据。这两对数据都是由main()函数传递过来的、以IEEE 754格式存储的双精度浮点数据。我们可看到首条FLD指令从栈内读取了1.2、然后把它推送到ST(0)。

图17.1　OllyDbg：执行首条 FLD 指令

在把64位IEEE 754格式的数据转换为FPU所用的80位浮点数据的过程中，会不可避免地存在误差。图中1.1999……所要表示的量，就是1.2的近似值。此后，EIP寄存器的值指向下一条指令FDIV。FDIV指令会从内存中读取双精度浮点常量。OllyDbg人性化地显示出了第二个参数的值—— 3.14。

继续执行FDIV指令。如图17.2所示，此时ST(0)寄存器存储着上一次运算的商0.382…

图17.2　OllyDbg：执行FDIV指令

执行第三条指令即FLD指令之后，ST(0)寄存器加载了数值3.4（3.39999……）。如图17.3所示。

图17.3　OllyDbg：执行第二个 FLD 指令

在3.4入栈的同时，先前运算出来的商被推送到ST(1)寄存器，而后EIP指针的值指向下一条指令FMUL。如同OllyDbg提示的那样，FMUL指令会从内存中读取因子4.1。

在执行FMUL指令之后，ST(0)寄存器将存储着乘法运算的积，如图17.4所示。

图17.4　OllyDbg：执行FMUL指令

然后运行FADDP指令，运算求得的和会被存储在ST(0)寄存器中。同时，指令会清空ST(1)寄存器。如图17.5所示。

图17.5　OllyDbg：执行FADDP指令

在FPU的运算指令结束之后，运算结果存储在ST(0)寄存器里。main()函数稍后会从这个寄存器提取运算结果。

值得注意的是ST(7)寄存器——它的值是13.93……这是为什么？

这不难理解。前文介绍过，FPU的寄存器构成了自己的栈结构。因为需要用硬件直接实现数据栈，所以栈结构也比较简单。在对FPU进行出入栈操作的时候，如果每次都要把所有7个寄存器的内容转移（或者说复制）到相邻的寄存器，那么开销会非常高。实际的FPU只有8个寄存器和1个栈顶指针（TOP）寄存器。栈顶指针寄存器专门记录“栈顶”寄存器的寄存器编号。在FPU进行数据入栈（PUSH）操作时，它首先令栈顶指针寄存器指向下一个寄存器，然后在那个寄存器里存储数据。出栈（POP）指令的过程相反。但是在进行出栈操作时，FPU不会清空原有寄存器（否则必定影响性能）。所以，在执行完程序的浮点运算指令后，FPU寄存器的状态就如图17.5所示。这种现象可以说是“FADDP指令把运算结果推送入栈，然后进行了出栈操作”，但是实际上这条指令把和存入寄存器后调整了栈顶指针寄存器的值。所以，确切地说，FPU的寄存器构成了循环缓冲区（circular buffer）。

GCC

使用GCC 4.4.1（启用 –O3选项）编译上述代码，生成的程序略有不同。

指令清单17.2　Optimizing GCC 4.4.1

                ublic f
f               proc near

arg_0           = qword ptr  8
arg_8           = qword ptr  10h

                push    ebp
                fld     ds:dbl_8048608 ; 3.14

; stack state now: ST(0) = 3.14
                mov     ebp, esp
                fdivr   [ebp+arg_0]

; stack state now: ST(0) = result of division

                fld     ds:dbl_8048610 ; 4.1

; stack state now: ST(0) = 4.1, ST(1) = result of division

                fmul    [ebp+arg_8]

; stack state now: ST(0) = result of multiplication, ST(1) = result of division
                pop     ebp
                faddp   st(1), st

; stack state now: ST(0) = result of addition

                retn
f               endp

第一处不同点，同时也是最显著的不同之处是：GCC把3.14送入FPU的栈（ST(0)寄存器），用作arg_0的除数。

FDIVR是Reverse Divide的缩写。FDIVR指令的除数和被除数，对应FDIV指令的被除数和除数，即位置相反。在乘法运算中，因子的位置不影响运算结果，所以没有FMULR指令。

FADDP指令从栈中POP出一个值进行加法运算，并用ST(0)存储和。

17.5.2　ARM: Optimizing Xcode 4.6.3 (LLVM) (ARM mode)

在ARM统一浮点运算标准之前，很多厂商都推出了各自的扩展指令以实现浮点运算。后来，VFP（Vector Floating Point）成为了行业的标准。

x86平台的FPU有自己的栈；但是ARM平台里没有栈结构，只能操作寄存器。

指令清单17.3　Optimizing Xcode 4.6.3 (LLVM) (ARM mode)

f
           VLDR       D16, =3.14
           VMOV       D17, R0, R1 ; load "a"
           VMOV       D18, R2, R3 ; load "b"
           VDIV.F64   D16, D17, D16 ; a/3.14
           VLDR       D17, =4.1
           VMUL.F64   D17, D18, D17 ; b*4.1
           vVADD.F64  D16, D17, D16 ; +
           VMOV       R0, R1, D16
           BX         LR

dbl_2C98   DCFD 3.14          ; DATA XREF: f
dbl_2CA0   DCFD 4.1           ; DATA XREF: f+10

上述程序出现了D字头的寄存器。ARM平台有32个64位的D字头寄存器。这些寄存器即可用来存储（双精度）浮点数，又可用于单指令流多数据流运算/SIMD（ARM平台下这种运算叫NEON）。ARM平台还有32个S字头的寄存器。S字头寄存器用于处理单精度浮点数据。简单地说，S字头寄存器用于存储单精度浮点，而D字头寄存器用于处理双精度浮点。详细规格请参见附录B.3.3。

本例中的两个浮点都以IEEE 754的格式存储于内存之中。

望文生义，VLDR和VMOV指令就是操作D字头寄存器的LDR和MOV指令。这些V字头的指令和D字头的寄存器，不仅能够处理浮点类型数据，而且可以用于SIMD（NEON）运算。

即使涉及浮点运算，但是它还是ARM平台的程序，还会遵循ARM规范使用R字头寄存器传递参数。双精度浮点数据是64位数据，所以每传递一个双精度浮点数据就需要使用2个R字头寄存器。

“VMOV D17, R0, R1”指令从R0和R1寄存器读取64位数据的2个部分，并把最终数值存储在D17寄存器中。

“VMOV R0, R1, D16”与上述指令的作用相反。它把D16寄存器的值（64位）分解成两个32位数据，并分别存储于R0和R1寄存器。

后面出现的VDIV、VMUL、VADD指令都是浮点运算指令，不再介绍。

使用Xcode生成Thumb-2模式的代码，会跟这段程序相同。

17.5.3　ARM: Optimizing Keil 6/2013 (Thumb mode)

f
                PUSH    {R3-R7,LR}
                MOVS    R7, R2
                MOVS    R4, R3
                MOVS    R5, R0
                MOVS    R6, R1
                LDR     R2, =0x66666666;4.1
                LDR     R3, =0x40106666
                MOVS    R0, R7
                MOVS    R1, R4
                BL      __aeabi_dmul
                MOVS    R7, R0
                MOVS    R4, R1
                LDR     R2, =0x51EB851F;3.14
                LDR     R3, =0x40091EB8
                MOVS    R0, R5
                MOVS    R1, R6
                BL      __aeabi_ddiv
                MOVS    R2, R7
                MOVS    R3, R4
                BL      __aeabi_dadd
                POP     {R3-R7,PC}

; 4.1 in IEEE 754 form:
dword_364       DCD 0x66666666      ; DATA XREF: f+A
dword_368       DCD 0x40106666      ; DATA XREF: f+C
; 3.14 in IEEE 754 form:
dword_36C       DCD 0x51EB851F      ; DATA XREF: f+1A
dword_370       DCD 0x40091EB8      ; DATA XREF: f+1C

Keil生成的Thumb模式程序不支持NEON运算和FPU浮点运算。Thumb模式程序使用两个通用的R字头寄存器传递双精度浮点型数据。因为不再使用FPU的专用指令，所以这类程序必须调用库函数（例如__aeabi_dmul, __aeabi_ddiv, __aeabi_dadd）“仿真”浮点运算。“仿真”意义上的模拟运算，其速度当然比不上FPU处理器的速度，但是聊胜于无。

在早年协作处理器还属于昂贵的奢侈品的时候，x86平台的浮点仿真运算的库函数曾经盛行一时。

在ARM系统里，FPU仿真处理叫作“软浮点/soft float”或者“armel”，而通过硬件实现的FPU指令叫作“硬浮点/hard float”或“armhf”。

17.5.4　ARM64: Optimizing GCC (Linaro) 4.9

这种程序十分短。

指令清单17.4　Optimizing GCC (Linaro) 4.9

f:
; D0 = a, D1 = b
        ldr     d2, .LC25       ; 3.14
; D2 = 3.14
        fdiv    d0, d0, d2
; D0 = D0/D2 = a/3.14
        ldr     d2, .LC26       ; 4.1
; D2 = 4.1
        fmadd   d0, d1, d2, d0
; D0 = D1*D2+D0 = b*4.1+a/3.14
        ret

; constants in IEEE 754 format:
.LC25:
        .word   1374389535      ; 3.14
        .word   1074339512
.LC26:
        .word   1717986918      ; 4.1
        .word   1074816614

17.5.5　ARM64: Non-optimizing GCC (Linaro) 4.9

指令清单17.5　Non-optimizing GCC (Linaro) 4.9

f:
        sub     sp, sp, #16
        str     d0, [sp,8]      ; save "a" in Register Save Area
        str     d1, [sp]        ; save "b" in Register Save Area
        ldr     x1, [sp,8]
; X1 = a
        ldr     x0, .LC25
; X0 = 3.14
        fmov    d0, x1
        fmov    d1, x0
; D0 = a, D1 = 3.14
        fdiv    d0, d0, d1
; D0 = D0/D1 = a/3.14

        fmov    x1, d0
; X1 = a/3.14
        ldr     x2, [sp]
; X2 = b
        ldr     x0, .LC26
; X0 = 4.1
        fmov    d0, x2
; D0 = b
        fmov    d1, x0
; D1 = 4.1
        fmul    d0, d0, d1
; D0 = D0*D1 = b*4.1

        fmov    x0, d0
; X0 = D0 = b*4.1
        fmov    d0, x1
; D0 = a/3.14
        fmov    d1, x0
; D1 = X0 = b*4.1
        fadd    d0, d0, d1
; D0 = D0+D1 = a/3.14 + b*4.1

        fmov    x0,d0   ;\ redundant code
        fmov    d0,x0   ;/
        add     sp, sp, 16
        ret
.LC25:
        .word   1374389535  ; 3.14
        .word   1074339512
.LC26:
        .word   1717986918  ; 4.1
        .word   1074816614

在没有启用优化功能的情况下，GCC生成的代码比较拖沓。在上述程序中，不仅出现了没有意义的数值交换指令，而且还出现了明显多余的指令（例如最后两条FMOV指令）。这可能是GCC 4.9在编译ARM64程序方面尚有不足。

值得注意的是，ARM64本身就具备64位寄存器，而D字头寄存器同样是64位寄存器。所以编译器可以调动通用寄存器GPR直接存储双精度浮点数，而不必非得使用本地栈来存储这种数据。毫无疑问，在32位CPU上，编译器无法使用这种寄存器分配方案。

建议读者用这个程序进行练习，在不使用FMADD之类的新指令的情况下，手动优化上述函数。

17.5.6　MIPS

MIPS平台支持多个（4个及以下）协作处理器。第0个协作处理器专门用于调度其他的协作处理器，第1个协作处理器就是FPU。

与ARM平台的情形相似，MIPS的协作处理器不是堆栈机（stack machine），只是32个32位寄存器（$F0～$F31）。有关FPU各寄存器的介绍，请参见附录C.1.2节。在处理64位双精度浮点数时，必须使用一对32位F字头寄存器。

指令清单17.6　Optimizing GCC 4.4.5 (IDA)

f:
; $f12-$f13=A
; $f14-$f15=B
                lui     $v0, (dword_C4 >> 16)   ; ?
; load low 32-bit part of 3.14 constant to $f0:
                lwc1    $f0, dword_BC
                or      $at, $zero              ; load delay slot, NOP
; load high 32-bit part of 3.14 constant to $f1:
                lwc1    $f1, $LC0
                lui     $v0, ($LC1 >> 16)       ; ?
; A in $f12-$f13, 3.14 constant in $f0-$f1, do division:
                div.d   $f0, $f12, $f0
; $f0-$f1=A/3.14
; load low 32-bit part of 4.1 to $f2:
                lwc1    $f2, dword_C4
                or      $at, $zero     ; load delay slot, NOP
; load high 32-bit part of 4.1 to $f3:
                lwc1    $f3, $LC1
                or      $at, $zero     ; load delay slot, NOP
; B in $f14-$f15, 4.1 constant in $f2-$f3, do multiplication:
                mul.d   $f2, $f14, $f2
; $f2-$f3=B*4.1
                jr      $ra
; sum 64-bit parts and leave result in $f0-$f1:
                add.d   $f0, $f2         ; branch delay slot, NOP

.rodata.cst8:000000B8 $LC0:            .word 0x40091EB8        # DATA XREF: f+C
.rodata.cst8:000000BC dword_BC:        .word 0x51EB851F        # DATA XREF: f+4
.rodata.cst8:000000C0 $LC1:            .word 0x40106666        # DATA XREF: f+10
.rodata.cst8:000000C4 dword_C4:        .word 0x66666666        # DATA XREF: f

需要介绍的指令有：

LWC1把一个32位Word数据传递给第一个协作处理器(Load Word to Coprocessor 1)。可见，指令中的1指代协作处理器的编号。成对出现的LWC1指令可能会被调试程序显示为伪指令LD。

DIV.D、MUL.D、ADD.D指令是双精度浮点数的除法、乘法和加法运算指令。其后缀“.D”表明数据类型是double/双精度浮点。顾名思义，后缀为“.S”的指令则是single/单精度浮点数据的运算指令。

文中用问号“？”标出的LUI指令应当没有实际意义，可能是编译器生成的异常指令。如果有读者知道其中奥秘，请发email给我。

17.6　利用参数传递浮点型数据

本节围绕下述程序进行演示：

#include <math.h>
#include <stdio.h>

int main ()
{
        printf ("32.01 ^ 1.54 = %lf\n", pow (32.01,1.54));

        return 0;
}

17.6.1　x86

使用MSVC 2010编译上述程序，可得到如下所示的指令。

指令清单17.7　MSVC 2010

CONST    SEGMENT
__real@40400147ae147ae1 DQ 040400147ae147ae1r    ; 32.01
__real@3ff8a3d70a3d70a4 DQ 03ff8a3d70a3d70a4r    ; 1.54
CONST    ENDS

_main    PROC
    push   ebp
    mov    ebp, esp
    sub    esp, 8  ; 为第1个变量分配空间
    fld    QWORD PTR __real@3ff8a3d70a3d70a4
    fstp   QWORD PTR [esp]
    sub    esp, 8  ; 为第2个变量分配空间
    fld    QWORD PTR __real@40400147ae147ae1
    fstp   QWORD PTR [esp]
    call   _pow
    add    esp, 8  ;单个变量的返回地址

; 栈分配了8个字节的空间
; 运算结果存储于ST(0)寄存器

    fstp   QWORD PTR [esp] ;把ST(0)的值转移到栈，供printf()调用
    push   OFFSET $SG2651
    call   _printf
    add    esp, 12
    xor    eax, eax
    pop    ebp
    ret    0
_main    ENDP

FLD和FSTP指令是在数据段（SEGMENT）和FPU的栈间交换数据的指令。FLD把内存里的数据推送入FPU的栈，而FSTP则把FPU栈顶的数据复制到内存中。pow()函数是指数运算函数，它从FPU的栈内读取两个参数进行计算，并把运算结果（x的y次幂）存储在ST(0)寄存器里。之后，printf()函数先从内存栈中读取8个字节的数据，再以双精度浮点的形式进行输出。

此外，这个例子里还可以直接成对使用MOV指令把浮点数据从内存复制到FPU的栈里。内存本身就把浮点数据存储为IEEE 754的数据格式，而pow()函数所需的参数就是这个格式的数据，所以此处没有格式转换的必要。下一节的例子就会用到这个技巧。

17.6.2　ARM + Non-optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

_main

var_C           = -0xC

                PUSH       {R7,LR}
                MOV        R7, SP
                SUB        SP, SP, #4
                VLDR       D16, =32.01
                VMOV       R0, R1, D16
                VLDR       D16, =1.54
                VMOV       R2, R3, D16
                BLX        _pow
                VMOV       D16, R0, R1
                MOV        R0, 0xFC1 ; "32.01 ^ 1.54 = %lf\n"
                ADD        R0, PC
                VMOV       R1, R2, D16
                BLX        _printf
                MOVS       R1, 0
                STR        R0, [SP,#0xC+var_C]
                MOV        R0, R1
                ADD        SP, SP, #4
                POP        {R7,PC}

dbl_2F90        DCFD 32.01        ; DATA XREF: _main+6
dbl_2F98        DCFD 1.54         ; DATA XREF: _main+E

前文介绍过，ARM系统可以在不借助D字头寄存器的情况下，通过一对R字头寄存器传递64位浮点数。但是由于我们没有启用编译器的优化选项，所以它还是用D字头寄存器传递浮点数。

从中可以看出，R0和R1寄存器给_pow函数传递了第一个参数，R2和R3寄存器给函数传递了第二个参数。函数把计算结果存储在R0和R1寄存器对。而后_pow的运算结果再通过D16寄存器传递给R1和R2寄存器，以此向printf()函数传递参数。

17.6.3　ARM + Non-optimizing Keil 6/2013 (ARM mode)

_main
           STMFD     SP!, {R4-R6,LR}
           LDR       R2, =0xA3D70A4       ;y
           LDR       R3, =0x3FF8A3D7
           LDR       R0, =0xAE147AE1      ;x
           LDR       R1, =0x40400147
           BL        pow
           MOV       R4, R0
           MOV       R2, R4
           MOV       R3, R1
           ADR       R0, a32_011_54Lf     ; "32.01 ^ 1.54 = %lf\n"
           BL        __2printf
           MOV       R0, #0
           LDMFD     SP!, {R4-R6,PC}

y          DCD 0xA3D70A4                  ; DATA XREF: _main+4
dword_520  DCD 0x3FF8A3D7                 ; DATA XREF: _main+8
; double x
x          DCD 0xAE147AE1                 ; DATA XREF: _main+C
dword_528  DCD 0x40400147                 ; DATA XREF: _main+10
a32_011_54Lf DCB "32.01 ^ 1.54 = %lf",0xA,0
                                          ; DATA XREF: _main+24

在没有启用优化功能时，编译器只使用了R-字头寄存器对，没有使用D-字头寄存器。

17.6.4　ARM64 + Optimizing GCC (Linaro) 4.9

指令清单17.8　Optimizing GCC (Linaro) 4.9

f:
        stp     x29, x30, [sp, -16]!
        add     x29, sp, 0
        ldr     d1, .LC1 ; load 1.54 into D1
        ldr     d0, .LC0 ; load 32.01 into D0
        bl      pow
; result of pow() in D0
        adrp    x0, .LC2
        add     x0, x0, :lo12:.LC2
        bl      printf
        mov     w0, 0
        ldp     x29, x30, [sp], 16
        ret
.LC0:
; 32.01 in IEEE 754 format
        .word   -1374389535
        .word   1077936455
.LC1:
; 1.54 in IEEE 754 format
        .word   171798692
        .word   1073259479
.LC2:
        .string"32.01 ^ 1.54 = %lf\n"

启用优化功能之后，编译器使用D0和D1寄存器加载常量、继而传递给pow()函数。pow()函数的运算结果再由D0寄存器传递给printf()函数。因为printf()函数不仅可以通过X-字头寄存器获取整型数据和指针，而且还可以直接访问D-字头寄存器获取浮点数参数，所以在传递浮点数时不需要修改或转移数据。

17.6.5　MIPS

指令清单17.9　Optimizing GCC 4.4.5 (IDA)

main:

var_10          = -0x10
var_4           = -4

; function prologue:
                lui   $gp, (dword_9C >> 16)
                addiu $sp, -0x20
                la    $gp, (__gnu_local_gp & 0xFFFF)
                sw    $ra, 0x20+var_4($sp)
                sw    $gp, 0x20+var_10($sp)
                lui   $v0, (dword_A4 >> 16) ; ?
; load low 32-bit part of 32.01:
                lwc1  $f12, dword_9C
; load address of pow() function:
                lw    $t9, (pow & 0xFFFF)($gp)
; load high 32-bit part of 32.01:
                lwc1  $f13, $LC0
                lui   $v0, ($LC1 >> 16) ; ?
; load low 32-bit part of 1.54:
                lwc1  $f14, dword_A4
                or    $at, $zero ; load delay slot, NOP
; load high 32-bit part of 1.54:
                lwc1  $f15, $LC1
; call pow():
                jalr  $t9
                or    $at, $zero ; branch delay slot, NOP
                lw    $gp, 0x20+var_10($sp)
; copy result from $f0 and $f1 to $a3 and $a2:
                mfc1  $a3, $f0
                lw    $t9, (printf & 0xFFFF)($gp)
                mfc1  $a2, $f1
; call printf():
                lui   $a0, ($LC2 >> 16)  # "32.01 ^ 1.54 = %lf\n"
                jalr  $t9
                la    $a0, ($LC2 & 0xFFFF)  # "32.01 ^ 1.54 = %lf\n"
; function epilogue:
                lw    $ra, 0x20+var_4($sp)
; return 0:
                move  $v0, $zero
                jr    $ra
                addiu $sp, 0x20

.rodata.str1.4:00000084 $LC2:   .ascii "32.01 ^ 1.54 = %lf\n"<0>

; 32.01:
.rodata.cst8:00000098 $LC0:     .word 0x40400147       # DATA XREF: main+20
.rodata.cst8:0000009C dword_9C: .word 0xAE147AE1       # DATA XREF: main
.rodata.cst8:0000009C                                  # main+18
; 1.54:
.rodata.cst8:000000A0 $LC1:     .word 0x3FF8A3D7       # DATA XREF: main+24
.rodata.cst8:000000A0                                  # main+30
.rodata.cst8:000000A4 dword_A4: .word 0xA3D70A4        # DATA XREF: main+14

这段程序的LUI指令将双精度浮点数的高16位复制到$V0寄存器。其中的（汇编宏>>16）是经IDA整理的伪代码，它的作用是对32位数据右移16位、以得到高16位数。LUI指令只能操作16位立即数。不过两个LWC1之前的LUI指令似乎没有意义，笔者给它们注释上了问号。如果哪位读者知晓其中玄机，还请联系作者本人。

MFC1是“Move From Coprocessor 1”的缩写。在MIPS系统上，第1号协作处理器是FPU。可见，这条指令首先读取协作处理器的寄存器的值，然后再把这个值复制到CPU通用寄存器GPR。不难看出，这条指令把pow()函数的运算结果复制到$A3和$A2寄存器里，然后printf()函数从这对寄存器里提取一对32位数据、再把它输出为64位双精度浮点数。

17.7　比较说明

本节围绕下述程序进行演示：

#include <stdio.h>

double d_max (double a, double b)
{
        if (a>b)
                return a;

        return b;
};

int main()
{
        printf ("%f\n", d_max (1.2, 3.4));
        printf ("%f\n", d_max (5.6, -4));
};

虽然这个函数很短，但是它的汇编代码并不那么简单。

17.7.1　x86

Non-optimizing MSVC

指令清单17.10　Non-optimizing 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位寄存器，用于描述FPU的当前状态。

在设置好相应比特位之后，FCOMP指令还会从栈里抛出（POP）一个值。FCOM与FCOMP的功能十分相似。FCOM指令只根据数值比较的结果设置状态字，而不会再操作FPU的栈。

不幸的是，在Intel P6[8]之前问世的CPU上，条件转移指令不能根据C3/C2/C0状态位进行条件判断。考虑到那时候的FPU在物理上尚与CPU分离，所以这种不足在当时大概还算不上是缺陷。

自Intel P6问世之后，FCOMI/FCOMIP/FUCOMI/FUCOMIP指令不仅延续了先前各指令的功能，而且新增了设置CPU标志位ZF/PF/CF的功能。

FNSTSW指令把FPU状态寄存器的值复制到AX寄存器。C3/C2/C0标志位对应AX的第14/10/8位。复制数值并不会改变标志位（bit）的数权（位置）。标志位会集中在AX寄存器的高地址位区域——即AH寄存器里。

如果b>a，则C3、C2、C0寄存器的值会分别是0、0、0。

如果a>b，则寄存器的值会分别是0、0、1。

如果a=b，则寄存器的值会分别是1、0、0。

如果出现了错误（NaN或数据不兼容），则寄存器的值是1、1、1。

在FNSTSW指令把FPU状态寄存器的值复制到AX寄存器后，AX寄存器各个bit位与C0～C3寄存器的对应关系如下图所示。

若以AH寄存器的视角来看，C0～C3与各bit位的对应关系则是：

“test ah, 5”指令把ah的值（FPU标志位的加权求和值）和0101（二进制的5）做与（AND）运算，并设置标志位。影响test结果的只有第0比特位的C0标志位和第2比特位的C2标志位，因为其他的位都会被置零。

接下来，我们首先要介绍奇偶校验位PF（parity flag）。

PF标志位的作用是判定运算结果中的“1”的个数，如果“1”的个数为偶数，则PF的值为1，否则其值为0。

检验奇偶位通常用于判断处理过程是否出现故障，并不能判断这个数值是奇数还是偶数。FPU有四个条件标志位（C0到C3）。但是，必须把标志位的值组织起来、存放在标志位寄存器中，才能进行奇偶校验位的正确性验证。FPU标志位的用途各有不同：C0位是进位标志位CF，C2是奇偶校验位PF，C3是零标志位ZF。在使用FUCOM指令（FPU比较指令的通称）时，如果操作数里出现了不可比较的浮点值（非数值型内容NaN或其他无法被指令支持的格式），则C2会被设为1。

如果C0和C2都是0或都是1，则设PF标志为1并触发JP跳转（Jump on Parity）。前面对C3/C2/C0的取值进行了分类讨论，C2和C0的数值相同的情况分为b>a和 a=b这两种情况。因为test指令把ah的值与5进行“与”运算，所以C3的值无关紧要。

在此之后的指令就很简单了。如果触发了JP跳转，则FLD指令把变量_b的值复制到ST(0)寄存器，否则变量_a的值将会传递给ST（0）寄存器。

如果需要检测C2的状态

如果TEST指令遇到错误（NaN等情形），则C2标志位的值会被设置为1。不过我们的程序不检测这类错误。如果编程人员需要处理FPU的错误，他就不得不添加额外的错误检查指令。

使用OllyDbg调试本章例一（a＝1.2，b＝3.4）

使用OllyDbg打开编译好的程序，如图17.6所示。

图17.6　OllyDbg：执行第一条FLD指令

我们可以在数据栈中看到两对32位的值，它们是当前函数的两个参数：a=1.2, b=3.4。此时ST(0)寄存器已经读取了变量b的值（3.4）。下一步将执行FCOMP指令。OllyDbg会提示FCOMP的第二个参数，这个参数也在栈里。

执行FCOMP指令，如图17.7所示。

图17.7　OllyDbg：执行FCOMP指令

此时FPU的条件标志位都是零。刚才被POP的数值已经转移到ST(7)寄存器里了。本章已经在5.1节介绍过FPU的寄存器和数据栈，这里不再复述。

然后运行FNSTSW指令，如图17.8所示。

图17.8　OllyDbg：执行FNSTSW指令

可见AX寄存器的值是零。确实，FPU的所有标志位目前都是零。OllyDbg将FNSTSW识别为FSTSW指令，这两条指令是同一条指令。

接下来运行TEST指令，如图17.9所示。

图17.9　OllyDbg：执行Test指令

PF标志的值为1。这是因为0里面有偶数个1，所以PF是1。OllyDbg将JP识别为JPE指令，它们是同一个指令。在下一步里，程序会触发JP跳转。

如图17.10所示，程序会触发JPE跳转，ST（0）将读取变量b的值3.4。

图17.10　执行第二条FLD指令

此后函数结束。

调试本章例二（a=5.6, b=−4）

首先使用OllyDbg加载编译好的可执行程序，如图17.11所示。

图17.11　OllyDbg：执行第一条FLD指令

这个函数有两个参数，a是5.6、b是−4。此刻，参数b已经加载到ST(0)寄存器，即将执行FCOMP指令。OllyDbg会在栈里显示FCOMP的另一个参数。

执行FCOMP指令，如图17.12所示。

图17.12　OllyDbg：执行FCOMP指令

C0之外的FPU标志位都是0。

然后执行FNSTSW指令，如图17.13所示。

图17.13　OllyDbg：执行FNSTSW指令

此时AX寄存器的值是0x100。C0标志位是AX寄存器的第8位（从第零位开始数）。

接下来执行TEST指令。

如图17.14所示，PF的值为0。毕竟，把0x100转换为2进制数后，里面只有1个1，1是奇数。此后不会触发JPE跳转。

图17.14　OllyDbg：执行TEST指令

因为不会触发JPE跳转，所以FLD从a里取值，把5.6赋值给了ST(0)寄存器，如图17.15所示。

图17.15　OllyDbg：执行第二条FLD指令

Optimizing MSVC 2010

指令清单17.11　Optimizing MSVC 2010

_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
; copy ST(0) to ST(1) and pop register,
; leave (_a) on top
    fstp    ST(1)

; current stack state: ST(0) = _a

    ret     0
$LN5@d_max:
; copy ST(0) to ST(0) and pop register,
; leave (_b) on top
    fstp    ST(0)

; current stack state: ST(0) = _b
    ret 0
_d_max ENDP

FCOM指令和前面用过的FCOMP指令略有不同，它不操作FPU栈。而且本例的操作数也和前文有区别，这里它是逆序的。所以，FCOM生成的条件标志位的涵义也与前例不同。

如果a>b，则C3、C2、C0位的值分别为0、0、0。

如果b>a，则对应数值为0、0、1。

如果a=b，则对应数值为1、0、0。

就是说，“test ah, 65”这条指令仅仅比较两个标志位——C3（第6位/bit）和C0（第0位/bit）。在a>b的情况下，两者都应为0：这种情况下，程序不会被触发JNE跳转，并会执行后面的FSTP ST(1)指令，把ST（0）的值复制到操作数中，然后从FPU栈里抛出一个值。换句话说，这条指令把ST（0）的值（即变量_a的值）复制到ST（1）寄存器；此后栈顶的2个值都是_a。然后，相当于POP出一个值来，使ST(0)寄存器的值为_a，函数随即结束。

在b>a或a==b的情况下，程序将触发条件转移指令JNE。从ST(0)取值、再赋值给ST(0)寄存器，相当于NOP操作没有实际意义。接着它从栈里POP出一个值，使ST(0)的值为先前ST(1)的值，也就是变量_b。然后结束本函数。大概是因为FPU的指令集里没有POP并舍弃栈顶值的指令，所以才会出现这样的汇报指令。

使用OllyDbg调试例一：a=1.2/b=3.4的程序

在执行两条FLD指令之后，情况如图17.16所示。

图17.16　OllyDbg：执行过两条FLD指令之后的情况

此后将执行FCOM指令。OllyDbg会显示ST(0)和ST(1)的值。

在执行FCOMP指令之后，C0为1，其他标志全部为0，如图17.17所示。

图17.17　OllyDbg：执行FCOM指令

在执行FNSTSW指令之后，AX＝0x3100，如图17.18所示。

图17.18　OllyDbg：执行FNSTSW指令

然后运行TEST，如图17.19所示。

图17.19　OllyDbg：执行TEST指令

此刻ZF＝0，即将触发条件转移指令。

在执行FSTP ST（即ST（0））的时候，FPU把1.2从栈里POP了出来，栈顶变为3.4，如图17.20所示。

图17.20　OllyDbg：执行FSTP指令

可见“FSTP ST”指令与“POP FPU栈”指令的作用相似。

使用OllyDbg调试例二：a=5.6/b=-4的程序

在执行两条FLD指令之后的情况如图17.21所示。

图17.21　OllyDbg：执行两条FLD指令

接下来将要运行FCOM指令，如图17.22所示。

图17.22　OllyDbg：执行FCOM指令

标志位寄存器都被置零。

执行过FNSTSW之后，AX＝0x3000，如图17.23所示。

图17.23　OllyDbg：执行FNSTSW指令

此后执行TEST指令，如图17.24所示。

图17.24　OllyDbg：执行TEST指令

在执行TEST置零之后，ZF＝1，不会触发条件转移指令。

如图17.25所示，在执行FSTP ST（1）的时候，FPU栈顶的值是5.6。

图17.25　OllyDbg：执行FSTP指令

可见，FSTP ST(1)指令不会操作FPU栈顶的值，而会清空ST(1)寄存器的值。

GCC 4.4.1

指令清单17.12　GCC 4.4.1

d_max proc near
b               = qword ptr -10h
a               = qword ptr -8
a_first_half    = dword ptr  8
a_second_half   = dword ptr  0Ch
b_first_half    = dword ptr  10h
b_second_half   = dword ptr  14h

    push    ebp
    mov     ebp, esp
    sub     esp, 10h

; put a and b to local stack:

    mov     eax, [ebp+a_first_half]
    mov     dword ptr [ebp+a], eax
    mov     eax, [ebp+a_second_half]
    mov     dword ptr [ebp+a+4], eax
    mov     eax, [ebp+b_first_half]
    mov     dword ptr [ebp+b], eax
    mov     eax, [ebp+b_second_half]
    mov     dword ptr [ebp+b+4], eax

; load a and b to FPU stack:
    fld     [ebp+a]
    fld     [ebp+b]

; current stack state: ST(0) - b; ST(1) - a

    fxch    st(1) ; this instruction swapping ST(1) and ST(0)

; current stack state: ST(0) - a; ST(1) – b

    fucompp    ; compare a and b and pop two values from stack, i.e., a and b
    fnstsw  ax ; store FPU status to AX
    sahf       ; load SF, ZF, AF, PF, and CF flags state from AH
    setnbe  al ; store 1 to AL if CF=0 and ZF=0
    test    al, al            ; AL==0 ?
    jz      short loc_8048453 ; yes
    fld     [ebp+a]
    jmp     short locret_8048456

loc_8048453:
    fld     [ebp+b]

locret_8048456:
    leave
    retn
d_max endp

FUCOMPP与FCOM指令的功能相似。它的全称是“Floating-Point Unsigned Compare And Pop”，所以它还能够从FPU栈中把两个比较的数值POP出来。此外，它们处理非数——“not-a-number/NaN”[9]的方式也有所不同。

FPU能够处理特定类型的NaN，如无限大、除以0的结果等。NaN又分为Quiet NaN和Signaling NaN。对Quiet NaN进行操作可能不会出现问题，但是对Signaling NaN进行运算将会引发错误（异常处理）。

只要在FCOM的操作数中有NaN，该指令就会引发异常处理机制。而FUCOM仅在处理Signaling NaN（简称为SNaN）时才会报错。

下一条指令是标志位传送指令SAHF（Store AH into Flags）。这条指令与FPU无关。具体来说，它把AH寄存器的8个比特位以下列顺序传递到CPU的8位标志位里：

在前文的例子中，我们关注过FSNSTSW指令。它把标志位C3/C2/C0以下列顺序复制到AH寄存器的第6、2、0位里：

换而言之，成对使用FNSTSW AX /SAHF这两条指令，可以把FPU的C3/C2/C0标志位复制到CPU的ZF/PF/CF标志位。

现在回忆一下C3/C2/C0标志位的几种情况：

如果a>b，则C3/C2/C0依次为0、0、0。

如果a<b，则它们依次为0、0、1。

如果a=b，则它们依次为1、0、0。

即，在执行过FUCOMPP/FNSTSW/SAHF这组指令之后，CPU的标志位：

如果a>b，则ZF＝0、PF＝0、CF＝0。

如果a<b，则ZF＝0、PF＝0、CF＝1。

如果a=b，则ZF＝1、PF＝0、CF＝0。

SETNBE可以根据CPU标志位和有关限定条件把AL寄存器设置为0或1。SETNBE只在CF和ZF寄存器都为0的情况下，设置AL为1，其他情况下设置AL＝0。SETcc和Jcc[10]是孪生兄弟。不过SETcc的作用是按条件赋值（0或1），而Jcc的作用是按条件进行跳转。

在本例中，只有在a>b的情况下，CF和ZF标志位才同时为0 。

这种情况下AL将会被赋值为1，程序不会触发JZ跳转，函数返回值是_a；否则函数返回值是_b。

Optimizing GCC 4.4.1

经GCC 4.4.1（启用优化选项－O3）编译上述程序，可得到如下所示的汇编指令。

指令清单17.13　Optimizing GCC 4.4.1

                public d_max
d_max           proc near

arg_0           = qword ptr  8
arg_8           = qword ptr  10h

                push    ebp
                mov     ebp, esp
                fld     [ebp+arg_0] ; _a
                fld     [ebp+arg_8] ; _b

; stack state now: ST(0) = _b, ST(1) = _a
                fxch    st(1)

; stack state now: ST(0) = _a, ST(1) = _b
                fucom   st(1) ; compare _a and _b
                fnstsw  ax
                sahf
                ja      short loc_8048448

; store ST(0) to ST(0) (idle operation), pop value at top of stack,
; leave _b at top
                fstp    st
                jmp     short loc_804844A

loc_8048448:
; store _a to ST(1), pop value at top of stack, leave _a at top
                fstp st(1)

loc_804844A:
                pop     ebp
                retn
d_max           endp

优化编译的效果集中体现在SAHF指令之后的JA指令上。实际上，依据无符号类型数据的比较结果进行跳转的条件转移指令（JA/JAE, JB/ JBE, JE/JZ, JNA/ JNAE, JNB/ JNBE, JNE/JNZ），只检测CF和ZF标志位。

在执行FSTSW/FNSTSW指令后，C3/C2/C0标志位的值将传递给AH寄存器。AH与Cx的关系是：

在执行标志位传送指令SAHF(Store AH into Flags)后，AH寄存器的各比特位与CPU的8位标志位的对应关系就变成了：

对照上述两个图表可知在比较数值的一系列操作之后C3和C0标志位的值被传送到ZF和CF标志位，以供后续的条件转移指令调用。如果CF和ZF都为0，则JA跳转将会被触发。

很显然，FPU的C3/C2/C0状态位之所以占用寄存器的相应数权，是为了方便把FPU的标志位复制到CPU标志位上、以便进行条件判断。这多半是有意而为之。

GCC 4.8.1 –启用优化选项-O3

Intel P6 系列[11]的FPU指令组新增加了一组指令。这些指令是FUCOMI（比较操作数并设置主CPU的标志位）和FCMOVcc（相当于处理FPU寄存器的CMOVcc指令）。GCC的维护人员采用了全新的指令集设计GCC，显然他们决定不再支持P6以前的CPU（也就是奔腾时代以前的CPU）。

另外，自Intel P6系列CPU起，Intel CPU都整合了FPU。这使得FPU直接修改、检测CPU标志位成为可能。

经GCC 4.8.1优化编译后，可得到如下所示的指令。

指令清单17.14　Optimizing GCC 4.8.1

fld      QWORD PTR [esp+4]       ; load "a"
fld      QWORD PTR [esp+12]      ; load "b"
; ST0=b, ST1=a
fxch    st(1)
; ST0=a, ST1=b
; compare "a" and "b"
fucomi  st, st(1)
; move ST1 (b here) to ST0 if a<=b
; leave a in ST0 otherwise
fcmovbe st, st(1)
; discard value in ST1
fstp    st(1)
ret

FXCH 指令把栈寄存器ST (1)的值与栈顶 ST (0)的值进行交换，并保留栈顶指针。实际上，如果交换前两条FLD指令、或者把FCMOVBE(BE代表below or equal)替换为FCMOVA(A代表above)指令，那么就可以不用FXCH指令了。或许是因为编译器的优化功能尚未到位。

其后，FUCOMI比较ST(0)（即变量a）和ST(1)的值（即变量b），并在CPU上设置标志位。接下来FCOMVBE指令检查这些标志位，并进行下述操作如果ST(0)ST(1)，即ab，就把ST（1）的值（此时是a）复制给ST（0）寄存器。条件不成立，就是a>b的情况，它将保持ST（0）的值不变。

最后一条FSTP指令将ST(0) 寄存器中的值复制到目标操作数ST(1)，然后弹出寄存器堆栈。为了弹出寄存器堆栈，处理器将 ST(0) 寄存器标记为空，并调整硬件上的堆栈指针（TOP）、便之递增1。

使用GDB调试这个程序，可得到如下所示的指令。

指令清单17.15　Optimizing GCC 4.8.1 and GDB

  1 dennis@ubuntuvm:~/polygon$ gcc -O3 d_max.c -o d_max -fno-inline
  2 dennis@ubuntuvm:~/polygon$ gdb d_max
  3 GNU gdb (GDB) 7.6.1-ubuntu
  4 Copyright (C) 2013 Free Software Foundation, Inc.
  5 License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
  6 This is free software: you are free to change and redistribute it.
  7 There is NO WARRANTY, to the extent permitted by law. Type "show copying"
  8 and "show warranty" for details.
  9 This GDB was configured as "i686-linux-gnu".
 10 For bug reporting instructions, please see:
 11 <http://www.gnu.org/software/gdb/bugs/>...
 12 Reading symbols from /home/dennis/polygon/d_max...(no debugging symbols found)...done.
 13 (gdb) b d_max
 14 Breakpoint 1 at 0x80484a0
 15 (gdb) run
 16 Starting program: /home/dennis/polygon/d_max
 17
 18 Breakpoint 1, 0x080484a0 in d_max ()
 19 (gdb) ni
 20 0x080484a4 in d_max ()
 21 (gdb) disas $eip
 22 Dump of assembler code for function d_max:
 23    0x080484a0 <+0>:    fldl 0x4(%esp)
 24 => 0x080484a4 <+4>:    fldl 0xc(%esp)
 25    0x080484a8 <+8>:    fxch %st(1)
 26    0x080484aa <+10>:   fucomi %st(1),%st
 27    0x080484ac <+12>:   fcmovbe %st(1),%st
 28    0x080484ae <+14>:   fstp %st(1)
 29    0x080484b0 <+16>:   ret
 30 End of assembler dump.
 31 (gdb) ni
 32 0x080484a8 in d_max ()
 33 (gdb) info float
 34   R7: Valid   0x3fff9999999999999800 +1.199999999999999956
 35 =>R6: Valid   0x4000d999999999999800 +3.399999999999999911
 36   R5: Empty   0x00000000000000000000
 37   R4: Empty   0x00000000000000000000
 38   R3: Empty   0x00000000000000000000
 39   R2: Empty   0x00000000000000000000
 40   R1: Empty   0x00000000000000000000
 41   R0: Empty   0x00000000000000000000
 42
 43 Status Word:         0x3000
 44                        TOP: 6
 45 Control Word:        0x037f IM DM ZM OM UM PM
 46                        PC: Extended Precision (64-bits)
 47                        RC: Round to nearest
 48 Tag Word:            0x0fff
 49 Instruction Pointer: 0x73:0x080484a4
 50 Operand Pointer:     0x7b:0xbffff118
 51 Opcode:              0x0000
 52 (gdb) ni
 53 0x080484aa in d_max ()
 54 (gdb) info float
 55   R7: Valid   0x4000d999999999999800 +3.399999999999999911
 56 =>R6: Valid   0x3fff9999999999999800 +1.199999999999999956
 57   R5: Empty   0x00000000000000000000
 58   R4: Empty   0x00000000000000000000
 59   R3: Empty   0x00000000000000000000
 60   R2: Empty   0x00000000000000000000
 61   R1: Empty   0x00000000000000000000
 62   R0: Empty   0x00000000000000000000
 63
 64 Status Word:         0x3000
 65                        TOP: 6
 66 Control Word:        0x037f IM DM ZM OM UM PM
 67                        PC: Extended Precision (64-bits)
 68                        RC: Round to nearest
 69 Tag Word:            0x0fff
 70 Instruction Pointer: 0x73:0x080484a8
 71 Operand Pointer:     0x7b:0xbffff118
 72 Opcode:              0x0000
 73 (gdb) disas $eip
 74 Dump of assembler code for function d_max:
 75    0x080484a0 <+0>:     fldl 0x4(%esp)
 76    0x080484a4 <+4>:     fldl 0xc(%esp)
 77    0x080484a8 <+8>:     fxch %st(1)
 78 => 0x080484aa <+10>:    fucomi %st(1),%st
 79    0x080484ac <+12>:    fcmovbe %st(1),%st
 80    0x080484ae <+14>:    fstp %st(1)
 81    0x080484b0 <+16>:    ret
 82 End of assembler dump.
 83 (gdb) ni
 84 0x080484ac in d_max ()
 85 (gdb) info registers
 86 eax           0x1      1
 87 ecx           0xbffff1c4    -1073745468
 88 edx           0x8048340     134513472
 89 ebx           0xb7fbf000    -1208225792
 90 esp           0xbffff10c    0xbffff10c
 91 ebp           0xbffff128    0xbffff128
 92 esi           0x0      0
 93 edi           0x0      0
 94 eip           0x80484ac     0x80484ac
 95 eflags        0x203    [ CF IF ]
 96 cs            0x73     115
 97 ss            0x7b     123
 98 ds            0x7b     123
 99 es            0x7b     123
100 fs            0x0      0
101 gs            0x33     51
102 (gdb) ni
103 0x080484ae in d_max ()
104 (gdb) info float
105   R7: Valid  0x4000d999999999999800 +3.399999999999999911
106 =>R6: Valid  0x4000d999999999999800 +3.399999999999999911
107   R5: Empty  0x00000000000000000000
108   R4: Empty  0x00000000000000000000
109   R3: Empty  0x00000000000000000000
110   R2: Empty  0x00000000000000000000
111   R1: Empty  0x00000000000000000000
112   R0: Empty  0x00000000000000000000
113
114 Status Word:         0x3000
115                        TOP: 6
116 Control Word:        0x037f IM DM ZM OM UM PM
117                        PC: Extended Precision (64-bits)
118                        RC: Round to nearest
119 Tag Word:            0x0fff
120 Instruction Pointer: 0x73:0x080484ac
121 Operand Pointer:     0x7b:0xbffff118
122 Opcode:              0x0000
123 (gdb) disas $eip
124 Dump of assembler code for function d_max:
125    0x080484a0 <+0>:    fldl 0x4(%esp)
126    0x080484a4 <+4>:    fldl 0xc(%esp)
127    0x080484a8 <+8>:    fxch %st(1)
128    0x080484aa <+10>:   fucomi %st(1),%st
129    0x080484ac <+12>:   fcmovbe %st(1),%st
130 => 0x080484ae <+14>:   fstp %st(1)
131 0x080484b0 <+16>:      ret
132 End of assembler dump.
133 (gdb) ni
134 0x080484b0 in d_max ()
135 (gdb) info float
136 =>R7: Valid   0x4000d999999999999800 +3.399999999999999911
137   R6: Empty   0x4000d999999999999800
138   R5: Empty   0x00000000000000000000
139   R4: Empty   0x00000000000000000000
140   R3: Empty   0x00000000000000000000
141   R2: Empty   0x00000000000000000000
142   R1: Empty   0x00000000000000000000
143   R0: Empty   0x00000000000000000000
144
145 Status Word:         0x3800
146                        TOP: 7
147 Control Word:        0x037f IM DM ZM OM UM PM
148                        PC: Extended Precision (64-bits)
149                        RC: Round to nearest
150 Tag Word:            0x3fff
151 Instruction Pointer: 0x73:0x080484ae
152 Operand Pointer:     0x7b:0xbffff118
153 Opcode:              0x0000
154 (gdb) quit
155 A debugging session is active.
156
157       Inferior 1 [process 30194] will be killed.
158
159 Quit anyway? (y or n) y
160 dennis@ubuntuvm:~/polygon$

使用“ni”指令可以执行头两条FLD指令。

再使用第33行的指令检查FPU寄存器的状态。

前文（17.5.1节）介绍过，FPU寄存器属于循环缓冲区的逻辑构造，它实际上不是标准的栈结构。所以GDB不会把寄存器名称显示为助记符“ST(x)”，而是显示出FPU寄存器的内部名称，Rx。第35行所示的箭头表示该行的寄存器是当前的栈顶。您可从第44行的“Status Word/状态字”里找到栈顶寄存器的编号。本例中栈顶状态字为6，所以栈顶是6号内部寄存器。

在第54行处，FXCH指令交换了变量a和变量b的数值。

在执行过第83行的FUCOMI指令后，我们可在第95行看到CF为1。

第104行，FCMOVBE指令复制变量b的值。

第136行的FSTP指令会调整栈顶，也会弹出一个值。而后TOP的值变为7，FPU栈指针指向第7寄存器。

17.7.2　ARM

Optimizing Xcode 4.6.3 (LLVM) (ARM mode)

指令清单17.16　Optimizing Xcode 4.6.3 (LLVM) (ARM mode)

VMOV                 D16, R2, R3 ; b
VMOV                 D17, R0, R1 ; a
VCMPE.F64            D17, D16
VMRS                 APSR_nzcv, FPSCR
VMOVGT.F64           D16, D17 ; copy "a" to D16
VMOV                 R0, R1, D16
BX                   LR

这段程序的代码很简短。函数把输入变量存储到D17、D16寄存器之后，使用VCMPE指令比较这两个变量的值。与x86处理器相仿，ARM处理器也有自己的状态寄存器和标识寄存器、其协作处理器也存在相关的FPSCR[12]。

虽然ARM模式的指令集存在条件转移指令，但是它的条件转移指令都不能直接访问协作处理器的状态寄存器。这个特点和x86系统相同。所以，ARM平台也有专门的指令把协作处理器的4个标识位（N、Z、C、V）复制到通用状态寄存器的ASPR寄存器里，即VMRS指令。

VMOVGT是FPU上的MOVGT指令，在操作数大于另一个操作数时进行赋值操作。这个指令的后缀GT代表“Greater Than”。

如果触发了VMOVGT指令，则会把D17里变量a的值复制到D16寄存器里。

否则，D16寄存器将会保持原来的变量b的值。

倒数第二条指令VMOV的作用是制备返回值，它把D16寄存器里64位的值拆分为1对32的值，并分别存储于通用寄存器R0和R1里。

Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

使用Xcode 4.6.3（开启优化选项）、以Thumb-2模式编译上述程序可得到如下所示的指令。

指令清单17.17　Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

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

Thumb-2模式的代码和ARM模式的程序大体相同。确实，很多ARM模式的指令都存在对应的依相应条件才会执行的衍生指令。

但是Thumb模式没有这种衍生的执行条件指令。Thumb模式的opcode只有16位。这个空间存储不下条件判断表达式所需的那4位的存储空间。

而扩充后的Thumb-2指令集则没有上述缺陷，它们可以封装Thumb模式欠缺的条件判断表达式。

在IDA显示的汇编指令清单里，我们可以看到Thumb-2模式的代码里也出现VMOVGT指令。

此处的实际指令是VMOV指令，IDA为其添加了－GT后缀。为了直观地体现前面那条指令“IT GT”的条件判断作用，IDA在此使用了伪指令。

IT指令与所谓的if－then语句存在明确的对应关系。在IT指令之后的指令（最多4条指令），相当于在then语句模块里的一组条件运行指令。在本例中“IT GT”的涵义是：如果前面比较的数值，第一个值“大于/Greater Than”第二个值，则执行后续模块的1条指令。

我们来看下“愤怒的小鸟（iOS版）”里的代码片段。

指令清单17.18　Angry Birds Classic

...
ITE NE
VMOVNE           R2, R3, D16
VMOVEQ           R2, R3, D17
BLX              _objc_msgSend ; not prefixed
...

“ITE”是“if-then-else”的缩写。这个指令后有两条指令：第一条就是then模块，第二条就是else模块。

我们再从“愤怒的小鸟”里找段更为复杂的代码。

指令清单17.19　Angry Birds Classic

...
ITTTT EQ
MOVEQ           R0, R4
ADDEQ           SP, SP, #0x20
POPEQ.W         {R8,R10}
POPEQ           {R4-R7,PC}
BLX             ___stack_chk_fail ; not prefixed
...

ITTTT里有4个T，代表then语句有4条指令。根据这个信息，IDA给后续的4条指令添加了EQ伪后缀。

确实有ITEEE EQ这种形式的指令，代表“if-then-else-else-else-else”。在解析到这条指令后，IDA会给其后的5条指令依次添加下述后缀：

-EQ
-NE
-NE
-NE

我们继续分析“愤怒的小鸟”里的其他程序片段。

指令清单17.20　Angry Birds Classic

...
CMP.W           R0, #0xFFFFFFFF
ITTE LE
SUBLE.W         R10, R0, #1
NEGLE           R0, R0
MOVGT           R10, R0
MOVS            R6, #0          ; not prefixed
CBZ             R0, loc_1E7E32  ; not prefixed
...

ITTELE表示如果“小于或等于/LE(Less or Equal)”的条件成立，则执行then模块的2条指令，否则（“大于”情况下）执行else模块的第3条指令。

虽然I-T-E类型的指令可以有多个T和多个E，但是编译器还没有聪明到按需分配所有排列组合的程序。以“愤怒的小鸟”（iOS经典版）为例，那个时候的编译器只能分配“IT,ITE,ITT,ITTE,ITTT,ITTTT”这几种判断语句。调查IDA生成的汇编指令清单就可以验证这点。在生成汇编指令清单文件的时候，启用相关选项IDA同步输出每条指令的4字节opcode。因为IT指令的高16位的opcode是0xBF，所以我们应当使用的Linux分析指令是：

cat AngryBirdsClassic.lst | grep " BF" | grep "IT" > results.lst

另外，如果您要使用ARM的汇编语言手工编写Thumb-2模式的应用程序，那么只要您在指令后面添加相应的条件判断后缀，编译器就会自动添加相应的IT指令验证相应标志位。

Non-optimizing Xcode 4.6.3 (LLVM) (ARM mode)

指令清单17.21　Non-optimizing Xcode 4.6.3 (LLVM) (ARM mode)

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，所以操作略微烦琐。其他方面很好理解。

Optimizing Keil 6/2013 (Thumb mode)

使用Keil 6/2013（开启优化选项）、以Thumb模式编译上述程序可得到如下所示的指令。

指令清单17.22　Optimizing Keil 6/2013 (Thumb mode)

           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}

因为运行Thumb指令的硬件平台未必会有FPU硬件，所以Keil不会生成FPU的硬指令。因此在比较浮点数时，Keil没有直接使用FPU的比较指令，而是使用了额外的库函数__aeabi_cdrcmple进行仿真处理。

需要注意的是，本程序调用的仿真函数会在比较数值后保留CPU标志位，所以后面可以直接执行BCS（B-Carry Set，大于或等于的情况下触发B跳转）之类的条件执行指令。

17.7.3　ARM64

Optimizing GCC (Linaro) 4.9

d_max:
; D0 - a, D1 - b
          fcmpe    d0, d1
          fcsel    d0, d0, d1, gt
; now result in D0
          ret

ARM64处理器的FPU指令集，能够不通过FPSCR直接设置APSR。至少，在逻辑上FPU不再独立于主处理器。此处的FCMPE指令负责比较D0和D1寄存器中的值（即函数的第一、第二参数），并根据比较结果设置相应的APSR标识位（N、Z、C、V）。

FCSEL（Floating Conditional Select）首先判断条件GT（Greater Than）是否成立，然后会选择性地复制D0或D1的值到D0寄存器。需要强调的是，在进行条件判断的时候，这个指令依据APSR寄存器里的标识、而非FPSCR里的标识进行判断。相比早期的CPU而言，ARM64平台的这种“可直接访问APSR”的特性算得上是一种进步。

如果条件表达式（GT）为真，那么D0将复制D0的值（即不发生值变化）。如果该条件表达式不成立，则D0将复制D1寄存器的值。

Non-optimizing GCC (Linaro) 4.9

d_max:
; save input arguments in "Register Save Area"
        sub      sp, sp, #16
        str      d0, [sp,8]
        str      d1, [sp]
; reload values
        ldr      x1, [sp,8]
        ldr      x0, [sp]
        fmov     d0, x1
        fmov     d1, x0
; D0 - a, D1 - b
        fcmpe    d0, d1
        ble      .L76
; a>b; load D0 (a) into X0
        ldr      x0, [sp,8]
        b        .L74
.L76:
; a<=b; load D1 (b) into X0
        ldr      x0, [sp]
.L74:
; result in X0
        fmov     d0, x0
; result in D0
        add      sp, sp, 16
        ret

在关闭优化编译功能之后，编译出来的程序很庞大。首先，函数把输入参数存储于局部数据栈（Register Save Area）。接着把这些值复制到X0/X1寄存器，再把它们复制到D0/D1中、使用FCMPE进行比较。虽然这种程序的效率不高，不过在非优化模式下，编译器就本来是这样生成程序的。FCMPE在进行比较之后设置相应的APSR标识。不过，我们可以看出编译器没有考虑使用更为方便的FCSEL指令。所以它使用了较为古老的方式进行编译：它在此次分配了BLE（Branch if Less than or Equal）指令。在a>b的情况下，变量a的值将传递到X0寄存器；否则，变量b的值将传递给X0寄存器。最终，X0的值传递给D0寄存器，成为函数的返回值。

练习题

请在不使用新指令（包括FCSEL指令）的前提下，优化本例的程序。

Optimizing GCC (Linaro) 4.9—float

在把参数的数据类型从double替换为float之后，再进行编译：

float f_max (float a, float b)
{
        if (a>b)
                return a;

        return b;
};

f_max:
; S0 - a, S1 - b
        fcmpe   s0, s1
        fcsel   s0, s0, s1, gt
; now result in S0
        ret

程序使用了S-字头的寄存器，而不再使用D-字头寄存器。这是因为S-字头寄存器的32位（即64位D字头寄存器的低32位）已经满足单精度浮点的存储需要了。

17.7.4　MIPS

即使是当今最受欢迎的MIPS处理器，其协作处理器也只能设置一个条件标识位。供CPU访问。早期的MIPS处理器只有一个标识条件位（即FCC0），现已逐步扩展到了8个（即FCC7～FCC0）。这些条件标识位位于浮点条件码寄存器（FCCR）。

指令清单17.23　Optimizing GCC 4.4.5 (IDA)

d_max:
; set FPU condition bit if $f14<$f12 (b<a):
                c.lt.d     $f14, $f12
                or         $at, $zero ; NOP
; jump to locret_14 if condition bit is set
                bc1t       locret_14
; this instruction is always executed (set return value to "a"):
                mov.d      $f0, $f12  ; branch delay slot
; this instruction is executed only if branch was not taken (i.e., if b>=a)
; set return value to "b":
                mov.d      $f0, $f14
locret_14:
                jr         $ra
                or         $at, $zero ; branch delay slot, NOP

“C.LT.D”是比较两个数值的指令。在它的名称中，“LT”表示条件为“Less Than”，“D”表示其数据类型为double。它将根据比较的结果设置、或清除FCC0条件位。

“BC1T”检测FCC0位，如果该标识位被置位（值为1）则进行跳转。“T”是True的缩写，表示该指令的运行条件是“标识位被置位（True）”。实际上确实存在对应的BC1F指令，在判定条件为False的时候进行跳转。

无论上述条件转移指令是否发生跳转，它都决定了$F0的最终取值。

17.8　栈、计算器及逆波兰表示法

现在，我们可以理解部分旧式计算器采取逆波兰表示法[13]的道理了。例如，在计算“12+34”时，这种计算器要依次按下“12”、“34”和加号。这种计算器采用了堆栈机器（stack machine）的构造。逆波兰记法不需要括号来标识操作符的优先级。所以，在计算复杂表达式时，这种构造计算器的操作十分简单。

17.9　x64

有关x86-64系统处理浮点数的方法，请参见本书第27章。

17.10　练习题

17.10.1　题目1

请去除17.7.1节所示例子中的FXCH指令，进行改写并进行测试。

17.10.2　题目2

请描述下述代码的功能。

指令清单17.24　Optimizing MSVC 2010

__real@4014000000000000 DQ 04014000000000000r   ; 5

_a1$ = 8        ;size=8
_a2$ = 16       ;size=8
_a3$ = 24       ;size=8
_a4$ = 32       ;size=8
_a5$ = 40       ;size=8

_f      PROC
        fld     QWORD PTR _a1$[esp-4]
        fadd    QWORD PTR _a2$[esp-4]
        fadd    QWORD PTR _a3$[esp-4]
        fadd    QWORD PTR _a4$[esp-4]
        fadd    QWORD PTR _a5$[esp-4]
        fdiv    QWORD PTR __real@4014000000000000
        ret     0
_f      ENDP

指令清单17.25　Non-optimizing Keil 6/2013 (Thumb mode/compiled for Cortex-R4F CPU)

f PROC
        VADD.F64 d0,d0,d1
        VMOV.F64 d1,#5.00000000
        VADD.F64 d0,d0,d2
        VADD.F64 d0,d0,d3
        VADD.F64 d2,d0,d4
        VDIV.F64 d0,d2,d1
        BX        lr
        ENDP

指令清单17.26　Optimizing GCC 4.9 (ARM64)

f:
        fadd    d0, d0, d1
        fmov    d1, 5.0e+0
        fadd    d2, d0, d2
        fadd    d3, d2, d3
        fadd    d0, d3, d4
        fdiv    d0, d0, d1
        ret

指令清单17.27　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:

arg_10           =  0x10
arg_14           =  0x14
arg_18           =  0x18
arg_1C           =  0x1C
arg_20           =  0x20
arg_24           =  0x24

                 lwc1     $f0, arg_14($sp)
                 add.d    $f2, $f12, $f14
                 lwc1     $f1, arg_10($sp)
                 lui      $v0, ($LC0 >> 16)
                 add.d    $f0, $f2, $f0
                 lwc1     $f2, arg_1C($sp)
                 or       $at, $zero
                 lwc1     $f3, arg_18($sp)
                 or       $at, $zero
                 add.d    $f0, $f2
                 lwc1     $f2, arg_24($sp)
                 or       $at, $zero
                 lwc1     $f3, arg_20($sp)
                 or       $at, $zero
                 add.d    $f0, $f2
                 lwc1     $f2, dword_6C
                 or       $at, $zero
                 lwc1     $f3, $LC0
                 jr       $ra
                 div.d    $f0, $f2

$LC0:            .word 0x40140000   # DATA XREF: f+C
                                    # f+44
dword_6C:        .word 0            # DATA XREF: f+3C

[1]　中文叫“堆栈机”或“堆栈结构机”。请参见http://en.wikipedia.org/wiki/Stack_machine。

[2]　请参见http://en.wikipedia.org/wiki/Forth_%28programming_language%29。

[3]　当初，为了使没有FPU的32位计算机（例如80386\80486 SX）兼容其研发的DOOM游戏，John Carmack设计了一套“软”FPU运算系统。这种系统使用CPU寄存器的高16位地址存储整数部分、低16位地址存储浮点数值的小数部分，仅通过标准32位通用寄存器即可实现浮点运算。更多详情请参阅：https://en.wikipedia.org/wiki/Fixed-point_arithmetic。

[4]　如需详细了解IEEE 754格式规范，请参见http://en.wikipedia.org/wiki/IEEE_754-2008。

[5]　如需详细了解这三种不同精度的数据类型，请参见：
http://en.wikipedia.org/wiki/Single-precision_floating-point_format。
http://en.wikipedia.org/wiki/Double-precision_floating-point_format。
http://en.wikipedia.org/wiki/Extended_precision。

[6]　https://en.wikipedia.org/wiki/Forth_(programming_language)。

[7]　Stack machines，https://en.wikipedia.org/wiki/Stack_machine。

[8]　Pentium Pro, Pentium II等CPU。

[9]　http://en.wikipedia.org/wiki/NaN。

[10]　cc即条件判断指令的通称，如AE、BE、E等。

[11]　Pentium Pro, Pentium-II之后的CPU。

[12]　ARM平台的Floating-Point Status and Control Register。

[13]　Reverse Polish notation，请参见https://en.wikipedia.org/wiki/Reverse_Polish_ notation。

第18章 数组

在内存中，数组就是按次序排列的、相同数据类型的一组数据。

18.1　简介

#include <stdio.h>

int main()
{
    int a[20];
    int i;

    for (i=0; i<20; i++)
        a[i]=i*2;

    for (i=0; i<20; i++)
        printf ("a[%d]=%d\n", i, a[i]);

    return 0;
};

18.1.1　x86

MSVC

使用MSVC 2008编译上述程序可得如下所示的指令。

指令清单18.1　MSVC 2008

_TEXT     SEGMENT
_i$ = -84              ; size = 4
_a$ = -80              ; size = 80
_main      PROC
    push   ebp
    mov    ebp, esp
    sub    esp, 84    ; 00000054H
    mov    DWORD PTR _i$[ebp], 0
    jmp    SHORT $LN6@main
$LN5@main:
    mov    eax, DWORD PTR _i$[ebp]
    add    eax,  1
    mov    DWORD PTR _i$[ebp], eax
$LN6@main:
    cmp    DWORD PTR _i$[ebp], 20; 00000014H
    jge    SHORT $LN4@main
    mov    ecx, DWORD PTR _i$[ebp]
    shl    ecx, 1
    mov    edx, DWORD PTR _i$[ebp]
    mov    DWORD PTR _a$[ebp+edx*4], ecx
    jmp    SHORT $LN5@main
$LN4@main:
    mov    DWORD PTR _i$[ebp], 0
    jmp    SHORT $LN3@main
$LN2@main:
    mov    eax, DWORD PTR _i$[ebp]
    add    eax, 1
    mov    DWORD PTR _i$[ebp], eax
$LN3@main:
    cmp    DWORD PTR _i$[ebp], 20    ; 00000014H
    jge    SHORT $LN1@main
    mov    ecx, DWORD PTR _i$[ebp]
    mov    edx, DWORD PTR _a$[ebp+ecx*4]
    push   edx
    mov    eax, DWORD PTR _i$[ebp]
    push   eax
    push   OFFSET $SG2463
    call   _printf
    add    esp, 12        ; 0000000cH
    jmp    SHORT $LN2@main
$LN1@main:
    xor    eax, eax
    mov    esp, ebp
    pop    ebp
    ret    0
_main      ENDP

这里除去两个循环之外没有非常特别之处。第一个循环填充数据，第二个循环打印数据。“shl ecx, 1”指令所做的运算是ecx=ecx*2，更详细的介绍请参见16.2.1节。

程序为数组申请了80字节的栈空间，以存储20个4字节元素。

现在使用OllyDbg打开这个执行程序。

如图18.1所示，数组中的每个元素都是32位的int型数据，数组每个元素的值都是其索引值的2倍。

图18.1　OllyDbg：填充数组

因为全部数组都存储于栈中，所以我们可以在内存数据窗口里看到整个数组。

GCC

若使用GCC 4.4.1编译上述程序，可得到如下所示的指令。

指令清单18.2　GCC 4.4.1

                public main
main            proc near                   ; DATA XREF: _start+17

var_70          = dword ptr -70h
var_6C          = dword ptr -6Ch
var_68          = dword ptr -68h
i_2             = dword ptr -54h
i               = dword ptr -4

                push     ebp
                mov      ebp, esp
                and      esp, 0FFFFFFF0h
                sub      esp, 70h
                mov      [esp+70h+i], 0     ; i=0
                jmp      short loc_804840A

loc_80483F7:
                mov      eax, [esp+70h+i]
                mov      edx, [esp+70h+i]
                add      edx, edx           ; edx=i*2
                mov      [esp+eax*4+70h+i_2], edx
                add      [esp+70h+i], 1     ;  i++

loc_804840A:
                cmp      [esp+70h+i], 13h
                jle      short loc_80483F7
                mov      [esp+70h+i], 0
                jmp      short loc_8048441

loc_804841B:
                mov      eax, [esp+70h+i]
                mov      edx, [esp+eax*4+70h+i_2]
                mov      eax, offset aADD ; "a[%d]=%d\n"
                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

实际上变量a的数据类型是整型指针。严格地说，在把数组传递给函数的时候，传递的数据就是指向第一个元素的指针，我们再根据这个指针就可以轻松地计算出数组每个元素的地址（即指针）。如果使用a[idx]的形式表示数组元素，其中idx是数组元素在数组里的排列序号（即索引号），那么就可以通过数组第一个元素的地址、索引号和数据容量求得各个元素的地址。

举个典型的例子：字符串常量“string”是字符型数组，它的每个字符元素都是const char*型数据。使用索引号之后，我们就可以使用“string”[i]的形式描述字符串中的第i个字符——这正是C/C＋＋表达式的表示方法！

18.1.2　ARM

Non-optimizing Keil 6/2013 (ARM mode)

                EXPORT _main
_main
                STMFD   SP!, {R4,LR}
                SUB     SP, SP, #0x50     ;分配20个int的存储空间

; 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? loop again
; second loop

                MOV     R4, #0            ; i
                B       loc_4C4
loc_4B0
                LDR R2, [SP,R4,LSL#2]     ; (printf的第2个参数) R2=*(SP+R4<<4)（等同于*（SP+R4*4））
                MOV     R1, R4            ; (printf的第1个参数) R1=i
                ADR     R0, aADD          ; "a[%d]=%d\n"
                BL      __2printf
                ADD     R4, R4, #1        ; i=i+1

loc_4C4
                CMP     R4, #20           ; i<20?
                BLT     loc_4B0           ; yes, run loop body again
                MOV     R0, #0            ; value to return
                ADD     SP, SP, #0x50     ; 释放20个int的存储空间
                LDMFD   SP!, {R4,PC}

单个整型数据占32位（4字节）存储空间。以此类推，要存储20个int型数据就需要80（0x50）个字节的空间。函数尾声的“SUB SP, SP, 0x50”指令释放的空间就是存储数组所用的空间。

在两个循环体内，循环控制变量i都使用R4寄存器。

在填充数组元素的时候，编译器用左移1位的运算“LSL#1”实现乘法运算“i×2”，形成了“MOV R0, R4,LSL#1”指令。

“STR R0，［SP, R4, LSL#2］”实现了把R0寄存器的值写入数组。计算数组元素的原理：因为SP可代表数组的起始地址、R4代表变量i，所以可通过i左移2位求得i×4的相对偏移地址，再计算地址“SP＋R4×4”即可推导出数组元素a[i]的指针地址。

在第二个循环里，“LDR R2, [SP,R4,LSL#2]”指令读取a[i]的值。

Optimizing Keil 6/2013 (Thumb mode)

_main
           PUSH    {R4,R5,LR}
; allocate place for 20 int variables + one more variable
           SUB     SP, SP, #0x54

;第一个循环
           MOV     R0，#0       ;i
           MOV     R5, SP       ; pointer to first array element

loc_1CE
           LSLS    R1, R0, #1   ; R1=i<<1 (same as i*2)
           LSLS    R2, R0, #2   ; R2=i<<2 (same as i*4)
           ADDS    R0, R0, #1   ; i=i+1
           CMP     R0, #20      ; i<20?
           STR     R1, [R5,R2]  ; store R1 to *(R5+R2) (same R5+i*4)
           BLT     loc_1CE      ; yes, i<20, run loop body again

;第二个循环
           MOVS    R4, #0       ; i=0
loc_1DC
           LSLS    R0, R4, #2   ; R0=i<<2 (same as i*4)
           LDR     R2, [R5,R0]  ; load from *(R5+R0) (same as R5+i*4)
           MOVS    R1, R4
           ADR     R0, aADD     ; "a[%d]=%d\n"
           BL      __2printf
           ADDS    R4, R4, #1   ; i=i+1
           CMP     R4, #20      ; i<20?
           BLT     loc_1DC      ; yes, i<20, run loop body again
           MOVS    R0, #0       ; value to return
; deallocate Chunk, allocated for 20 int variables + one more variable
           ADD     SP, SP, #0x54
           POP     {R4,R5,PC}

Thumb模式的程序和ARM模式的程序十分相近。在Thumb模式里，位移操作有特定的对应指令（如LSLS）。上述程序借助Thumb模式的指令计算每个数组元素的地址。

另外，本例分配的栈空间比实际需求大了一些。这个栈里的最后4个字节不会被占用。

Non-optimizing GCC 4.9.1 (ARM64)

指令清单18.3　Non-optimizing GCC 4.9.1 (ARM64)

.LC0:
       .string "a[%d]=%d\n"
main:
; save FP and LR in stack frame:
        stp      x29, x30, [sp, -112]!
; set stack frame (FP=SP)
        add      x29, sp, 0
; setting initial counter variable at 0 (WZR is the register always holding zero):
        str      wzr, [x29,108]
; jump to loop condition checking code:
        b        .L2
.L3:
; load value of "i" variable:
        ldr      w0, [x29,108]
; multiplicate it by 2:
        lsl      w2, w0, 1
; find a place of an array in local stack:
        add      x0, x29, 24
; load 32-bit integer from local stack and sign extend it to 64-bit one:
        ldrsw    x1, [x29,108]
; calculate address of element (X0+X1<<2=array address+i*4) ;  and store W2 (i*2) there:
        str      w2, [x0,x1,lsl 2]
; increment counter (i):
        ldr      w0, [x29,108]
        add      w0, w0, 1
        str      w0, [x29,108]
.L2:
; check if we finished:
        ldr      w0, [x29,108]
        cmp      w0, 19
; jump to L3 (loop body begin) if not:
        ble      .L3
; second part of the function begins here.
; setting initial counter variable at 0.
; by the way, the same place in the local stack was used for counter,
; because the same local variable (i) is being used as counter.
        str      wzr, [x29,108]
        b        .L4
.L5:
; calculate array address:
        add      x0, x29, 24
; load "i" value:
        ldrsw    x1, [x29,108]
; load value from the array at the address (X0+X1<<2 = address of array + i*4)
        ldr      w2, [x0,x1,lsl 2]
; load address of the "a[%d]=%d\n" string:
        adrp     x0, .LC0
        add      x0, x0, :lo12:.LC0
; load "i" variable to W1 and pass it to printf() as second argument:
        ldr      w1, [x29,108]
; W2 still contains the value of array element which was just loaded.
; call printf():
        bl       printf
; increment "i" variable:
        ldr      w0, [x29,108]
        add      w0, w0, 1
        str      w0, [x29,108]
.L4:
; are we finished?
        ldr      w0, [x29,108]
        cmp      w0, 19
; jump to the loop body begin if not:
        ble      .L5
; return 0
        mov      w0, 0
; restore FP and LR:
        ldp      x29, x30, [sp], 112
        ret

18.1.3　MIPS

面向MIPS的编译器会大量使用S-字头寄存器。因为S-字头寄存器不属于临时寄存器，所以函数在序言和尾声部分对这些寄存器的值进行了备份和还原。

指令清单18.4　Optimizing GCC 4.4.5 (IDA)

main:

var_70     = -0x70
var_68     = -0x68
var_14     = -0x14
var_10     = -0x10
var_C      = -0xC
var_8      =-8
var_4      =-4
; function prologue:
                lui       $gp, (__gnu_local_gp >> 16)
                addiu     $sp, -0x80
                la        $gp, (__gnu_local_gp & 0xFFFF)
                sw        $ra, 0x80+var_4($sp)
                sw        $s3, 0x80+var_8($sp)
                sw        $s2, 0x80+var_C($sp)
                sw        $s1, 0x80+var_10($sp)
                sw        $s0, 0x80+var_14($sp)
                sw        $gp, 0x80+var_70($sp)
                addiu     $s1, $sp, 0x80+var_68
                move      $v1, $s1
                move      $v0, $zero
; that value will be used as a loop terminator.
; it was precalculated by GCC compiler at compile stage:
                li        $a0, 0x28  # '('
loc_34:                                         # CODE XREF: main+3C
; store value into memory:
                sw        $v0, 0($v1)
; increase value to be stored by 2 at each iteration:
                addiu     $v0, 2
; loop terminator reached?
                bne       $v0, $a0, loc_34
; add 4 to address anyway:
                addiu     $v1, 4
; array filling loop is ended
; second loop begin
                la        $s3, $LC0             # "a[%d]=%d\n"
; "i" variable will reside in $s0:
                move      $s0, $zero
                li        $s2, 0x14
loc_54:                                         # CODE XREF: main+70
; call printf():
                lw        $t9, (printf & 0xFFFF)($gp)
                lw        $a2, 0($s1)
                move      $a1, $s0
                move      $a0, $s3
                jalr      $t9
; increment "i":
                addiu     $s0, 1
                lw        $gp, 0x80+var_70($sp)
; jump to loop body if end is not reached:
                bne       $s0, $s2, loc_54
; move memory pointer to the next 32-bit word:
                addiu     $s1, 4
; function epilogue
                lw        $ra, 0x80+var_4($sp)
                move      $v0, $zero
                lw        $s3, 0x80+var_8($sp)
                lw        $s2, 0x80+var_C($sp)
                lw        $s1, 0x80+var_10($sp)
                lw        $s0, 0x80+var_14($sp)
                jr        $ra
                addiu     $sp, 0x80

$LC0:           .ascii "a[%d]=%d\n"<0>   # DATA XREF: main+44

编译器对第一个循环使用了代入的技术对变量之进行了等效处理。在第一个循环的循环体中，数组元素的值（$V0的值）是控制变量i的2倍，即i×2，所以在每次迭代后控制变量的增量都是2。另外，数组元素的地址增量为4，编译器单独分配了$V1寄存器给这个指针使用，也令其增量为4。

第二个循环体根据数组索引值i、通过printf()函数依次输出数组元素。编译器首先使用$S0寄存器存储索引值，$S0在每次迭代中的增量为1。与前一个循环体相似，它单独使用$S1寄存器存储内存地址，并使其在迭代间的增量为4。

这便是本书第39章中会提到的循环优化技术。通过这种技术，编译器可尽量避免效率较低的乘法运算。

18.2　缓冲区溢出

18.2.1　读取数组边界以外的内容

综上所述，编译器借助索引index、以array[index]的形式表示数组。若仔细审查二进制程序的代码，那么您可能会发现程序并没有对数组进行边界检查、没有判断索引是否在20以内。那么，如果程序访问数组边界以外的数据，又会发生什么情况？C/C++编译器确实不会进行边界检查，这也是它备受争议之处。

编译、并运行下面的程序，会发现整个过程中不会遇到错误提示。

#include <stdio.h>

int main()
{
        int a[20];
        int i;

        for (i=0; i<20; i++)
                  a[i]=i*2;

        printf ("a[20]=%d\n", a[20]);

        return 0;
};

使用MSVC 2008编译后可得到如下所示的指令。

指令清单18.5　Non-optimizing MSVC 2008

$SG2474 DB     'a[20]=%d', 0aH, 00H

_i$ = -84 ; size = 4
_a$ = -80 ; size = 80
_main    PROC
    push   ebp
    mov    ebp, esp
    sub    esp, 84
    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
    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+80]
    push   eax
    push   OFFSET $SG2474 ; 'a[20]=%d'
    call   DWORD PTR __imp__printf
    add    esp, 8
    xor    eax, eax
    mov    esp, ebp
    pop    ebp
    ret    0
_main     ENDP
_TEXT     ENDS
END

其执行结果如图18.2所示。

图18.2　OllyDbg：运算结果

这是地址接近栈内数组的数据的值。它的地址距离数组的起始地址有80字节。

让我们通过OllyDbg看看这个数从哪里来。如图18.3所示，使用OllyDbg加载这个程序，找到数组最后一个值之后的数据。

图18.3　OllyDbg：读取数组中的第20个元素、并使用printf()输出

这是什么的值？根据栈结构来判断，这个值是先前保存的EBP寄存器的值。我们继续跟踪这个程序，如图18.4所示，看看它的提取过程。

图18.4　OllyDbg：还原EBP寄存器的值

到底有没有什么办法控制数组的访问边界问题呢？如果需要控制数组边界，就要修改编译器，强制其生成的程序检查索引index是否在边界之内。某些高级编程语言，如Python和Java，就会进行边界检查。但是这种控制会严重影响程序的性能。

18.2.2　向数组边界之外的地址赋值

既然我们可以“非法地”利用栈来读取数组边界以外的数值，那么我们是否也可以向数组边界以外的地址里写入数据呢？

可通过下面的程序来进行验证。

#include <stdio.h>

int main()
{
        int a[20];
        int i;

        for (i=0; i<30; i++)
                 a[i]=i;

        return 0;
};

MSVC

使用MSVC编译后可得到如下所示的指令。

指令清单18.6　Non-optimizing MSVC 2008

_TEXT    SEGMENT
_i$ = -84                             ; size = 4
_a$ = -80                             ; size = 80
_main    PROC
 push    ebp
 mov     ebp, esp
 sub     esp, 84
 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]      ; 这条指令用处不大
 mov     DWORD PTR _a$[ebp+ecx*4], edx; 因为也可以用ECX 取代edx
 jmp     SHORT $LN2@main
$LN1@main:
 xor     eax, eax
 mov     esp, ebp
 pop     ebp
 ret     0
_main    ENDP

运行这个可执行程序的时候，程序崩溃了。崩溃并没什么希奇的，但我们要研究一下它是在哪里崩溃的。

使用OllyDbg加载整个程序，等到写满30个元素，如图18.5所示。

图18.5　OllyDbg：恢复EBP的值之后

然后逐步执行程序，等待函数结束，如图18.6所示。

图18.6　OllyDbg：对EIP进行出栈操作，但是OllyDbg找不到0x15处的程序代码

现在，我们再来分析寄存器状态。

现在EIP的值（PC）是0x15。这不是合法的程序地址——至少对于win32系统来说这个值有问题！程序在此处发生异常。同样有趣的是，这时EBP的值是0x14，ECX和EDX的值是0x1D。

我们一起回顾一下栈结构。

待程序进入main()函数之时，程序使用栈来保存EBP寄存器的值。然后程序分配了84个字节，用于存储数组和变量i。计算方法是“(20+1)*sizeof(int)”。栈顶指针ESP现在指向栈内的变量_i，在执行PUSH入栈操作之后它会指向_i的下一个地址。

即，main()函数的栈结构如下：

ESP | i所占用的4字节
ESP+4 | a[20]占用的80字节
ESP+84 | 保存过的EBP
ESP+88 | 返回地址

赋值给a[19]的时候，数组a[]已经被全部赋值。

赋值给a[20]，实际修改的是栈里保存的EBP。

观察程序崩溃时的寄存器状态。本例将数字20（0x14）赋值给a[19]。在函数退出之前，CPU会通过栈恢复EBP的初始值。本例它会收到的值是20（0x14）最后运行RET指令，相当于执行POP EIP指令。

RET指令将程序的控制权传递给栈里的返回地址（该地址为CRT内部调用main()的地址），不过这个RA的值被改为21（0x15）。CPU会寻找0x15处的代码，以继续执行程序。但是那处地址里没有可执行代码，所以程序就崩溃了。

恭喜！您已经了解了缓冲区溢出[1]的精妙之处。

设想一下：用字符串替代int数组，刻意构造个超长字符串，把字符串传递给程序；因为函数不会检查字符串长度，会直接赋值给较短的缓冲区，您就可以强制这个程序跳转到其他程序的地址。纸上谈兵固然简单，但是无论实际情况再怎么复杂，其原理都是一样的。

GCC

使用GCC 4.4.1编译上述程序，可得到：

                public main
main            proc near

                = dword ptr -54h
i               = dword ptr -4

                push    ebp
                mov     ebp, esp
                sub     esp, 60h ; 96
                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系统里执行这个程序会引发段错误/Segmentation fault。

我们使用GDB Debugger调试它：

(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)

因为Linux和Windows的栈结构不完全相同，所以Linux/Win32下寄存器的值也存在细微的差别。

18.3　缓冲区溢出的保护方法

无论C/C++程序员是粗心大意还是严肃认真，我们都可通过编译器的技术手段防止缓冲区溢出。例如，可以启用MSVC的编译器选项[2]：

/RTCs  启用栈帧的实时检查
/GZ 启用栈检测(/RTCs)

另外，还可以在函数启动时构造一些本地变量，在里面写入随机数，在函数结束之前检查这些值是否发生变化。如果这些值发生了变化，就不应当以RET方式结束函数，而是通过其他手段终止（stop/hang）程序。强行关闭应用程序或许确实不怎么体面，但是总比让他人入侵主机要高明。

有人把这种写入的随机值叫作“百灵鸟canary”，这个绰号来自于“矿工的百灵鸟miners’ canary”。所谓“矿工的百灵鸟”，确实是指过去矿工下矿时所带的百灵鸟。矿工使用百灵鸟来快速检测矿道里的毒气。百灵鸟对井下气体十分敏感，它们在遇到不良气体的时候就会表现得烦躁不安，甚至会直接死亡。

如果启用MSVC的RTC1和RTCs选项编译本章开头的那段程序，就会在汇编指令里看到函数在退出之前调用@_RTC_CheckStackVars@8，用以检测“百灵鸟”是否会报警。

还是来看看GCC预防缓冲区溢出的方法吧。我们借用5.2.4节的例子来进行说明：

#ifdef __GNUC__
#include <alloca.h> // GCC
#else
#include <malloc.h> // MSVC
#endif
#include <stdio.h>

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);
};

即使不指定任何参数，默认情况下GCC 4.7.3也会在代码里加入百灵鸟。

经GCC 4.7.3编译前面的程序可得到如下所示的指令。

指令清单18.7　GCC4.7.3

.LC0:
        .string "hi! %d, %d, %d\n"
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\n"
        mov     DWORD PTR [esp+4], 600
        mov     DWORD PTR [esp], ebx
        mov     eax, DWORD PTR gs:20;
        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；
        jne     .L5
        mov     ebx, DWORD PTR [ebp-4]
        leave
        ret
.L5:
        call    __stack_chk_fail

随机值位于gs:20。在函数启动的时候，程序在栈里写入这个百灵鸟，并且在函数退出之前检测它是否发生了变化、是否与gs:20一致。如果其值发生了变化，将会调用__stack_chk_fail，在Ubuntu 13.04 x86下，我们会在控制台看见下述报错信息：

*** buffer overflow detected ***: ./2_1 terminated
======= Backtrace: =========
/lib/i386-linux-gnu/libc.so.6(__fortify_fail+0x63)[0xb7699bc3]
/lib/i386-linux-gnu/libc.so.6(+0x10593a)[0xb769893a]
/lib/i386-linux-gnu/libc.so.6(+0x105008)[0xb7698008]
/lib/i386-linux-gnu/libc.so.6(_IO_default_xsputn+0x8c)[0xb7606e5c]
/lib/i386-linux-gnu/libc.so.6(_IO_vfprintf+0x165)[0xb75d7a45]
/lib/i386-linux-gnu/libc.so.6(__vsprintf_chk+0xc9)[0xb76980d9]
/lib/i386-linux-gnu/libc.so.6(__sprintf_chk+0x2f)[0xb7697fef]
./2_1[0x8048404]
/lib/i386-linux-gnu/libc.so.6(__libc_start_main+0xf5)[0xb75ac935]
======= Memory map: ========
08048000-08049000 r-xp 00000000 08:01 2097586  /home/dennis/2_1
08049000-0804a000 r--p 00000000 08:01 2097586  /home/dennis/2_1
0804a000-0804b000 rw-p 00001000 08:01 2097586  /home/dennis/2_1
094d1000-094f2000 rw-p 00000000 00:00 0         ［heap］
b7560000-b757b000 r-xp 00000000 08:01 1048602  /lib/i386-linux-gnu/libgcc_s.so.1
b757b000-b757c000 r--p 0001a000 08:01 1048602  /lib/i386-linux-gnu/libgcc_s.so.1
b757c000-b757d000 rw-p 0001b000 08:01 1048602  /lib/i386-linux-gnu/libgcc_s.so.1
b7592000-b7593000 rw-p 00000000 00:00 0
b7593000-b7740000 r-xp 00000000 08:01 1050781  /lib/i386-linux-gnu/libc-2.17.so
b7740000-b7742000 r--p 001ad000 08:01 1050781  /lib/i386-linux-gnu/libc-2.17.so
b7742000-b7743000 rw-p 001af000 08:01 1050781  /lib/i386-linux-gnu/libc-2.17.so
b7743000-b7746000 rw-p 00000000 00:00 0
b775a000-b775d000 rw-p 00000000 00:00 0
b775d000-b775e000 r-xp 00000000 00:00 0          [vdso]
b775e000-b777e000 r-xp 00000000 08:01 1050794  /lib/i386-linux-gnu/ld-2.17.so
b777e000-b777f000 r--p 0001f000 08:01 1050794  /lib/i386-linux-gnu/ld-2.17.so
b777f000-b7780000 rw-p 00020000 08:01 1050794  /lib/i386-linux-gnu/ld-2.17.so
bff35000-bff56000 rw-p 00000000 00:00 0          [stack]
Aborted (core dumped)

gs开头的寄存器就是常说的段寄存器。在MS-DOS和基于DOS的系统里，段寄存器的作用很广泛。但是，今天它的作用发生了变化。简单地说，Linux下的gs 寄存器总是指向TLS（参见第65章）——存储线程的多种特定信息。（Win32环境下的fs寄存器起到Linux下gs寄存器的作用。Win32的fs寄存器指向TIB[3]）

如需详细了解gs寄存器的作用，请在3.11以上版本的Linux源代码中查阅文件（arch/x86/include/ asm/stackprotector.h）。这个文件的注释详细描述了有关变量的功能。

18.3.1　Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

我们使用LLVM编译本章开头的数组程序，看看它是如何使用百灵鸟的：

_main

var_64          = -0x64
var_60          = -0x60
var_5C          = -0x5C
var_58          = -0x58
var_54          = -0x54
var_50          = -0x50
var_4C          = -0x4C
var_48          = -0x48
var_44          = -0x44
var_40          = -0x40
var_3C          = -0x3C
var_38          = -0x38
var_34          = -0x34
var_30          = -0x30
var_2C          = -0x2C
var_28          = -0x28
var_24          = -0x24
var_20          = -0x20
var_1C          = -0x1C
var_18          = -0x18
canary          = -0x14
var_10          = -0x10

    PUSH        {R4-R7,LR}
    ADD         R7, SP, #0xC
    STR.W       R8, [SP,#0xC+var_10]!
    SUB         SP, SP, #0x54
    MOVW        R0, #aObjc_methtype ; "objc_methtype"
    MOVS        R2, #0
    MOVT.W      R0, #0
    MOVS        R5, #0
    ADD         R0, PC
    LDR.W       R8, [R0]
    LDR.W       R0, [R8]
    STR         R0, [SP,#0x64+canary]
    MOVS        R0, #2
    STR         R2, [SP,#0x64+var_64]
    STR         R0, [SP,#0x64+var_60]
    MOVS        R0, #4
    STR         R0, [SP,#0x64+var_5C]
    MOVS        R0, #6
    STR         R0, [SP,#0x64+var_58]
    MOVS        R0, #8
    STR         R0, [SP,#0x64+var_54]
    MOVS        R0, #0xA
    STR         R0, [SP,#0x64+var_50]
    MOVS        R0, #0xC
    STR         R0, [SP,#0x64+var_4C]
    MOVS        R0, #0xE
    STR         R0, [SP,#0x64+var_48]
    MOVS        R0, #0x10
    STR         R0, [SP,#0x64+var_44]
    MOVS        R0, #0x12
    STR         R0, [SP,#0x64+var_40]
    MOVS        R0, #0x14
    STR         R0, [SP,#0x64+var_3C]
    MOVS        R0, #0x16
    STR         R0, [SP,#0x64+var_38]
    MOVS        R0, #0x18
    STR         R0, [SP,#0x64+var_34]
    MOVS        R0, #0x1A
    STR         R0, [SP,#0x64+var_30]
    MOVS        R0, #0x1C
    STR         R0, [SP,#0x64+var_2C]
    MOVS        R0, #0x1E
    STR         R0, [SP,#0x64+var_28]
    MOVS        R0, #0x20
    STR         R0, [SP,#0x64+var_24]
    MOVS        R0, #0x22
    STR         R0, [SP,#0x64+var_20]
    MOVS        R0, #0x24
    STR         R0, [SP,#0x64+var_1C]
    MOVS        R0, #0x26
    STR         R0, [SP,#0x64+var_18]
    MOV         R4, 0xFDA ; "a[%d]=%d\n"
    MOV         R0, SP
    ADDS        R6, R0, #4
    ADD         R4, PC
    B           loc_2F1C
; second loop begin

loc_2F14
    ADDS        R0, R5, #1
    LDR.W       R2, [R6, R5, LSL#2]
    MOV         R5, R0

loc_2F1C
    MOV         R0, R4
    MOV         R1, R5
    BLX         _printf
    CMP         R5, #0x13
    BNE         loc_2F14
    LDR.W       R0, [R8]
    LDR         R1, [SP,#0x64+canary]
    CMP         R0, R1
    ITTTT EQ         ;
    MOVEQ       R0, #0
    ADDEQ       SP, SP, #0x54
    LDREQ.W     R8, [SP+0x64+var_64],#4
    POPEQ       {R4-R7,PC}
    BLX         ___stack_chk_fail

这段Thumb-2模式程序最显著的特点就是循环体被逐一展开了。LLVM编译器判定这样的效率较高。另外，ARM模式下这种指令的效率可能更高。本书就不再进行有关验证了。

函数尾声检测了“百灵鸟”的状态。如果栈内的值没被修改（和R8寄存器所指向的值相等），则会执行ITTTT EQ后面的4条指令——令R0为0、运行函数尾声并且退出函数。如果“百灵鸟”发生了变化，程序将跳过ITTTT EQ后面的4条带“－EQ”的指令，转而调用___stack_chk_fail进行异常处理，实际上会终止个程序。

18.4　其他

现在我们应该可以理解为什么C/C++编译不了下面的程序了。[4]

void f(int size)
{
    int a[size];
...
};

在编译阶段，编译器必须确切地知道需要给数组分配多大的存储空间，它需要事先明确分配局部栈或数据段（全局变量）的格局，所以编译器无法处理上述这种长度可变的数组。

如果无法事先确定数组的长度，那么我们就应当使用malloc()函数分配出一块内存，然后直接按照常规变量数组的方式访问这块内存；或者遵循C99标准（参见ISO07，6.7.5/2）进行处理，但是遵循C99标准而设计出来的程序，内部实现的方法更接近alloca()函数（详情请参阅5.2.4节）。

18.5　字符串指针

本节以下述程序为例。

指令清单18.8　查询月份名称

#include <stdio.h>

const char* month1[]=
{
        "January",
        "February",
        "March",
        "April",
        "May",
        "June",
        "July",
        "August",
        "September",
        "October",
        "November",
        "December"
};

// in 0..11 range
const char* get_month1 (int month)
{
        return month1[month];
};

18.5.1　x64

指令清单18.9　Optimizing MSVC 2013 x64

_DATA   SEGMENT
month1  DQ      FLAT:$SG3122
        DQ      FLAT:$SG3123
        DQ      FLAT:$SG3124
        DQ      FLAT:$SG3125
        DQ      FLAT:$SG3126
        DQ      FLAT:$SG3127
        DQ      FLAT:$SG3128
        DQ      FLAT:$SG3129
        DQ      FLAT:$SG3130
        DQ      FLAT:$SG3131
        DQ      FLAT:$SG3132
        DQ      FLAT:$SG3133
$SG3122 DB     'January', 00H
$SG3123 DB     'February', 00H
$SG3124 DB     'March', 00H
$SG3125 DB     'April', 00H
$SG3126 DB     'May', 00H
$SG3127 DB     'June', 00H
$SG3128 DB     'July', 00H
$SG3129 DB     'August', 00H
$SG3130 DB     'September', 00H
$SG3156 DB     '%s', 0aH, 00H
$SG3131 DB     'October', 00H
$SG3132 DB     'November', 00H
$SG3133 DB     'December', 00H
_DATA   ENDS

month$ = 8
get_month1 PROC
         movsxd  rax, ecx
         lea     rcx, OFFSET FLAT:month1
         mov     rax, QWORD PTR [rcx+rax*8]
         ret     0
get_month1 ENDP

上述程序的功能如下：

第一条指令MOVSXD把ECX的32位整型数值、连同其符号扩展位(sign-extension)扩展为64位整型数据，再存储于RAX寄存器中。ECX存储的“月份”信息是32位整形数据。因为程序随后还要进行64位运算，所以需要把输入变量转换为64位值。

然后函数把指针表的地址存储于RCX寄存器。

最后，函数的输入变量(month)的值乘以8、再与指针表的地址相加。因为是64位系统的缘故，每个地址（即指针）的数据需要占用64位（即8字节）。所以指针表中的每个元素都占用8字节空间。因此，最终字符串的地址要加上month*8。MOV指令不仅完成了字符串地址的计算，而且还完成了指针表的查询。在输入值为1时，函数将返回字符串“February”的指针地址。

编译器的优化编译结果更为直接。

指令清单18.10　Optimizing GCC 4.9 x64

        movsx   rdi, edi
        mov     rax, QWORD PTR month1[0+rdi*8]
        ret

18.5.2　32位MSVC

使用32位MSVC编译器编译上述程序可得如下所示的指令。

指令清单18.11　Optimizing MSVC 2013 x86

_month$ = 8
_get_month1 PROC
        mov     eax, DWORD PTR _month$[esp-4]
        mov     eax, DWORD PTR _month1[eax*4]
        ret     0
_get_month1 ENDP

32位程序就不用把输入值转化为64位数据了。此外，32位系统的指针属于4字节数据，所以相关的计算因子变为了4。

18.5.3　32位ARM

ARM in ARM mode

指令清单18.12　Optimizing Keil 6/2013 (ARM mode)

get_month1 PROC
        LDR      r1,|L0.100|
        LDR      r0,[r1,r0,LSL #2]
        BX       lr
        ENDP

|L0.100|
        DCD      ||.data||

        DCB      "January",0
        DCB      "February",0
        DCB      "March",0
        DCB      "April",0
        DCB      "May",0
        DCB      "June",0
        DCB      "July",0
        DCB      "August",0
        DCB      "September",0
        DCB      "October",0
        DCB      "November",0
        DCB      "December",0

        AREA ||.data||, DATA, ALIGN=2
month1
        DCD      ||.conststring||
        DCD      ||.conststring||+0x8
        DCD      ||.conststring||+0x11
        DCD      ||.conststring||+0x17
        DCD      ||.conststring||+0x1d
        DCD      ||.conststring||+0x21
        DCD      ||.conststring||+0x26
        DCD      ||.conststring||+0x2b
        DCD      ||.conststring||+0x32
        DCD      ||.conststring||+0x3c
        DCD      ||.conststring||+0x44
        DCD      ||.conststring||+0x4d

数据表的首地址存储于R1寄存器。其余元素的指针地址则通过LDR指令现场计算。实参month在左移2位之后与R1的值（表地址）相加，从而计算出地址表中的指针地址。此后，再用这个指针在32位地址表中进行查询，把最终的字符串地址存储在R0寄存器里。

Thumb模式的ARM程序

由于在Thumb模式里的LDR指令不能进行LSL运算，所以代码略长一些。

get_month1 PROC
        LSLS     r0,r0,#2
        LDR      r1,|L0.64|
        LDR      r0,[r1,r0]
        BX       lr
        ENDP

18.5.4　ARM64

指令清单18.13　Optimizing GCC 4.9 ARM64

get_month1:
        adrp    x1, .LANCHOR0
        add     x1, x1, :lo12:.LANCHOR0
        ldr     x0, [x1,w0,sxtw 3]
        ret

.LANCHOR0 = . + 0
        .type   month1, %object
        .size   month1, 96
month1:
        .xword  .LC2
        .xword  .LC3
        .xword  .LC4
        .xword  .LC5
        .xword  .LC6
        .xword  .LC7
        .xword  .LC8
        .xword  .LC9
        .xword  .LC10
        .xword  .LC11
        .xword  .LC12
        .xword  .LC13
.LC2:
        .string "January"
.LC3:
        .string "February"
.LC4:
        .string "March"
.LC5:
        .string "April"
.LC6:
        .string "May"
.LC7:
        .string "June"
.LC8:
        .string "July"
.LC9:
        .string "August"
.LC10:
        .string "September"
.LC11:
        .string "October"
.LC12:
        .string "November"
.LC13:
        .string "December"

表地址由ADRP/ADD指令对传送到X1寄存器。然后，单条LDR指令完成表查询的运算。它把W0寄存器（传递实参month）的值左移3位（相当于乘以8），将其进行有符号数扩展（sxtw后缀的含义）并与X0寄存器的值相加。通过表查询获取的64位运算结果最终存储在X0寄存器里。

18.5.5　MIPS

指令清单18.14　Optimizing GCC 4.4.5 (IDA)

get_month1:
; load address of table into $v0:
                la       $v0, month1
; take input value and multiply it by 4:
                sll      $a0, 2
; sum up address of table and multiplied value:
                addu     $a0, $v0
; load table element at this address into $v0:
                lw       $v0, 0($a0)
; return
                jr       $ra
                or       $at, $zero ; branch delay slot, NOP

                .data # .data.rel.local
                .globl month1
month1:         .word aJanuary            # "January"
                .word aFebruary           # "February"
                .word aMarch              # "March"
                .word aApril              # "April"
                .word aMay                # "May"
                .word aJune               # "June"
                .word aJuly               # "July"
                .word aAugust             # "August"
                .word aSeptember          # "September"
                .word aOctober            # "October"
                .word aNovember           # "November"
                .word aDecember           # "December"

                .data # .rodata.str1.4
aJanuary:       .ascii "January"<0>
aFebruary:      .ascii "February"<0>
aMarch:         .ascii "March"<0>
aApril:         .ascii "April"<0>
aMay:           .ascii "May"<0>
aJune:          .ascii "June"<0>
aJuly:          .ascii "July"<0>
aAugust:        .ascii "August"<0>
aSeptember:     .ascii "September"<0>
aOctober:       .ascii "October"<0>
aNovember:      .ascii "November"<0>
aDecember:      .ascii "December"<0>

18.5.6　数组溢出

本例参数的取值范围是0～11。如果输入值为12，会发生什么情况？虽然查询表里没有对应的值，但是程序还是会计算、取值并返回结果，只是返回值不可预料。如果其他函数访问这种超越数据边界的地址，那么在读取字符串地址时可能引发程序崩溃。

本文通过MSVC编译器生成一个Win64程序，然后使用IDA打开这个可执行程序，观察编译器的linker组件在数组后面存储了哪些信息：

指令清单18.15　在IDA里观察问题程序

off_140011000   dq offset aJanuary_1      ; DATA XREF: .text:0000000140001003
                                          ; "January"
                dq offset aFebruary_1     ; "February"
                dq offset aMarch_1        ; "March"
                dq offset aApril_1        ; "April"
                dq offset aMay_1          ; "May"
                dq offset aJune_1         ; "June"
                dq offset aJuly_1         ; "July"
                dq offset aAugust_1       ; "August"
                dq offset aSeptember_1    ; "September"
                dq offset aOctober_1      ; "October"
                dq offset aNovember_1     ; "November"
                dq offset aDecember_1     ; "December"
aJanuary_1      db 'January',0            ; DATA XREF: sub_140001020+4
                                          ; .data:off_140011000
aFebruary_1     db 'February',0           ; DATA XREF: .data:0000000140011008
                align 4
aMarch_1        db 'March',0              ; DATA XREF: .data:0000000140011010
                align 4
aApril_1        db 'April',0              ; DATA XREF: .data:0000000140011018

本例的程序很小，所以数据段里的信息以月份名称为主，没有什么其他信息。但是应当注意的是，linker可能在实际生成的程序中安插“任何信息”。

输入值为12时会发生什么情况？程序应当返回数组首地址之后的第13个元素。我们一起来看CPU是如何处理第“13”个元素的那个64位值：

指令清单18.16　Executable file in IDA

off_140011000   dq offset qword_140011060
                                          ; DATA XREF: .text:0000000140001003
                dq offset aFebruary_1     ; "February"
                dq offset aMarch_1        ; "March"
                dq offset aApril_1        ; "April"
                dq offset aMay_1          ; "May"
                dq offset aJune_1         ; "June"
                dq offset aJuly_1         ; "July"
                dq offset aAugust_1       ; "August"
                dq offset aSeptember_1    ; "September"
                dq offset aOctober_1      ; "October"
                dq offset aNovember_1     ; "November"
                dq offset aDecember_1     ; "December"
                qword_140011060 dq 797261756E614Ah ; DATA XREF: sub_140001020+4
                                          ; .data:off_140011000
                aFebruary_1 db 'February',0 ; DATA XREF: .data:0000000140011008
                align 4
                aMarch_1 db 'March',0     ; DATA XREF: .data:0000000140011010

那个地址的值是0x797261756E614A。假如某个函数以字符串指针的格式访问这个地址，那么整个程序多半会立即崩溃。毕竟这个值不太会是一个有效地址。

数组溢出保护

如果指望调用函数的用户能够保证参数不超出正常的取值范围，那么这个程序员真的算得上是很傻很天真了。在编程领域里，成熟的应用程序应当具备“Fail Early, Fail Loudly”或“Fail-fast”这类的早期错误处理功能。

C/C++ assertions就是处理手段之一。我们可以修改源程序，令其验证输入参数是否在预期范围之内。

指令清单18.17　assert() added

const char* get_month1_checked (int month)
{
        assert (month<12);
        return month1[month];
};

只要在函数启动的时候调用一个验证参数有效性的assert()函数，那么当输入值在取值区间之外时，程序将进行异常处理。

指令清单18.18　Optimizing MSVC 2013 x64

$SG3143 DB      'm', 00H, 'o', 00H, 'n', 00H, 't', 00H, 'h', 00H, '.', 00H
        DB      'c', 00H, 00H, 00H
$SG3144 DB      'm', 00H, 'o', 00H, 'n', 00H, 't', 00H, 'h', 00H, '<', 00H
        DB      '1', 00H, '2', 00H, 00H, 00H

month$ = 48
get_month1_checked PROC
$LN5:
        push    rbx
        sub     rsp, 32
        movsxd  rbx, ecx
        cmp     ebx, 12
        jl      SHORT $LN3@get_month1
        lea     rdx, OFFSET FLAT:$SG3143
        lea     rcx, OFFSET FLAT:$SG3144
        mov     r8d, 29
        call    _wassert
$LN3@get_month1:
        lea     rcx, OFFSET FLAT:month1
        mov     rax, QWORD PTR [rcx+rbx*8]
        add     rsp, 32
        pop     rbx
        ret     0
get_month1_checked ENDP

严格地说，assert()只能算作宏而不能算作函数。它对条件表达式进行判断，然后把行号信息和文件名信息传递给另外一个函数，并由后者向用户进行提示。

在可执行程序里，我们可以看到以UTF-16格式封装文件名和条件表达式，以及assert()在原文件中的行号（29）。

所有编译器在编译assert宏时的处理机制基本都大同小异。本文仅演示GCC的编译结果。

指令清单18.19　Optimizing GCC 4.9 x64

.LC1:
        .string "month.c"
.LC2:
        .string "month<12"

get_month1_checked:
        cmp     edi, 11
        jg      .L6
        movsx   rdi, edi
        mov     rax, QWORD PTR month1[0+rdi*8]
        ret
.L6:
        push    rax
        mov     ecx, OFFSET FLAT:__PRETTY_FUNCTION__.2423
        mov     edx, 29
        mov     esi, OFFSET FLAT:.LC1
        mov     edi, OFFSET FLAT:.LC2
        call    __assert_fail

__PRETTY_FUNCTION__.2423:
        .string "get_month1_checked"

从中可知，GCC向异常处理函数传递了其调用方函数的函数名称。

这种边界检查其实存在很明显的开销问题。它会降低程序的运行速度——在调用间隔较短的时间敏感函数上使用assert()宏，性能影响会非常大。以MSVC为例，Debug版的MSVC还存在大量的assert()，而最终发行版的MSVC里就不再使用这个宏了。

微软Windows NT内核也有“checked”和“free”两个版本。前一个版本还存在输入值的边界检查，而后面的第二版就不再使用asssert()了。

18.6　多维数组

多维数组和线性数组在本质上是相同的。

计算机内存是连续的线性空间，它可以与一维数组直接对应。在被拆分成多个一维数组之后，多维数组与内栈线性空间同样存在直接对应的存储关系。

举例来说，二维数组a[3][4]可转化为a[12]这样的一个12个元素一维数组，如表18.1所示。

表18.1　使用一维数组表示二维数组

存储地址

数组元素

0

[0] [0]

1

[0] [1]

2

[0] [2]

3

[0] [3]

4

[1] [0]

5

[1] [1]

6

[1] [2]

7

[1] [3]

8

[2] [0]

9

[2] [1]

10

[2] [2]

11

[2] [3]

在内存之中，3×4的二维数组将依次存储为连续的12个元素，如表18.2所示。

表18.2　二维数组在内存中的存储逻辑

0 | 1 | 2 | 3
4 | 5 | 6 | 7
8 | 9 | 10 | 11

在计算上述数组中某个特定元素的内存存储编号时，可以先将二维索引号的第一个索引号乘以4（矩阵宽度），而后加上第二个索引号。这种方式就是C/C++、Python所用的“行优先的顺序”（row-majororder）。所谓行优先，就是先用第一行排满第一个索引号下的所有元素，然后再依次编排其他各行。

相应地，也有“列优先的顺序”(column-major order)。Fortran、Matlab、R语言就使用了这种顺序。这种顺序优先使用列的存储空间。

这两种优先的顺序孰优孰劣？从性能及缓存的角度来看，与数据的存储方案（scheme)和组织方式（data organization）匹配的优先顺序最优。只要相互匹配，那么程序就可以连续访问数据，整体性能就会提高。所以，如果程序以“逐行”的方式访问数据，那么就应当以行优先的顺序组织数组；反之亦然。

18.6.1　二维数组举例

本例用char类型数据构造一个二维数组，其中每个数组元素都占用1字节内存。

以行优先的顺序填充数据

下面这个例子将用数字“0～3”填充数组的第二行。

指令清单18.20　逐行填充数据

#include <stdio.h>

char a[3][4];

int main()
{
        int x, y;

        //清空数组
        for (x=0; x<3; x++)
                for (y=0; y<4; y++)
                        a[x][y]=0

        //填充第二行
        for (y=0; y<4; y++)
                a[1][y]=y;
};

图18.7中所示的3个红圈代表3个不同的行。我们可以看到第二行的值是0、1、2、3。

图18.7　OllyDbg：填充数据后的数组

以列优先的顺序填充数据

下列程序将数组的第三列填充为0、1、2。

指令清单18.21　逐列填充数据

#include <stdio.h>

char a[3][4];

int main()
{
        int x, y;

        // 清空数组
        for (x=0; x<3; x++)
                 for (y=0; y<4; y++)
                          a[x][y]=0;

        //填充第3列
        for (x=0; x<3; x++)
                 a[x][2]=x;
};

图18.8中所示的3个红圈代表3个不同的行。各行的第三列分别是0、1、2。

图18.8　OllyDbg：填充数据之后的数组

18.6.2　以一维数组的方式访问二维数组

以一维数组的方式访问二维数组的方法至少有两种。

#include <stdio.h>

char a[3][4];

char get_by_coordinates1 (char array[3][4], int a, int b)
{
        return array[a][b];
};

char get_by_coordinates2 (char *array, int a, int b)
{
        // treat input array as one-dimensional
        // 4 is array width here
        return array[a*4+b];
};

char get_by_coordinates3 (char *array, int a, int b)
{
        // treat input array as pointer,
        // calculate address, get value at it
        // 4 is array width here
        return *(array+a*4+b);
};

int main() {
        a[2][3]=123;
        printf ("%d\n", get_by_coordinates1(a, 2, 3));
        printf ("%d\n", get_by_coordinates2(a, 2, 3));
        printf ("%d\n", get_by_coordinates3(a, 2, 3));
};

编译并运行上述程序，可观测到数组元素的相应数值。

MSVC 2013的编译方法可圈可点，三个函数的指令完全一致。

指令清单18.22　Optimizing MSVC 2013 x64

array$ = 8
a$ = 16
b$ = 24
get_by_coordinates3 PROC
; RCX=address of array
; RDX=a
; R8=b
        movsxd  rax, r8d
; EAX=b
        movsxd  r9, edx
; R9=a
        add     rax, rcx
; RAX=b+address of array
        movzx   eax, BYTE PTR [rax+r9*4]
; AL=load byte at address RAX+R9*4=b+address of array+a*4=address of array+a*4+b
        ret     0
get_by_coordinates3 ENDP

array$ = 8
a$ = 16
b$ = 24
get_by_coordinates2 PROC
        movsxd  rax, r8d
        movsxd  r9, edx
        add     rax, rcx
        movzx   eax, BYTE PTR [rax+r9*4]
        ret     0
get_by_coordinates2 ENDP
array$ = 8
a$ = 16
b$ = 24
get_by_coordinates1 PROC
        movsxd  rax, r8d
        movsxd  r9, edx
        add     rax, rcx
        movzx   eax, BYTE PTR [rax+r9*4]
        ret     0
get_by_coordinates1 ENDP

GCC 会生成等效的指令，只是各函数的指令略有区别。

指令清单18.23　Optimizing GCC 4.9 x64

; RDI=address of array
; RSI=a
; RDX=b

get_by_coordinates1:
; sign-extend input 32-bit int values "a" and "b" to 64-bit ones
        movsx   rsi, esi
        movsx   rdx, edx
        lea     rax, [rdi+rsi*4]
; RAX=RDI+RSI*4=address of array+a*4
        movzx   eax, BYTE PTR [rax+rdx]
; AL=load byte at address RAX+RDX=address of array+a*4+b
        ret

get_by_coordinates2:
        lea     eax, [rdx+rsi*4]
; RAX=RDX+RSI*4=b+a*4
        cdqe
        movzx   eax, BYTE PTR [rdi+rax]
; AL=load byte at address RDI+RAX=address of array+b+a*4
        ret

get_by_coordinates3:
        sal     esi, 2
; ESI=a<<2=a*4
; sign-extend input 32-bit int values "a*4" and "b" to 64-bit ones
        movsx   rdx, edx
        movsx   rsi, esi
        add     rdi, rsi
; RDI=RDI+RSI=address of array+a*4
        movzx   eax, BYTE PTR [rdi+rdx]
; AL=load byte at address RDI+RDX=address of array+a*4+b
        ret

18.6.3　三维数组

多维数组的情况也差不多。

在下面的例子里，我们演示一个由整型数据构成的三维数组，每个整数元素占用4字节内存。

我们来看如下所示的指令。

指令清单18.24　simple example

#include <stdio.h>

int a[10][20][30];

void insert(int x, int y, int z, int value)
{
        a[x][y][z]=value;
};

x86

用MSVC 2010 编译后，这个函数对应的汇编代码如下所示。

指令清单18.25　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

中规中矩。对于三维数组（int a[x][y][z];）来说，计算各元素指针地址的公式是：数组元素地址=600×4x + 30×4y + 4z。32位系统里int类型是32位（4字节）数据，所以要每项都要乘以4。

使用GCC 4.4.1编译上述程序可得如下所示的指令。

指令清单18.26　GCC 4.4.1

           public  insert
insert     proc near

x          = dword ptr 8
y          = dword ptr  0Ch
z          = dword ptr  10h
value      = dword ptr  14h

           push    ebp
           mov     ebp, esp
           push    ebx
           mov     ebx, [ebp+x]
           mov     eax, [ebp+y]
           mov     ecx, [ebp+z]
           lea     edx, [eax+eax]              ; edx=y*2
           mov     eax, edx                    ; eax=y*2
           shl     eax, 4                      ; eax=(y*2)<<4 = y*2*16 = y*32
           sub     eax, edx                    ; eax=y*32 - y*2=y*30
           imul    edx, ebx, 600               ; edx=x*600
           add     eax, edx                    ; eax=eax+edx=y*30 + x*600
           lea     edx, [eax+ecx]              ; edx=y*30 + x*600 + z
           mov     eax, [ebp+value]
           mov     dword ptr ds:a[edx*4], eax  ; *(a+edx*4)=value
           pop     ebx
           pop     ebp
           retn
insert     endp

GCC的处理方法和MSVC不同。在计算30y的时候，GCC没有使用乘法指令。它的计算公式是(y + y) << 4 − (y + y) = (2 y)<< 4 − 2 y = 2×16y − 2 y = 32 y − 2 y = 30 y。可见，GCC组合了加法、位移和减法运算，替代了MSVC采用的乘法运算。GCC算法的运算速度比直接进行乘法运算的速度还要快。

ARM + Non-optimizing Xcode 4.6.3 (LLVM) (Thumb mode)

指令清单18.27　Non-optimizing Xcode 4.6.3 (LLVM) (Thumb mode)

_insert

value   = -0x10
z       = -0xC
y       =-8
x       =-4

; allocate place in local stack for 4 values of int type
SUB     SP, SP, #0x10
MOV     R9, 0xFC2 ; a
ADD     R9, PC
LDR.W   R9, [R9]  ; get pointer to array
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 4.6.3 (LLVM) + Thumb mode

启用优化选项后，使用LLVM以Thumb模式编译上述程序可得到如下所示的指令。

指令清单18.28　Optimizing Xcode 4.6.3 (LLVM) (Thumb mode)

_insert
MOVW    R9, #0x10FC
MOV.W   R12, #2400
MOVT.W  R9, #0
RSB.W   R1,R1,R1,LSL#4     ;R1-y.R1=y<<4-y=y*16-y=y*15
ADD     R9, PC
LDR.W   R9, [R9]           ; R9 = pointer to a array
MLA.W   R0, R0, R12, R9    ; R0 - x, R12 - 2400, R9 - pointer to a. R0=x*2400 + ptr to a
ADD.W   R0, R0, R1,LSL#3   ; R0 = R0+R1<<3 = R0+R1*8 = x*2400 + ptr to a + y*15*8 =
                           ; ptr to a + y*30*4 + x*600*4
STR.W   R3, [R0,R2,LSL#2]  ; R2 - z, R3 - value. address=R0+z*4 =
BX      LR                 ; ptr to a + y*30*4 + x*600*4 + z*4

LLVM也用到了GCC的地址计算方法，组合了加法、位移、减法三种运算。

其中的“RSB（Reverse Subtract）”指令尚未在前面介绍过。RSB 指令称为逆向减法指令，用于把操作数2（第三个参数）减去操作数1（第二个参数），并将结果存放到目的寄存器中。为什么专门从SUB指令里派生出RSB指令呢？这是因为SUB和RSB都只能对第二个操作数进行位移运算。如果要使用SUB指令，则LSL＃4就要写在第1个操作数后面，会造成识别上的混乱。而RSB指令替换了减数和被减数的位置，从而避免了这个问题。

“LDR.W R9, [R9]”指令和x86的LEA指令相似。只是这里的这条指令不影响计算结果，所以是冗余代码。很明显，编译器没有进行相应优化。

MIPS

因为源程序很小的缘故，GCC以全局指针GP在64KB可寻址空间进行表查询。

指令清单18.29　Optimizing GCC 4.4.5 (IDA)

insert:
; $a0=x
; $a1=y
; $a2=z
; $a3=value
                sll        $v0, $a0, 5
; $v0 = $a0<<5 = x*32
                sll        $a0, 3
; $a0 = $a0<<3 = x*8
                addu       $a0, $v0
; $a0 = $a0+$v0 = x*8+x*32 = x*40
                sll        $v1, $a1, 5
; $v1 = $a1<<5 = y*32
                sll        $v0, $a0, 4
; $v0 = $a0<<4 = x*40*16 = x*640
                sll        $a1, 1
; $a1 = $a1<<1 = y*2
                subu       $a1, $v1, $a1
; $a1 = $v1-$a1 = y*32-y*2 = y*30
                subu       $a0, $v0, $a0
; $a0 = $v0-$a0 = x*640-x*40 = x*600
                la         $gp, __gnu_local_gp
                addu       $a0, $a1, $a0
; $a0 = $a1+$a0 = y*30+x*600
                addu       $a0, $a2
; $a0 = $a0+$a2 = y*30+x*600+z
; load address of table:
                lw         $v0, (a & 0xFFFF)($gp)
; multiply index by 4 to seek array element:
                sll        $a0, 2
; sum up multiplied index and table address:
                addu       $a0, $v0, $a0
; store value into table and return:
                jr         $ra
                sw         $a3, 0($a0)

                .comm a:0x1770

18.6.4　更多案例

计算机的显示屏幕是一个2D显示空间，但是显存却是一个一维线性数组。

本书第83章将详细介绍有关内容。

18.7　二维字符串数组的封装格式

在指令清单18.8所示的程序里，如果要制备指向月份名称的指针，就必须要进行一次以上的内存操作。有没有可能略去这步内存加载的内存操作？只要把二维数组进行相应处理，即可省略有关操作。

#include <stdio.h>
#include <assert.h>

const char month2[12][10]=
{
        { 'J','a','n','u','a','r','y',  0,  0,  0 },
        { 'F','e','b','r','u','a','r','y',  0,  0 },
        { 'M','a','r','c','h',  0,  0,  0,  0,  0 },
        { 'A','p','r','i','l',  0,  0,  0,  0,  0 },
        { 'M','a','y',  0,  0,  0,  0,  0,  0,  0 },
        { 'J','u','n','e',  0,  0,  0,  0,  0,  0 },
        { 'J','u','l','y',  0,  0,  0,  0,  0,  0 },
        { 'A','u','g','u','s','t',  0,  0,  0,  0 },
        { 'S','e','p','t','e','m','b','e','r',  0 },
        { 'O','c','t','o','b','e','r',  0,  0,  0 },
        { 'N','o','v','e','m','b','e','r',  0,  0 },
        { 'D','e','c','e','m','b','e','r',  0,  0 }
};

// in 0..11 range
const char* get_month2 (int month)
{
        return &month2[month][0];
};

编译上述程序可得如下所示的指令。

指令清单18.30　Optimizing MSVC 2013 x64

month2  DB      04aH
        DB      061H
        DB      06eH
        DB      075H
        DB      061H
        DB      072H
        DB      079H
        DB      00H
        DB      00H
        DB      00H
...
get_month2 PROC
; sign-extend input argument and promote to 64-bit value
        movsxd  rax, ecx
        lea     rcx, QWORD PTR [rax+rax*4]
; RCX=month+month*4=month*5
        lea     rax, OFFSET FLAT:month2
; RAX=pointer to table
        lea     rax, QWORD PTR [rax+rcx*2]
; RAX=pointer to table + RCX*2=pointer to table + month*5*2=pointer to table + month*10
        ret     0
get_month2  ENDP

上述程序完全不访问内存。整个函数的功能，只是计算月份名称字符串的首字母指针pointer_to_the_table+month*10。它使用单条LEA指令，替代了多条MUL和MOV指令。

上述数组的每个字符串都占用10字节空间。最长的字符串由“September”和内容为零的字节构成，其余的字符串使用零字节对齐，所以每个字符串都占用10个字节。如此一来，计算字符串首地址的方式变得简单，整个函数的效率也会有所提高。

使用GCC 4.9进行优化编译，程序的指令甚至会更少。

指令清单18.31　Optimizing GCC 4.9 x64

        movsx   rdi, edi
        lea     rax, [rdi+rdi*4]
        lea     rax, month2[rax+rax]
        ret

它同样使用LEA指令进行“乘以10”的运算。

若由GCC进行非优化编译，那么乘法运算的实现方式将会有所不同。

指令清单18.32　Non-optimizing GCC 4.9 x64

get_month2:
        push    rbp
        mov     rbp, rsp
        mov     DWORD PTR [rbp-4], edi
        mov     eax, DWORD PTR [rbp-4]
        movsx   rdx, eax
; RDX = sign-extended input value
        mov     rax, rdx
; RAX = month
        sal     rax, 2
; RAX = month<<2 = month*4
        add     rax, rdx
; RAX = RAX+RDX = month*4+month = month*5
        add     rax, rax
; RAX = RAX*2 = month*5*2 = month*10
        add     rax, OFFSET FLAT:month2
; RAX = month*10 + pointer to the table
        pop     rbp
        ret

而在不开启优化选项的情况下，MSVC编译器会直接使用IMUL指令。

指令清单18.33　Non-optimizing MSVC 2013 x64

month$ = 8
get_month2 PROC
        mov      DWORD PTR [rsp+8], ecx
        movsxd   rax, DWORD PTR month$[rsp]
; RAX = sign-extended input value into 64-bit one
        imul     rax, rax, 10
; RAX = RAX*10
        lea      rcx, OFFSET FLAT:month2
; RCX = pointer to the table
        add      rcx, rax
; RCX = RCX+RAX = pointer to the table+month*10
        mov      rax, rcx
; RAX = pointer to the table+month*10
        mov      ecx, 1
; RCX = 1
        imul     rcx, rcx, 0
; RCX = 1*0 = 0
        add      rax, rcx
; RAX = pointer to the table+month*10 + 0 = pointer to the table+month*10
        ret      0
get_month2 ENDP

不过此处有些令人费解：为什么RCX要乘以零，再把零加在最终结果里？虽然说它不影响最终运行结果，但是也足以说是编译器的某种怪癖了。本文刻意演示这种怪癖代码，是希望读者不要拘泥于编译器生成的指令的形式，而要从编程人员的角度理解程序的源代码。

18.7.1　32位ARM

由Keil优化编译而生成的Thumb模式程序，会直接使用乘法运算指令MULS。

指令清单18.34　Optimizing Keil 6/2013 (Thumb mode)

; R0 = month
        MOVS      r1,#0xa
; R1 = 10
        MULS      r0,r1,r0
; R0 = R1*R0 = 10*month
        LDR       r1,|L0.68|
; R1 = pointer to the table
        ADDS      r0,r0,r1
; R0 = R0+R1 = 10*month + pointer to the table
        BX        lr

而优化编译生成的ARM模式程序，会使用加法运算和位移操作指令。

指令清单18.35　Optimizing Keil 6/2013 (ARM mode)

; R0 = month
        LDR       r1,|L0.104|
; R1 = pointer to the table
        ADD       r0,r0,r0,LSL #2
; R0 = R0+R0<<2 = R0+R0*4 = month*5
        ADD       r0,r1,r0,LSL #1
; R0 = R1+R0<<2 = pointer to the table + month*5*2 = pointer to the table + month*10
        BX        lr

18.7.2　ARM64

指令清单18.36　Optimizing GCC 4.9 ARM64

; W0 = month
        sxtw     x0, w0
; X0 = sign-extended input value
        adrp     x1, .LANCHOR1
        add      x1, x1, :lo12:.LANCHOR1
; X1 = pointer to the table
        add      x0, x0, x0, lsl 2
; X0 = X0+X0<<2 = X0+X0*4 = X0*5
        add      x0, x1, x0, lsl 1
; X0 = X1+X0<<1 = X1+X0*2 = pointer to the table + X0*10
        ret

SXTW指令把32位有符号数据扩展为64位有符号数据，并将之存储于X0寄存器。ADRP/ADD指令对则负责数组寻址。ADD指令中的LSL后缀用于进行乘法运算。

18.7.3　MIPS

指令清单18.37　Optimizing GCC 4.4.5 (IDA)

                .globl get_month2
get_month2:
; $a0=month
                sll     $v0, $a0, 3
; $v0 = $a0<<3 = month*8
                sll     $a0, 1
; $a0 = $a0<<1 = month*2
                addu    $a0, $v0
; $a0 = month*2+month*8 = month*10
; load address of the table:
                la      $v0, month2
; sum up table address and index we calculated and return:
                jr      $ra
                addu    $v0, $a0

month2:         .ascii "January"<0>
                .byte 0, 0
aFebruary:      .ascii "February"<0>
                .byte    0
aMarch:         .ascii "March"<0>
                .byte 0, 0, 0, 0
aApril:         .ascii "April"<0>
                .byte 0, 0, 0, 0
aMay:           .ascii "May"<0>
                .byte 0, 0, 0, 0, 0, 0
aJune:          .ascii "June"<0>
                .byte 0, 0, 0, 0, 0
aJuly:          .ascii "July"<0>
                .byte 0, 0, 0, 0, 0
aAugust:        .ascii "August"<0>
                .byte 0, 0, 0
aSeptember:     .ascii "September"<0>
aOctober:       .ascii "October"<0>
                .byte 0, 0
aNovember:      .ascii "November"<0>
                .byte    0
aDecember:      .ascii "December"<0>
                .byte 0, 0, 0, 0, 0, 0, 0, 0, 0

18.7.4　总结

字符串存储技术属于经典的数组技术。Oracle RDBMS的程序中就大量使用了这种技术。虽然当代计算机程序中可能很少应用这种数组技术了，但是字符串型数据可充分演示数组的各方面特点。

18.8　本章小结

在内存中，数组就是依次排列的一组同类型数据。数组元素可以是任意类型的数据，甚至可以是结构体型的数据。如果要访问数组中的特定元素，首先就要计算其地址。

18.9　练习题

18.9.1　题目1

请描述下述代码的功能。

指令清单18.38　MSVC 2010+/O1

_a$ = 8          ; size = 4
_b$ = 12         ; size = 4
_c$ = 16         ; size = 4

?s@@YAXPAN00@Z PROC; s, COMDAT
    mov    eax, DWORD PTR _b$[esp-4]
    mov    ecx, DWORD PTR _a$[esp-4]
    mov    edx, DWORD PTR _c$[esp-4]
    push   esi
    push   edi
    sub    ecx, eax
    sub    edx, eax
    mov    edi, 200     ; 000000c8H
$LL6@s:
    push   100          ; 00000064H
    pop    esi
$LL3@s:
    fld    QWORD PTR [ecx+eax]
    fadd   QWORD PTR [eax]
    fstp   QWORD PTR [edx+eax]
    add    eax, 8
    dec    esi
    jne    SHORT $LL3@s
    dec    edi
    jne    SHORT $LL6@s
    pop    edi
    pop    esi
    ret    0
?s@@YAXPAN00@Z  ENDP   ; s

/O1 代表minimize space，即生成最小长度的程序。

通过Keil 5.03 （启用优化选项-O3）编译而得的ARM模式代码如下所示。

指令清单18.39　Optimizing Keil 6/2013 (ARM mode)

        PUSH    {r4-r12,lr}
        MOV     r9,r2
        MOV     r10,r1
        MOV     r11,r0
        MOV     r5,#0
|L0.20|
        ADD     r0,r5,r5,LSL #3
        ADD     r0,r0,r5,LSL #4
        MOV     r4,#0
        ADD     r8,r10,r0,LSL #5
        ADD     r7,r11,r0,LSL #5
        ADD     r6,r9,r0,LSL #5
|L0.44|
        ADD     r0,r8,r4,LSL #3
        LDM     r0,{r2,r3}
        ADD     r1,r7,r4,LSL #3
        LDM     r1,{r0,r1}
        BL      __aeabi_dadd
        ADD     r2,r6,r4,LSL #3
        ADD     r4,r4,#1
        STM     r2,{r0,r1}
        CMP     r4,#0x64
        BLT     |L0.44|
        ADD     r5,r5,#1
        CMP     r5,#0xc8
        BLT     |L0.20|
        POP     {r4-r12,pc}

指令清单18.40　Optimizing Keil 6/2013 (Thumb mode)

        PUSH    {r0-r2,r4-r7,lr}
        MOVS    r4, #0
        SUB     sp, sp, #8
|L0.6|
        MOVS    r1,#0x19
        MOVS    r0,r4
        LSLS    r1,r1,#5
        MULS    r0,r1,r0
        LDR     r2,[sp,#8]
        LDR     r1,[sp,#0xc]
        ADDS    r2,r0,r2
        STR     r2,[sp,#0]
        LDR     r2,[sp,#0x10]
        MOVS    r5,#0
        ADDS    r7,r0,r2
        ADDS    r0,r0,r1
        STR     r0,[sp,#4]
|L0.32|
        LSLS    r6,r5,#3
        ADDS    r0,r0,r6
        LDM     r0!,{r2,r3}
        LDR     r0,[sp,#0]
        ADDS    r1,r0,r6
        LDM     r1,{r0,r1}
        BL      __aeabi_dadd
        ADDS    r2,r7,r6
        ADDS    r5,r5,#1
        STM     r2!,{r0,r1}
        CMP     r5,#0x64
        BGE     |L0.62|
        LDR     r0,[sp,#4]
        B       |L0.32|
|L0.62|
        ADDS    r4,r4,#1
        CMP     r4,#0xc8
        BLT     |L0.6|
        ADD     sp,sp,#0x14
        POP     {r4-r7,pc}

指令清单18.41　Non-optimizing GCC 4.9 (ARM64)

s:
        sub     sp, sp, #48
        str     x0, [sp,24]
        str     x1, [sp,16]
        str     x2, [sp,8]
        str     wzr, [sp,44]
        b       .L2
.L5:
        str     wzr, [sp,40]
        b       .L3
.L4:
        ldr     w1, [sp,44]
        mov     w0, 100
        mul     w0, w1, w0
        sxtw    x1, w0
        ldrsw   x0, [sp,40]
        add     x0, x1, x0
        lsl     x0, x0, 3
        ldr     x1, [sp,8]
        add     x0, x1, x0
        ldr     w2, [sp,44]
        mov     w1, 100
        mul     w1, w2, w1
        sxtw    x2, w1
        ldrsw   x1, [sp,40]
        add     x1, x2, x1
        lsl     x1, x1, 3
        ldr     x2, [sp,24]
        add     x1, x2, x1
        ldr     x2, [x1]
        ldr     w3, [sp,44]
        mov     w1, 100
        mul     w1, w3, w1
        sxtw    x3, w1
        ldrsw   x1, [sp,40]
        add     x1, x3, x1
        lsl     x1, x1, 3
        ldr     x3, [sp,16]
        add     x1, x3, x1
        ldr     x1, [x1]
        fmov    d0, x2
        fmov    d1, x1
        fadd    d0, d0, d1
        fmov    x1, d0
        str     x1, [x0]
        ldr     w0, [sp,40]
        add     w0, w0, 1
        str     w0, [sp,40]
.L3:
        ldr     w0, [sp, 40]
        cmp     w0, 99
        ble     .L4
        ldr     w0, [sp,44]
        add     w0, w0, 1
        str     w0, [sp,44]
.L2:
        ldr     w0, [sp,44]
        cmp     w0, 199
        ble     .L5
        add     sp, sp, 48
        ret

指令清单18.42　Optimizing GCC 4.4.5 (MIPS) (IDA)

sub_0:
        li      $t3, 0x27100
        move    $t2, $zero
        li      $t1, 0x64  # 'd'

loc_10:                             # CODE XREF: sub_0+54
        addu    $t0, $a1, $t2
        addu    $a3, $a2, $t2
        move    $v1, $a0
        move    $v0, $zero

loc_20:                             # CODE XREF: sub_0+48
        lwc1    $f2, 4($v1)
        lwc1    $f0, 4($t0)
        lwc1    $f3, 0($v1)
        lwc1    $f1, 0($t0)
        addiu   $v0, 1
        add.d   $f0, $f2, $f0
        addiu   $v1, 8
        swc1    $f0, 4($a3)
        swc1    $f1, 0($a3)
        addiu   $t0, 8
        bne     $v0, $t1, loc_20
        addiu   $a3, 8
        addiu   $t2, 0x320
        bne     $t2, $t3, loc_10
        addiu   $a0, 0x320
        jr      $ra
        or      $at, $zero

18.9.2　题目2

请描述下述程序的功能。

通过MSVC 2010 （启用 /O1选项）编译而得的代码如下所示。

指令清单18.43　MSVC 2010 + /O1

tv315 = -8            ; size = 4
tv291 = -4            ; size = 4
_a$ = 8               ; size = 4
_b$ = 12              ; size = 4
_c$ = 16              ; size = 4
?m@@YAXPAN00@Z PROC; m, COMDAT
    push   ebp
    mov    ebp, esp
    push   ecx
    push   ecx
    mov    edx, DWORD PTR _a$[ebp]
    push   ebx
    mov    ebx, DWORD PTR _c$[ebp]
    push   esi
    mov    esi, DWORD PTR _b$[ebp]
    sub    edx, esi
    push   edi
    sub    esi, ebx
    mov    DWORD PTR tv315[ebp], 100  ; 00000064H
$LL9@m:
    mov    eax, ebx
    mov    DWORD PTR tv291[ebp], 300  ; 0000012cH
$LL6@m:
    fldz
    lea    ecx, DWORD PTR [esi+eax]
    fstp   QWORD PTR [eax]
    mov    edi, 200                   ; 000000c8H
$LL3@m:
    dec    edi
    fld    QWORD PTR [ecx+edx]
    fmul   QWORD PTR [ecx]
    fadd   QWORD PTR [eax]
    fstp   QWORD PTR [eax]
    jne    HORT $LL3@m
    add    eax, 8
    dec    DWORD PTR tv291[ebp]
    jne    SHORT $LL6@m
    add    ebx, 800                   ; 00000320H
    dec    DWORD PTR tv315[ebp]
    jne    SHORT $LL9@m
    pop    edi
    pop    esi
    pop    ebx
    leave
    ret    0
?m@@YAXPAN00@Z ENDP                   ; m

指令清单18.44　Optimizing Keil 6/2013 (ARM mode)

        PUSH    {r0-r2,r4-r11,lr}
        SUB     sp,sp,#8
        MOV     r5,#0
|L0.12|
        LDR     r1,[sp,#0xc]
        ADD     r0,r5,r5,LSL #3
        ADD     r0,r0,r5,LSL #4
        ADD     r1,r1,r0,LSL #5
        STR     r1,[sp,#0]
        LDR     r1,[sp,#8]
        MOV     r4,#0
        ADD     r11,r1,r0,LSL #5
        LDR     r1,[sp,#0x10]
        ADD     r10,r1,r0,LSL #5
|L0.52|
        MOV     r0,#0
        MOV     r1,r0
        ADD     r7,r10,r4,LSL #3
        STM     r7,{r0,r1}
        MOV     r6,r0
        LDR     r0,[sp,#0]
        ADD     r8,r11,r4,LSL #3
        ADD     r9,r0,r4,LSL #3
|L0.84|
        LDM     r9,{r2,r3}
        LDM     r8,{r0,r1}
        BL      __aeabi_dmul
        LDM     r7,{r2,r3}
        BL      __aeabi_dadd
        ADD     r6,r6,#1
        STM     r7,{r0,r1}
        CMP     r6,#0xc8
        BLT     |L0.84|
        ADD     r4,r4,#1
        CMP     r4,#0x12c
        BLT     |L0.52|
        ADD     r5,r5,#1
        CMP     r5,#0x64
        BLT     |L0.12|
        ADD     sp,sp,#0x14
        POP     {r4-r11,pc}

指令清单18.45　Optimizing Keil 6/2013 (Thumb mode)

        PUSH    {r0-r2,r4-r7,lr}
        MOVS    r0,#0
        SUB     sp,sp,#0x10
        STR     r0,[sp,#0]
|L0.8|
        MOVS    r1,#0x19
        LSLS    r1,r1,#5
        MULS    r0,r1,r0
        LDR     r2,[sp,#0x10]
        LDR     r1,[sp,#0x14]
        ADDS    r2,r0,r2
        STR     r2,[sp,#4]
        LDR     r2,[sp,#0x18]
        MOVS    r5,#0
        ADDS    r7,r0,r2
        ADDS    r0,r0,r
        STR     r0,[sp,#8]
|L0.32|
        LSLS    r4,r5,#3
        MOVS    r0,#0
        ADDS    r2,r7,r4
        STR     r0,[r2,#0]
        MOVS    r6,r0
        STR     r0,[r2,#4]
|L0.44|
        LDR     r0,[sp,#8]
        ADDS    r0,r0,r4
        LDM     r0!,{r2,r3}
        LDR     r0,[sp,#4]
        ADDS    r1,r0,r4
        LDM     r1,{r0,r1}
        BL      __aeabi_dmul
        ADDS    r3,r7,r4
        LDM     r3,{r2,r3}
        BL      __aeabi_dadd
        ADDS    r2,r7,r4
        ADDS    r6,r6,#1
        STM     r2!,{r0,r1}
        CMP     r6,#0xc8
        BLT     |L0.44|
        MOVS    r0,#0xff
        ADDS    r5,r5,#1
        ADDS    r0,r0,#0x2d
        CMP     r5,r0
        BLT     |L0.32|
        LDR     r0,[sp,#0]
        ADDS    r0,r0,#1
        CMP     r0,#0x64
        STR     r0,[sp,#0]
        BLT     |L0.8|
        ADD     sp,sp,#0x1c
        POP     {r4-r7,pc}

指令清单18.46　Non-optimizing GCC 4.9 (ARM64)

m:
        sub     sp, sp, #48
        str     x0, [sp,24]
        str     x1, [sp,16]
        str     x2, [sp,8]
        str     wzr, [sp,44]
        b       .L2
.L7:
        str     wzr, [sp,40]
        b       .L3
.L6:
        ldr     w1, [sp,44]
        mov     w0, 100
        mul     w0, w1, w0
        sxtw    x1, w0
        ldrsw   x0, [sp,40]
        add     x0, x1, x0
        lsl     x0, x0, 3
        ldr     x1, [sp,8]
        add     x0, x1, x0
        ldr     x1, .LC0
        str     x1, [x0]
        str     wzr, [sp,36]
        b       .L4
.L5:
        ldr     w1, [sp,44]
        mov     w0, 100
        mul     w0, w1, w0
        sxtw    x1, w0
        ldrsw   x0, [sp,40]
        add     x0, x1, x0
        lsl     x0, x0, 3
        ldr     x1, [sp,8]
        add     x0, x1, x0
        ldr     w2, [sp,44]
        mov     w1, 100
        mul     w1, w2, w1
        sxtw    x2, w1
        ldrsw   x1, [sp,40]
        add     x1, x2, x1
        lsl     x1, x1, 3
        ldr     x2, [sp,8]
        add     x1, x2, x1
        ldr     x2, [x1]
        ldr     w3, [sp,44]
        mov     w1, 100
        mul     w1, w3, w1
        sxtw    x3, w1
        ldrsw   x1, [sp,40]
        add     x1, x3, x1
        lsl     x1, x1, 3
        ldr     x3, [sp,24]
        add     x1, x3, x1
        ldr     x3, [x1]
        ldr     w4, [sp,44]
        mov     w1, 100
        mul     w1, w4, w1
        sxtw    x4, w1
        ldrsw   x1, [sp,40]
        add     x1, x4, x1
        lsl     x1, x1, 3
        ldr     x4, [sp,16]
        add     x1, x4, x1
        ldr     x1, [x1]
        fmov    d0, x3
        fmov    d1, x1
        fmul    d0, d0, d1
        fmov    x1, d0
        fmov    d0, x2
        fmov    d1, x1
        fadd    d0, d0, d1
        fmov    x1, d0
        str     x1, [x0]
        ldr     w0, [sp,36]
        add     w0, w0, 1
        str     w0, [sp,36]
.L4:
        ldr     w0, [sp, 36]
        cmp     w0, 199
        ble     .L5
        ldr     w0, [sp,40]
        add     w0, w0, 1
        str     w0, [sp,40]
.L3:
        ldr     w0, [sp,40]
        cmp     w0, 299
        ble     .L6
        ldr     w0, [sp,44]
        add     w0, w0, 1
        str     w0, [sp,44]
.L2:
        ldr     w0, [sp,44]
        cmp     w0, 99
        ble     .L7
        add     sp, sp, 48
        ret

指令清单18.47　Optimizing GCC 4.4.5 (MIPS) (IDA)

m:
                li      $t5, 0x13880
                move    $t4, $zero
                li      $t1, 0xC8
                li      $t3, 0x12C

loc_14:                                   # CODE XREF: m+7C
                addu    $t0, $a0, $t4
                addu    $a3, $a1, $t4
                move    $v1, $a2
                move    $t2, $zero

loc_24:                                   # CODE XREF: m+70
                mtc1    $zero, $f0
                move    $v0, $zero
                mtc1    $zero, $f1
                or      $at, $zero
                swc1    $f0, 4($v1)
                swc1    $f1, 0($v1)

loc_3C:                                   # CODE XREF: m+5C
                lwc1    $f4, 4($t0)
                lwc1    $f2, 4($a3)
                lwc1    $f5, 0($t0)
                lwc1    $f3, 0($a3)
                addiu   $v0, 1
                mul.d   $f2, $f4, $f2
                add.d   $f0, $f2
                swc1    $f0, 4($v1)
                bne     $v0, $t1, loc_3C
                swc1    $f1, 0($v1)
                addiu   $t2, 1
                addiu   $v1, 8
                addiu   $t0, 8
                bne     $t2, $t3, loc_24
                addiu   $a3, 8
                addiu   $t4, 0x320
                bne     $t4, $t5, loc_14
                addiu   $a2, 0x320
                jr      $ra
                or      $at, $zero

18.9.3　题目3

请描述下列代码的作用。

通过MSVC 2010（启用/Ox选项）编译而得的代码如下所示。

指令清单18.48　Optimizing MSVC 2010

_array$ = 8
_x$ = 12
_y$ = 16
_f      PROC
        mov     eax, DWORD PTR _x$[esp-4]
        mov     edx, DWORD PTR _y$[esp-4]
        mov     ecx, eax
        shl     ecx, 4
        sub     ecx, eax
        lea     eax, DWORD PTR [edx+ecx*8]
        mov     ecx, DWORD PTR _array$[esp-4]
        fld     QWORD PTR [ecx+eax*8]
        ret     0
_f      ENDP

指令清单18.49　Non-optimizing Keil 6/2013 (ARM mode)

f PROC
        RSB     r1,r1,r1,LSL #4
        ADD     r0,r0,r1,LSL #6
        ADD     r1,r0,r2,LSL #3
        LDM     r1,{r0,r1}
        BX      lr
        ENDP

指令清单18.50　Non-optimizing Keil 6/2013 (Thumb mode)

f PROC
        MOVS    r3,#0xf
        LSLS    r3,r3,#6
        MULS    r1,r3,r1
        ADDS    r0,r1,r0
        LSLS    r1,r2,#3
        ADDS    r1,r0,r1
        LDM     r1,{r0,r1}
        BX      lr
        ENDP

指令清单18.51　Optimizing GCC 4.9 (ARM64)

f:
        sxtw    x1, w1
        add     x0, x0, x2, sxtw 3
        lsl     x2, x1, 10
        sub     x1, x2, x1, lsl 6
        ldr     d0, [x0,x1]
        ret

指令清单18.52　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:
           sll     $v0, $a1, 10
           sll     $a1, 6
           subu    $a1, $v0, $a1
           addu    $a1, $a0, $a1
           sll     $a2, 3
           addu    $a1, $a2
           lwc1    $f0, 4($a1)
           or      $at, $zero
           lwc1    $f1, 0($a1)
           jr      $ra
           or      $at, $zero

18.9.4　题目4

请描述下列代码的作用。

请判断数组的维度。

指令清单18.53　Optimizing MSVC 2010

_array$ = 8
_x$ = 12
_y$ = 16
_z$ = 20
_f      PROC
        mov     eax, DWORD PTR _x$[esp-4]
        mov     edx, DWORD PTR _y$[esp-4]
        mov     ecx, eax
        shl     ecx, 4
        sub     ecx, eax
        lea     eax, DWORD PTR [edx+ecx*4]
        mov     ecx, DWORD PTR _array$[esp-4]
        lea     eax, DWORD PTR [eax+eax*4]
        shl     eax, 4
        add     eax, DWORD PTR _z$[esp-4]
        mov     eax, DWORD PTR [ecx+eax*4]
        ret     0
_f      ENDP

指令清单18.54　Non-optimizing Keil 6/2013 (ARM mode)

f PROC
        RSB      r1,r1,r1,LSL #4
        ADD      r1,r1,r1,LSL #2
        ADD      r0,r0,r1,LSL #8
        ADD      r1,r2,r2,LSL #2
        ADD      r0,r0,r1,LSL #6
        LDR      r0,[r0,r3,LSL #2]
        BX       lr
        ENDP

指令清单18.55　Non-optimizing Keil 6/2013 (Thumb mode)

f PROC
        PUSH     {r4,lr}
        MOVS     r4,#0x4b
        LSLS     r4,r4,#8
        MULS     r1,r4,r1
        ADDS     r0,r1,r0
        MOVS     r1,#0xff
        ADDS     r1,r1,#0x41
        MULS     r2,r1,r2
        ADDS     r0,r0,r2
        LSLS     r1,r3,#2
        LDR      r0,[r0,r1]
        POP      {r4,pc}
        ENDP

指令清单18.56　Optimizing GCC 4.9 (ARM64)

f:
        sxtw    x2, w2
        mov     w4, 19200
        add     x2, x2, x2, lsl 2
        smull   x1, w1, w4
        lsl     x2, x2, 4
        add     x3, x2, x3, sxtw
        add     x0, x0, x3, lsl 2
        ldr     w0, [x0,x1]
        ret

指令清单18.57　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:
           sll     $v0, $a1, 10
           sll     $a1, 8
           addu    $a1, $v0
           sll     $v0, $a2, 6
           sll     $a2, 4
           addu    $a2, $v0
           sll     $v0, $a1, 4
           subu    $a1, $v0, $a1
           addu    $a2, $a3
           addu    $a1, $a0, $a1
           sll     $a2, 2
           addu    $a1, $a2
           lw      $v0, 0($a1)
           jr      $ra
           or      $at, $zero

18.9.5　题目5

请描述下列代码的作用。

指令清单18.58　Optimizing MSVC 2012 /GS-

COMM    _tbl:DWORD:064H

tv759 = -4      ;size= 4
_main   PROC
        push    ecx
        push    ebx
        push    ebp
        push    esi
        xor     edx, edx
        push    edi
        xor     esi, esi
        xor     edi, edi
        xor     ebx, ebx
        xor     ebp, ebp
        mov     DWORD PTR tv759[esp+20], edx
        mov     eax, OFFSET _tbl+4
        npad    8 ; align next label
$LL6@main:
        lea     ecx, DWORD PTR [edx+edx]
        mov     DWORD PTR [eax+4], ecx
        mov     ecx, DWORD PTR tv759[esp+20]
        add     DWORD PTR tv759[esp+20], 3
        mov     DWORD PTR [eax+8], ecx
        lea     ecx, DWORD PTR [edx*4]
        mov     DWORD PTR [eax+12], ecx
        lea     ecx, DWORD PTR [edx*8]
        mov     DWORD PTR [eax], edx
        mov     DWORD PTR [eax+16], ebp
        mov     DWORD PTR [eax+20], ebx
        mov     DWORD PTR [eax+24], edi
        mov     DWORD PTR [eax+32], esi
        mov     DWORD PTR [eax-4], 0
        mov     DWORD PTR [eax+28], ecx
        add     eax, 40
        inc     edx
        add     ebp, 5
        add     ebx, 6
        add     edi, 7
        add     esi, 9
        cmp     eax, OFFSET _tbl+404
        jl      SHORT $LL6@main
        pop     edi
        pop     esi
        pop     ebp
        xor     eax, eax
        pop     ebx
        pop     ecx
        ret     0
_main   ENDP

指令清单18.59　Non-optimizing Keil 6/2013 (ARM mode)

main PROC
        LDR     r12,|L0.60|
        MOV     r1,#0
|L0.8|
        ADD     r2,r1,r1,LSL #2
        MOV     r0,#0
        ADD     r2,r12,r2,LSL #3
|L0.20|
        MUL     r3,r1,r0
        STR     r3,[r2,r0,LSL #2]
        ADD     r0,r0,#1
        CMP     r0,#0xa
        BLT     |L0.20|
        ADD     r1,r1,#1
        CMP     r1,#0xa
        MOVGE   r0,#0
        BLT     |L0.8|
        BX      lr
        ENDP

|L0.60|
        DCD     ||.bss||
        AREA ||.bss||, DATA, NOINIT, ALIGN=2
tbl
        %       400

指令清单18.60　Non-optimizing Keil 6/2013 (Thumb mode)

main PROC
        PUSH     {r4,r5,lr}
        LDR      r4,|L0.40|
        MOVS     r1,#0
|L0.6|
        MOVS     r2,#0x28
        MULS     r2,r1,r2
        MOVS     r0,#0
        ADDS     r3,r2,r4
|L0.14|
        MOVS     r2,r1
        MULS     r2,r0,r2
        LSLS     r5,r0,#2
        ADDS     r0,r0,#1
        CMP      r0,#0xa
        STR      r2,[r3,r5]
        BLT      |L0.14|
        ADDS     r1,r1,#1
        CMP      r1,#0xa
        BLT      |L0.6|
        MOVS     r0,#0
        POP      {r4,r5,pc}
        ENDP

        DCW      0x0000
|L0.40|
        DCD      ||.bss||
        AREA ||.bss||, DATA, NOINIT, ALIGN=2

tbl
        %        400

指令清单18.61　Non-optimizing GCC 4.9 (ARM64)

        .comm   tbl,400,8
main:
        sub     sp, sp, #16
        str     wzr, [sp,12]
        b       .L2
.L5:
        str     wzr, [sp,8]
        b       .L3
.L4:
        ldr     w1, [sp,12]
        ldr     w0, [sp,8]
        mul     w3, w1, w0
        adrp    x0, tbl
        add     x2, x0, :lo12:tbl
        ldrsw   x4, [sp,8]
        ldrsw   x1, [sp,12]
        mov     x0, x1
        lsl     x0, x0, 2
        add     x0, x0, x1
        lsl     x0, x0, 1
        add     x0, x0, x4
        str     w3, [x2,x0,lsl 2]
        ldr     w0, [sp,8]
        add     w0, w0, 1
        str     w0, [sp,8]
.L3:
        ldr     w0, [sp,8]
        cmp     w0, 9
        ble     .L4
        ldr     w0, [sp,12]
        add     w0, w0, 1
        str     w0, [sp,12]
.L2:
        ldr     w0, [sp,12]
        cmp     w0, 9
        ble     .L5
        mov     w0, 0
        add     sp, sp, 16
        ret

指令清单18.62　Non-optimizing GCC 4.4.5 (MIPS) (IDA)

main:

var_18     = -0x18
var_10     = -0x10
var_C      = -0xC
var_4      = -4

           addiu   $sp, -0x18
           sw      $fp, 0x18+var_4($sp)
           move    $fp, $sp
           la      $gp, __gnu_local_gp
           sw      $gp, 0x18+var_18($sp)
           sw      $zero, 0x18+var_C($fp)
           b       loc_A0
           or      $at, $zero

loc_24:                              # CODE XREF: main+AC
           sw      $zero, 0x18+var_10($fp)
           b       loc_7C
           or      $at, $zero

loc_30:                              # CODE XREF: main+88
           lw      $v0, 0x18+var_C($fp)
           lw      $a0, 0x18+var_10($fp)
           lw      $a1, 0x18+var_C($fp)
           lw      $v1, 0x18+var_10($fp)
           or      $at, $zero
           mult    $a1, $v1
           mflo    $a2
           lw      $v1, (tbl & 0xFFFF)($gp)
           sll     $v0, 1
           sll     $a1, $v0, 2
           addu    $v0, $a1
           addu    $v0, $a0
           sll     $v0, 2
           addu    $v0, $v1, $v0
           sw      $a2, 0($v0)
           lw      $v0, 0x18+var_10($fp)
           or      $at, $zero
           addiu   $v0, 1
           sw      $v0, 0x18+var_10($fp)

loc_7C:                              # CODE XREF: main+28
           lw      $v0, 0x18+var_10($fp)
           or      $at, $zero
           slti    $v0, 0xA
           bnez    $v0, loc_30
           or      $at, $zero
           lw      $v0, 0x18+var_C($fp)
           or      $at, $zero
           addiu   $v0, 1
           sw      $v0, 0x18+var_C($fp)

loc_A0:                              # CODE XREF: main+1C
           lw      $v0, 0x18+var_C($fp)
           or      $at, $zero
           slti    $v0, 0xA
           bnez    $v0, loc_24
           or      $at, $zero
           move    $v0, $zero
           move    $sp, $fp
           lw      $fp, 0x18+var_4($sp)
           addiu   $sp, 0x18
           jr      $ra
           or      $at, $zero

           .comm tbl:0x64            # DATA XREF: main+4C

[1]　http://en.wikipedia.org/wiki/Stack_buffer_overflow。

[2]　请参见维基百科https://en.wikipedia.org/wiki/Buffer_overflow_protection。

[3]　TIB是Thread Information Block。请参见https://en.wikipedia.org/wiki/Win32_Thread_ Information_Block。

[4]　但是根据C99标准[ISO07，pp 6.7.5/2]这种可变长数组是合法数据，可以被编译。GCC确实可以使用栈处理动态数组。这时候，它实现数组的方式和5.2.4节介绍过的alloca（）函数的实现方式相近。

第19章 位操作

有很多程序都把输入参数的某些比特位当作标识位处理。在表面看来，使用布尔变量足以替代标志位寄存器，但是这种替代的做法并不理智。

19.1　特定位

19.1.1　x86

Win32的API 中有这么一段接口声明：

    HANDLE fh;

    fh=CreateFile ("file", GENERIC_WRITE | GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_ALWAYS ,↙
    ↘FILE_ATTRIBUTE_NORMAL, NULL);

经过MSVC 2010编译，可得到如下所示的指令。

指令清单19.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对有关常量的相关位域进行了定义。

指令清单19.2　WinNT.h

#define GENERIC_READ           (0x80000000L)
#define GENERIC_WRITE          (0x40000000L)
#define GENERIC_EXECUTE        (0x20000000L)
#define GENERIC_ALL            (0x10000000L)

毫无疑问，在API声明里，CreatFile()函数的第二个参数为“GENERIC_READ | GENERIC_WRITE” = 0x80000000 | 0x40000000 = 0xC0000000。

CreatFile()函数如何检测标志位呢？

我们可以在Windows XP SP3 x86的KERNEL32.DLL中，找到CreateFileW()函数的相关代码。

指令清单19.3　KERNEL32.DLL (Windows XP SP3 x86)

.text:7C83D429  test       byte ptr [ebp+dwDesiredAccess+3], 40h
.text:7C83D42   Dmov       [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指令之间也存在这种差别（请参见本书7.3.1节）。

即，上述可执行的程序源代码逻辑是：

if ((dwDesiredAccess&0x40000000) == 0) goto loc_7C83D417

如果此处的与运算（&）的结果是1，则会清零ZF标志位，而不会触发JZ跳转。只有在dwDesiredAccess变量与0x40000000的计算结果为0的情况下，条件转移指令才会被触发；即与运算（&）的结果是零、ZF标志位为1，从而触发条件转移指令。

如果使用Linux的GCC 4.4.1编译下面的代码：

#include <stdio.h>
#include <fcntl.h>

void main()
{
        int handle;
        handle=open ("file", O_RDWR | O_CREAT);
};

得到的汇编指令会如下所示。

指令清单19.4　GCC 4.4.1

           public main
main       proc near

var_20     = dword ptr -20h
var_1C     = dword ptr -1Ch
var_4      = dword ptr -4

           push    ebp
           mov     ebp, esp
           and     esp, 0FFFFFFF0h
           sub     esp, 20h
           mov     [esp+20h+var_1C], 42h
           mov     [esp+20h+var_20], offset aFile ; "file"
           call    _open
           mov     [esp+20h+var_4], eax
           leave
           retn
main       endp

在库文件libc.so.6里，open()函数只是调用syscall而已。

指令清单19.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()函数时，Linux内核会检测某些标识位。

当然，我们可以下载Glibc和Linux 内核源代码来一窥究竟。但是本文旨在粗略地介绍原理，就不逐行地解释代码了。

在Linux 2.6中，当程序通过syscall调用 sys_open的时候，它调用的函数实际上是内核函数 do_sys_open()。在执行函数do_sys_open()的时候，系统又会调用do_filp_open() 函数。有兴趣的读者，可以在Linux内核源代码里的fs/namei.c里找到这部分内容。

编译器不仅会使用栈来传递参数，它还会使用寄存器传递部分参数。编译器最常采用的函数调用约定是fastcall（参见6.4.3节）。这个规范优先使用寄存器来传递参数。这使得CPU不必每次都要访问内存里的数据栈，大大提升了程序的运行效率。此外，我们可以通过GCC的regparm选项[1]，设置传递参数的寄存器数量。

在Linux 2.6内核的编译选项中，设定了寄存器选项“-mregparm=3”。[2]

这个选项决定，编译器首先通过EAX、EDX和ECX寄存器传递函数所需的头3个参数，再通过栈传递其余的参数。当然，如果参数的总数不足3个，它只会用到部分寄存器。

在Ubuntu里下载Linux Kernel 2.6.31，并用“make vmlinux”指令编译，然后使用IDA打开程序，寻找函数do_filp_open()。这个函数的开头部分大体是这样的。

指令清单19.6　do_filp_open() (Linux kernel 2.6.31)

do_filp_open    proc near
......
                push    ebp
                mov     ebp, esp
                push    edi
                push    esi
                push    ebx
                mov     ebx, ecx
                add     ebx, 1
                sub     esp, 98h
                mov     esi, [ebp+arg_4]  ; acc_mode (5th arg)
                test    bl, 3
                mov     [ebp+var_80], eax ; dfd (1th arg)
                mov     [ebp+var_7C], edx ; pathname (2th arg)
                mov     [ebp+var_78], ecx ; open_flag (3th arg)
                jnz     short loc_C01EF684
                mov     ebx, ecx          ; ebx <- open_flag

函数序言部分，GCC就把3个寄存器里的参数值存储在栈里。若非如此，那么后续程序可能出现寄存器不够使用的资源紧张问题。

我们再来看下do_filp_open()函数的另外一段代码。

指令清单19.7　do\_filp\_open() (Linux kernel 2.6.31)

loc_C01EF6B4:                       ; CODE XREF: do_filp_open+4F
                test    bl, 40h     ; O_CREAT
                jnz     loc_C01EF810
                mov     edi, ebx
                shr     edi, 11h
                xor     edi, 1
                and     edi, 1
                test    ebx, 10000h
                jz      short loc_C01EF6D3
                or      edi, 2

汇编宏O_CREAT的值就是0x40h。TEST指令检查open_flag里是否存在0x40的标志位，如果这个位是1，则会触发下一条JNZ指令。

19.1.2　ARM

在Linux Kernel 3.8.0平台的系统里，O_CREAT的检查操作有所不同。

指令清单19.8　Linux kernel 3.8.0

struct file *do_filp_open(int dfd, struct filename *pathname,
                const struct open_flags *op)
{
...
        filp = path_openat(dfd, pathname, &nd, op, flags | LOOKUP_RCU);
...
}

static struct file *path_openat(int dfd, struct filename *pathname,
                struct nameidata *nd, const struct open_flags *op, int flags)
{
...
        error = do_last(nd, &path, file, op, &opened, pathname);
...
}
static int do_last(struct nameidata *nd, struct path *path,
                   struct file *file, const struct open_flags *op,
                   int *opened, struct filename *name)
{ ...
        if (!(open_flag & O_CREAT)) {
    ...
                error = lookup_fast(nd, path, &inode);
    ...
        } else {
    ...
                error = complete_walk(nd);
        }
...
}

使用IDA打开ARM模式的内核程序，可以看到do_last()函数的代码如下所示。

指令清单19.9　do_last() (vmlinux)

...
.text:C0169EA8     MOV        R9, R3  ; R3 - (4th argument) open_flag
...
.text:C0169ED4     LDR        R6, [R9] ; R6 - open_flag
...
.text:C0169F68     TST        R6, #0x40 ; jumptable C0169F00 default case
.text:C0169F6C     BNE        loc_C016A128
.text:C0169F70     LDR        R2, [R4,#0x10]
.text:C0169F74     ADD        R12, R4, #8
.text:C0169F78     LDR        R3, [R4,#0xC]
.text:C0169F7C     MOV        R0, R4
.text:C0169F80     STR        R12, [R11,#var_50]
.text:C0169F84     LDRB       R3, [R2,R3]
.text:C0169F88     MOV        R2, R8
.text:C0169F8C     CMP        R3, #0
.text:C0169F90     ORRNE      R1, R1, #3
.text:C0169F94     STRNE      R1, [R4,#0x24]
.text:C0169F98     ANDS       R3, R6, #0x200000
.text:C0169F9C     MOV        R1, R12
.text:C0169FA0     LDRNE      R3, [R4,#0x24]
.text:C0169FA4     ANDNE      R3, R3, #1
.text:C0169FA8     EORNE      R3, R3, #1
.text:C0169FAC     STR        R3, [R11,#var_54]
.text:C0169FB0     SUB        R3, R11, #-var_38
.text:C0169FB4     BL         lookup_fast
...
.text:C016A128 loc_C016A128                  ; CODE XREF: do_last.isra.14+DC
.text:C016A128     MOV        R0, R4
.text:C016A12C     BL         complete_walk
...

TST指令的功能与x86平台的TEST指令相同。

显而易见，程序在某一条件下会调用lookup_fast()函数，而在另一条件下会调用complete_walk()函数。这种行为符合do_last()函数的源代码。

这里的汇编宏O_CREAT也等于0x40。

19.2　设置/清除特定位

本节将会围绕下面这个程序进行讨论：

#include <stdio.h>

#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;
};

int main()
{
    f(0x12340678);
};

19.2.1　x86

Non-optimizing MSVC

经过MSVC 2010（未启用任何优化选项）编译上述程序，可得到如下所示的指令。

指令清单19.10　MSVC 2010

_rt$ = -4             ; size = 4
_a$ = 8               ; size = 4

_f  PROC
    push   ebp
    mov    ebp, esp
    push   ecx
    mov    eax, DWORD PTR _a$[ebp]
    mov    DWORD PTR _rt$[ebp], eax
    mov    ecx, DWORD PTR _rt$[ebp]
    or     ecx, 16384               ; 00004000H
    mov    DWORD PTR _rt$[ebp], ecx
    mov    edx, DWORD PTR _rt$[ebp]
    and    edx, -513                ; fffffdffH
    mov    DWORD PTR _rt$[ebp], edx
    mov    eax, DWORD PTR _rt$[ebp]
    mov    esp, ebp
    pop    ebp
    ret    0
_f  ENDP

OR 指令就是逐位进行或运算的指令，可用来将指定位置1。

AND指令的作用是重置bit位。效果上说，如果立即数的某个bit位为1，AND指令则会保留寄存器里的这个bit位；如果立即数的某个bit位为0，AND指令就会把寄存器里这个bit的值设置为0。按照bit位掩码（bitmask）的方式理解，就会非常容易地记住这些指令的作用。

OllyDbg

用OllyDbg打开这个程序，可以清楚地看到常量的各个位：

0x200的二进制数是00000000000000000001000000000，第10位是1。

0x200的逻辑非运算结果是0xFFFFFDFF(11111111111111111110111111111)。

0x4000的第15位是1。

如图19.1所示，输入变量为0x12340678 (10010001101000000011001111000)。

图19.1　OllyDbg：ECX寄存器载入输入变量

执行OR指令的情形如图19.2所示。

图19.2　OllyDbg：执行OR指令

在执行OR指令的时候，第15位被设为1。

因为没有进行优化编译，所以程序中有些无用指令。如图19.3所示，它又加载了一次这个值。

图19.3　OllyDbg：EDX寄存器再次加载运算结果

然后进行了AND与操作，如图19.4所示。

图19.4　OllyDbg：执行与操作

这时寄存器的第10位被清零。换句话说，第10位之外的所有位都被保留了。最终运算结果是0x12344478，即二进制的10010001101000100010001111000。

Optimizing MSVC

如果开启了MSVC的优化选项/Ox，生成的代码就会精简很多。

指令清单19.11　Optimizing MSVC

_a$ = 8                      ; size = 4
_f     PROC
    mov    eax, DWORD PTR _a$[esp-4]
    and    eax, -513         ; fffffdffH
    or     eax, 16384        ; 00004000H
    ret    0
_f     ENDP

Non-optimizing GCC

如果关闭GCC 4.4.1的优化选项，则会生成下列代码。

指令清单19.12　Non-optimizing GCC

           public f
f          proc near

var_4      = dword ptr -4
arg_0      = dword ptr  8

           push     ebp
           mov      ebp, esp
           sub      esp, 10h
           mov      eax, [ebp+arg_0]
           mov      [ebp+var_4], eax
           or       [ebp+var_4], 4000h
           and      [ebp+var_4], 0FFFFFDFFh
           mov      eax, [ebp+var_4]
           leave
           retn
f          endp

虽然GCC的非优化编译结果中存在冗余指令，但是这个程序仍然比MSVC的非优化编译程序要短。

接下来启用GCC的-O3选项进行优化编译。

Optimizing GCC

指令清单19.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

它比MSVC优化编译生成的程序还要短。值得注意的是，它直接使用了EAX寄存器的部分空间——EAX寄存器的第8到第15位（第1个字节），即AH寄存器。

7th （字节号）

6th

5th

4th

3rd

2nd

1st

0th

RAX x64

EAX

AX

AH

AL

在早期的16位8086 CPU里，AX寄存器分为2个8位寄存器——即AL/AH寄存器。而80386差不多把所有的寄存器都扩展为32位，形成了EAX寄存器。但是出于兼容性的考虑，后续的CPU保留了AX/AH/AL寄存器的命名空间。

世上先有的16位 8086 CPU，后有的32位x86 CPU。在早期16位CPU上运行短程序，其opcode比在32位CPU上运行的opcode要短。所以，操作AH寄存器的“or ah，40h”的指令只占用了3字节的opcode。虽然说似乎“or eax,04000h”这样的指令更合乎情理，但是那样就会占用5字节的opcode；如果第一个操作符不是EAX寄存器，同等的指令甚至会使用6字节的opcode。

Optimizing GCC and regparm

如果同时启用优化选项“-O3”和寄存器分配选项“regparm＝3”，生成的指令会更为简短。

指令清单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寄存器里（fastcall规范），所以可直接使用EAX寄存器。函数引言的“push ebp / mov ebp,esp”指令，以及函数尾声的“pop ebp”完全可以省略；但是GCC还做不到这种程度的智能优化。这种代码完全可以作为内嵌函数（参见第43章）来使用。

19.2.2　ARM + Optimizing Keil 6/2013 (ARM mode)

指令清单19.15　Optimizing Keil 6/2013 (ARM mode)

02 0C C0 E3     BIC     R0, R0, #0x200
01 09 80 E3     ORR     R0, R0, #0x4000
1E FF 2F E1     BX      LR

BIC是逐位清除的“（反码）逻辑与”指令，它将第一个操作数与第二个操作数的反码（非求非运算的结果，进行逻辑与运算，比）x86指令集里“逻辑与”指令多作了一次求非运算，ORR是“逻辑或指令”，与x86“逻辑或”的作用相同。

19.2.3　ARM + Optimizing Keil 6/2013 (Thumb mode)

指令清单19.16　Optimizing Keil 6/2013 (Thumb mode)

01 21 89 03     MOVS      R1, 0x4000
08 43ORRS       R0,       R1
49 11ASRS       R1,       R1, #5   ; generate 0x200 and place to R1
88 43           BICS      R0, R1
70 47           BX        LR

编译器借助了算术右移指令，由0x4000>>5生成0x200。与直接对寄存器进行立即数赋值（0x200）相比，这种算术指令的opcode比较短。

所以，后面的ASRS（算术右移）指令通过计算0x4000>>5得到0x200。

19.2.4　ARM + Optimizing Xcode (LLVM) + ARM mode

指令清单19.17　Optimizing Xcode 4.6.3 (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? 或许，LLVM优化功能对原有指令进行了再加工，或许这是负负得正。总之这段代码没有质量问题。

有时候编译器确实会做这种类型的代码处理，详细情况请参见第91章。

使用Xcode 4.6.3 进行优化编译而生成的代码也非常相似，本文不再介绍。

19.2.5　ARM：BIC指令详解

把本章开头的源代码略加修改：

int f(int a)
{
    int rt=a;

    REMOVE_BIT (rt, 0x1234);

    return rt;
};

用Keil 5.03进行优化编译[3]，可得ARM模式的指令如下：

f PROC
        BIC      r0,r0,#0x1000
        BIC      r0,r0,#0x234
        BX       lr
        ENDP

上述程序使用了两条BIC指令，即它需要通过两次处理才能清除0x1234对应的比特位。这是因为立即数0x1234无法和BIC指令封装在同一条指令里。所以编译器进行了等效处理，通过两条指令实现清空0x1000和0x234相应的比特位。

19.2.6　ARM64: Optimizing GCC(Linaro) 4.9

面向ARM64的GCC编译器，可直接使用AND指令、而不用BIC指令。

指令清单19.18　Optimizing GCC (Linaro) 4.9

f:
        add   w0, w0, -513    ; 0xFFFFFFFFFFFFFDFF
        orr   w0, w0, 16384   ; 0x4000
        ret

19.2.7　ARM64: Non-optimizing GCC (Linaro) 4.9

关闭优化选项之后，GCC生成的代码中会加载无用指令，但是功能相同。

指令清单19.19　Non-optimizing GCC (Linaro) 4.9

f:
        sub     sp, sp, #32
        str     w0, [sp,12]
        ldr     w0, [sp,12]
        str     w0, [sp,28]
        ldr     w0, [sp,28]
        orr     w0, w0, 16384        ; 0x4000
        str     w0, [sp,28]
        ldr     w0, [sp,28]
        and     w0, w0, -513         ; 0xFFFFFFFFFFFFFDFF
        str     w0, [sp,28]
        ldr     w0, [sp,28]
        add     sp, sp, 32
        ret

19.2.8　MIPS

指令清单19.20　Optimizing GCC 4.4.5 (IDA)

f:
; $a0=a
                ori     $a0, 0x4000
; $a0=a|0x4000
                li      $v0, 0xFFFFFDFF
                jr      $ra
                and     $v0, $a0, $v0
; at finish: $v0 = $a0&$v0 = a|0x4000 & 0xFFFFFDFF

ORI无疑还是OR指令。指令中的“I”意味着它使用机械码存储数值。

但是后面的AND指令可就不能是ANDI指令了。这是因为它涉及的立即数0xFFFFFDFF太长，无法连同操作指令封装在同一条指令里。所以编译器把这个数赋值给$V0寄存器，然后让它与其他寄存器进行AND/与运算。

19.3　位移

C/C++的位移运算符是<<和>>。

x86指令集里有对应的左移SHL和右移SHR指令。

位移操作通常用来实现与“2的n次幂”有关的乘法和除法运算。有关介绍请参见本书的16.1.2节和16.2.1节。

位移指令可用来对特定位进行取值或隔离，用途十分广泛。

19.4　在FPU上设置特定位

FPU存储数据的IEEE 754格式如下：

最高数权位MSB为符号位。是否可能在不使用FPU指令的前提下变更浮点数的符号呢？

#include <stdio.h>

float my_abs (float i)
{
        unsigned int tmp=(*(unsigned int*)&i) & 0x7FFFFFFF;
        return *(float*)&tmp;
};

float set_sign (float i)
{
        unsigned int tmp=(*(unsigned int*)&i) | 0x80000000;
        return *(float*)&tmp;
};
float negate (float i)
{
        unsigned int tmp=(*(unsigned int*)&i) ^ 0x80000000;
        return *(float*)&tmp;
};

int main()
{
        printf ("my_abs():\n");
        printf ("%f\n", my_abs (123.456));
        printf ("%f\n", my_abs (-456.123));
        printf ("set_sign():\n");
        printf ("%f\n", set_sign (123.456));
        printf ("%f\n", set_sign (-456.123));
        printf ("negate():\n");
        printf ("%f\n", negate (123.456));
        printf ("%f\n", negate (-456.123));
};

使用位操作C/C++指令之后，我们不必在CPU和FPU的寄存器之间传递数据、再进行数学运算了。本节列举了三个位操作函数：清除MSB符号位的my_abs()函数、设置符号位的set_sign()函数及求负函数negate()。

19.4.1　XOR操作详解

XOR指令常用于使一个操作数的某些位取反的场合。若操作数A的某个位为1，那么XOR指令将对另一个数的相应位求非。

输入A

输入B

输出

0

0

0

0

1

1

1

0

1

1

1

0

若某个参数为零，XOR则不会进行任何操作。这种指令也就是所谓的空操作。这是XOR指令非常重要的属性，建议记住它。

19.4.2　x86

编译生成的指令简单易懂。

指令清单19.21　Optimizing MSVC 2012

_tmp$ = 8
_i$ = 8
_my_abs PROC
        and     DWORD PTR _i$[esp-4], 2147483647    ; 7fffffffH
        fld     DWORD PTR _tmp$[esp-4]
        ret     0
_my_abs ENDP

_tmp$ = 8
_i$ = 8
_set_sign PROC
        or      DWORD PTR _i$[esp-4], -2147483648   ; 80000000H
        fld     DWORD PTR _tmp$[esp-4]
        ret     0
_set_sign ENDP

_tmp$ = 8
_i$ = 8
_negate PROC
        xor     DWORD PTR _i$[esp-4], -2147483648   ; 80000000H
        fld     DWORD PTR _tmp$[esp-4]
        ret     0
_negate ENDP

函数从栈中提取一个浮点类型的数据，但是把它当作整数类型数据进行处理。

AND和OR设置相应的比特位，而XOR用来设置相反的符号位。

因为要把浮点数还给FPU，所以最后把修改后的处理结果保存到ST0寄存器。

接下来，使用 64位的MSVC 2012进行优化编译，得到的代码如下所示。

指令清单19.22　Optimizing MSVC 2012 x64

tmp$ = 8
i$ = 8
my_abs  PROC
        movss   DWORD PTR [rsp+8], xmm0
        mov     eax, DWORD PTR i$[rsp]
        btr     eax, 31
        mov     DWORD PTR tmp$[rsp], eax
        movss   xmm0, DWORD PTR tmp$[rsp]
        ret     0
my_abs  ENDP
_TEXT   ENDS

tmp$ = 8
i$ = 8
set_sign PROC
        movss   DWORD PTR [rsp+8], xmm0
        mov     eax, DWORD PTR i$[rsp]
        bts     eax, 31
        mov     DWORD PTR tmp$[rsp], eax
        movss   xmm0, DWORD PTR tmp$[rsp]
        ret     0
set_sign ENDP

tmp$ = 8
i$ = 8
negate  PROC
        movss   DWORD PTR [rsp+8], xmm0
        mov     eax, DWORD PTR i$[rsp]
        btc     eax, 31
        mov     DWORD PTR tmp$[rsp], eax
        movss   xmm0, DWORD PTR tmp$[rsp]
        ret     0
negate  END

输入值会经XMM0寄存器存储到局部数据栈，然后通过BTR、BTS、BTC等指令进行处理。

这些指令用于重置（BTR）、置位（BTS）和翻转（BTC）特定比特位。如果从0开始数的话，浮点数的第31位才是MSB。

最终，运算结果被复制到XMM0寄存器。在Win64系统中，浮点型返回值要保存在这个寄存器里。

19.4.3　MIPS

面向MIPS的GCC 4.4.5生成的程序几乎没什么区别。

指令清单19.23　Optimizing GCC 4.4.5 (IDA)

my_abs:
; move from coprocessor 1:
                   mfc1     $v1, $f12
                   li       $v0, 0x7FFFFFFF
; $v0=0x7FFFFFFF
; do AND:
                   and      $v0, $v1
; move to coprocessor 1:
                   mtc1     $v0, $f0
; return
                   jr       $ra
                   or       $at, $zero ; branch delay slot

set_sign:
; move from coprocessor 1:
                   mfc1     $v0, $f12
                   lui      $v1, 0x8000
; $v1=0x80000000
; do OR:
                   or       $v0, $v1, $v0
; move to coprocessor 1:
                   mtc1    $v0, $f0
; return
                   jr       $ra
                   or       $at, $zero ; branch delay slot
negate:
; move from coprocessor 1:
                   mfc1     $v0, $f12
                   lui      $v1, 0x8000
; $v1=0x80000000
; do XOR
                   xor      $v0, $v1, $v0
; move to coprocessor 1:
                   mtc1     $v0, $f0
; return
                   jr       $ra
                   or       $at, $zero ; branch delay slot

因为LUI在传递高16位数据时会清除寄存器的低16位，所以单条LUI指令就可以完成0x80000000的赋值，无需再用ORI指令。

19.4.4　ARM

Optimizing Keil 6/2013 (ARM mode)

指令清单19.24　Optimizing Keil 6/2013 (ARM mode)

my_abs PROC
; clear bit:
        BIC      r0,r0,#0x80000000
        BX       lr
        ENDP

set_sign PROC
; do OR:
        ORR      r0,r0,#0x80000000
        BX       lr
        ENDP

negate PROC
; do XOR:
        EOR      r0,r0,#0x80000000
        BX       lr
        ENDP

ARM模式的程序可以使用BIC指令直接清除指定的比特位。上述程序中的EOR指令是ARM模式下进行XOR（异或）运算的指令。

Optimizing Keil 6/2013 (Thumb mode)

指令清单19.25　Optimizing Keil 6/2013 (Thumb mode)

my_abs PROC
        LSLS     r0,r0,#1
; r0=i<<1
        LSRS     r0,r0,#1
; r0=(i<<1)>>1
        BX       lr
        ENDP

set_sign PROC
        MOVS     r1,#1
; r1=1
        LSLS     r1,r1,#31
; r1=1<<31=0x80000000
        ORRS     r0,r0,r1
; r0=r0 | 0x80000000
        BX       lr
        ENDP

negate PROC
        MOVS     r1,#1
; r1=1
        LSLS     r1,r1,#31
; r1=1<<31=0x80000000
        EORS     r0,r0,r1
; r0=r0 ^ 0x80000000
        BX       lr
        ENDP

ARM平台的Thumb模式指令都是16位定长指令。因为单条指令无法封装太大的数据，所以编译器使用了MOVS/LSLS指令对传递常量0x80000000。这种赋值操作基于数学机制：1<<31=0x80000000。

然而my_abs的指令令人费解，它做的运算是(i<<1)>>1。虽然看上去是画蛇添足，但是在进行“输入变量<<1”的运算时，最高数权位被舍弃了。继而在执行“运算结果>>1”的语句时，根据右移运算规则最高数权位变为零，而其他各位数值回归原位。借助LSLS/LSRS指令对，该函数清除了最高数权位MSB。

Optimizing GCC 4.6.3 (Raspberry Pi, ARM mode)

指令清单19.26　Optimizing GCC 4.6.3 for Raspberry Pi (ARM mode)

my_abs
; copy from S0 to R2:
                FMRS    R2, S0
; clear bit:
                BIC     R3, R2, #0x80000000
; copy from R3 to S0:
                FMSR    S0, R3
                BX      LR

set_sign
; copy from S0 to R2:
                FMRS    R2, S0
; do OR:
                ORR     R3, R2, #0x80000000
; copy from R3 to S0:
                FMSR    S0, R3
                BX      LR

negate
; copy from S0 to R2:
                FMRS    R2, S0
; do ADD:
                ADD     R3, R2, #0x80000000
; copy from R3 to S0:
                FMSR    S0, R3
                BX      LR

在 QEMU的仿真环境下启动Raspberry Pi Linux即可模拟ARM FPU。所以上述程序使用S-字头寄存器存储浮点数，而不会使用R-字头寄存器。

FMRS指令用于在通用寄存器GPR和FPU之间交换数据。

上述程序中的my_abs()函数和set_sign()函数都中规中矩。但是negate()函数在应该出现XOR指令的地方使用了ADD指令。

其实“ADD register, 0x80000000”和“XOR register, 0x80000000”是等效指令，只是不特别直观而已。整个函数用于变更最高数权位的值。在数学上，1000与任意三位数相加，运算结果的最后3位仍然和被加数的最后三位相等。同理，1234567+10000=1244567，和的最后四位与第一个数的最后四位等值。以二进制数的角度来看，0x80000000就是100000000000000000000000000000000，只有最高位是1。因此，当0x80000000与任意数值相加时，运算结果的最后31位还是被加数的最后31位，只是最高数权位会发生变化。再看MSB：1+0=1，1+1=0（高于32位的数值被舍弃了）。因此，对于这个特定加数来说，ADD指令和XOR指令可以互换。虽然替换的必要性不甚明朗，但是整个程序还是没有问题的。

19.5　位校验

我们介绍一个测算输入变量的2进制数里有多少个1的函数。这种函数叫作“population count/点数”函数。在支持SSE4的x86 CPU的指令集里，甚至有直接对应的POPCNT指令。

#include <stdio.h>

#define IS_SET(flag, bit)    ((flag) & (bit))

int f(unsigned int a)
{
    int i;
    int rt=0;

for (i=0; i<32; i++)
    if (IS_SET (a, 1<<i))
        rt++;

    return rt;
};
int main()
{
    f(0x12345678); // test
};

在这个循环里，循环计数变量i的取值范围是0～31。因此“1<<i”表达式的取值范围是0到0x80000000，用人类语言解释，它的含义就是“将数字1左移n位”。“1<<i”将会使“1”逐一遍历32位数字的每一位，同时将其他位置零。

这段程序里，1<<i可能产生的值有：

C/C++表达式

2的指数

十进制数

十六进制数

1<<0

2⁰

1

1

1<<1

2¹

2

2

1<<2

2²

4

4

1<<3

2³

8

8

1<<4

2⁴

16

0x10

1<<5

2⁵

32

0x20

1<<6

2⁶

64

0x40

1<<7

2⁷

128

0x80

1<<8

2⁸

256

0x100

1<<9

2⁹

512

0x200

1<<10

2¹⁰

1024

0x400

1<<11

2¹¹

2048

0x800

1<<12

2¹²

4096

0x1000

1<<13

2¹³

8192

0x2000

1<<14

2¹⁴

16384

0x4000

1<<15

2¹⁵

32768

0x8000

1<<16

2¹⁶

65536

0x10000

1<<17

2¹⁷

131072

0x20000

1<<18

2¹⁸

262144

0x40000

1<<19

2¹⁹

524288

0x80000

1<<20

2²⁰

1048576

0x100000

1<<21

2²¹

2097152

0x200000

1<<22

2²²

4194304

0x400000

1<<23

2²³

8388608

0x800000

1<<24

2²⁴

16777216

0x1000000

1<<25

2²⁵

33554432

0x2000000

1<<26

2²⁶

67108864

0x4000000

1<<27

2²⁷

134217728

0x8000000

1<<28

2²⁸

268435456

0x10000000

1<<29

2²⁹

536870912

0x20000000

1<<30

2³⁰

1073741824

0x40000000

1<<31

2³¹

2147483648

0x80000000

逆向工程的实际工作中经常出现这些常数（bit mask）。逆向工程人员应当常备这个表格。虽然10进制常数很难记忆，但是背下十六进制数并不太困难。

它们经常被用作各种标志位。例如，在Apache 2.4.6的源代码中，ssl_private.h就用到了其中的部分常量：

/**
 * Define the SSL options
 */
#define SSL_OPT_NONE            (0)
#define SSL_OPT_RELSET          (1<<0)
#define SSL_OPT_STDENVVARS      (1<<1)
#define SSL_OPT_EXPORTCERTDATA  (1<<3)
#define SSL_OPT_FAKEBASICAUTH   (1<<4)
#define SSL_OPT_STRICTREQUIRE   (1<<5)
#define SSL_OPT_OPTRENEGOTIATE  (1<<6)
#define SSL_OPT_LEGACYDNFORMAT  (1<<7)

现在言归正传，看看“点数”程序。

宏IS_SET的作用是检测flag参数（变量a）的第n位是否为1。

如果变量a对应的比特位不是1，则宏IS_SET返回0；如果变量a对应的比特位是1，则返回bit mask。因为触发if()then{}语句的条件是“条件表达式不是零”，所以哪怕条件返回值是123456，它都会照样执行then模块的指令。

19.5.1　x86

MSVC

使用MSVC2010编译上述程序，得到的代码如下所示。

指令清单19.27　MSVC 2010

_rt$ = -8             ;size=4
_i$ =-4               ;size=4
_a$ =8                ;size=4

_f PROC
    push   ebp
    mov    ebp, esp
    sub    esp, 8
    mov    WORD PTR _rt$[ebp], 0
    mov    DWORD PTR _i$[ebp], 0
    jmp    SHORT $LN4@f
$LN3@f:
    mov    ax, DWORD PTR _i$[ebp]    ; 递增
    add    x, 1
    mov    WORD PTR _i$[ebp], eax
$LN4@f:
    cmp    DWORD PTR _i$[ebp], 32    ; 00000020H
    jge    SHORT $LN2@f              ; 循环结束？
    mov    edx, 1
    mov    ecx, DWORD PTR _i$[ebp]
    shl    edx, cl                   ; EDX=EDX<<CL
    and    edx, DWORD PTR _a$[ebp]
    je     SHORT $LN1@f              ; result of AND instruction was 0?
                                     ; then skip next instructions
    mov    eax, DWORD PTR _rt$[ebp]  ; no, not zero
    add    eax, 1                    ; increment rt
    mov    DWORD PTR _rt$[ebp], eax
$LN1@f:
    jmp    SHORT $LN3@f
    $LN2@f:
    mov    eax, DWORD PTR _rt$[ebp]
    mov    esp, ebp
    pop    ebp
    ret    0
_f    ENDP

OllyDbg

为了方便使用OllyDbg进行演示，我们设置变量a的值为0x12345678。

在i=1时的情况如图19.5所示。

图19.5　OllyDbg：i=1时，i被加载到ECX寄存器

此刻EDX的值为1。

执行SHL指令的情况，如图19.6所示。

图19.6　OllyDbg：i = 1,EDX=1<<1 = 2

EDX寄存器的值是bit mask ,1<<1=2。

在执行AND指令后，ZF＝1。就是说，输入变量0x12345678和数字2的AND计算结果是0，如图19.7所示。

图19.7　OllyDbg：i=1，输入变量的对应比特位是1么？否（ZF=1）

这意味着输入变量的对应比特位是0。这种情况下，程序通过JZ指令进行跳转，绕过了操作点数计数器rt的递增指令。

继续执行程序，看看i＝4的情况。如图19.8所示，此时要执行SHL指令。

图19.8　OllyDbg：i=4, ECX加载i的值

此刻EDX寄存器的值是0x10，它是1<<4的结果，如图19.9所示。

图19.9　OllyDbg：i = 4, EDX =1<<4 = 0x10

当i＝4时，对应的bit mask存在。

执行AND指令的情形如图19.10所示。

图19.10　OllyDbg：i＝4，输入变量的对应位是否为1？是（ZF＝0）

因为对应bit位是1，所以ZF＝0。确实，0x12345678 & 0x10 = 0x10。这种情况下，将不会触发jz转移指令，点数计数器rt的值递增。

最终，函数的返回值是13。输入变量0x12345678的二进制数里有13个1。

GCC

使用GCC 4.4.1编译上述程序，可得如下所示的指令。

指令清单19.28　GCC 4.4.1

             public f
f            proc near

rt           = dword ptr -0Ch
i            = dword ptr -8
arg_0        = dword ptr  8

             push    ebp
             mov     ebp, esp
             push    ebx
             sub     esp, 10h
             mov     [ebp+rt], 0
             mov     [ebp+i], 0
             jmp     short loc_80483EF
loc_80483D0:
             mov     eax, [ebp+i]
             mov     edx, 1
             mov     ebx, edx
             mov     ecx, eax
             shl     ebx, cl
             mov     eax, ebx
             and     eax, [ebp+arg_0]
             test    eax, eax
             jz      short loc_80483EB
             add     [ebp+rt], 1
loc_80483EB:
             add     [ebp+i], 1
loc_80483EF:
             cmp     [ebp+i], 1Fh
             jle     short loc_80483D0
             mov     eax, [ebp+rt]
             add     esp, 10h
             pop     ebx
             pop     ebp
             retn
f            endp

19.5.2　x64

为了进行演示，本节对源程序略加修改，将数据类型扩展为64位：

#include <stdio.h>
#include <stdint.h>

#define IS_SET(flag, bit)   ((flag) & (bit))

int f(uint64_t a)
{
    uint64_t i;
    int rt=0;

    for (i=0; i<64; i++)
        if (IS_SET (a, 1ULL<<i))
            rt++;

    return rt;
};

Non-optimizing GCC 4.8.2

指令清单19.29　Non-optimizing GCC 4.8.2

f:
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-24], rdi   ; a
        mov     DWORD PTR [rbp-12], 0     ; rt=0
        mov     QWORD PTR [rbp-8], 0      ; i=0
        jmp     .L2
.L4:
        mov     rax, QWORD PTR [rbp-8]
        mov     rdx, QWORD PTR [rbp-24]
; RAX = i, RDX = a
        mov     ecx, eax
; ECX = i
        shr     rdx, cl
; RDX = RDX>>CL = a>>i
        mov     rax, rdx
; RAX = RDX = a>>i
        and     eax, 1
; EAX = EAX&1 = (a>>i)&1
        test    rax, rax
; the last bit is zero?
; skip the next ADD instruction, if it was so.
        je      .L3
        add     DWORD PTR [rbp-12], 1     ; rt++
.L3:
        add     QWORD PTR [rbp-8], 1      ; i++
.L2:
        cmp     QWORD PTR [rbp-8], 63     ; i< 63?
        jbe     .L4                       ; jump to the loop boday begin, if so
        mov     eax, DWORD PTR [rbp-12]   ; return rt
        pop     rbp
        ret

Optimizing GCC 4.8.2

指令清单19.30　Optimizing GCC 4.8.2

 1 f:
 2      xor     eax, eax             ; rt variable will be in EAX register
 3      xor     ecx, ecx             ; i variable will be in ECX register
 4 .L3:
 5      mov     rsi, rdi             ; load input value
 6      lea     edx, [rax+1]         ; EDX=EAX+1
 7 ; EDX here is a "new version of rt", which will be written into rt variable, if the last bit is 1
 8      shr     rsi, cl              ; RSI=RSI>>CL
 9      and     esi, 1               ; ESI=ESI&1
10 ; the last bit is 1? If so, write "new version of rt" into EAX
11      cmovne  eax, edx
12      add     rcx, 1               ; RCX++
13      cmp     rcx, 64
14      jne     .L3
15      rep ret                      ; AKA fatret

这段程序简洁又兼具特色。在本节前面介绍过的各个例子里，程序都是先比较特定位、然后在递增“rt”的值。但是这段程序的顺序有所不同，它先递增rt再把新的值写到EDX寄存器。这样一来，如果最后的比特位是1，那么程序将使用CMOVNE[4]指令（等同于CMOVNZ[5]）把EDX寄存器（rt的候选值）传递给EAX寄存器（当前的rt值，也是函数的返回值），从而完成rt的更新。所以，无论输入值是什么，每次迭代结束之后，循环控制变量的值都要进行递增，它肯定会被递增64次。

因为它只含有一个条件转移指令（在循环的尾部），所以这种编译方法独具优势。如果编译的方式过于机械化，那么程序要在递增rt和循环尾部进行两次条件转移。现在的CPU都具有分支预测的功能（请参加本书33.1节）。在性能方面，本例这类程序的效率更高。

最后一则指令是REP RET（opcode为F3 C3）。它就是MSVC里的FATRET。这个指令是由RET衍生出来的向优化指令。AMD建议：如果RET指令的前一条指令是条件转移指令，那么在函数最后的返回指令最好使用REP RET指令。（请参见AMD13b，p15）[6]

Optimizing MSVC 2010

指令清单19.31　MSVC 2010

a$ = 8
f       PROC
; RCX = input value
        xor     eax, eax
        mov     edx, 1
        lea     r8d, QWORD PTR [rax+64]
; R8D=64
        npad    5
$LL4@f:
        test    rdx, rcx
; there are no such bit in input value?
; skip the next INC instruction then.
        je      SHORT $LN3@f
        inc     eax     ; rt++
$LN3@f:
        rol     rdx, 1  ; RDX=RDX<<1
        dec     r8      ; R8--
        jne     SHORT $LL4@f
        fatret  0
f       ENDP

本例中，编译器用ROL指令替代了SHL指令。ROL的确切作用是“rotate left”，SHL的含义则是“shift left”。不过在本例中，它们的效果相同。

有关旋转指令的详细介绍，请参见附录A.6.3。

R8的值将从64逐渐递减为0，它的变化过程和变量i完全相反。

在程序的执行过程中，寄存器的关系如下图所示。

RDX

R8

0x0000000000000001

64

0x0000000000000002

63

0x0000000000000004

62

0x0000000000000008

61

……

……

0x4000000000000000

2

0x8000000000000000

1

程序最后使用了FATRET指令，我们在上一个例子里已经介绍过它了。

Optimizing MSVC 2012

指令清单19.32　MSVC 2012

a$ = 8
f       PROC
; RCX = input value
        xor      eax, eax
        mov      edx, 1
        lea      r8d, QWORD PTR [rax+32]
; EDX = 1, R8D = 32
        npad     5
$LL4@f:
; pass 1 ------------------------------------
        test     rdx, rcx
        je       SHORT $LN3@f
        inc      eax     ; rt++
$LN3@f:
        rol      rdx, 1  ; RDX=RDX<<1
; -------------------------------------------
; pass 2 ------------------------------------
        test     rdx, rcx
        je       SHORT $LN11@f
        inc      eax     ; rt++
$LN11@f:
        rol      rdx, 1  ; RDX=RDX<<1
; -------------------------------------------
        dec      r8      ; R8--
        jne      SHORT $LL4@f
        fatret  0
f       ENDP

MSVC 2012优化编译而生成的代码与MSVC 2010大体相同。但是MSVC 2012把迭代次数为64次的循环，拆解为两个迭代32次的循环。坦白讲，笔者也不清楚个中缘由。或许是优化的结果，或许循环体更长一些比较好。总之，我特地把这个例子记录下来，希望读者注意：编译器可能生成各种匪夷所思的代码，但是这些代码的功能仍然会忠实于源程序。

19.5.3　ARM + Optimizing Xcode 4.6.3 (LLVM) + ARM mode

指令清单19.33　Optimizing Xcode 4.6.3 (LLVM) (ARM mode)

           MOV             R1, R0
           MOV             R0, #0
           MOV             R2, #1
           MOV             R3, R0
loc_2E54
           TST             R1, R2,LSL R3 ; 根据R1 & (R2<<R3)设置标记
           ADD             R3, R3, #1    ; R3++
           ADDNE           R0, R0, #1    ; if ZF flag is cleared by TST, then R0++
           CMP             R3, #32
           BNE             loc_2E54
           BX              LR

ARM模式的TST指令相当于x86指令集的TEST指令。

ARM模式的指令里没有单独的位移操作指令（参见本书41.2.1节）。但是在数据处理指令——即MOV、TST、CMP、ADD、SUB、RSB指令中，可以使用参数调节符（如LSL、LSR、ASR、ROR、RRX）实现位移运算。

通过“参数调节符（modificators）”进行位移运算的指令，必须明确位移的方法和位移的位数。

此处的“TST R1, R2, LSL R3”，可理解为R1^(R2<<R3)。

19.5.4　ARM + Optimizing Xcode 4.6.3（LLVM）+ Thumb-2 mode

Thumb-2模式的代码与ARM模式的代码基本相同。只不过在Thumb-2模式下，不可能直接在TST指令里直接使用参数调节符LSL，所以它使用LSL.W和TST指令替代了ARM模式下的单条TST指令。

           MOV             R1, R0
           MOVS            R0, #0
           MOV.W           R9, #1
           MOVS            R3, #0
loc_2F7A
           LSL.W           R2, R9, R3
           TST             R2, R1
           ADD.W           R3, R3, #1
           IT NE
           ADDNE           R0, #1
           CMP             R3, #32
           BNE             loc_2F7A
           BX              LR

19.5.5　ARM64 + Optimizing GCC 4.9

本节使用的源程序，是19.5.2节中所示程序的64位修改版。

指令清单19.34　Optimizing GCC (Linaro) 4.8

f:
        mov     w2, 0       ; rt=0
        mov     x5, 1
        mov     w1, w2
.L2:
        lsl     x4, x5, x1  ; w4 = w5<<w1 = 1<<i
        add     w3, w2, 1   ; new_rt=rt+1
        tst     x4, x0      ; (1<<i) & a
        add     w1, w1, 1   ; i++
; result of TST was non-zero?
; then w2=w3 or rt=new_rt.
; otherwise: w2=w2 or rt=rt (idle operation)
        csel    w2, w3, w2, ne
        cmp     w1, 64      ; i<64?
        bne     .L2         ;yes
        mov     w0, w2      ; return rt
        ret

这个程序与GCC生成的x64程序（请参见指令清单19.30）十分相似。

其中，CSEL指令的全称是“Conditional SELect”。它依据TST指令设置的标志位，相应地从候选的两个操作符中选取一个数据并复制给W2。本例中，它传递的值是rt。

19.5.6　ARM64 + Non-optimizing GCC 4.9

本节再次使用19.5.2节中所示程序的64位修改版。

由于编译时关闭了优化功能，所以编译器生成的代码十分庞大。

指令清单19.35　Non-optimizing GCC (Linaro) 4.8

f:
        sub     sp, sp, #32
        str     x0, [sp,8]      ; store "a" value to Register Save Area
        str     wzr, [sp,24]    ; rt=0
        str     wzr, [sp,28]    ; i=0
        b       .L2
.L4:
        ldr     w0, [sp,28]
        mov     x1, 1
        lsl     x0, x1, x0      ; X0 = X1<<X0 = 1<<i
        mov     x1, x0
; X1 = 1<<i
        ldr     x0, [sp,8]
; X0 = a
        and     x0, x1, x0
; X0 = X1&X0 = (1<<i) & a
; X0 contain zero? then jump to .L3, skipping "rt" increment
        cmp     x0, xzr
        beq     .L3
; rt++
        ldr     w0, [sp,24]
        add     w0, w0, 1
        str     w0, [sp,24]
.L3:
; i++
        ldr     w0, [sp,28]
        add     w0, w0, 1
        str     w0, [sp,28]
.L2:
; i<=63? then jump to .L4
        ldr     w0, [sp,28]
        cmp     w0, 63
        ble     .L4
; return rt
        ldr     w0, [sp,24]
        add     sp, sp, 32
        ret

19.5.7　MIPS

Non-optimizing GCC

指令清单19.36　Non-optimizing GCC 4.4.5 (IDA)

f:
; IDA is not aware of variable names, we gave them manually:
rt              = -0x10
i               = -0xC
var_4           = -4
a               = 0

                addiu   $sp, -0x18
                sw      $fp, 0x18+var_4($sp)
                move    $fp, $sp
                sw      $a0, 0x18+a($fp)
; initialize rt and i variables to zero:
                sw      $zero, 0x18+rt($fp)
                sw      $zero, 0x18+i($fp)
; jump to loop check instructions:
                b       loc_68
                or      $at, $zero ; branch delay slot, NOP
loc_20:
                li      $v1, 1
                lw      $v0, 0x18+i($fp)
                or      $at, $zero ; load delay slot, NOP
                sllv    $v0, $v1, $v0
; $v0 = 1<<i
                move    $v1, $v0
                lw      $v0, 0x18+a($fp)
                or      $at, $zero ; load delay slot, NOP
                and     $v0, $v1, $v0
; $v0 = a&(1<<i)
; is a&(1<<i) equals to zero? jump to loc_58 then:
                beqz    $v0, loc_58
                or      $at, $zero
; no jump occurred, that means a&(1<<i)!=0, so increment "rt" then:
                lw      $v0, 0x18+rt($fp)
                or      $at, $zero ; load delay slot, NOP
                addiu   $v0, 1
                sw      $v0, 0x18+rt($fp)

loc_58:
; increment i:
                lw       $v0, 0x18+i($fp)
                or       $at, $zero ; load delay slot, NOP
                addiu    $v0, 1
                sw       $v0, 0x18+i($fp)

loc_68:
; load i and compare it with 0x20 (32).
; jump to loc_20 if it is less then 0x20 (32):
                lw       $v0, 0x18+i($fp)
                or       $at, $zero ; load delay slot, NOP
                slti     $v0, 0x20 # ' '
                bnez     $v0, loc_20
                or       $at, $zero ; branch delay slot, NOP
; function epilogue. return rt:
                lw       $v0, 0x18+rt($fp)
                move     $sp, $fp ; load delay slot
                lw       $fp, 0x18+var_4($sp)
                addiu    $sp, 0x18 ; load delay slot
                jr       $ra
                or       $at, $zero ; branch delay slot, NOP

这段程序十分直观：程序首先把所有的局部变量存储在局部栈里。在使用变量的时候，再把它们从栈里取出来。SLLV指令的全称是“Shift Word Left Logical Variable”。SLL指令只能把某个操作位移事先确定的比特位（这个位数是封装在opcode里的固定值），而SLLV指令从寄存器里读取位移的具体位数（编译时不能确定的变量）。

Optimizing GCC

启用优化功能之后，编译器生成的程序就会简略许多。不过它却使用了2条位移指令，比非优化编译而生成的程序还要多用一条位移指令。这是为什么？固然第一条SLLV指令可以替换为“跳转到第二个 SLLV的无条件转移指令”，但是如此一来函数中就需要多用一个转移指令。而编译器的优化功能会刻意减少这种转移指令的使用次数，更多详情请参见本书33.1节。

指令清单19.37　Optimizing GCC 4.4.5 (IDA)

f:
; $a0=a
; rt variable will reside in $v0:
                move    $v0, $zero
; i variable will reside in $v1:
                move    $v1, $zero
                li      $t0, 1
                li      $a3, 32
                sllv    $a1, $t0, $v1
; $a1 = $t0<<$v1 = 1<<i
loc_14:
                and     $a1, $a0
; $a1 = a&(1<<i)
; increment i:
                addiu   $v1, 1
; jump to loc\_28 if a&(1<<i)==0 and increment rt:
                beqz    $a1, loc_28
                addiu   $a2, $v0, 1
; if BEQZ was not triggered, save updated rt into $v0:
                move    $v0, $a2
loc_28:
; if i!=32, jump to loc_14 and also prepare next shifted value:
                bne     $v1, $a3, loc_14
                sllv    $a1, $t0, $v1
; return
                jr      $ra
                or      $at, $zero ; branch delay slot, NOP

19.6　本章小结

与C/C++语言中的位移操作符<<和>>相对应，x86指令集中有操作无符号数的SHR/SHL指令和操作有符号数的SAR/SHL指令。

在ARM平台上，对无符号数进行位移运算的指令是LSR/LSL，对有符号数进行位移运算的指令是ASR/LSL。而且，ARM指令还可以通过“参数调节符”的形式使用“后缀型”位移运算指令。此类对其他数进行运算的指令，又叫做“数据处理指令”。

19.6.1　检测特定位（编译阶段）

对某数值的二进制1000000位(即16进制的0x40)进行检测的指令大体如下。

指令清单19.38　C/C++

if (input&0x40)
    ...

指令清单19.39　x86

TEST REG, 40h
JNZ is_set
; bit is not set

指令清单19.40　x86

TEST REG, 40h
JZ is_cleared
; bit is set

指令清单19.41　ARM (ARM mode)

TST REG, #0x40
BNE is_set
; bit is not set

某些情况下，编译器会使用AND指令而不使用TEST指令。无论编译器选取了哪个指令，都不会影响程序将要检测的标志位。

19.6.2　检测特定位（runtime阶段）

在C/C++代码编译而得的程序中，检测特定位的指令（右移n位，然后舍弃最低位）大体如下。

指令清单19.42　C/C++

if ((value>>n)&1)
    ...

相应的x86代码如下所示。

指令清单19.43　x86

; REG=input_value
; CL=n
SHR REG, CL
AND REG, 1

此外还可以把1左移n次，逐次鉴定各位是否为零。

指令清单19.44　C/C++

if (value & (1<<n))
    ...

上述源程序对应的x86指令如下所示。

指令清单19.45　x86

; CL=n
MOV REG, 1
SHL REG, CL
AND input_value, REG

19.6.3　设置特定位（编译阶段）

指令清单19.46　C/C++

value=value|0x40;

指令清单19.47　x86

OR REG, 40h

指令清单19.48　ARM (ARM mode) and ARM64

ORR R0, R0, #0x40

19.6.4　设置特定位（runtime阶段）

指令清单19.49　C/C++

value=value|(1<<n);

对应的x86指令大体如下所示。

指令清单19.50　x86

; CL=n
MOV REG, 1
SHL REG, CL
OR input_value, REG

19.6.5　清除特定位（编译阶段）

如需清除特定位，只需使用AND指令操作相应数值即可。

指令清单19.51　C/C++

value=value&(~0x40);

指令清单19.52　x86

AND REG, 0FFFFFFBFh

指令清单19.53　x64

AND REG, 0FFFFFFFFFFFFFFBFh

上述指令仅仅把被操作数的某个位置零，其他位保持不变。

ARM平台的ARM模式指令集有BIC指令。它相当于NOT和AND指令对。

指令清单19.54　ARM (ARM mode)

BIC R0, R0, #0x40

19.6.6　清除特定位（runtime阶段）

指令清单19.55　C/C++

value=value&(~(1<<n));

指令清单19.56　x86

; CL=n
MOV REG, 1
SHL REG, CL
NOT REG
AND input_value, REG

19.7　练习题

19.7.1　题目1

请描述下述代码的功能。

指令清单19.57　Optimizing MSVC 2010

_a$ = 8
_f      PROC
        mov     ecx, DWORD PTR _a$[esp-4]
        mov     eax, ecx
        mov     edx, ecx
        shl     edx, 16         ; 00000010H
        and     eax, 65280      ; 0000ff00H
        or      eax, edx
        mov     edx, ecx
        and     edx, 16711680   ; 00ff0000H
        shr     ecx, 16         ; 00000010H
        or      edx, ecx
        shl     eax, 8
        shr     edx, 8
        or      eax, edx
        ret     0
_f      ENDP

指令清单19.58　Optimizing Keil 6/2013 (ARM mode)

f PROC
        MOV     r1,#0xff0000
        AND     r1,r1,r0,LSL #8
        MOV     r2,#0xff00
        ORR     r1,r1,r0,LSR #24
        AND     r2,r2,r0,LSR #8
        ORR     r1,r1,r2
        ORR     r0,r1,r0,LSL #24
        BX      lr
        ENDP

指令清单19.59　Optimizing Keil 6/2013 (Thumb mode)

f  PROC
        MOVS    r3,#0xff
        LSLS    r2,r0,#8
        LSLS    r3,r3,#16
        ANDS    r2,r2,r3
        LSRS    r1,r0,#24
        ORRS    r1,r1,r2
        LSRS    r2,r0,#8
        ASRS    r3,r3,#8
        ANDS    r2,r2,r3
        ORRS    r1,r1,r2
        LSLS    r0,r0,#24
        ORRS    r0,r0,r1
        BX      lr
        ENDP

指令清单19.60　Optimizing GCC 4.9 (ARM64)

f:
        rev     w0, w0
        ret

指令清单19.61　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:
           srl     $v0, $a0, 24
           sll     $v1, $a0, 24
           sll     $a1, $a0, 8
           or      $v1, $v0
           lui     $v0, 0xFF
           and     $v0, $a1, $v0
           srl     $a0, 8
           or      $v0, $v1, $v0
           andi    $a0, 0xFF00
           jr      $ra
           or      $v0, $a0

19.7.2　题目2

请描述下述程序的功能。

指令清单19.62　Optimizing MSVC 2010

_a$ = 8                                    ; size = 4
_f      PROC
        push   esi
        mov    esi, DWORD PTR _a$[esp]
        xor    ecx, ecx
        push   edi
        lea    edx, DWORD PTR [ecx+1]
        xor    eax, eax
        npad   3 ; align next label
$LL3@f:
        mov    edi, esi
        shr    edi, cl
        add    ecx, 4
        and    edi, 15
        imul   edi, edx
        lea    edx, DWORD PTR [edx+edx*4]
        add    eax, edi
        add    edx, edx
        cmp    ecx, 28
        jle    SHORT $LL3@f
        pop    edi
        pop    esi
        ret    0
_f      ENDP

指令清单19.63　Optimizing Keil 6/2013 (ARM mode)

f PROC
        MOV    r3,r0
        MOV    r1,#0
        MOV    r2,#1
        MOV    r0,r1
|L0.16|
        LSR    r12,r3,r1
        AND    r12,r12,#0xf
        MLA    r0,r12,r2,r0
        ADD    r1,r1,#4
        ADD    r2,r2,r2,LSL #2
        CMP    r1,#0x1c
        LSL    r2,r2,#1
        BLE    |L0.16|
        BX     lr
        ENDP

指令清单19.64　Optimizing Keil 6/2013 (Thumb mode)

f  PROC
        PUSH     {r4,lr}
        MOVS     r3,r0
        MOVS     r1,#0
        MOVS     r2,#1
        MOVS     r0,r1
|L0.10|
        MOVS     r4,r3
        LSRS     r4,r4,r1
        LSLS     r4,r4,#28
        LSRS     r4,r4,#28
        MULS     r4,r2,r4
        ADDS     r0,r4,r0
        MOVS     r4,#0xa
        MULS     r2,r4,r2
        ADDS     r1,r1,#4
        CMP      r1,#0x1c
        BLE      |L0.10|
        POP      {r4,pc}
        ENDP

指令清单19.65　Non-optimizing GCC 4.9 (ARM64)

f:
        sub     sp, sp, #32
        str     w0, [sp,12]
        str     wzr, [sp,28]
        mov     w0, 1
        str     w0, [sp,24]
        str     wzr, [sp,20]
        b       .L2
.L3:
        ldr     w0, [sp,28]
        ldr     w1, [sp,12]
        lsr     w0, w1, w0
        and     w1, w0, 15
        ldr     w0, [sp,24]
        mul     w0, w1, w0
        ldr     w1, [sp,20]
        add     w0, w1, w0
        str     w0, [sp,20]
        ldr     w0, [sp,28]
        add     w0, w0, 4
        str     w0, [sp,28]
        ldr     w1, [sp,24]
        mov     w0, w1
        lsl     w0, w0, 2
        add     w0, w0, w1
        lsl     w0, w0, 1
        str     w0, [sp,24]
.L2:
        ldr     w0, [sp,28]
        cmp     w0, 28
        ble     .L3
        ldr     w0, [sp,20]
        add     sp, sp, 32
        ret

指令清单19.66　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:
                rl      $v0, $a0, 8
                srl     $a3, $a0, 20
                andi    $a3, 0xF
                andi    $v0, 0xF
                srl     $a1, $a0, 12
                srl     $a2, $a0, 16
                andi    $a1, 0xF
                andi    $a2, 0xF
                sll     $t2, $v0, 4
                sll     $v1, $a3, 2
                sll     $t0, $v0, 2
                srl     $t1, $a0, 4
                sll     $t5, $a3, 7
                addu    $t0, $t2
                subu    $t5, $v1
                andi    $t1, 0xF
                srl     $v1, $a0, 28
                sll     $t4, $a1, 7
                sll     $t2, $a2, 2
                sll     $t3, $a1, 2
                sll     $t7, $a2, 7
                srl     $v0, $a0, 24
                addu    $a3, $t5, $a3
                subu    $t3, $t4, $t3
                subu    $t7, $t2
                andi    $v0, 0xF
                sll     $t5, $t1, 3
                sll     $t6, $v1, 8
                sll     $t2, $t0, 2
                sll     $t4, $t1, 1
                sll     $t1, $v1, 3
                addu    $a2, $t7, $a2
                subu    $t1, $t6, $t1
                addu    $t2, $t0, $t2
                addu    $t4, $t5
                addu    $a1, $t3, $a1
                sll     $t5, $a3, 2
                sll     $t3, $v0, 8
                sll     $t0, $v0, 3
                addu    $a3, $t5
                subu    $t0, $t3, $t0
                addu    $t4, $t2, $t4
                sll     $t3, $a2, 2
                sll     $t2, $t1, 6
                sll     $a1, 3
                addu    $a1, $t4, $a1
                subu    $t1, $t2, $t1
                addu    $a2, $t3
                sll     $t2, $t0, 6
                sll     $t3, $a3, 2
                andi    $a0, 0xF
                addu    $v1, $t1, $v1
                addu    $a0, $a1
                addu    $a3, $t3
                subu    $t0, $t2, $t0
                sll     $a2, 4
                addu    $a2, $a0, $a2
                addu    $v0, $t0, $v0
                sll     $a1, $v1, 2
                sll     $a3, 5
                addu    $a3, $a2, $a3
                addu    $v1, $a1
                sll     $v0, 6
                addu    $v0, $a3, $v0
                sll     $v1, 7
                jr      $ra
                addu    $v0, $v1, $v0

19.7.3　题目3

请查阅MSDN资料，找到MessageBox()函数使用了哪些标志位。

指令清单19.67　Optimizing MSVC 2010

_main   PROC
        push    278595          ; 00044043H
        push    OFFSET $SG79792 ; 'caption'
        push    OFFSET $SG79793 ; 'hello, world!'
        push    0
        call    DWORD PTR __imp__MessageBoxA@16
        xor     eax, eax
        ret     0
_main   ENDP

19.7.4　题目4

请描述下述代码的功能。

指令清单19.68　Optimizing MSVC 2010

_m$ = 8         ; size = 4
_n$ = 12        ; size = 4
_f      PROC
        mov     ecx, DWORD PTR _n$[esp-4]
        xor     eax, eax
        xor     edx, edx
        test    ecx, ecx
        je      SHORT $LN2@f
        push    esi
        mov     esi, DWORD PTR _m$[esp]
$LL3@f:
        test    cl, 1
        je      SHORT $LN1@f
        add     eax, esi
        adc     edx, 0
$LN1@f:
        add     esi, esi
        shr     ecx, 1
        jne     SHORT $LL3@f
        pop     esi
$LN1@f:
        ret     0
_f      ENDP

指令清单19.69　Optimizing Keil 6/2013 (ARM mode)

f PROC
        PUSH     {r4,lr}
        MOV      r3,r0
        MOV      r0,#0
        MOV      r2,r0
        MOV      r12,r0
        B        |L0.48|
|L0.24|
        TST      r1,#1
        BEQ      |L0.40|
        ADDS     r0,r0,r3
        ADC      r2,r2,r12
|L0.40|
        LSL      r3,r3,#1
        LSR      r1,r1,#1
|L0.48|
        CMP      r1,#0
        MOVEQ    r1,r2
        BNE      |L0.24|
        POP      {r4,pc}
        ENDP

指令清单19.70　Optimizing Keil 6/2013 (Thumb mode)

f  PROC
        PUSH    {r4,r5,lr}
        MOVS    r3,r0
        MOVS    r0,#0
        MOVS    r2,r0
        MOVS    r4,r0
        B       |L0.24|
|L0.12|
        LSLS    r5,r1,#31
        BEQ     |L0.20|
        ADDS    r0,r0,r3
        ADCS    r2,r2,r4
|L0.20|
        LSLS    r3,r3,#1
        LSRS    r1,r1,#1
|L0.24|
        CMP     r1,#0
        BNE     |L0.12|
        MOVS    r1,r2
        POP     {r4,r5,pc}
        ENDP

指令清单19.71　Optimizing GCC 4.9 (ARM64)

f:
        mov     w2, w0
        mov     x0, 0
        cbz     w1, .L2
.L3:
        and     w3, w1, 1
        lsr     w1, w1, 1
        cmp     w3, wzr
        add     x3, x0, x2, uxtw
        lsl     w2, w2, 1
        csel    x0, x3, x0, ne
        cbnz    w1, .L3
.L2:
        ret

指令清单19.72　Optimizing GCC 4.4.5 (MIPS) (IDA)

mult:
               beqz   $a1, loc_40
               move   $a3, $zero
               move   $a2, $zero
               addu   $t0, $a3, $a0
loc_10:                               # CODE XREF: mult+38
               sltu   $t1, $t0, $a3
               move   $v1, $t0
               andi   $t0, $a1, 1
               addu   $t1, $a2
               beqz   $t0, loc_30
               srl    $a1, 1
               move   $a3, $v1
               move   $a2, $t1
loc_30:                               # CODE XREF: mult+20
               beqz   $a1, loc_44
               sll    $a0, 1
               b      loc_10
               addu   $t0, $a3, $a0
loc_40:                               # CODE XREF: mult
               move   $a2, $zero
loc_44:                               # CODE XREF: mult:loc_30
               move   $v0, $a2
               jr     $ra
               move   $v1, $a3

[1]　http://ohse.de/uwe/articles/gcc-attributes.html#func-regparm。

[2]　参见：http://kernelnewbies.org/Linux_2_6_20#head-042c62f290834eb1fe0a1942bbf5bb9a4accbc8f以及源代码文件。

[3]　确切地说，使用的编译器是LLVM build 2410.2.00 bundled with Apple Xcode 4.6.3。

[4]　Conditional MOVe if Not Equal。

[5]　Conditional MOVe if Not Zero。

[6]　更多介绍请参见http://repzret.org/p/repzret/。

第20章 线性同余法与伪随机函数

“线性同余法”大概是生成随机数的最简方法。虽然现在的随机函数基本不采用这种技术了[1]，但是它原理简单（只涉及乘法、加法和与运算），仍然值得研究。

#include <stdint.h>

// constants from the Numerical Recipes book
#define RNG_a 1664525
#define RNG_c 1013904223

static 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;
}

上述程序包含两个函数。第一个函数用于初始化内部状态，第二个函数生成伪随机数字。

这个程序中的两个常量来自于参考书目［Pre+07］。本文直接使用C/C++的#define语句，把它们定义为两个宏。C/C++对宏和常量的处理方法有所区别。C/C++编译器的预处理程序会把宏直接替换为相应的值。所以宏并不像变量那样占用内存。相对而言，编译器把常量当作只读变量处理。因为指针的本质是内存地址，所以C/C++的指针可以指向常量，但是无法指向宏。

函数最后使用了“&”/与操作符。根据C语言有关标准，my_rand()函数的返回值应当介于0～32767之间。如果您有意生成32位的伪随机数，那么就不必在此处进行与运算。

20.1　x86

指令清单20.1　Optimizing MSVC 2013

_BSS     SEGMENT
_rand_state DD  01H DUP (?)
_BSS     ENDS

_init$ = 8
_srand  PROC
        mov     eax, DWORD PTR _init$[esp-4]
        mov     DWORD PTR _rand_state, eax
        ret     0
_srand  ENDP

_TEXT   SEGMENT
_rand   PROC
        imul    eax, DWORD PTR _rand_state, 1664525
        add     eax, 1013904223         ; 3c6ef35fH
        mov     DWORD PTR _rand_state, eax
        and     eax, 32767              ; 00007fffH
        ret     0
_rand   ENDP

_TEXT   ENDS

编译器把源代码中的宏直接替换为宏所绑定的常量。这个例子证明。编译器不会给宏单独分配内存。my_srand()函数直接把输入值传递给内部的rand_state变量。

此后，my_rand()函数接收了这个值，以此计算rand_state。在调整了它的宽度之后，把返回值保留在EAX寄存器里。

更为详尽的计算方法可参见如下所示的非优化编译而生成的程序。

指令清单20.2　Non-optimizing MSVC 2013

_BSS    SEGMENT
_rand_state DD  01H DUP (?)
_BSS    ENDS

_init$ = 8
_srand  PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _init$[ebp]
        mov     DWORD PTR _rand_state, eax
        pop     ebp
        ret     0
_srand  ENDP

_TEXT   SEGMENT
_rand   PROC
        push    ebp
        mov     ebp, esp
        imul    eax, DWORD PTR _rand_state, 1664525
        mov     DWORD PTR _rand_state, eax
        mov     ecx, DWORD PTR _rand_state
        add     ecx, 1013904223         ; 3c6ef35fH
        mov     DWORD PTR _rand_state, ecx
        mov     eax, DWORD PTR _rand_state
        and     eax, 32767              ;00007 fffH
        pop     ebp
        ret     0
_rand   ENDP

_TEXT   ENDS

20.2　x64

x64的程序与x86的程序几乎相同。因为函数的返回值属于int型数据，所以它没有使用64位寄存器，而是使用了32位寄存器的助记符。但是，my_srand()函数从ECX寄存器获取的所需参数，没有通过栈读取参数，这构成了64位程序的显著特征。

指令清单20.3　Optimizing MSVC 2013 x64

_BSS     SEGMENT
rand_state DD   01H DUP (?)
_BSS     ENDS

init$ = 8
my_srand PROC
; ECX = input argument
        mov      DWORD PTR rand_state, ecx
        ret      0
my_srand ENDP

_TEXT   SEGMENT
my_rand PROC
        imul     eax, DWORD PTR rand_state, 1664525     ; 0019660dH
        add      eax, 1013904223                        ; 3c6ef35fH
        mov      DWORD PTR rand_state, eax
        and      eax, 32767                             ; 00007fffH
        ret      0
my_rand ENDP

_TEXT ENDS

GCC生成的程序与此类似。

20.3　32位ARM

指令清单20.4　Optimizing Keil 6/2013 (ARM mode)

my_srand PROC
        LDR      r1,|L0.52|  ; load pointer to rand_state
        STR      r0,[r1,#0]  ; save rand_state
        BX       lr
        ENDP

my_rand PROC
        LDR      r0,|L0.52|  ; load pointer to rand_state
        LDR      r2,|L0.56|  ; load RNG_a
        LDR      r1,[r0,#0]  ; load rand_state
        MUL      r1,r2,r1
        LDR      r2,|L0.60|  ; load RNG_c
        ADD      r1,r1,r2
        STR      r1,[r0,#0]  ; save rand_state
; AND with 0x7FFF:
        LSL      r0,r1,#17
        LSR      r0,r0,#17
        BX       lr
        ENDP

|L0.52|
        DCD      ||.data||
|L0.56|
        DCD      0x0019660d
|L0.60|
        DCD      0x3c6ef35f

        AREA ||.data||, DATA, ALIGN=2

rand_state
        DCD      0x00000000

ARM模式的单条指令本来就是32位opcode，所以单条指令不可能容纳得下32位常量。因此，Keil单独开辟了一些空间来存储常量，再通过额外的指令读取它们。

值得关注的是，常量0x7fff也无法封装在单条指令之中。Keil把rand_state左移17位之后再右移17位，这相当于C/C++程序中的“(rand_state<<17)>>17”语句。虽然看上去这是画蛇添足，但是它清除了寄存器的高17位，保留了低15位数据，与源代码的功能相符。

Keil优化编译而生成的程序与此雷同，本文不再单独介绍。

20.4　MIPS

指令清单20.5　Optimizing GCC 4.4.5 (IDA)

my_srand:
; store $a0 to rand_state:
                lui     $v0, (rand_state >> 16)
                jr      $ra
                sw      $a0, rand_state
my_rand:
; load rand_state to $v0:
                lui     $v1, (rand_state >> 16)
                lw      $v0, rand_state
                or      $at, $zero  ; load delay slot
; multiplicate rand_state in $v0 by 1664525 (RNG_a):
                sll     $a1, $v0, 2
                sll     $a0, $v0, 4
                addu    $a0, $a1, $a0
                sll     $a1, $a0, 6
                subu    $a0, $a1, $a0
                addu    $a0, $v0
                sll     $a1, $a0, 5
                addu    $a0, $a1
                sll     $a0, 3
                addu    $v0, $a0, $v0
                sll     $a0, $v0, 2
                addu    $v0, $a0
; add 1013904223 (RNG_c)
; the LI instruction is coalesced by IDA from LUI and ORI
                li      $a0, 0x3C6EF35F
                addu    $v0, $a0
; store to rand_state:
                sw      $v0, (rand_state & 0xFFFF)($v1)
                jr      $ra
                andi    $v0, 0x7FFF ; branch delay slot

在上述程序中，常量只有0x3C6EF35F （即1013904223）。另一个值为1664525的常量去哪了？

编译器通过位移和加法运算实现了“乘以1664525”的运算。我们假设编译器自行生成了以下函数：

#define RNG_a 1664525

int f (int a)
{
         return a*RNG_a;
}

那么，上述程序的汇编指令应当如下所示。

指令清单20.6　Optimizing GCC 4.4.5 (IDA)

f:
                sll     $v1, $a0, 2
                sll     $v0, $a0, 4
                addu    $v0, $v1, $v0
                sll     $v1, $v0, 6
                subu    $v0, $v1, $v0
                addu    $v0, $a0
                sll     $v1, $v0, 5
                addu    $v0, $v1
                sll     $v0, 3
                addu    $a0, $v0, $a0
                sll     $v0, $a0, 2
                jr      $ra
                addu    $v0, $a0, $v0 ; branch delay slot

我们确实可以在可执行程序中找到对应的指令！

MIPS的重新定位

本节重点介绍寄存器与内存交换数据的具体方法。在展现MIPS程序的细节方面，IDA略有不足（“智能”整理得太严重）。因此本文使用objdump程序，分别观测到汇编层面的指令清单及重定位表（relocation list）。

指令清单20.7　Optimizing GCC 4.4.5 (objdump)

# objdump -D rand_O3.o
...

00000000 <my_srand>:
   0:   3c020000    lui     v0,0x0
   4:   03e00008    jr      ra
   8:   ac440000    sw      a0,0(v0)

0000000c <my_rand>:
   c:   3c030000    lui     v1,0x0
  10:   8c620000    lw      v0,0(v1)
  14:   00200825    move    at,at
  18:   00022880    sll     a1,v0,0x2
  1c:   00022100    sll     a0,v0,0x4
  20:   00a42021    addu    a0,a1,a0
  24:   00042980    sll     a1,a0,0x6
  28:   00a42023    subu    a0,a1,a0
  2c:   00822021    addu    a0,a0,v0
  30:   00042940    sll     a1,a0,0x5
  34:   00852021    addu    a0,a0,a1
  38:   000420c0    sll     a0,a0,0x3
  3c:   00821021    addu    v0,a0,v0
  40:   00022080    sll     a0,v0,0x2
  44:   00441021    addu    v0,v0,a0
  48:   3c043c6e    lui     a0,0x3c6e
  4c:   3484f35f    ori     a0,a0,0xf35f
  50:   00441021    addu    v0,v0,a0
  54:   ac620000    sw      v0,0(v1)
  58:   03e00008    jr      ra
  5c:   30427fff    andi    v0,v0,0x7fff

...
# objdump -r rand_O3.o
...

RELOCATION RECORDS FOR [.text]:
OFFSET   TYPE              VALUE
00000000 R_MIPS_HI16       .bss
00000008 R_MIPS_LO16       .bss
0000000c R_MIPS_HI16       .bss
00000010 R_MIPS_LO16       .bss
00000054 R_MIPS_LO16       .bss
...

函数my_srand()两次出现了relocation现象。第一次出现在地址0处，其类型为R_MIPS_HI16。第二处出现在地址8处，类型为R_MIPS_L016。这意味着.bss段的起始地址要写到地址为0、8（分别为16位的高、低地址）的指令中去。

位于.bss段起始位置的值，是变量rand_state。

在前几条LUI和SW的指令中，某些操作符是零。因为编译器在编译阶段(complier)还不能确定这些值，所以此时把它空了出来。编译器将会在链接阶段（linker）处理这些数据，把地址的高地址位传递给LUI指令的相应操作符中，并把低地址位传递给SW指令。SW指令会对地址的低地址位和$V0寄存器的高地址位进行加权求和。

有了上述概念之后我们就不难理解my_rand()函数中的重定位：重定位标记R_MIPS_H16告诉linker程序“要把.bss段地址的高地址位传递给LUI指令”。所以变量rand_state地址的高地址位将会存储在$V1寄存器。地址为0x10的LW指令再把变量地址的低半部分读出来，然后加到$V0寄存器中。地址为0x54的SW指令再次进行这种求和，把新的数值传递给全局变量rand_state。

在读取MIPS程序的重定位信息之后，IDA程序会把数据与指令进行智能匹配。因此我们无法在IDA中看到这些原始的汇编指令。不过，无论IDA是否显示它们，重定位信息确实存在。

20.5　本例的线程安全改进版

请参见本书65.1节查询本例的线程安全（thread-safe）改进版。

[1]　多数随机函数都采用梅森旋转算法（Mersenne twister）。

第21章 结构体

在C/C++的数据结构里结构体（structure）是由一系列数据简单堆积而成的数据类型。结构体中的各项数据元素，可以是相同类型的数据、也可以是不同类型的数据。[1]

21.1　MSVC: systemtime

本节以Win32描述系统时间的SYSTEMTIME结构体为例。

库函数对它的定义如下[2]。

指令清单21.1　WinBase.h

typedef struct _SYSTEMTIME {
  WORD wYear;
  WORD wMonth;
  WORD wDayOfWeek;
  WORD wDay;
  WORD wHour;
  WORD wMinute;
  WORD wSecond;
  WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;

根据上述声明，获取系统时间的程序大致如下：

#include <windows.h>
#include <stdio.h>
void main()
{
    SYSTEMTIME t;
    GetSystemTime (&t);

    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        t.wYear, t.wMonth, t.wDay,
        t.wHour, t.wMinute, t.wSecond);

    return;
};

使用MSVC 2010（启用/GS-选项）编译这个程序，可得如下所示的代码。

指令清单21.2　MSVC 2010 /GS-

_t$ = -16 ; size = 16
_main       PROC
     push   ebp
     mov    ebp, esp
     sub    esp, 16
     lea    eax, DWORD PTR _t$[ebp]
     push   eax
     call   DWORD PTR __imp__GetSystemTime@4
     movzx  ecx, WORD PTR _t$[ebp+12] ; wSecond
     push   ecx
     movzx  edx, WORD PTR _t$[ebp+10] ; wMinute
     push   edx
     movzx  eax, WORD PTR _t$[ebp+8] ; wHour
     push   eax
     movzx  ecx, WORD PTR _t$[ebp+6] ; wDay
     push   ecx
     movzx  edx, WORD PTR _t$[ebp+2] ; wMonth
     push   edx
     movzx  eax, WORD PTR _t$[ebp] ; wYear
     push   eax
     push   OFFSET $SG78811 ; ’%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H
     call   _printf
     add    esp, 28
     xor    eax, eax
     mov    esp, ebp
     pop    ebp
     ret    0
_main     ENDP

函数在栈里为这个结构体申请了16字节空间。这个结构体由8个WORD型数据构成，每个WORD型数据占用2字节，所以整个结构体正好需要16字节的存储空间。

这个结构体的第一个字段是wYear。根据MSDN有关SYSTEMTIME结构体的相关声明[3]，在使用GetSystemTime()函数时，传递给函数的是SYSTEMTIME结构体的指针。但是换个角度看，这也是wYear字段的指针。GetSystemTime()函数首先会在结构体的首地址写入年份信息，然后再把指针调整2个字节并写入月份信息，如此类推写入全部信息。

21.1.1　OllyDbg

我们使用MSVC 2010（指定/GS- /MD 选项）编译上述程序，并用OllyDbg打开MSVC生成的可执行文件。找到传递给GetSystemTime()函数的指针地址，然后在数据观察窗口里观察这部分数据。此时数据如图21.1所示。

图21.1　OllyDbg：执行GetSystemTime()

在执行函数时精确的系统时间是“9 december 2014, 22:29:52”如图21.2所示。

图21.2　OllyDbg：printf()函数的输出结果

我们在数据窗口看到这个地址开始的16字节空间的值是：

DE 07 0C 00 02 00 09 00 16 00 1D 00 34 00 D4 03

这段空间的每2个字节代表结构体的一个字段。由于采用了小端字节序，所以就同一个WORD型数据而言，数权较小的一个字节在前，数权较大的字节在后。我们将其整理一下，看看它们的实际涵义：

十六进制数

十进制含义

字段名

0x07DE

2014

wYear

0x000C

12

wMonth

0x0002

2

wDayOfWeek

0x0009

9

wDay

0x0016

22

wHour

0x001D

29

wMinute

0x0034

52

wSecond

0x03D4

980

wMilliSeconds

在栈窗口里看到的值与此相同，不过栈窗口以32位数据的格式组织数据。

而后printf()函数从结构体中获取所需数据，并在屏幕上进行相应输出。

printf()函数并没有将所有的字段都打印出来。wDayOfWeek 和wMilliSeconds都未被输出，但是内存中确实有它们对应的值。

21.1.2　以数组替代结构体

结构体中的各个元素，在内存里依次排列。为了验证它在内存中的存储状况和数组相同，我用数组替代了SYSTEMTIME结构体：

#include <windows.h>
#include <stdio.h>
void main()
{
    WORD array[8];
    GetSystemTime (array);
    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        array[0] /* wYear */, array[1] /* wMonth */, array[3] /* wDay */,
        array[4] /* wHour */, array[5] /* wMinute */, array[6] /* wSecond */);
    return;
};

编译器会提示警告信息：

systemtime2.c(7) : warning C4133: ’function’ : incompatible types - from ’WORD [8]’ to ’LPSYSTEMTIME’

即使如此，MSVC 2010仍能够进行编译。它生成的代码如下所示。

指令清单21.3　Non-optimizing MSVC 2010

$SG78573 DB     '%04d-%02d-%02d %02d:%02d:%02d', 0aH, 00H
_array$ = -16;size=16
_main   PROC
        Push    ebp
        mov     ebp, esp
        sub     esp, 16
        lea     eax, DWORD PTR _array$[ebp]
        push    eax
        call    DWORD PTR __imp__GetSystemTime@4
        movzx   ecx, WORD PTR _array$[ebp+12] ; wSecond
        push    ecx
        movzx   edx, WORD PTR _array$[ebp+10] ; wMinute
        push    edx
        movzx   eax, WORD PTR _array$[ebp+8] ; wHoure
        push    eax
        movzx   ecx, WORD PTR _array$[ebp+6] ; wDay
        push    ecx
        movzx   edx, WORD PTR _array$[ebp+2] ; wMonth
        push    edx
        movzx   eax, WORD PTR _array$[ebp] ; wYear
        push    eax
        push    OFFSET $SG78573
        call    _printf
        add     esp, 28
        xor     eax, eax
        mov     esp, ebp
        pop     ebp
        ret     0
_main   ENDP

即使调整了数据类型，编译器生成的程序仍然没有什么变化。

这个现象说明，即使我们使用数组替代了原有结构体，编译器生成的汇编指令依旧完全相同。仅仅从汇编指令分析，很难判断出到底源程序使用的是多变量的结构体还是数组。

好在正常人不会做这种别扭的替换。毕竟结构体的可读性、易用性都比数组强，也方便编程人员替换结构体中的字段。

因为这个程序和前面的程序完全相同，本书就不再演示OllyDbg的调试过程了。

21.2　用malloc()分配结构体的空间

在某些情况下，使用堆（heap）来存储结构体要比栈（stack）容易一些：

#include <windows.h>
#include <stdio.h>

void main()
{
    SYSTEMTIME *t;
    t=(SYSTEMTIME *)malloc (sizeof (SYSTEMTIME));
    GetSystemTime (t);
    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        t->wYear, t->wMonth, t->wDay,
        t->wHour, t->wMinute, t->wSecond);
    free (t);
    return;
};

现在启用MSVC的优化选项/Ox，编译上述程序，得到的代码如下所示。

指令清单21.4　Optimizing MSVC

_main      PROC
    push   esi
    push   16
    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
    xor    eax, eax
    pop    esi
    ret    0
_main      ENDP

16就是sizeof(SYSTEMTIME)，即malloc()分配空间的确切大小。malloc()函数根据参数指定的大小分配一块空间，并把空间的指针传递给EAX寄存器。而后ESI寄存器获取了这个指针。Win32的GetSystemTime()函数把返回值的各个项存储到esi指针指向的对应空间里，接下来几个寄存器依次读取这些返回值并将之依次推送入栈、给printf()函数调用。

此处的“MOVZX(Move with Zero eXtent)”是前文没有介绍过的指令。这个指令的作用和MOVSX（参见第14章）的相似。与MOVSX的不同之处是，MOVZX在进行格式转换的时候会将其他bit位清零。因为printf()函数需要的数据类型是32 位整型数据，而我们的结构体SYSTEMTIME里对应的字段是WORD型数据，所以此处要转换数据类型。WORD型数据是16位无符号数据，因而要把WORD型数据照抄到int型数据空间的低地址位，并把高地址位（第16位到第31位）清零。高地址位必须要清零，否则转换的int型数据很可能会受到脏数据问题的不良影响。

本例中，我们可以使用8个WORD型数组重新构造上述结构体：

#include <windows.h>
#include <stdio.h>

void main()
{
    WORD *t;

    t=(WORD *)malloc (16);

    GetSystemTime (t);

    printf ("%04d-%02d-%02d %02d:%02d:%02d\n",
        t[0] /* wYear */, t[1] /* wMonth */, t[3] /* wDay */,
        t[4] /* wHour */, t[5] /* wMinute */, t[6] /* wSecond */);

    free (t);

    return;
};

使用MSVC（启用优化选项）编译上述程序，可得到如下所示的代码。

指令清单21.5　Optimizing MSVC

$SG78594 DB     '%04d-%02d-%02d %02d:%02d:%02d’, 0aH, 00H

_main   PROC
        push    esi
        push    16
        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
        xor     eax, eax
        pop     esi
        ret     0
_main   ENDP

这个代码和结构体生成的代码完全相同。我再次强调，这种“用数组替代结构体”的做法没有什么实际意义。除非有必要，否则不必做这种替换。

21.3　UNIX: struct tm

21.3.1　Linux

我们研究一下Linux源文件 time.h里的tm结构体。

#include <stdio.h>
#include <time.h>

void main()
{
    struct tm t;
    time_t unix_time;

    unix_time=time(NULL);

    localtime_r (&unix_time, &t);

    printf ("Year: %d\n", t.tm_year+1900);
    printf ("Month: %d\n", t.tm_mon);
    printf ("Day: %d\n", t.tm_mday);
    printf ("Hour: %d\n", t.tm_hour);
    printf ("Minutes: %d\n", t.tm_min);
    printf ("Seconds: %d\n", t.tm_sec);
};

使用GCC 4.4.1 编译，可得如下所示的代码。

指令清单21.6　GCC 4.4.1

main proc  near
     push     ebp
     mov      ebp, esp
     and      esp, 0FFFFFFF0h
     sub      esp, 40h
     mov      dword ptr [esp], 0 ; first argument for time()
     call     time
     mov      [esp+3Ch], eax
     lea      eax, [esp+3Ch]  ; take pointer to what time() returned
     lea      edx, [esp+10h]  ; at ESP+10h struct tm will begin
     mov      [esp+4], edx    ; pass pointer to the structure begin
     mov      [esp], eax      ; pass pointer to result of time()
     call     localtime_r
     mov      eax, [esp+24h]  ; tm_year
     lea      edx, [eax+76Ch] ; edx=eax+1900
     mov      eax, offset format ; "Year: %d\n"
     mov      [esp+4], edx
     mov      [esp], eax
     call     printf
     mov      edx, [esp+20h]      ; tm_mon
     mov      eax, offset aMonthD ; "Month: %d\n"
     mov      [esp+4], edx
     mov      [esp], eax
     call     printf
     mov      edx, [esp+1Ch]     ; tm_mday
     mov      eax, offset aDayD  ; "Day: %d\n"
     mov      [esp+4], edx
     mov      [esp], eax
     call     printf
     mov      edx, [esp+18h]     ; tm_hour
     mov      eax, offset aHourD ; "Hour: %d\n"
     mov      [esp+4], edx
     mov      [esp], eax
     call     printf
     mov      edx, [esp+14h]        ; tm_min
     mov      eax, offset aMinutesD ; "Minutes: %d\n"
     mov      [esp+4], edx
     mov      [esp], eax
     call     printf
     mov      edx, [esp+10h]
     mov      eax, offset aSecondsD ; "Seconds: %d\n"
     mov      [esp+4], edx          ; tm_sec
     mov      [esp], eax
     call     printf
     leave
     retn
main endp

可惜的是IDA未能在局部栈里将这些局部变量逐一识别出来，因此未能标记变量的别名。但是读到这里的读者都是有经验的逆向工程分析人员了，不再需要辅助信息仍然可以分析出数据的作用。

需要注意的是“lea edx, [eax+76Ch]”指令。它给EAX寄存器里的值加上0x76C（1900），而不会修改任何标志位。有关LEA指令的更多信息，请参见附录A.6.2。

GDB

我们使用GDB调试这个程序，可得到如下所示的代码。[4]

指令清单21.7　GDB

dennis@ubuntuvm:~/polygon$ date
Mon Jun  2 18:10:37 EEST 2014
dennis@ubuntuvm:~/polygon$ gcc GCC_tm.c -o GCC_tm
dennis@ubuntuvm:~/polygon$ gdb GCC_tm
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/GCC_tm...(no debugging symbols found)...done.
(gdb) b printf
Breakpoint 1 at 0x8048330
(gdb) run
Starting program: /home/dennis/polygon/GCC_tm
Breakpoint 1, __printf (format=0x80485c0 "Year: %d\n") at printf.c:29
29      printf.c: No such file or directory.
(gdb) x/20x $esp
0xbffff0dc:     0x080484c3      0x080485c0      0x000007de      0x00000000
0xbffff0ec:     0x08048301      0x538c93ed      0x00000025      0x0000000a
0xbffff0fc:     0x00000012      0x00000002      0x00000005      0x00000072
0xbffff10c:     0x00000001      0x00000098      0x00000001      0x00002a30
0xbffff11c:     0x0804b090      0x08048530      0x00000000      0x00000000
(gdb)

在数据栈中，结构体的构造非常清晰。首先我们看看time.h里的定义。

指令清单21.8　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;
};

Linux的tm结构体的每个元素都是int型数据。在数据类型上它就和Windows 的SYSTEMTIME结构体采用的WORD型数据不同。

我们继续分析局部栈的情况：

0xbffff0dc:0x080484c3       0x080485c0       0x000007de        0x00000000
0xbffff0ec:0x08048301       0x538c93ed       0x00000025 秒     0x0000000a 分
0xbffff0fc:0x00000012时     0x00000002 mday  0x00000005 月     0x00000072 年
0xbffff10c:0x00000001 wday  0x00000098 yday  0x00000001 isdst  0x00002a30
0xbffff11c:0x0804b090       0x08048530       0x00000000        0x00000000

整理一下，结果如下表所示。

十六进制数

十进制

字段

0x00000025

37

tm_sec

0x0000000a

10

tm_min

0x00000012

18

tm_hour

0x00000002

2

tm_mday

0x00000005

5

tm_mon

0x00000072

114

tm_year

0x00000001

1

tm_wday

0x00000098

152

tm_yday

0x00000001

1

tm_isdst

这个结构体比SYSTEMTIME多了一些字段，例如tm_wday/ tm_yday/ tm_isdst字段，不过本例用不到这些字段。

21.3.2　ARM

Optimizing Keil 6/2013 (Thumb mode)

使用Keil 6（启用优化选项）编译上述结构体程序，可得到Thumb模式的程序如下所示。

指令清单21.9　Optimizing Keil 6/2013 (Thumb mode)

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\n"
       BL        __2printf
       LDR      R1, [SP,#0x38+var_28]
       ADR      R0, aMonthD     ; "Month: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_2C]
       ADR      R0, aDayD       ; "Day: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_30]
       ADR      R0, aHourD      ; "Hour: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_34]
       ADR      R0, aMinutesD   ; "Minutes: %d\n"
       BL       __2printf
       LDR      R1, [SP,#0x38+var_38]
       ADR      R0, aSecondsD   ; "Seconds: %d\n"
       BL       __2printf
       ADD      SP, SP, #0x34
       POP      {PC}

Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

通过分析被调用方函数的函数名称（例如本例的localtime_r()函数），IDA能够“识别”出返回值为结构体型数据，并能给结构体中的字段重新标注名称。

指令清单21.10　Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

var_38 = -0x38
var_34 = -0x34

       PUSH {R7,LR}
       MOV  R7, SP
       SUB  SP, SP, #0x30
       MOVS R0, #0  ; time_t *
       BLX  _time
       ADD  R1, SP, #0x38+var_34 ; struct tm *
       STR  R0, [SP,#0x38+var_38]
       MOV  R0, SP  ; time_t *
       BLX  _localtime_r
       LDR  R1, [SP,#0x38+var_34.tm_year]
       MOV  R0, 0xF44 ; "Year: %d\n"
       ADD  R0, PC  ; char *
       ADDW R1, R1, #0x76C
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_mon]
       MOV  R0, 0xF3A ; "Month: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_mday]
       MOV  R0, 0xF35 ; "Day: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_hour]
       MOV  R0, 0xF2E ; "Hour: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34.tm_min]
       MOV  R0, 0xF28 ; "Minutes: %d\n"
       ADD  R0, PC  ; char *
       BLX  _printf
       LDR  R1, [SP,#0x38+var_34]
       MOV  R0, 0xF25 ; "Seconds: %d\n"
       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.3.3　MIPS

指令清单21.11　Optimizing GCC 4.4.5 (IDA)

 1 main:
 2
 3 ; IDA is not aware of structure field names, we named them manually:
 4
 5 var_40          = -0x40
 6 var_38          = -0x38
 7 seconds         = -0x34
 8 minutes         = -0x30
 9 hour            = -0x2C
10 day             = -0x28
11 month           = -0x24
12 year            = -0x20
13 var_4           = -4
14
15                 lui     $gp, (__gnu_local_gp >> 16)
16                 addiu   $sp, -0x50
17                 la      $gp, (__gnu_local_gp & 0xFFFF)
18                 sw      $ra, 0x50+var_4($sp)
19                 sw      $gp, 0x50+var_40($sp)
20                 lw      $t9, (time & 0xFFFF)($gp)
21                 or      $at, $zero ; load delay slot, NOP
22                 jalr    $t9
23                 move    $a0, $zero ; branch delay slot, NOP
24                 lw      $gp, 0x50+var_40($sp)
25                 addiu   $a0, $sp, 0x50+var_38
26                 lw      $t9, (localtime_r & 0xFFFF)($gp)
27                 addiu   $a1, $sp, 0x50+seconds
28                 jalr    $t9
29                 sw      $v0, 0x50+var_38($sp) ; branch delay slot
30                 lw      $gp, 0x50+var_40($sp)
31                 lw      $a1, 0x50+year($sp)
32                 lw      $t9, (printf & 0xFFFF)($gp)
33                 la      $a0, $LC0 # "Year: %d\n"
34                 jalr    $t9
35                 addiu   $a1, 1900 ; branch delay slot
36                 lw      $gp, 0x50+var_40($sp)
37                 lw      $a1, 0x50+month($sp)
38                 lw      $t9, (printf & 0xFFFF)($gp)
39                 lui     $a0, ($LC1 >> 16) # "Month: %d\n"
40                 jalr    $t9
41                 la      $a0, ($LC1 & 0xFFFF) # "Month: %d\n" ; branch delay slot
42                 lw      $gp, 0x50+var_40($sp)
43                 lw      $a1, 0x50+day($sp)
44                 lw      $t9, (printf & 0xFFFF)($gp)
45                 lui     $a0, ($LC2 >> 16) # "Day: %d\n"
46                 jalr    $t9
47                 la      $a0, ($LC2 & 0xFFFF) # "Day: %d\n" ; branch delay slot
48                 lw      $gp, 0x50+var_40($sp)
49                 lw      $a1, 0x50+hour($sp)
50                 lw      $t9, (printf & 0xFFFF)($gp)
51                 lui     $a0, ($LC3 >> 16) # "Hour: %d\n"
52                 jalr    $t9
53                 la      $a0, ($LC3 & 0xFFFF) # "Hour: %d\n" ; branch delay slot
54                 lw      $gp, 0x50+var_40($sp)
55                 lw      $a1, 0x50+minutes($sp)
56                 lw      $t9, (printf & 0xFFFF)($gp)
57                 lui     $a0, ($LC4 >> 16) # "Minutes: %d\n"
58                 jalr    $t9
59                 la      $a0, ($LC4 & 0xFFFF) # "Minutes: %d\n" ; branch delay slot
60                 lw      $gp, 0x50+var_40($sp)
61                 lw      $a1, 0x50+seconds($sp)
62                 lw      $t9, (printf & 0xFFFF)($gp)
63                 lui     $a0, ($LC5 >> 16) # "Seconds: %d\n"
64                 jalr    $t9
65                 la      $a0, ($LC5 & 0xFFFF) # "Seconds: %d\n" ; branch delay slot
66                 lw      $ra, 0x50+var_4($sp)
67                 or      $at, $zero ; load delay slot, NOP
68                 jr      $ra
69                 addiu   $sp, 0x50
70
71                 $LC0: .ascii "Year: %d\n"<0>
72                 $LC1: .ascii "Month: %d\n"<0>
73                 $LC2: .ascii "Day: %d\n"<0>
74                 $LC3: .ascii "Hour: %d\n"<0>
75                 $LC4: .ascii "Minutes: %d\n"<0>
76                 $LC5: .ascii "Seconds: %d\n"<0>

这个案例再次演示了延时槽影响人工分析的严重程度。例如，位于第35行的“addiu $a1, 1900”指令，把返回值加上1900。千万别忘记它的执行顺序先于第34行的JALR指令。

21.3.4　数组替代法

在内存中，结构体是依次排列的一系列数据。为了演示它的这一特色，我们对指令清单21.8的程序进行少许修改，以使用数组替代tm结构体：

#include <stdio.h>
#include <time.h>
void main()
{
    int tm_sec, tm_min, tm_hour, tm_mday, tm_mon, tm_year, tm_wday, tm_yday, tm_isdst;
    time_t unix_time;

    unix_time=time(NULL);

    localtime_r (&unix_time, &tm_sec);
    printf ("Year: %d\n", tm_year+1900);
    printf ("Month: %d\n", tm_mon);
    printf ("Day: %d\n", tm_mday);
    printf ("Hour: %d\n", tm_hour);
    printf ("Minutes: %d\n", tm_min);
    printf ("Seconds: %d\n", tm_sec);
};

注解：指向结构体的指针，实际上指向结构体的第一个元素。

使用GCC 4.7.3编译的时候，会看到编译器提示如下所示。

指令清单21.12　GCC 4.7.3

GCC_tm2.c: In function ’main’:
GCC_tm2.c:11:5: warning: passing argument 2 of ’localtime_r’ from incompatible pointer type [ enabled↙
↘by default]
In file included from GCC_tm2.c:2:0:
/usr/include/time.h:59:12: note: expected ’struct tm *’ but argument is of type ’int *’

不过这些问题不影响正常编译，所得可执行程序的具体指令如下所示。

指令清单21.13　GCC 4.7.3

main       proc near

var_30     = dword ptr -30h
var_2C     = dword ptr -2Ch
unix_time  = dword ptr -1Ch
tm_sec     = dword ptr -18h
tm_min     = dword ptr -14h
tm_hour    = dword ptr -10h
tm_mday    = dword ptr -0Ch
tm_mon     = dword ptr -8
tm_year    = dword ptr -4

           push    ebp
           mov     ebp, esp
           and     esp, 0FFFFFFF0h
           sub     esp, 30h
           call    __main
           mov     [esp+30h+var_30], 0 ; arg 0
           call    time
           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\n"
           call    printf
           mov     eax, [esp+30h+tm_mon]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aMonthD ; "Month: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_mday]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aDayD ; "Day: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_hour]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aHourD ; "Hour: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_min]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aMinutesD ; "Minutes: %d\n"
           call    printf
           mov     eax, [esp+30h+tm_sec]
           mov     [esp+30h+var_2C], eax
           mov     [esp+30h+var_30], offset aSecondsD ; "Seconds: %d\n"
           call    printf
           leave
           retn
main       endp

从汇编代码上看，采用数组的程序与先前使用结构体的程序最终竟会如此一致，以至于我们无法从汇编代码上区分出源程序的区别。

虽然上述程序可以照常运行，但是使用数组替换结构体的做法并不值得推荐。一般来说，“不启用优化编译选项”的编译器通常以代码声明变量的次序，在局部栈里分配变量的空间，但是无法保证每次编译的结果都严丝合缝。

如果使用GCC以外的编译器的话，部分编译器可能警告除变量tm_sec之外的tm_year、tm_mon、tm_mday、tm_hour、tm_min没有被初始化。这是因为编译器并不能分析出这些变量将被localtime_r()函数赋值。

在这个程序里，结构体各字段都是int型数据，所以本例十分直观。如果源程序中结构体的字段都是 16 位 WORD型数据，且采取了SYSTEMTIME那样的数据结构，因为局部变量向32位边界对齐的缘故，这将使GetSystemTime()无法正常赋值。有关结构体的字段封装问题，请参考21.4节。

由此可见，结构体就是一连串变量的封装体。在内存中结构体的各字段依次排列。我认为结构体就是语体上的糖块，使其内各个变量像糖分一样粘成一个统一体，以便编译器把它们分配到连续空间里。即使别人可能认为我是编程专家，但是我毕竟不是，所以这种说法很可能不够确切。顺便提一下，早期的（1972年之前）C语言不支持结构体structure。[5]

这个可执行程序完全和前面的程序一样，本书就不演示相关调试过程了。

21.3.5　替换为32位words

#include <stdio.h>
#include <time.h>

void main()
{
    struct tm t;
    time_t unix_time;
    int i;

    unix_time=time(NULL);

    localtime_r (&unix_time, &t);

    for (i=0; i<9; i++)
    {
        int tmp=((int*)&t)[i];
        printf ("0x%08X (%d)\n", tmp, tmp);
    };
};

上述程序把同一地址（指针）的数据分别当作结构体和整型数据、分别进行写和读的访问操作，完全可以正常工作。在当前时间为“23:51:45 26-July-2014”的时刻，内存中的数据如下：

0x0000002D (45)
0x00000033 (51)
0x00000017 (23)
0x0000001A (26)
0x00000006 (6)
0x00000072 (114)
0x00000006 (6)
0x000000CE (206)
0x00000001 (1)

这些变量的排列数据，与结构体在源代码中的声明顺序相同。详细内容请参见本书第21章第8节。

编译而得的程序如下所示。

指令清单21.14　Optimizing GCC 4.8.1

main            proc near
                push     ebp
                mov      ebp, esp
                push     esi
                push     ebx
                and      esp, 0FFFFFFF0h
                sub      esp, 40h
                mov      dword ptr [esp], 0 ; timer
                lea      ebx, [esp+14h]
                call     _time
                lea      esi, [esp+38h]     ;tp
                mov      [esp+4], ebx
                mov      [esp+10h], eax
                lea      eax, [esp+10h]
                mov      [esp], eax        ; timer
                call     _localtime_r
                nop
                lea      esi, [esi+0]      ; nop

loc_80483D8:
; EBX here is pointer to structure, ESI is the pointer to the end of it.
                mov      eax, [ebx]      ; get 32-bit word from array
                add      ebx, 4          ; next field in structure
                mov      dword ptr [esp+4], offset a0x08xD ; "0x%08X (%d)\n"
                mov      dword ptr [esp], 1
                mov      [esp+0Ch], eax  ; pass value to printf()
                mov      [esp+8], eax    ; pass value to printf()
                call     ___printf_chk
                cmp      ebx, esi        ; meet structure end?
                jnz      short loc_80483D8 ; no - load next value then
                lea      esp, [ebp-8]
                pop      ebx
                pop      esi
                pop      ebp
                retn
main            endp

栈空间内的数据依次被看作两种数据：先被当作结构体、再被当作数组。

本例表明，我们可以借助指针修改结构体中的各别字段。

本文再次强调：若不是以hack目的研究代码，就不必做这种处理。在编写生产环境下的程序时，不建议使用这种处理手段。

21.3.6　替换为字节型数组

更进一步的实验表明，也可以让时间结构体与字节型数组共用一个指针。

#include <stdio.h>
#include <time.h>

void main()
{
    struct tm t;
    time_t unix_time;
    int i, j;

    unix_time=time(NULL);

    localtime_r (&unix_time, &t);

    for (i=0; i<9; i++)
    {
        for (j=0; j<4; j++)
            printf ("0x%02X ", ((unsigned char*)&t)[i*4+j]);
        printf ("\n");
    };
};

0x2D 0x00 0x00 0x00
0x33 0x00 0x00 0x00
0x17 0x00 0x00 0x00
0x1A 0x00 0x00 0x00
0x06 0x00 0x00 0x00
0x72 0x00 0x00 0x00
0x06 0x00 0x00 0x00
0xCE 0x00 0x00 0x00
0x01 0x00 0x00 0x00

假如此时的系统时间和21.3.5节那个程序同时启动，它肯定会提示“23:51:45 26-July-2014”。需要注意的是，因为采用了小端字节序（参见第31章），数权较小的字节反而排在数权较大的字节之前。

指令清单21.15　Optimizing GCC 4.8.1

main             proc near
                 push     ebp
                 mov      ebp, esp
                 push     edi
                 push     esi
                 push     ebx
                 and      esp, 0FFFFFFF0h
                 sub      esp, 40h
                 mov      dword ptr [esp], 0 ; timer
                 lea      esi, [esp+14h]
                 call     _time
                 lea      edi, [esp+38h]  ; struct end
                 mov      [esp+4], esi    ; tp
                 mov      [esp+10h], eax
                 lea      eax, [esp+10h]
                 mov      [esp], eax      ; timer
                 call     _localtime_r
                 lea      esi, [esi+0]    ; NOP
; ESI here is the pointer to structure in local stack. EDI is the pointer to structure end.
loc_8048408:
                 xor      ebx, ebx        ; j=0
loc_804840A:
                 movzx    eax, byte ptr [esi+ebx] ; load byte
                 add      ebx, 1          ; j=j+1
                 mov      dword ptr [esp+4], offset a0x02x ; "0x%02X "
                 mov      dword ptr [esp], 1
                 mov      [esp+8], eax   ; pass loaded byte to printf()
                 call     ___printf_chk
                 cmp      ebx, 4
                 jnz      short loc_804840A
; print carriage return character (CR)
                 mov      dword ptr [esp], 0Ah ; c
                 add      esi, 4
                 call     _putchar
                 cmp      esi, edi       ; meet struct end?
                 jnz      short loc_8048408 ; j=0
                 lea      esp, [ebp-0Ch]
                 pop      ebx
                 pop      esi
                 pop      edi
                 pop      ebp
                 retn
main             endp

21.4　结构体的字段封装

结构体的字段封装方法构成了这种数据类型的一个重要特性。[6]

我们以下面的例子来加以说明。

#include <stdio.h>
struct s
{
    char a;
    int b;
    char c;
    int d;
};
void f(struct s s)
{
    printf ("a=%d; b=%d; c=%d; d=%d\n", s.a, s.b, s.c, s.d);
};
int main()
{
    struct s tmp;
    tmp.a=1;
    tmp.b=2;
    tmp.c=3;
    tmp.d=4;
    f(tmp);
};

其中有2个单字节型char字段和2个4字节的int型变量。

21.4.1　x86

使用MSVC 2012（启用选项/GS- /Ob0）编译，可得如下所示的代码。

指令清单21.16　MSVC 2012/GS-/O60

 1 _tmp$ = -16
 2 _main    PROC
 3     push   ebp
 4     mov    ebp, esp
 5     sub    esp, 16
 6     mov    BYTE PTR _tmp$[ebp], 1       ; set field a
 7     mov    DWORD PTR _tmp$[ebp+4], 2    ; set field b
 8     mov    BYTE PTR _tmp$[ebp+8], 3     ; set field c
 9     mov    DWORD PTR _tmp$[ebp+12], 4   ; set field d
10     sub    esp, 16                      ; allocate place for temporary structure
11     mov    eax, esp
12     mov    ecx, DWORD PTR _tmp$[ebp]    ; copy our structure to the temporary one
13     mov    DWORD PTR [eax], ecx
14     mov    edx, DWORD PTR _tmp$[ebp+4]
15     mov    DWORD PTR [eax+4], edx
16     mov    ecx, DWORD PTR _tmp$[ebp+8]
17     mov    DWORD PTR [eax+8], ecx
18     mov    edx, DWORD PTR _tmp$[ebp+12]
19     mov    DWORD PTR [eax+12], edx
20     call    _f
21     add    esp, 16
22     xor    eax, eax
23     mov    esp, ebp
24     pop    ebp
25     ret    0
26 _main   ENDP
27
28 _s$ = 8                ; size = 16
29 ?f@@YAXUs@@@Z PROC     ; f
30     push  ebp
31     mov   ebp, esp
32     mov   eax, DWORD PTR _s$[ebp+12]
33     push  eax
34     movsx ecx, BYTE PTR _s$[ebp+8]
35     push  ecx
36     mov   edx, DWORD PTR _s$[ebp+4]
37     push  edx
38     movsx eax, BYTE PTR _s$[ebp]
39     push  eax
40     push  OFFSET $SG3842
41     call  _printf
42     add   esp, 20
43     pop   ebp
44     ret   0
45 ?f@@YAXUs@@@Z ENDP ; f
46 _TEXT     ENDS

虽然我们在代码里一次性分配了结构体tmp，并依次给它的四个字段赋值，但是可执行程序的指令有些不同：它将结构体的指针复制到临时地址（第10行指令分配的空间里），然后通过临时的中间变量把结构体的四个值赋值给临时结构体（第12～19行的指令），还把指针也复制出来供f()调用。这主要是因为编译器无法判断f()函数是否会修改结构体的内容。借助中间变量，编译器可以保证main()函数里tmp结构体的值不受被调用方函数的影响。我们也可以改动源程序、使用指针传递数据，那样编译器生成的汇编指令也基本相同，但是不会再复制数据了。

另外，这个程序里结构体的字段向4字节边界对齐。也就是说它的char型数据也和int型数据一样占4字节存储空间。这主要是为了方便CPU从内存读取数据，提高读写和缓存的效率。

这样做的缺点是浪费存储空间。

接下来我们启用编译器的/Zp1 (/Zp[n]表示向n个字节的边界对齐)选项。

使用MSVC 2012(启用/GS- /Zp1选项)编译上述程序，可得到如下所示的代码。

指令清单21.17　MSVC 2012 /GS- /Zp1

 1 _main    PROC
 2     push   ebp
 3     mov    ebp, esp
 4     sub    esp, 12
 5     mov    BYTE PTR _tmp$[ebp], 1       ; set field a
 6     mov    DWORD PTR _tmp$[ebp+1], 2    ; set field b
 7     mov    BYTE PTR _tmp$[ebp+5], 3     ; set field c
 8     mov    DWORD PTR _tmp$[ebp+6], 4    ; set field d
 9     sub    esp, 12                      ; allocate place for temporary structure
10     mov    eax, esp
11     mov    ecx, DWORD PTR _tmp$[ebp]    ; copy 10 bytes
12     mov    DWORD PTR [eax], ecx
13     mov    edx, DWORD PTR _tmp$[ebp+4]
14     mov    DWORD PTR [eax+4], edx
15     mov    cx, WORD PTR _tmp$[ebp+8]
16     mov    WORD PTR [eax+8], cx
17     call   _f
18     add    esp, 12
19     xor    eax, eax
20     mov    esp, ebp
21     pop    ebp
22     ret    0
23 _main    ENDP
24
25 _TEXT    SEGMENT
26 _s$ = 8      ; size = 10
27 ?f@@YAXUs@@@Z PROC       ; f
28     push   ebp
29     mov    ebp, esp
30     mov    eax, DWORD PTR _s$[ebp+6]
31     push   eax
32     movsx  ecx, BYTE PTR _s$[ebp+5]
33     push   ecx
34     mov    edx, DWORD PTR _s$[ebp+1]
35     push   edx
36     movsx  eax, BYTE PTR _s$[ebp]
37     push   eax
38     push   OFFSET $SG3842
39     call   _printf
40     add    esp, 20
41     pop    ebp
42     ret    0
43 ?f@@YAXUs@@@Z ENDP                    ; f

经过这种处理之后，结构体只占用10字节空间，其中的char型数据占用1字节。这将提高代码的空间利用效率，不过这样做会同时降低CPU的IO读取效率。

main()函数里同样使用临时结构体复制了传入参数的信息，再把临时结构体传递给其他函数。不过这10字节数据并不是一个字段一个字段地、按变量声明的那样分4次复制过去的，编译器分配3对MOV指令复制它们。为什么不是4对赋值指令？这是因为编译器认为，在复制10字节数据时，3个MOV指令对的效率，比4对MOV指令对（按字段赋值）的效率要高。这是编译器常用的优化手段。编译器使用MOV指令直接实现(替代)memcpy()函数的情况十分普遍，主要就是因为在复制小型数据时memcpy()函数没有MOV指令的效率高。如需了解这方面详细知识，请参见本书43.1.5节。

当然，如果某个结构体被多个源文件、目标文件（object files）调用，那么在编译这些程序时，结构封装格式和数据对其规范(/Zp[n])必须完全匹配。

MSVC编译器有指定结构体字段对齐标准的/Zp选项。此外，还可以通过在源文件里设定#pragma pack的方法来指定这个选项。MSVC和GCC都支持这种代码级的宏指令。[7]

我们回顾一下使用16位型数据的结构体SYSTEMTIME。编译器如何知道要将它的字段向1字节边界对齐呢？

文件WinNT.h有如下声明。

指令清单21.18　WinNT.h

#include "pshpack1.h"

而且还有下述声明。

指令清单21.19　WinNT.h

#include "pshpack4.h"//默认情况下进行4字节边界对齐

PshPack1.h文件有下列内容。

指令清单21.20　PshPack1.h

#if ! (defined(lint) || defined(RC_INVOKED))
#if ( _MSC_VER >= 800 && !defined(_M_I86)) || defined(_PUSHPOP_SUPPORTED)
#pragma warning(disable:4103)
#if !(defined( MIDL_PASS )) || defined( __midl )
#pragma pack(push,1)
#else
#pragma pack(1)
#endif
#else
#pragma pack(1)
#endif
#endif /* ! (defined(lint) || defined(RC_INVOKED)) */

根据#pragma pack的信息，编译器会在封装结构体时向相应的边界对齐。

OllyDbg + 默认封装格式

现在打开OllyDbg，加载上述以默认封装格式（4字节边界对齐）的程序，如图21.3所示。

图21.3　OllyDbg：调用printf()函数之前

我们在数据窗口可以找到四个字段的值。但是，第一个字段（变量a）和第三个字段（变量c）空间之后的数据工具出现了随机值(0x30,0x37, 0x01)。它们是怎么产生的？在指令清单21.16的源程序中，变量a和变量c都是char型单字节数据。程序的第6、第8行，分别给它们赋值1、3。它们在内存里占用了4个字节，而其他32位存储空间里有3个字节并没有被赋值！在这3个字节空间里的数据，就是随机脏数据。因为在给printf()函数传递参数时，编译器会使用的是单字节数据赋值指令MOVSX（参见指令清单21.16的第34行和第38行）传递数据，所以这些脏数据并不会影响printf()函数的输出结果。

另外，因为a和c是char型数据，char型数据属于有符号（signed）型数据，所以复制操作所对应的汇编指令是MOVSX（SX是sign-extending的缩写）。如果它们是unsigned char或uint8_t型数据，那么此处就会是MOVZX指令。

OllyDbg + 字段向单字节边界对齐

这种情况下，整个结构体的各个变量在内存里依次排列，如图21.4所示。

图21.4　OllyDbg：在调用printf()函数之前

21.4.2　ARM

Optimizing Keil 6/2013 (Thumb mode)

使用Keil 6/2013（开启优化选项）、以Thumb模式编译上述程序可得如下所示的代码。

指令清单21.21　Optimizing Keil 6/2013 (Thumb mode)

.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\n"
.text:00000292 05 F0 AD FF        BL      __2printf
.text:00000296 D2 E6              B       exit

本例向被调用方函数传递的是结构体型数据，不是结构体的指针。因为ARM会利用寄存器传递函数所需的前4个参数，所以编译器利用3R0～R3寄存器向printf()函数传递结构体的全部字段。

LDRB从内存中加载1个字节并转换为32位有符号数据，用来从结构体中读取字段a和字段c。它相当于x86指令集中的MOVSX指令。

请注意，在函数退出时，它借用了另一个函数的函数尾声！这种借助B指令跳转到其他完全不相干的函数、共用同一个函数尾声的现象，应当是因为两个函数的局部变量的存储空间完全相同。或许因为这两个函数在启动时分配的栈大小相同（都分配了4×5=0x14的数据栈），导致退出语句也完全相同的缘故，再加上它们在内存中的地址相近的因素，所以编译器进行了这种处理。确实，使用同一组退出语句不会影响程序的任何功能。这明显是Keil编译器出于经济因素而进行的指令复用。JMP指令只占用2个字节，而标准的函数尾声要占用4字节。

ARM + Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

指令清单21.22　Optimizing Xcode 4.6.3 (LLVM) (Thumb-2 mode)

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\n"
      SXTB      R1, R1 ;制备a
      MOVT.W    R0, #0
      STR       R3, [SP,#0xC+var_C] ; 推送d 入栈，供 printf() 调用
      ADD       R0, PC ; 格式化字符串
      SXTB      R3, R2 ; 制备c
      MOV       R2,R9;制备b
      BLX       _printf
      ADD       SP, SP, #4
      POP       {R7,PC}

SXTB（Signed Extend Byte）对应x86的MOVSX指令，但是它只能处理寄存器的数据，不能直接对内存进行操作。程序中的其余指令与前例一样，本文不再进行重复说明。

21.4.3　MIPS

指令清单21.23　Optimizing GCC 4.4.5 (IDA)

 1 f:
 2
 3 var_18       = -0x18
 4 var_10       = -0x10
 5 var_4        = -4
 6 arg_0        = 0
 7 arg_4        = 4
 8 arg_8        = 8
 9 arg_C        = 0xC
10
11 ; $a0=s.a
12 ; $a1=s.b
13 ; $a2=s.c
14 ; $a3=s.d
15              lui       $gp, (__gnu_local_gp >> 16)
16              addiu     $sp, -0x28
17              la        $gp, (__gnu_local_gp & 0xFFFF)
18              sw        $ra, 0x28+var_4($sp)
19              sw        $gp, 0x28+var_10($sp)
20 ; prepare byte from 32-bit big-endian integer:
21              sra       $t0, $a0, 24
22              move      $v1, $a1
23 ; prepare byte from 32-bit big-endian integer:
24              sra       $v0, $a2, 24
25              lw        $t9, (printf & 0xFFFF)($gp)
26              sw        $a0, 0x28+arg_0($sp)
27              lui       $a0, ($LC0 >> 16) # "a=%d; b=%d; c=%d; d=%d\n"
28              sw        $a3, 0x28+var_18($sp)
29              sw        $a1, 0x28+arg_4($sp)
30              sw        $a2, 0x28+arg_8($sp)
31              sw        $a3, 0x28+arg_C($sp)
32              la        $a0, ($LC0 & 0xFFFF) # "a=%d; b=%d; c=%d; d=%d\n"
33              move      $a1, $t0
34              move      $a2, $v1
35              jalr      $t9
36              move      $a3, $v0 ; branch delay slot
37              lw        $ra, 0x28+var_4($sp)
38              or        $at, $zero ; load delay slot, NOP
39              jr        $ra
40              addiu     $sp, 0x28 ; branch delay slot
41
42 $LC0:        .ascii "a=%d; b=%d; c=%d; d=%d\n"<0>

结构体各字段首先被安置于$A0～$A3寄存器，然后又被重新安放于$A1～$A3寄存器以传递给printf()函数。较为特殊的是，上述程序使用了两次SRA(Shift Word Right Arithmetic)指令，而SRA指令用于制备char型数据的字段。这是为什么？MIPS默认采用大端字节序（big-endian，参见本书第31章），此外我用的Debian Linux也使用大端字节序。当使用32位空间存储字节型变量时，数据占用第31～24位。因此，当把char型数据扩展为32位数据时，必须右移24位。再加上char型数据属于有符号型数据，所以此处必须用算术位移指令而不能使用逻辑位移指令。

21.4.4　其他

在向被调用方函数传递结构体时（不是传递结构体的指针），传递参数的过程相当于依次传递结构体的各字段。即是说，如果结构体各字段的定义不变，那么f()函数的源代码可改写为：

void f(char a, int b, char c, int d)
{
    printf ("a=%d; b=%d; c=%d; d=%d\n", a, b, c, d);
};

即使经过上述改动，编译器生成的可执行程序也完全不会发生变化。

21.5　结构体的嵌套

结构体套用另外一个结构体的情况大体如下：

#include <stdio.h>

struct inner_struct
{
    int a;
    int b;
};

struct outer_struct
{
    char a;
    int b;
    struct inner_struct c;
    char d;
    int e;
};
void f(struct outer_struct s)
{
    printf ("a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d\n",
        s.a, s.b, s.c.a, s.c.b, s.d, s.e);
};

int main()
{
    struct outer_struct s;
    s.a=1;
    s.b=2;
    s.c.a=100;
    s.c.b=101;
    s.d=3;
    s.e=4;
    f(s);
};

这个程序把结构体inner_struct当作另一个结构体outer_struct 的字段来用，它和outer_struct的a、b、d、e一样，都是一个字段。

我们使用MSVC 2010（启用/Ox /Ob0选项）编译上述程序，可得到如下所示的代码。

指令清单21.24　Optimizing MSVC 2010 /Ob0

$SG2802 DB    ’a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d’, 0aH, 00H

_TEXT    SEGMENT
_s$ = 8
_f    PROC
      mov    eax, DWORD PTR _s$[esp+16]
      movsx  ecx, BYTE PTR _s$[esp+12]
      mov    edx, DWORD PTR _s$[esp+8]
      push   eax
      mov    eax, DWORD PTR _s$[esp+8]
      push   ecx
      mov    ecx, DWORD PTR _s$[esp+8]
      push   edx
      movsx  edx, BYTE PTR _s$[esp+8]
      push   eax
      push   ecx
      push   edx
      push   OFFSET $SG2802 ; ’a=%d; b=%d; c.a=%d; c.b=%d; d=%d; e=%d’
      call   _printf
      add    esp, 28
      ret    0
_f       ENDP

_s$ = -24
_main    PROC
    sub    esp, 24
    push   ebx
    push   esi
    push   edi
    mov    ecx, 2
    sub    esp, 24
    mov    eax, esp
    mov    BYTE PTR _s$[esp+60], 1
    mov    ebx, DWORD PTR _s$[esp+60]
    mov    DWORD PTR [eax], ebx
    mov    DWORD PTR [eax+4], ecx
    lea    edx, DWORD PTR [ecx+98]
    lea    esi, DWORD PTR [ecx+99]
    lea    edi, DWORD PTR [ecx+2]
    mov    DWORD PTR [eax+8], edx
    mov    BYTE PTR _s$[esp+76], 3
    mov    ecx, DWORD PTR _s$[esp+76]
    mov    DWORD PTR [eax+12], esi
    mov    DWORD PTR [eax+16], ecx
    mov    DWORD PTR [eax+20], edi
    call   _f
    add    esp, 24
    pop    edi
    pop    esi
    xor    eax, eax
    pop    ebx
    add    esp, 24
    ret    0
_main    ENDP

在汇编代码中，我们找不到内嵌结构体的影子。所以，我们可以断定，嵌套模式的结构体会被编译器展开，最终形成一维结构体。

当然，如果使用“struct inter_stuct *c”替代源程序中的“struct iner_struct c”，汇编指令会大不相同。

OllyDbg

我们使用OllyDbg加载上述程序，观测outer_struct。如图21.5所示。

图21.5　OllyDbg：在执行printf()之前

它在内存中的构造如下：

(outer_struct.a)值为1的字节，其后3字节是随机脏数据。

(outer_struct.b) 32 位word型数据2。

(outer_struct.a)32 位word型数据0x64(100)。

(outer_struct.b)32 位word型数据0x65(101)。

(outer_struct.d)值为3的字节，以及其后3字节的脏数据。

(outer_struct.e)32 位word型数据4。

21.6　结构体中的位操作

21.6.1　CPUID

C/C++语言可以精确操作结构体中的位域。这能够帮助程序员大幅度地节省程序的内存消耗。例如，bool型数据就需要1位空间。但是时间开销和空间效率不可两全，如果要节约内存开销，程序的性能就会下降。

以CPUID指令为例。该指令用于获取CPU及其特性信息。[8]

如果在调用指令之前设置EAX寄存器的值为1，那么CPUID指令将会按照下列格式在EAX寄存器里存储CPU的特征信息。

3:0 (4 bits)
7:4 (4 bits)
11:8 (4 bits)
13:12(2 bits)
19:16(4 bits)
27:20(8 bits)
Stepping
Model
Family
Processor Type
Extended Model
Extended Family

MSVC 2010 有CPUID宏，而GCC 4.4.1没有这个宏。所以，我们自己写一个函数，再用GCC编译它。[9]

#include <stdio.h>

#ifdef __GNUC__
static inline void cpuid(int code, int *a, int *b, int *c, int *d) {
  asm volatile("cpuid":"=a"(*a),"=b"(*b),"=c"(*c),"=d"(*d):"a"(code));
}
#endif

#ifdef _MSC_VER
#include <intrin.h>
#endif

struct CPUID_1_EAX
{
    unsigned int stepping:4;
    unsigned int model:4;
    unsigned int family_id:4;
    unsigned int processor_type:2;
    unsigned int reserved1:2;
    unsigned int extended_model_id:4;
    unsigned int extended_family_id:8;
    unsigned int reserved2:4;
};
int main()
{
    struct CPUID_1_EAX *tmp;
    int b[4];

#ifdef _MSC_VER
    __cpuid(b,1);
#endif

#ifdef __GNUC__
    cpuid (1, &b[0], &b[1], &b[2], &b[3]);
#endif

    tmp=(struct CPUID_1_EAX *)&b[0];

    printf ("stepping=%d\n", tmp->stepping);
    printf ("model=%d\n", tmp->model);
    printf ("family_id=%d\n", tmp->family_id);
    printf ("processor_type=%d\n", tmp->processor_type);
    printf ("extended_model_id=%d\n", tmp->extended_model_id);
    printf ("extended_family_id=%d\n", tmp->extended_family_id);

    return 0;
};

在CPUID将返回值存储到EAX/EBX/ECX/EDX之后，程序使用数组b[]收集相关信息。然后，我们通过指向结构体CPUID_1_EAX的指针，从数组b[]中获取EAX寄存器里的值。

换而言之，我们把32位int型数据分解为结构体型数据，再从结构体中读取各项数值。

MSVC

使用MSVC 2008（启用/Ox 选项）编译上述程序，可得如下所示的代码。

指令清单21.25　Optimizing MSVC 2008

b$ = -16 ;size=16
_main   PROC
    sub    esp, 16
    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
    push   eax
    push   OFFSET $SG15435 ; ’stepping=%d’, 0aH, 00H
    call   _printf

    mov    ecx, esi
    shr    ecx, 4
    and    ecx, 15
    push   ecx
    push   OFFSET $SG15436 ; ’model=%d’, 0aH, 00H
    call   _printf

    mov    edx, esi
    shr    edx, 8
    and    edx, 15
    push   edx
    push   OFFSET $SG15437 ; ’family_id=%d’, 0aH, 00H
    call   _printf

    mov    eax, esi
    shr    eax, 12
    and    eax, 3
    push   eax
    push   OFFSET $SG15438 ; ’processor_type=%d’, 0aH, 00H
    call   _printf

    mov    ecx, esi
    shr    ecx, 16
    and    ecx, 15
    push   ecx
    push   OFFSET $SG15439 ; ’extended_model_id=%d’, 0aH, 00H
    call   _printf

    shr    esi, 20
    and    esi, 255
    push   esi
    push   OFFSET $SG15440 ; ’extended_family_id=%d’, 0aH, 00H
    call   _printf
    add    esp, 48
    pop    esi

    xor    eax, eax
    pop    ebx

    add    esp, 16
    ret    0
_main    ENDP

SHR指令用于过滤位于EAX寄存器中右侧那些不参与运算的bit位。

而AND指令用于过滤位于寄存器左侧且不参与运算的相关位。换句话说，这两条指令用于筛选寄存器的特定bit位。

MSVC＋OllyDbg

现在使用OllyDbg调试这个程序。如图21.6所示，在执行CPUID之后，EAX/EBX/ECX/EDX寄存器保存着返回值。

图21.6　OllyDbg：执行CPUID之后的寄存器情况

因为我使用的是Xeon E3-1200 CPU，所以EAX的值是0x000206A7 。它的二进制数值为00000000000000100000011010100111。运行结果如图29.7所示。

图21.7　OllyDbg：运行结果

各字段涵义如下表所示。

字段

二进制值

十进制值

reserved2

0000

0

Extended_family_id

00000000

0

Extended_model_id

0010

2

reserved1

00

0

Processor_id

00

0

Family_id

0110

6

model

1010

10

stepping

0111

7

GCC

使用GCC 4.4.1（启用优化选项–O3）编译上述程序，可得如下所示的代码。

指令清单21.26　Optimizing 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\n"
    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\n"
    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\n"
    mov     dword ptr [esp], 1
    call    ___printf_chk
    mov     eax, esi
    shr     eax, 0Ch
    and     eax, 3
    mov     [esp+8], eax
    mov     dword ptr [esp+4], offset aProcessor_type ; "processor_type=%d\n"
    mov     dword ptr [esp], 1
    call    ___printf_chk
    mov     eax, esi
    shr     eax, 10h
    shr     esi, 14h
    and     eax, 0Fh
    and     esi, 0FFh
    mov     [esp+8], eax
    mov     dword ptr [esp+4], offset aExtended_model ; "extended_model_id=%d\n"
    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生成的汇编指令与MSVC库函数基本相同。二者的主要区别是：GCC编译的程序在调用printf()指令之前把extended_model_id和extended_family_id 放在连续的内存块里一并处理了，而没有像MSVC编译的程序那样、在每次调用printf()之前逐一进行计算。

21.6.2　用结构体构建浮点数

前文已经指出，每个单精度float或双精度double型浮点数，都由符号、小数和指数三部分组成。到底能否直接利用结构体构建浮点型数据呢？

符号位（第31位）

指数部分（23～30位）

小数部分（0～11位）

#include <stdio.h>
#include <assert.h>
#include <stdlib.h>
#include <memory.h>

struct float_as_struct
{
    unsigned int fraction : 23; // 小数
    unsigned int exponent : 8;  // 指数 + 0x3FF
    unsigned int sign : 1;// 符号位
};
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; // multiply d by 2^n (n here is 2)

    memcpy (&f, &t, sizeof (float));

    return f;
};

int main()
{
    printf ("%f\n", f(1.234));
};

通过结构体构建而成的float_as_struct和单精度浮点数据占用相同大小的内存空间，即32位/4字节。

在程序里，我们设置符号位为负（1），并把指数增加2，以进行乘以4（2的n次方（n＝2））的乘法运算。

使用MSVC 2008（不开启任何优化选项）编译上述程序，可得如下所示的代码。

指令清单21.27　Non-optimizing MSVC 2008

_t$ = -8 ; size = 4
_f$ = -4 ; size = 4
__in$ = 8 ; size = 4
?f@@YAMM@Z PROC ; f
    push   ebp
    mov    ebp, esp
    sub    esp, 8

    fld    DWORD PTR __in$[ebp]
    fstp   DWORD PTR _f$[ebp]

    push   4
    lea    eax, DWORD PTR _f$[ebp]
    push   eax
    lea    ecx, DWORD PTR _t$[ebp]
    push   ecx
    call   _memcpy
    add    esp, 12
    mov    edx, DWORD PTR _t$[ebp]
    or     edx, -2147483648 ; 80000000H - set minus sign
    mov    DWORD PTR _t$[ebp], edx

    mov    eax, DWORD PTR _t$[ebp]
    shr    eax, 23         ; 00000017H - drop significand
    and    eax, 255        ; 000000ffH - leave here only exponent
    add    eax, 2          ; add 2 to it
    and    eax, 255        ; 000000ffH
    shl    eax, 23         ; 00000017H - shift result to place of bits 30:23
    mov    ecx, DWORD PTR _t$[ebp]
    and    ecx, -2139095041 ; 807fffffH - drop exponent

; add original value without exponent with new calculated exponent
    or     ecx, eax
    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

    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选项又会如何？

指令清单21.28　Optimizing GCC 4.4.1

; f(float)
       public _Z1ff
_Z1ff  proc near

var_4  = dword  ptr -4
arg_0  = dword  ptr 8

       push     ebp
       mov      ebp, esp
       sub      esp, 4
       mov      eax, [ebp+arg_0]
       or       eax, 80000000h  ; set minus sign
       mov      edx, eax
       and      eax, 807FFFFFh  ; leave onlySign and significand 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        ; join new exponent and original value without exponent
       mov      [ebp+var_4], eax
       fld      [ebp+var_4]
       leave
       retn
_Z1ff  endp

       public main
main   proc near
       push     ebp
       mov      ebp, esp
       and      esp, 0FFFFFFF0h
       sub      esp, 10h
       fld      ds:dword_8048614 ; -4.936
       fstp     qword ptr [esp+8]
       mov      dword ptr [esp+4], offset asc_8048610 ; "%f\n"
       mov      dword ptr [esp], 1
       call     ___printf_chk
       xor      eax, eax
       leave
       retn
main   endp

f()函数的指令几乎可以自然解释。有趣的是，尽管结构体的各个字段如同大杂烩一样复杂，但是GCC能够在编译阶段就计算出函数表达式f(1.234)的值，并且把这个结果传递给printf()函数！

21.7　练习题

21.7.1　题目1

Linux程序[10]：

请参见http://beginners.re/exercises/per_chapter/struct_exercise_Linux86.tar

MIPS程序[11]：

请参见http://beginners.re/exercises/per_chapter/struct_exercise_MIPS.tar

。

这个Linux x86程序能够打开文件并在屏幕上打印数字。请问它打印的是什么？

21.7.2　题目2

这个函数的输入变量是结构体。请尝试逆向推出结构体的各个字段，不必推敲函数的具体功能。

由MSVC 2010 /Ox 选项编译而得的代码如下所示。

指令清单21.29　Optimizing MSVC 2010

$SG2802 DB      '%f', 0aH, 00H
$SG2803 DB      '%c, %d', 0aH, 00H
$SG2805 DB      'error #2', 0aH, 00H
$SG2807 DB      'error #1', 0aH, 00H

__real@405ec00000000000 DQ 0405ec00000000000r ; 123
__real@407bc00000000000 DQ 0407bc00000000000r ; 444

_s$ = 8
_f      PROC
        push    esi
        mov     esi, DWORD PTR _s$[esp]
        cmp     DWORD PTR [esi], 1000
        jle     SHORT $LN4@f
        cmp     DWORD PTR [esi+4], 10
        jbe     SHORT $LN3@f
        fld     DWORD PTR [esi+8]
        sub     esp, 8
        fmul    QWORD PTR __real@407bc00000000000
        fld     QWORD PTR [esi+16]
        fmul    QWORD PTR __real@405ec00000000000
        faddp   ST(1), ST(0)
        fstp    QWORD PTR [esp]
        push    OFFSET $SG2802 ; ’%f’
        call    _printf
        movzx   eax, BYTE PTR [esi+25]
        movsx   ecx, BYTE PTR [esi+24]
        push    eax
        push    ecx
        push    OFFSET $SG2803 ; ’%c, %d’
        call    _printf
        add     esp, 24
        pop     esi
        ret     0
$LN3@f:
        pop     esi
        mov     DWORD PTR _s$[esp-4], OFFSET $SG2805 ; ’error #2’
        jmp     _printf
$LN4@f:
        pop     esi
        mov     DWORD PTR _s$[esp-4], OFFSET $SG2807 ; ’error #1’
        jmp     _printf
_f      ENDP

指令清单21.30　Non-optimizing Keil 6/2013 (ARM mode)

f  PROC
        PUSH    {r4-r6,lr}
        MOV     r4,r0
        LDR     r0,[r0,#0]
        CMP     r0,#0x3e8
        ADRLE   r0,|L0.140|
        BLE     |L0.132|
        LDR     r0,[r4,#4]
        CMP     r0,#0xa
        ADRLS   r0,|L0.152|
        BLS     |L0.132|
        ADD     r0,r4,#0x10
        LDM     r0,{r0,r1}
        LDR     r3,|L0.164|
        MOV     r2,#0
        BL      __aeabi_dmul
        MOV     r5,r0
        MOV     r6,r1
        LDR     r0,[r4,#8]
        LDR     r1,|L0.168|
        BL      __aeabi_fmul
        BL      __aeabi_f2d
        MOV     r2,r5
        MOV     r3,r6
        BL      __aeabi_dadd
        MOV     r2,r0
        MOV     r3,r1
        ADR     r0,|L0.172|
        BL      __2printf
        LDRB    r2,[r4,#0x19]
        LDRB    r1,[r4,#0x18]
        POP     {r4-r6,lr}
        ADR     r0,|L0.176|
        B       __2printf
|L0.132|
        POP     {r4-r6,lr}
        B       __2printf
        ENDP
|L0.140|
        DCB     "error #1\n",0
        DCB     0
        DCB     0
|L0.152|
        DCB     "error #2\n",0
        DCB     0
        DCB     0
|L0.164|
        DCB     0x405ec000
|L0.168|
        DCB     0x43de0000
|L0.172|
        DCB     "%f\n",0
|L0.176|
        DCB     "%c, %d\n",0

指令清单21.31　Non-optimizing Keil 6/2013 (Thumb mode)

f  PROC
        PUSH     {r4-r6,lr}
        MOV      r4,r0
        LDR      r0,[r0,#0]
        CMP      r0,#0x3e8
        ADRLE    r0,|L0.140|
        BLE      |L0.132|
        LDR      r0,[r4,#4]
        CMP      r0,#0xa
        ADRLS    r0,|L0.152|
        BLS      |L0.132|
        ADD      r0,r4,#0x10
        LDM      r0,{r0,r1}
        LDR      r3,|L0.164|
        MOV      r2,#0
        BL       __aeabi_dmul
        MOV      r5,r0
        MOV      r6,r1
        LDR      r0,[r4,#8]
        LDR      r1,|L0.168|
        BL       __aeabi_fmul
        BL       __aeabi_f2d
        MOV      r2,r5
        MOV      r3,r6
        BL       __aeabi_dadd
        MOV      r2,r0
        MOV      r3,r1
        ADR      r0,|L0.172|
        BL       __2printf
        LDRB     r2,[r4,#0x19]
        LDRB     r1,[r4,#0x18]
        POP      {r4-r6,lr}
        ADR      r0,|L0.176|
        B        __2printf
|L0.132|
        POP      {r4-r6,lr}
        B        __2printf
        ENDP
|L0.140|
        DCB      "error #1\n",0
        DCB      0
        DCB      0
|L0.152|
        DCB      "error #2\n",0
        DCB      0
        DCB      0
|L0.164|
        DCD      0x405ec000
|L0.168|
        DCD      0x43de0000
|L0.172|
        DCB      "%f\n",0
|L0.176|
        DCB      "%c, %d\n",0

指令清单21.32　Optimizing GCC 4.9 (ARM64)

f:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0
        ldr     w1, [x0]
        str     x19, [sp,16]
        cmp     w1, 1000
        ble     .L2
        ldr     w1, [x0,4]
        cmp     w1, 10
        bls     .L3
        ldr     s1, [x0,8]
        mov     x19, x0
        ldr     s0, .LC1
        adrp    x0, .LC0
        ldr     d2, [x19,16]
        add     x0, x0, :lo12:.LC0
        fmul    s1, s1, s0
        ldr d0, .LC2
        fmul    d0, d2, d0
        fcvt    d1, s1
        fadd    d0, d1, d0
        bl      printf
        ldrb    w1, [x19,24]
        adrp    x0, .LC3
        ldrb    w2, [x19,25]
        add     x0, x0, :lo12:.LC3
        ldr     x19, [sp,16]
        ldp     x29, x30, [sp], 32
        b       printf
.L3:
        ldr     x19, [sp,16]
        adrp    x0, .LC4
        ldp     x29, x30, [sp], 32
        add     x0, x0, :lo12:.LC4
        b       puts
.L2:
        ldr     x19, [sp,16]
        adrp    x0, .LC5
        ldp     x29, x30, [sp], 32
        add     x0, x0, :lo12:.LC5
        b       puts
        .size   f, .-f
.LC1:
        .word   1138622464
.LC2:
        .word 0
        .word   1079951360
.LC0:
        .string "%f\n"
.LC3:
        .string "%c, %d\n"
.LC4:
        .string "error #2"
.LC5:
        .string "error #1"

指令清单21.33　Optimizing GCC 4.4.5 (MIPS) (IDA)

f:

var_10          = -0x10
var_8           =-8
var_4           =-4

                lui     $gp, (__gnu_local_gp >> 16)
                addiu   $sp, -0x20
                la      $gp, (__gnu_local_gp & 0xFFFF)
                sw      $ra, 0x20+var_4($sp)
                sw      $s0, 0x20+var_8($sp)
                sw      $gp, 0x20+var_10($sp)
                lw      $v0, 0($a0)
                or      $at, $zero
                slti    $v0, 0x3E9
                bnez    $v0, loc_C8
                move    $s0, $a0
                lw      $v0, 4($a0)
                or      $at, $zero
                sltiu   $v0, 0xB
                bnez    $v0, loc_AC
                lui     $v0, (dword_134 >> 16)
                lwc1    $f4, $LC1
                lwc1    $f2, 8($a0)
                lui     $v0, ($LC2 >> 16)
                lwc1    $f0, 0x14($a0)
                mul.s   $f2, $f4, $f2
                lwc1    $f4, dword_134
                lwc1    $f1, 0x10($a0)
                lwc1    $f5, $LC2
                cvt.d.s $f2, $f2
                mul.d   $f0, $f4, $f0
                lw      $t9, (printf & 0xFFFF)($gp)
                lui     $a0, ($LC0 >> 16)  # "%f\n"
                add.d   $f4, $f2, $f0
                mfc1    $a2, $f5
                mfc1    $a3, $f4
                jalr    $t9
                la      $a0, ($LC0 & 0xFFFF)  # "%f\n"
                lw      $gp, 0x20+var_10($sp)
                lbu     $a2, 0x19($s0)
                lb      $a1, 0x18($s0)
                lui     $a0, ($LC3 >> 16)  # "%c, %d\n"
                lw      $t9, (printf & 0xFFFF)($gp)
                lw      $ra, 0x20+var_4($sp)
                lw      $s0, 0x20+var_8($sp)
                la      $a0, ($LC3 & 0xFFFF)  # "%c, %d\n"
                jr      $t9
                addiu   $sp, 0x20
loc_AC:                                 # CODE XREF: f+38
                lui     $a0, ($LC4 >> 16)  # "error #2"
                lw      $t9, (puts & 0xFFFF)($gp)
                lw      $ra, 0x20+var_4($sp)
                lw      $s0, 0x20+var_8($sp)
                la      $a0, ($LC4 & 0xFFFF)  # "error #2"
                jr      $t9
                addiu   $sp, 0x20
loc_C8:                                 # CODE XREF: f+24
                lui     $a0, ($LC5 >> 16)  # "error #1"
                lw      $t9, (puts & 0xFFFF)($gp)
                lw      $ra, 0x20+var_4($sp)
                lw      $s0, 0x20+var_8($sp)
                la      $a0, ($LC5 & 0xFFFF)  # "error #1"
                jr      $t9
                addiu   $sp, 0x20

$LC0:           .ascii "%f\n"<0>
$LC3:           .ascii "%c, %d\n"<0>
$LC4:           .ascii "error #2"<0>
$LC5:           .ascii "error #1"<0>

                .data # .rodata.cst4
$LC1:           .word 0x43DE0000

                .data # .rodata.cst8
$LC2:           .word 0x405EC000
dword_134:      .word 0

[1]　又称为“异构容器/heterogeneous container”。

[2]　https://msdn.microsoft.com/en-us/library/ms724950(VS.85).aspx。

[3]　https://msdn.microsoft.com/en-us/library/ee488017.aspx。

[4]　为了便于演示，作者略微调整了date的返回结果。在实际情况下，手动输入GDB指令的速度不会那么快，多次操作的返回结果不可能分毫不差。

[5]　请参见Dennis M. Ritchie撰写的<< The development of the c language>>，“SIGPLAN Not”的第28章第3节:201-208页，您也可以在作者的网站下载：http://yurichev.com/mirrors/C/ dmr-The%20Development%20of%20the%20C%20Language-1993.pdf.。

[6]　更多内容请参见https://en.wikipedia.org/wiki/Data_structure_alignment。

[7]　请参阅https://msdn.microsoft.com/en-us/library/ms253935.aspx和https://gcc.gnu.org/ onlinedocs/gcc/Structure-Packing-Pragmas.html。

[8]　请参见http://en.wikipedia.org/wiki/CPUID。

[9]　请参见http://www.ibiblio.org/gferg/ldp/GCC-Inline-Assembly-HOWTO.html。

[10]　GCC 4.8.1 -O3。

[11]　GCC 4.4.5-O3。

第22章 共用体（union）类型

22.1　伪随机数生成程序

我们可以通过不同的方法生成一个介于0～1之间的随机浮点数。最简单的做法是：使用Mersenne twister（马特赛特旋转演算法）之类的PRNG[1]生成32位DWORD值，把这个值转换为单精度float之后再除以RAND_MAX（本例中是0xFFFFFFFF）。这样就可以得到介于0～1之间的单精度浮点数。

但是除法的运算速度非常慢。而且从效率的角度考虑，随机函数同样应当尽量少用FPU的操作指令。那么，我们是否可以彻底脱离除法运算实现随机函数呢？

单精度浮点数（float）型数据由符号位、有效数字和指数三部分构成。这种数据结构决定，只要随机填充有效数字位就可以生成随机的单精度数！

依据有关规范，随机浮点数的指数部分不可以是0。那么我们不妨把指数部分直接设置为01111111（即十进制的1），用随机数字填充有效数字部分，再把符号位设置为零（表示正数）。瞧！像模像样！这就可以生成一个介于1～2之间的随机数。再把它减去1就可以得到一个标准的随机函数。

本例利用了线性同余随机数生成随机数的算法[2]。它使用UNIX格式的系统时间作为PRNG的随机种子，继而生成32位数字。

在采用这种算法时，我们可采用共用体类型（union）的数据表示单精度浮点数（float）。这个方法可利用C/C++的数据结构，按照一种与读取方式不同的数据类型存储浮点数据的各个组成部分。本例中，我们创建一个union型变量，然后把它当作float型或uint32_t型数据进行读取。换句话说，这是一种hack，而且还是比较深度的hack。

第20章的例子介绍过整数型PRNG的有关程序。因而本节不再复述其数据格式。

include <stdio.h>
#include <stdint.h>
#include <time.h>

// integer PRNG definitions, data and routines:

//constants from the Numerical Recipes book
const uint32_t RNG_a=1664525;
const uint32_t RNG_c=1013904223;
uint32_t RNG_state; // global variable

void my_srand(uint32_t i)
{
         RNG_state=i;
};
uint32_t my_rand()
{
         RNG_state=RNG_state*RNG_a+RNG_c;
         return RNG_state;
};

// FPU PRNG definitions and routines:

union uint32_t_float
{
         uint32_t i;
         float f;
};

float float_rand()
{
         union uint32_t_float tmp;
         tmp.i=my_rand() & 0x007fffff | 0x3F800000;
         return tmp.f-1;
};

// test

int main()
{
         my_srand(time(NULL)); // PRNG initialization

         for (int i=0; i<100; i++)
                  printf ("%f\n", float_rand());

         return 0;
};

22.1.1　x86

使用MSVC 2010（启用选项/Ox）编译上述程序，可得到如下所示的代码。

指令清单22.1　Optimizing MSVC 2010

$SG4232  DB     '%f', 0aH, 00H

__real@3ff0000000000000 DQ 03ff0000000000000r ;1

tv140 = -4
_tmp$ = -4
?float_rand@@YAMXZ PROC
        push    ecx
        call    ?my_rand@@YAIXZ
; EAX=pseudorandom value
        and     eax, 8388607        ; 007fffffH
        or      eax, 1065353216     ; 3f800000H
; EAX=pseudorandom value & 0x007fffff | 0x3f800000
; store it into local stack:
        mov     DWORD PTR _tmp$[esp+4], eax
; reload it as float point number:
        fld     DWORD PTR _tmp$[esp+4]
; subtract 1.0:
        fsub    QWORD PTR __real@3ff0000000000000
; store value we got into local stack and reload it:
        fstp    DWORD PTR tv130[esp+4] ; \  these instructions are redundant
        fld     DWORD PTR tv130[esp+4] ; /
        pop     ecx
        ret     0
?float_rand@@YAMXZ ENDP

_main   PROC
        push    esi
        xor     eax, eax
        call    _time
        push    eax
        call    ?my_srand@@YAXI@Z
        add     esp, 4
        mov     esi, 100
$LL3@main:
        call    ?float_rand@@YAMXZ
        sub     esp, 8
        fstp    QWORD PTR [esp]
        push    OFFSET $SG4238
        call    _printf
        add     esp, 12
        dec     esi
        jne     SHORT $LL3@main
        xor     eax, eax
        pop     esi
        ret     0
_main   ENDP

在使用C++编译器编译之后，可执行程序中的函数名称彻底走样。这是编译器对函数的命名规范所决定的。本书51.1.1节将详细介绍这种规范。

如果使用MSVC 2012编译器进行编译，那么可执行程序则会使用面向FPU的SIMD指令。本书27.5节会对SMID指令进行单独介绍。

22.1.2　MIPS

指令清单22.2　Optimizing GCC 4.4.5

float_rand:

var_10          = -0x10
var_4           = -4
                lui       $gp, (__gnu_local_gp >> 16)
                addiu     $sp, -0x20
                la        $gp, (__gnu_local_gp & 0xFFFF)
                sw        $ra, 0x20+var_4($sp)
                sw        $gp, 0x20+var_10($sp)
; call my_rand():
                jal       my_rand
                or        $at, $zero ; branch delay slot, NOP
; $v0=32-bit pseudorandom value
                li        $v1, 0x7FFFFF
; $v1=0x7FFFFF
                and       $v1, $v0, $v1
; $v1=pseudorandom value & 0x7FFFFF
                lui       $a0, 0x3F80
; $a0=0x3F800000
                or        $v1, $a0
; $v1=pseudorandom value & 0x7FFFFF | 0x3F800000
; matter of the following instruction is still hard to get:
                lui       $v0, ($LC0 >> 16)
; load 1.0 into $f0:
                lwc1      $f0, $LC0
; move value from $v1 to coprocessor 1 (into register $f2)
; it behaves like bitwise copy, no conversion done:
                mtc1      $v1, $f2
                lw        $ra, 0x20+var_4($sp)
; subtract 1.0. leave result in $f0:
                sub.s     $f0, $f2, $f0
                jr        $ra
                addiu     $sp, 0x20 ; branch delay slot
main:

var_18          = -0x18
var_10          = -0x10
var_C           = -0xC
var_8           = -8
var_4           = -4
                lui       $gp, (__gnu_local_gp >> 16)
                addiu     $sp, -0x28
                la        $gp, (__gnu_local_gp & 0xFFFF)
                sw        $ra, 0x28+var_4($sp)
                sw        $s2, 0x28+var_8($sp)
                sw        $s1, 0x28+var_C($sp)
                sw        $s0, 0x28+var_10($sp)
                sw        $gp, 0x28+var_18($sp)
                lw        $t9, (time & 0xFFFF)($gp)
                or        $at, $zero ; load delay slot, NOP
                jalr      $t9
                move      $a0, $zero ; branch delay slot
                lui       $s2, ($LC1 >> 16) # "%f\n"
                move      $a0, $v0
                la        $s2, ($LC1 & 0xFFFF) # "%f\n"
                move      $s0, $zero
                jal       my_srand
                li        $s1, 0x64 # 'd' ; branch delay slot
loc_104:
                jal       float_rand
                addiu     $s0, 1
                lw        $gp, 0x28+var_18($sp)
; convert value we got from float_rand() to double type (printf() need it):
                cvt.d.s   $f2, $f0
                lw        $t9, (printf & 0xFFFF)($gp)
                mfc1      $a3, $f2
                mfc1      $a2, $f3
                jalr      $t9
                move      $a0, $s2
                bne       $s0, $s1, loc_104
                move      $v0, $zero
                lw        $ra, 0x28+var_4($sp)
                lw        $s2, 0x28+var_8($sp)
                lw        $s1, 0x28+var_C($sp)
                lw        $s0, 0x28+var_10($sp)
                jr        $ra
                addiu     $sp, 0x28 ; branch delay slot

$LC1:           .ascii "%f\n"<0>
$LC0:           .float 1.0

上述程序同样出现了无实际意义的LUI指令。17.5.6节解释过这种情况了，本节不再对其进行复述。

22.1.3　ARM (ARM mode)

指令清单22.3　Optimizing GCC 4.6.3 (IDA)

float_rand
                STMFD   SP!, {R3,LR}
                BL      my_rand
; R0=pseudorandom value
                FLDS    S0, =1.0
; S0=1.0
                BIC     R3, R0, #0xFF000000
                BIC     R3, R3, #0x800000
                ORR     R3, R3, #0x3F800000
; R3=pseudorandom value & 0x007fffff | 0x3f800000
; copy from R3 to FPU (register S15).
; it behaves like bitwise copy, no conversion done:
                FMSR    S15, R3
; subtract 1.0 and leave result in S0:
                FSUBS   S0, S15, S0
                LDMFD   SP!, {R3,PC}
flt_5C          DCFS 1.0

main
                STMFD   SP!, {R4,LR}
                MOV     R0, #0
                BL      time
                BL      my_srand
                MOV     R4, #0x64 ; 'd'
loc_78
                BL      float_rand
; S0=pseudorandom value
                LDR     R0, =aF         ; "%f"
; convert float type value into double type value (printf() will need it):
                FCVTDS  D7, S0
; bitwise copy from D7 into R2/R3 pair of registers (for printf()):
                FMRRD   R2, R3, D7
                BL      printf
                SUBS    R4, R4, #1
                BNE     loc_78
                MOV     R0, R4
                LDMFD   SP!, {R4,PC}

aF              DCB "%f",0xA,0

通过objdump工具对上述程序进行分析，可看到objdump所显示的FPU指令和IDA所显示的指令并不一致。这大概是因为IDA与binutils的研发人员使用的是不同的指令手册。不管原因如何，同一个FPU指令会有不同对应名称的情况确实存在。

指令清单22.4　Optimizing GCC 4.6.3 (objdump)

00000038 <float_rand>:
  38:   e92d4008        push    {r3, lr}
  3c:   ebfffffe        bl      10 <my_rand>
  40:   ed9f0a05        vldr    s0, [pc, #20]        ; 5c <float_rand+0x24>
  44:   e3c034ff        bic     r3, r0, #-16777216   ; 0xff000000
  48:   e3c33502        bic     r3, r3, #8388608     ; 0x800000
  4c:   e38335fe        orr     r3, r3, #1065353216  ; 0x3f800000
  50:   ee073a90        vmov    s15, r3
  54:   ee370ac0        vsub.f32        s0, s15, s0
  58:   e8bd8008        pop     {r3, pc}
  5c:   3f800000        svccc   0x00800000

00000000 <main>:
   0:   e92d4010        push    {r4, lr}
   4:   e3a00000        mov     r0, #0
   8:   ebfffffe        bl      0 <time>
   c:   ebfffffe        bl      0 <main>
   10:  e3a04064        mov     r4, #100        ; 0x64
   14:  ebfffffe        bl      38 <main+0x38>
   18:  e59f0018        ldr     r0, [pc, #24]   ; 38 <main+0x38>
   1c:  eeb77ac0        vcvt.f64.f32    d7, s0
   20:  ec532b17        vmov    r2, r3, d7
   24:  ebfffffe        bl      0 <printf>
   28:  e2544001        subs    r4, r4, #1
   2c:  1afffff8        bne     14 <main+0x14>
   30:  e1a00004        mov     r0, r4
   34:  e8bd8010        pop     {r4, pc}
   38:  00000000        andeq   r0, r0, r0

在float_rand()函数里地址5c处的指令和main()函数里地址38处的指令都是随机噪音。

22.2　计算机器精度

单精度浮点数的“机器精度/machine epsilon”指的是相对误差的上限，即FPU操作的最小值。因此，浮点数的数据位越多，误差越小、精度越高。故而单精度float型数据的最高精度为2⁻²³=1.19e⁻⁷，而双精度double型的最高精度为2⁻⁵²=2.22e⁻¹⁶。

所以计算某一数值的机器精度是可能的。

#include <stdio.h>
#include <stdint.h>

union uint_float
{
        uint32_t i;
        float f;
};

float calculate_machine_epsilon(float start)
{
        union uint_float v;
        v.f=start;
        v.i++;
        return v.f-start;
};
void main()
{
        printf ("%g\n", calculate_machine_epsilon(1.0));
};

上述程序从IEEE 754格式的浮点数中提取小数部分，把这部分当作整数处理并且给它加上1。运算的中间值是“输入值＋机器精度”。通过测算输入值（单精度型数值）与中间值的差值来测算具体float型数据的机器精度。

程序使用union型数据结构解析IEEE 754格式的float型浮点数，并利用这种数据结构把float数据的小数部分提取为整数。“1”实际上加到浮点数小数部分中去了。当然，这可能造成溢出，可能把最高位的进位加到浮点数的指数部分。

22.2.1　x86

指令清单22.5　Optimizing MSVC 2010

tv130 = 8
_v$ = 8
_start$ = 8
_calculate_machine_epsilon PROC
        fld      DWORD PTR _start$[esp-4]
        fst      DWORD PTR _v$[esp-4]      ;冗余代码
        inc      DWORD PTR _v$[esp-4]
        fsubr    DWORD PTR _v$[esp-4]
        fstp     DWORD PTR tv130[esp-4]    ;冗余代码
        fld      DWORD PTR tv130[esp-4]    ;冗余代码
        ret      0
_calculate_machine_epsilon ENDP

上述程序的第二个FST指令是冗余指令：没有理由把输入值在同一个地址存储两次。这是编译器的问题：它决定把变量v的存储地址和传递参数所用的栈内地址分配成同一个地址。

接下来的INC指令把输入值的小数部分当作整数数据处理。处理结果的中间值再被当作IEEE 754型数据传递给FPU。FSUBR指令计算差值，最后返回值被存储到ST0之中。

程序中最后两条FSTP/FLD指令没有实际作用。编译器没能进行相应的优化。

22.2.2　ARM64

把数据类型扩展为64位数据的程序如下所示。

#include <stdio.h>
#include <stdint.h>

typedef union
{
        uint64_t i;
        double  d;
} uint_double;

double calculate_machine_epsilon(double start)
{
        uint_double v;
        v.d=start;
        v.i++;
        return v.d-start;
}
void main()
{
        printf ("%g\n", calculate_machine_epsilon(1.0));
};

ARM64平台的指令不能直接在FPU的D-字头寄存器里进行加法运算。因此输入值首先从D0寄存器复制到GPR，在那里进行运算并把结果复制给D1寄存器，从而在FPU里进行减法运算。

指令清单22.6　Optimizing GCC 4.9 ARM64

calculate_machine_epsilon:
        fmov     x0, d0      ; load input value of double type into X0
        add      x0, x0, 1   ; X0++
        fmov     d1, x0      ; move it to FPU register
        fsub     d0, d1, d0  ; subtract
        ret

可见上述x64程序使用到了SIMD指令。有关介绍请参见本书27.4节。

22.2.3　MIPS

面向MIPS平台的编译器使用MTC1（Move To Coprocessor 1）指令把GPR的数据传递给FPU寄存器。

指令清单22.7　Optimizing GCC 4.4.5 (IDA)

calculate_machine_epsilon:
                mfc1    $v0, $f12
                or      $at, $zero ; NOP
                addiu   $v1, $v0, 1
                mtc1    $v1, $f2
                jr      $ra
                sub.s   $f0, $f2, $f12 ; branch delay slot

22.2.4　本章小结

本章程序所使用的技巧，在实际程序中出现的几率不大。但是这些技巧可充分演示IEEE 754型数据和C/C++ UNIONS型数据结构的特点。

22.3　快速平方根计算

浮点数解释为整数的另一个著名的算法，是快速平方根计算。

指令清单22.8　取自http://go.yurichev.com/17364的源代码

/* Assumes that float is in the IEEE 754 single precision floating point format
 * and that int is 32 bits. */
float sqrt_approx(float z)
{
    int val_int = *(int*)&z; /* Same bits, but as an int */
    /*
     * To justify the following code, prove that
     *
     * ((((val_int / 2^m) - b) / 2) + b) * 2^m = ((val_int - 2^m) / 2) + ((b + 1) / 2) * 2^m)
     *
     *where
     *
     *b = exponent bias
     * m = number of mantissa bits
     *
     *
     .
     */

    val_int -= 1 << 23; /* Subtract 2^m. */
    val_int >>= 1; /* Divide by 2. */
    val_int += 1 << 29; /* Add ((b + 1) / 2) * 2^m. */

    return *(float*)&val_int; /* Interpret again as float */
}

作为一个练习，你可以尝试编译这个函数，以理解它是如何工作的。还有一个著名的快速计算\frac{1}{\sqrt{x}} 的算法。据说这一算法因为用于QuakeⅢArena中而变得很流行。

该算法的描述和源代码参见http://go.yurichev.com/17360

。

[1]　Pseudorandom number generator，伪随机数生成函数。

[2]　参考了网络文章http://xor0110.wordpress.com/2010/09/24/how-to-generate-floating-point-random-numbers-efficiently。

第23章 函数指针

函数指针和其他指针没有太大区别。它代表的地址为函数代码段的起始地址。

在函数指针（地址）以函数参数的形式传递给另一个函数，继而被用来调用该指针所指向的函数时，这种函数指针的所指向的函数就是“回调函数”/callback function。[1]

此类指针主要应用于：

标准C函数库里的qsort()函数和atexit()函数[2]。

*NIX系统里的信号（Signals）[3]。

启动线程的CreateThread()（windows函数）和pthread_create()（POSIX函数）。

Win32的多种函数，例如EnumChildWindows()[4]。

Linux内核。例如，Linux以callback的方式调用文件系统的驱动函数：http://lxr.free-electrons.com/source/include/linux/fs.h?v=3.14#L1525。

GCC插件：https://gcc.gnu.org/onlinedocs/gccint/Plugin-API.html#Plugin-API。

Linux窗口管理程序定义快捷方式的dwm表。当键盘接收到可匹配的特定键时，对应的快捷方式就通过callback调用相应函数。请参见GitHub（https://github.com/cdown/dwm/ blob/master/config.def.h#L117），callback方法要比大量使用switch()语句的方法更为简单。

其中，qsort()函数是C/C++编译器函数库自带的快速排序函数。无论待排序的数据是何种数据类型，只要编写出比较两个元素的函数，那么就可以用qsort()函数以callback的方式调用比较函数。

例如，我们可声明比较函数为：

int (*compare)(const void *, const void *)

我们来看下面这段程序：

 1  /* ex3 Sorting ints with qsort */
 2
 3  #include <stdio.h>
 4  #include <stdlib.h>
 5
 6  int comp(const void * _a, const void * _b)
 7  {
 8    const int *a=(const int *)_a;
 9    const int *b=(const int *)_b;
10
11    if (*a==*b)
12      return 0;
13    else
14      if (*a < *b)
15           return -1;
16       else
17        return 1;
18  }
19
20  int main(int argc, char* argv[])
21  {
22    int numbers[10]={1892,45,200,-98,4087,5,-12345,1087,88,-100000};
23    int i;
24
25   /* Sort the array */
26   qsort(numbers,10,sizeof(int),comp) ;
27   for (i=0;i<9;i++)
28     printf("Number = %d\n",numbers[ i ]) ;
29   return 0;
30  }

23.1　MSVC

使用MSVC 2010（启用选项/Ox/GS-/MD）编译上述程序，可得到下述汇编指令（为了凸出重点而进行了精简）。

指令清单23.1　Optimizing MSVC 2010: /GS- /MD

__a$ = 8          ; size = 4
__b$ = 12         ; size = 4
_comp   PROC
         mov  eax, DWORD PTR __a$[esp-4]
         mov  ecx, DWORD PTR __b$[esp-4]
         mov  eax, DWORD PTR [eax]
         mov  eax, 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()函数的第一条指令的内存地址。

qsort()函数又是如何调用comp()函数的？

qsort()函数位于MSVCR80.DLL（含有C函数标准库的MSVC DLL）中。我们来分析这个文件中的具体指令。

指令清单23.2　MSVCR80.DLL

.text:7816CBF0 ; void __cdecl qsort(void *, unsigned int, unsigned int, int (__cdecl *)(const ↙
    ↘ void *, const void *))
.text:7816CBF0                 public _qsort
.text:7816CBF0 _qsort          proc near
.text:7816CBF0
.text:7816CBF0 lo              = dword ptr -104h
.text:7816CBF0 hi              = dword ptr -100h
.text:7816CBF0 var_FC          = dword ptr -0FCh
.text:7816CBF0 stkptr          = dword ptr -0F8h
.text:7816CBF0 lostk           = dword ptr -0F4h
.text:7816CBF0 histk           = dword ptr -7Ch
.text:7816CBF0 base            = dword ptr  4
.text:7816CBF0 num             = dword ptr  8
.text:7816CBF0 width           = dword ptr  0Ch
.text:7816CBF0 comp            = dword ptr  10h
.text:7816CBF0
.text:7816CBF0                 sub     esp, 100h

 …

.text:7816CCE0 loc_7816CCE0:                      ; CODE XREF: _qsort+B1
.text:7816CCE0                 shr      eax, 1
.text:7816CCE2                 imul     eax, ebp
.text:7816CCE5                 add      eax, ebx
.text:7816CCE7                 mov      edi, eax
.text:7816CCE9                 push     edi
.text:7816CCEA                 push     ebx
.text:7816CCEB                 call     [esp+118h+comp]
.text:7816CCF2                 add      esp, 8
.text:7816CCF5                 test     eax, eax
.text:7816CCF7                 jle      short loc_7816CD04

MSVCR80.DLL中的comp参数，是传递给qsort()函数的第四个参数。在执行qsort()的过程中，系统会把控制权传递给comp参数指向的函数指针的地址。在调用它之前，comp()函数所需的两个参数已经传递到位。在执行它之后，排序已经完成。

可见，函数指针十分危险。首先，如果传递给qsort()函数的函数指针有误，那么qsort()函数仍然会把控制权传递给错误的指针地址，届时程序多半将会崩溃，而且人工排错很难发现问题所在。

其次，callback函数必须严格遵守调用规范。无论是函数不当、参数不当还是数据类型不当，都会引发严重的问题。相比之下，关键问题并不是程序是否会崩溃，而是排查程序崩溃的手段是什么。在编译器处理函数指针的时候，它不会对潜在问题进行任何提示。

23.1.1　MSVC+OllyDbg

我们使用OllyDbg加载这个程序，并在首次调用comp()函数的地址设置断点。

图23.1所示为程序首次调用comp()函数时的情况。OllyDbg在代码窗口下显示出被比较的两个值。此时SP指向RA，即qsort()函数的地址（实际上是MSVCR100.DLL内部的地址）。

图23.1　OllyDbg:第一次调用comp()函数

在程序运行完RETN指令之前，我们一直按F8键，等待它进入qsort()函数，如图23.2所示。

图23.2　OllyDbg:调用comp()函数之后返回qsort()函数

程序将再次调用比较函数。

图23.3所示的是第二次调用comp()函数的情形。这一时刻被比较的两个值不相同。

图23.3　OllyDbg:第二次调用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

当程序执行到该断点时，各寄存器的情况如下：

PID=4336|New process 17_1.exe
(0) 17_1.exe!0x40100c
EAX=0x00000764 EBX=0x0051f7c8 ECX=0x00000005 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=IF
(0) 17_1.exe!0x40100c
EAX=0x00000005 EBX=0x0051f7c8 ECX=0xfffe7960 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=PF ZF IF
(0) 17_1.exe!0x40100c
EAX=0x00000764 EBX=0x0051f7c8 ECX=0x00000005 EDX=0x00000000
ESI=0x0051f7d8 EDI=0x0051f7b4 EBP=0x0051f794 ESP=0x0051f67c
EIP=0x0028100c
FLAGS=CF PF ZF IF
...

从中筛选EAX和ECX寄存器的值：

EAX=0x00000764 ECX=0x00000005
EAX=0x00000005 ECX=0xfffe7960
EAX=0x00000764 ECX=0x00000005
EAX=0x0000002d ECX=0x00000005
EAX=0x00000058 ECX=0x00000005
EAX=0x0000043f ECX=0x00000005
EAX=0xffffcfc7 ECX=0x00000005
EAX=0x000000c8 ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0x00000ff7 ECX=0x00000005
EAX=0x00000ff7 ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0x00000005 ECX=0xffffcfc7
EAX=0xffffff9e ECX=0x00000005
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0xffffff9e ECX=0xffffcfc7
EAX=0xffffcfc7 ECX=0xfffe7960
EAX=0x000000c8 ECX=0x00000ff7
EAX=0x0000002d ECX=0x00000ff7
EAX=0x0000043f ECX=0x00000ff7
EAX=0x00000058 ECX=0x00000ff7
EAX=0x00000764 ECX=0x00000ff7
EAX=0x000000c8 ECX=0x00000764
EAX=0x0000002d ECX=0x00000764
EAX=0x0000043f ECX=0x00000764
EAX=0x00000058 ECX=0x00000764
EAX=0x000000c8 ECX=0x00000058
EAX=0x0000002d ECX=0x000000c8
EAX=0x0000043f ECX=0x000000c8
EAX=0x000000c8 ECX=0x00000058
EAX=0x0000002d ECX=0x000000c8
EAX=0x0000002d ECX=0x00000058

得到上面所列的34组数据。也就是说，quick sort算法需要把这10个数字进行34次比较。

23.1.3　MSVC + tracer（指令分析）

本节利用tracer程序收集寄存器里出现过的所有值，稍后在IDA里显示它们。

首先使用tracer程序追踪comp()函数里的所有指令：

tracer.exe -l:17_1.exe bpf=17_1.exe!0x00401000,trace:cc

然后再用IDA加载刚才生成的.idc-script文件。如图23.4所示。

图23.4　tracer与IDA的联动/某些值在屏幕右侧边界之外

通过分析qsort()函数调用的函数指针，IDA能够显示出相应的函数名称（PtFuncCompare）。

由于数组里存储的是32位数据，所以指针a和指针b多次指向数组里的不同地方，且它们每次变换之间的地址差是4字节。

我们还注意到0x401010和0x401012处的指令就没有被执行过（所以被标记为白色）。这是因为传递给comp()函数的值都不相同，函数返回值不会是0。

23.2　GCC

GCC的编译方法与MSVC的编译方式十分相近。

指令清单23.3　GCC

                lea     eax, [esp+40h+var_28]
                mov     [esp+40h+var_40], eax
                mov     [esp+40h+var_28], 764h
                mov     [esp+40h+var_24], 2Dh
                mov     [esp+40h+var_20], 0C8h
                mov     [esp+40h+var_1C], 0FFFFFF9Eh
                mov     [esp+40h+var_18], 0FF7h
                mov     [esp+40h+var_14], 5
                mov     [esp+40h+var_10], 0FFFFCFC7h
                mov     [esp+40h+var_C], 43Fh
                mov     [esp+40h+var_8], 58h
                mov     [esp+40h+var_4], 0FFFE7960h
                mov     [esp+40h+var_34], offset comp
                mov     [esp+40h+var_38], 4
                mov     [esp+40h+var_3C], 0Ah
                call    _qsort

comp()函数对应的代码如下所示。

                public comp
comp            proc near

arg_0           = dword ptr  8
arg_4           = dword ptr  0Ch

                push    ebp
                mov     ebp, esp
                mov     eax, [ebp+arg_4]
                mov     ecx, [ebp+arg_0]
                mov     edx, [eax]
                xor     eax, eax
                cmp     [ecx], edx
                jnz     short loc_8048458
                pop     ebp
                retn
loc_8048458:
                setnl   al
                movzx   eax, al
                lea     eax, [eax+eax-1]
                pop     ebp
                retn
comp            endp

qsort()函数的计算过程封装在库文件libc.so.6里。因此，Linux的qsort()只是qsort_r()的wrapper。

此处会调用quicksort()函数，后者再通过函数指针调用我们编写的comp()函数。

glibc version -2.10.1中的lib.so.6文件有下述指令。

指令清单23.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（有源代码的情况）

在有源代码的情况下[5]，我们能够针对源代码的行号（第11行，第一次调用比较函数）设置断点（b指令）。这种调试方法有一个前提条件：我们还要在编译源代码时保留调试信息（启用编译选项-g），以便保留地址表和行号之间的对应关系。这样，我们就能根据变量名打印变量的值（p指令）：调试信息会保留寄存器和（或）数据栈元素与变量之间的对应关系。

我们也可以查看数据栈（bt），并且找出Glibc的msort_with_tmp()函数使用了哪些中间函数。

调试过程如下。

指令清单23.5　GDB调试过程

dennis@ubuntuvm:~/polygon$ gcc 17_1.c -g
dennis@ubuntuvm:~/polygon$ gdb ./a.out
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/a.out...done.
(gdb) b 17_1.c:11
Breakpoint 1 at 0x804845f: file 17_1.c, line 11.
(gdb) run
Starting program: /home/dennis/polygon/./a.out
Breakpoint 1, comp (_a=0xbffff0f8, _b=_b@entry=0xbffff0fc) at 17_1.c:11
11          if (*a==*b)
(gdb) p *a
$1 = 1892
(gdb) p *b
$2 = 45
(gdb) c
Continuing.

Breakpoint 1, comp (_a=0xbffff104, _b=_b@entry=0xbffff108) at 17_1.c:11
11          if (*a==*b)
(gdb) p *a
$3 = -98
(gdb) p *b
$4 = 4087
(gdb) bt
#0  comp (_a=0xbffff0f8, _b=_b@entry=0xbffff0fc) at 17_1.c:11
#1  0xb7e42872 in msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=2)
    at msort.c:65
#2  0xb7e4273e in msort_with_tmp (n=2, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#3  msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=5) at msort.c:53
#4  0xb7e4273e in msort_with_tmp (n=5, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#5  msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=10) at msort.c:53
#6  0xb7e42cef in msort_with_tmp (n=10, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#7  __GI_qsort_r (b=b@entry=0xbffff0f8, n=n@entry=10, s=s@entry=4, cmp=cmp@entry=0x804844d < comp>,
    arg=arg@entry=0x0) at msort.c:297
#8  0xb7e42dcf in __GI_qsort (b=0xbffff0f8, n=10, s=4, cmp=0x804844d <comp>) at msort.c:307
#9  0x0804850d in main (argc=1, argv=0xbffff1c4) at 17_1.c:26
(gdb)

23.2.2　GCC+GDB（没有源代码的情况）

但是实际情况是，多数情况下我们没有程序的源代码。此时需要反编译comp()函数（disas指令），找到第一个CMP指令并在该处设置断点。在此之后，我们要查看所有寄存器的值（info registers）。虽然此时还能够查看数据栈（bt），但是所得信息非常有限：程序里没有保存comp()函数的行号信息。

调试过程如下。

指令清单23.6　GDB调试过程

dennis@ubuntuvm:~/polygon$ gcc 17_1.c
dennis@ubuntuvm:~/polygon$ gdb ./a.out
GNU gdb (GDB) 7.6.1-ubuntu
Copyright (C) 2013 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "i686-linux-gnu".
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>...
Reading symbols from /home/dennis/polygon/a.out...(no debugging symbols found)...done.
(gdb) set disassembly-flavor intel
(gdb) disas comp
Dump of assembler code for function comp:
   0x0804844d <+0>:     push   ebp
   0x0804844e <+1>:     mov    ebp,esp
   0x08048450 <+3>:     sub    esp,0x10
   0x08048453 <+6>:     mov    eax,DWORD PTR [ebp+0x8]
   0x08048456 <+9>:     mov    DWORD PTR [ebp-0x8],eax
   0x08048459 <+12>:    mov    eax,DWORD PTR [ebp+0xc]
   0x0804845c <+15>:    mov    DWORD PTR [ebp-0x4],eax
   0x0804845f <+18>:    mov    eax,DWORD PTR [ebp-0x8]
   0x08048462 <+21>:    mov    edx,DWORD PTR [eax]
   0x08048464 <+23>:    mov    eax,DWORD PTR [ebp-0x4]
   0x08048467 <+26>:    mov    eax,DWORD PTR [eax]
   0x08048469 <+28>:    cmp    edx,eax
   0x0804846b <+30>:    jne    0x8048474 <comp+39>
   0x0804846d <+32>:    mov    eax,0x0
   0x08048472 <+37>:    jmp    0x804848e <comp+65>
   0x08048474 <+39>:    mov    eax,DWORD PTR [ebp-0x8]
   0x08048477 <+42>:    mov    edx,DWORD PTR [eax]
   0x08048479 <+44>:    mov    eax,DWORD PTR [ebp-0x4]
   0x0804847c <+47>:    mov    eax,DWORD PTR [eax]
   0x0804847e <+49>:    cmp    edx,eax
   0x08048480 <+51>:    jge    0x8048489 <comp+60>
   0x08048482 <+53>:    mov    eax,0xffffffff
   0x08048487 <+58>:    jmp    0x804848e <comp+65>
   0x08048489 <+60>:    mov    eax,0x1
   0x0804848e <+65>:    leave
   0x0804848f <+66>:    ret
End of assembler dump.
(gdb) b *0x08048469
Breakpoint 1 at 0x8048469
(gdb) run
Starting program: /home/dennis/polygon/./a.out

Breakpoint 1, 0x08048469 in comp ()
(gdb) info registers
eax            0x2d     45
ecx            0xbffff0f8     -1073745672
edx            0x764    1892
ebx            0xb7fc0000     -1208221696
esp            0xbfffeeb8     0xbfffeeb8
ebp            0xbfffeec8     0xbfffeec8
esi            0xbffff0fc     -1073745668
edi            0xbffff010     -1073745904
eip            0x8048469      0x8048469 <comp+28>
eflags         0x286    [ PF SF IF ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x00     0
gs             0x33     51
(gdb) c
Continuing.

Breakpoint 1, 0x08048469 in comp ()
(gdb) info registers
eax            0xff7    4087
ecx            0xbffff104     -1073745660
edx            0xffffff9e     -98
ebx            0xb7fc0000     -1208221696
esp            0xbfffee58     0xbfffee58
ebp            0xbfffee68     0xbfffee68
esi            0xbffff108     -1073745656
edi            0xbffff010     -1073745904
eip            0x8048469      0x8048469 <comp+28>
eflags         0x282    ［ SF IF ］
cs            0x73       115
ss            0x7b       123
ds            0x7b       123
es            0x7b       123
fs            0x00       0
gs            0x33       51
(gdb) c
Continuing.

Breakpoint 1, 0x08048469 in comp ()
(gdb) info registers
eax            0xffffff9e     -98
ecx            0xbffff100     -1073745664
edx            0xc8     200
ebx            0xb7fc0000     -1208221696
esp            0xbfffeeb8     0xbfffeeb8
ebp            0xbfffeec8     0xbfffeec8
esi            0xbffff104     -1073745660
edi            0xbffff010     -1073745904
eip            0x8048469     0x8048469 <comp+28>
eflags         0x286    [ PF SF IF ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x33     51
(gdb) bt
#0  0x08048469 in comp ()
#1  0xb7e42872 in msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=2)
    at msort.c:65
#2  0xb7e4273e in msort_with_tmp (n=2, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#3  msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=5) at msort.c:53
#4  0xb7e4273e in msort_with_tmp (n=5, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#5  msort_with_tmp (p=p@entry=0xbffff07c, b=b@entry=0xbffff0f8, n=n@entry=10) at msort.c:53
#6  0xb7e42cef in msort_with_tmp (n=10, b=0xbffff0f8, p=0xbffff07c) at msort.c:45
#7  __GI_qsort_r (b=b@entry=0xbffff0f8, n=n@entry=10, s=s@entry=4, cmp=cmp@entry=0x804844d < comp>,
    arg=arg@entry=0x0) at msort.c:297
#8  0xb7e42dcf in __GI_qsort (b=0xbffff0f8, n=10, s=4, cmp=0x804844d <comp>) at msort.c:307
#9  0x0804850d in main ()

[1]　又称“回调函数”。确切地说，callback指的是一种调用方法，而非某个函数，故而本书保留英文名词。请参见http://en.wikipedia. org/wiki/Callback_(computer_science)。

[2]　请参见：http://pubs.opengroup. org/onlinepubs/009695399/functions/atexit.html。

[3]　请参见：http://en.wikipedia.org/wiki/Signal.h。

[4]　https://msdn.microsoft.com/en-us/library/ms633494(VS.85).aspx。

[5]　请参阅本章第一个源程序。

第24章 32位系统处理64位数据

32位系统的通用寄存器GPR都只能容纳32位数据，所以这种平台必须把64位数据转换为一对32位数据才能进行运算。[1]

24.1　64位返回值

本节围绕下述程序进行演示。

#include <stdint.h>

uint64_t f ()
{
       return 0x1234567890ABCDEF;
};

24.1.1　x86

32位系统用寄存器组合EDX:EAX来回传64位值。

指令清单24.1　Optimizing MSVC 2010

_f         PROC
           mov      eax, -1867788817       ; 90abcdefH
           mov      edx, 305419896         ; 12345678H
           ret      0
_f         ENDP

24.1.2　ARM

ARM系统用寄存器组合R0-R1来回传64位值。其中，返回值的高32位存储在R1寄存器中，低32位存储于R0寄存器中。

指令清单24.2　Optimizing Keil 6/2013 (ARM mode)

||f|| PROC
        LDR     r0,|L0.12|
        LDR     r1,|L0.16|
        BX      lr
        ENDP

|L0.12|
        DCD     0x90abcdef
|L0.16|
        DCD     0x12345678

24.1.3　MIPS

MIPS系统使用V0-V1($2-$3)寄存器对来回传64位值。其中，返回值的高32位存储在V0($2)寄存器中，低32位存储于$V1($3)寄存器中。

指令清单24.3　Optimizing GCC 4.4.5 (assembly listing)

        li      $3,-1867841536          # 0xffffffff90ab0000
        li      $2,305397760            # 0x12340000
        ori     $3,$3,0xcdef
        j       $31
        ori     $2,$2,0x5678

指令清单24.4　Optimizing GCC 4.4.5 (IDA)

                lui     $v1, 0x90AB
                lui     $v0, 0x1234
                li      $v1, 0x90ABCDEF
                jr      $ra
                li      $v0, 0x12345678

24.2　参数传递及加减运算

本节围绕下列程序进行演示。

#include <stdint.h>

uint64_t f_add (uint64_t a, uint64_t b)
{
        return a+b;
};

void f_add_test ()
{
#ifdef __GNUC__
        printf ("%lld\n", f_add(12345678901234, 23456789012345));
#else
        printf ("%I64d\n", f_add(12345678901234, 23456789012345));
#endif
};

uint64_t f_sub (uint64_t a, uint64_t b)
{
        return a-b;
};

24.2.1　x86

使用MSVC 2012（启用选项/Ox/Ob1）编译上述程序，可得到如下所示的代码。

指令清单24.5　Optimizing MSVC 2012 /Ob1

_a$ = 8         ; size = 8
_b$ = 16        ; size = 8
_f_add   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
_f_add ENDP

_f_add_test PROC
        push   5461            ; 00001555H
        push   1972608889      ; 75939f79H
        push   2874            ; 00000b3aH
        push   1942892530      ; 73ce2ff_subH
        call   _f_add
        push   edx
        push   eax
        push   OFFSET $SG1436  ; ’%I64d’, 0aH, 00H
        call   _printf
        add    esp, 28
        ret    0
_f_add_test ENDP

_f_sub  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
_f_sub ENDP

在f1_test()函数中，每个64位数据都被拆分为了2个32位数据。在内存中，高32位数据在前（高地址位），低32位在后。

加减法运算的处理方法也完全相同。

在进行加法运算时，先对低32位相加。如果产生了进位，则设置CF标识位。然后ADC指令对高32位进行运算。如果此时CF标识位的值为1，则再把高32位的运算结果加上1。

减法运算也是分步进行的。第一次的减法运算可能影响CF标识位。第二次减法运算会根据CF标识位把借位代入计算结果。

在32位系统中，函数在返回64位数据时都使用EDX:EAX寄存器对。当f_add()函数的返回值传递给printf()函数时，就可以清楚地观测到这一现象。

使用GCC 4.8.1（启用选项–O1–fno–inline）编译上述程序，可得到如下所示的代码。

指令清单24.6　GCC 4.8.1 -O1 -fno-inline

_f_add:
         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

_f_add_test:
         sub     esp, 28
         mov     DWORD PTR [esp+8], 1972608889   ; 75939f79H
         mov     DWORD PTR [esp+12], 5461        ; 00001555H
         mov     DWORD PTR [esp], 1942892530     ; 73ce2ff_subH
         mov     DWORD PTR [esp+4], 2874         ; 00000b3aH
         call    _f_add
         mov     DWORD PTR [esp+4], eax
         mov     DWORD PTR [esp+8], edx
         mov     DWORD PTR [esp], OFFSET FLAT:LC0 ; "%lld\12\0"
         call    _printf
         add     esp, 28
         ret

_f_sub:
         mov     eax, DWORD PTR [esp+4]
         mov     edx, DWORD PTR [esp+8]
         sub     eax, DWORD PTR [esp+12]
         sbb     edx, DWORD PTR [esp+16]
         ret

GCC的编译结果和MSVC的编译结果相同。

24.2.2　ARM

指令清单24.7　Optimizing Keil 6/2013 (ARM mode)

f_add PROC
         ADDS     r0,r0,r2
         ADC      r1,r1,r3
         BX       lr
         ENDP

f_sub PROC
         SUBS     r0,r0,r2
         SBC      r1,r1,r3
         BX       lr
         ENDP

f_add_test PROC
         PUSH     {r4,lr}
         LDR      r2,|L0.68| ; 0x75939f79
         LDR      r3,|L0.72| ; 0x00001555
         LDR      r0,|L0.76| ; 0x73ce2ff2
         LDR      r1,|L0.80| ; 0x00000b3a
         BL       f_add
         POP      {r4,lr}
         MOV      r2,r0
         MOV      r3,r1
         ADR      r0,|L0.84| ; "%I64d\n"
         B         __2printf
         ENDP

|L0.68|
         DCD      0x75939f79
|L0.72|
         DCD      0x00001555
|L0.76|
         DCD      0x73ce2ff2
|L0.80|
         DCD      0x00000b3a
|L0.84|
         DCB      "%I64d\n",0

首个64位值被拆分存储到R0和R1寄存器里，第二个64位值则存储于R2和R3寄存器对。ARM平台的指令集里有可进行进位加法运算的ADC指令和借位减法运算的SBC指令。

需要注意的是：在对64位数据的低32位数据进行加减运算时，需要使用ADDS和SUBS指令。指令名词中的-S后缀代表该指令会设置进（借）位标识（Carry Flag）。它们设置过的进借位标识将被高32位运算的ADC/SBC指令读取并纳入运算结果之中。

没有-S后缀的ADD和SUB指令则不会设置借/进位标识位。

24.2.3　MIPS

指令清单24.8　Optimizing GCC 4.4.5 (IDA)

f_add:
; $a0 - high part of a
; $a1 - low part of a
; $a2 - high part of b
; $a3 - low part of b
                  addu     $v1, $a3, $a1 ; sum up low parts
                  addu     $a0, $a2, $a0 ; sum up high parts
; will carry generated while summing up low parts?
; if yes, set $v0 to 1
                  sltu     $v0, $v1, $a3
                  jr        $ra
; add 1 to high part of result if carry should be generated:
                  addu     $v0, $a0 ; branch delay slot
; $v0 - high part of result
; $v1 - low part of result

f_sub:
; $a0 - high part of a
; $a1 - low part of a
; $a2 - high part of b
; $a3 - low part of b
                  subu     $v1, $a1, $a3 ; subtract low parts
                  subu     $v0, $a0, $a2 ; subtract high parts
; will carry generated while subtracting low parts?
; if yes, set $a0 to 1
                  sltu     $a1, $v1
                  jr       $ra
; subtract 1 from high part of result if carry should be generated:
                  subu     $v0, $a1 ; branch delay slot
; $v0 - high part of result
; $v1 - low part of result

f_add_test:

var_10            = -0x10
var_4             = -4

                  lui      $gp, (__gnu_local_gp >> 16)
                  addiu    $sp, -0x20
                  la       $gp, (__gnu_local_gp & 0xFFFF)
                  sw       $ra, 0x20+var_4($sp)
                  sw       $gp, 0x20+var_10($sp)
                  lui      $a1, 0x73CE
                  lui      $a3, 0x7593
                  li       $a0, 0xB3A
                  li       $a3, 0x75939F79
                  li       $a2, 0x1555
                  jal      f_add
                  li       $a1, 0x73CE2FF2
                  lw       $gp, 0x20+var_10($sp)
                  lui      $a0, ($LC0 >> 16)  # "%lld\n"
                  lw       $t9, (printf & 0xFFFF)($gp)
                  lw       $ra, 0x20+var_4($sp)
                  la       $a0, ($LC0 & 0xFFFF)  # "%lld\n"
                  move     $a3, $v1
                  move     $a2, $v0
                  jr       $t9
                  addiu    $sp, 0x20

$LC0:             .ascii   "%lld\n"<0>

MIPS 处理器没有标识位寄存器。这种平台的加减运算完全不会存储借/进位信息。它的指令集里也没有x86指令集里的那种ADC或SBB指令。当需要存储借/进位信息时，编译器通常使用SLTU指令把有关信息（也就是0或1）存储在既定寄存器里，继而在下一步的高数权位运算中纳入借进位信息。

24.3　乘法和除法运算

#include <stdint.h>

uint64_t f_mul (uint64_t a, uint64_t b)
{
        return a*b;
};

uint64_t f_div (uint64_t a, uint64_t b)
{
        return a/b;
};

uint64_t f_rem (uint64_t a, uint64_t b)
{
        return a % b;
};

24.3.1　x86

使用MSVC 2012（启用选项/Ox/Ob1）编译上述程序，可得到如下所示的代码。

指令清单24.9　Optimizing MSVC 2012 /Ob1

_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f_mul  PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _b$[ebp+4]
        push    eax
        mov     ecx, DWORD PTR _b$[ebp]
        push    ecx
        mov     edx, DWORD PTR _a$[ebp+4]
        push    edx
        mov     eax, DWORD PTR _a$[ebp]
        push    eax
        call    __allmul ; long long multiplication
        pop     ebp
        ret     0
_f_mul  ENDP

_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f_div  PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _b$[ebp+4]
        push    eax
        mov     ecx, DWORD PTR _b$[ebp]
        push    ecx
        mov     edx, DWORD PTR _a$[ebp+4]
        push    edx
        mov     eax, DWORD PTR _a$[ebp]
        push    eax
        call    __aulldiv ; unsigned long long division
        pop     ebp
        ret     0
_f_div ENDP

_a$ = 8 ; size = 8
_b$ = 16 ; size = 8
_f_rem  PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _b$[ebp+4]
        push    eax
        mov     ecx, DWORD PTR _b$[ebp]
        push    ecx
        mov     edx, DWORD PTR _a$[ebp+4]
        push    edx
        mov     eax, DWORD PTR _a$[ebp]
        push    eax
        call    __aullrem ; unsigned long long remainder
        pop     ebp
        ret     0
_f_rem   ENDP

乘除运算复杂很多。所以编译器通常借助标准库函数来处理乘除运算。

如需了解库函数的各种详细信息，请参见附录E。

使用GCC 4.8.1（启用选项-O3-fno-inline）编译上述程序，可得到如下所示的代码。

指令清单24.10　Optimizing GCC 4.8.1 -fno-inline

_f_mul:
        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

_f_div:
        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

_f_rem:
        sub     esp, 28
        mov     eax, DWORD PTR [esp+40]
        mov     edx, DWORD PTR [esp+44]
        mov     DWORD PTR [esp+8], eax
        mov     eax, DWORD PTR [esp+32]
        mov     DWORD PTR [esp+12], edx
        mov     edx, DWORD PTR [esp+36]
        mov     DWORD PTR [esp], eax
        mov     DWORD PTR [esp+4], edx
        call    ___umoddi3 ; unsigned modulo
        add     esp, 28
        ret

GCC把乘法运算进行内部展开处理，大概是它认为这样的效率更高一些。另外它所调用的库函数的名称也和MSVC不同（参见附录D）。除此之外，这段汇编指令和MSVC的编译结果之间几乎没有区别。

24.3.2　ARM

在编译Thumb模式的程序时，Keil调用库函数进行仿真运算。

指令清单24.11　Optimizing Keil 6/2013 (Thumb mode)

||f_mul|| PROC
         PUSH     {r4,lr}
         BL       __aeabi_lmul
         POP      {r4,pc}
         ENDP

||f_div|| PROC
         PUSH     {r4,lr}
         BL       __aeabi_uldivmod
         POP      {r4,pc}
         ENDP

||f_rem|| PROC
         PUSH     {r4,lr}
         BL       __aeabi_uldivmod
         MOVS     r0,r2
         MOVS     r1,r3
         POP      {r4,pc}
         ENDP

相比之下，在编译ARM模式的程序时，Keil能够直接进行64位乘法运算。

指令清单24.12　Optimizing Keil 6/2013 (ARM mode)

||f_mul|| PROC
         PUSH     {r4,lr}
         UMULL    r12,r4,r0,r2
         MLA      r1,r2,r1,r4
         MLA      r1,r0,r3,r1
         MOV      r0,r12
         POP      {r4,pc}
         ENDP

||f_div|| PROC
         PUSH     {r4,lr}
         BL       __aeabi_uldivmod
         POP      {r4,pc}
         ENDP

||f_rem|| PROC
         PUSH     {r4,lr}
         BL       __aeabi_uldivmod
         MOV      r0,r2
         MOV      r1,r3
         POP      {r4,pc}
         ENDP

24.3.3　MIPS

在启用优化选项的情况下编译MIPS程序时，GCC能够直接使用汇编指令进行64位乘法运算。但是在进行64位除法运算时，编译器还是会用库函数进行处理。

指令清单24.13　Optimizing GCC 4.4.5 (IDA)

f_mul:
                mult    $a2, $a1
                mflo    $v0
                or      $at, $zero  ; NOP
                or      $at, $zero  ; NOP
                mult    $a0, $a3
                mflo    $a0
                addu    $v0, $a0
                or      $at, $zero  ; NOP
                multu   $a3, $a1
                mfhi    $a2
                mflo    $v1
                jr      $ra
                addu    $v0, $a2

f_div:

var_10          = -0x10
var_4           =-4

                lui     $gp, (__gnu_local_gp >> 16)
                addiu   $sp, -0x20
                la      $gp, (__gnu_local_gp & 0xFFFF)
                sw      $ra, 0x20+var_4($sp)
                sw      $gp, 0x20+var_10($sp)
                lw      $t9, (__udivdi3 & 0xFFFF)($gp)
                or      $at, $zero
                jalr    $t9
                or      $at, $zero
                lw      $ra, 0x20+var_4($sp)
                or      $at, $zero
                jr      $ra
                addiu   $sp, 0x20

f_rem:

var_10          = -0x10
var_4           =-4

                lui     $gp, (__gnu_local_gp >> 16)
                addiu   $sp, -0x20
                la      $gp, (__gnu_local_gp & 0xFFFF)
                sw      $ra, 0x20+var_4($sp)
                sw      $gp, 0x20+var_10($sp)
                lw      $t9, (__umoddi3 & 0xFFFF)($gp)
                or      $at, $zero
                jalr    $t9
                or      $at, $zero
                lw      $ra, 0x20+var_4($sp)
                or      $at, $zero
                jr      $ra
                addiu   $sp, 0x20

上述程序中夹杂着大量的NOP指令。或许这是因为乘法运算的时间较长，且运算之后需要较长等待时间的缘故；不过这一观点尚无法证实。

24.4　右移

#include <stdint.h>

uint64_t f (uint64_t a)
{
         return a>>7;
};

24.4.1　x86

指令清单24.14　Optimizing MSVC 2012 /Ob1

_a$ = 8         ; size = 8
_f      PROC
        mov     eax, DWORD PTR _a$[esp-4]
        mov     edx, DWORD PTR _a$[esp]
        shrd    eax, edx, 7
        shr     edx, 7
        ret     0
_f      ENDP

指令清单24.15　Optimizing GCC 4.8.1 -fno-inline

_f:
        mov     edx, DWORD PTR [esp+8]
        mov     eax, DWORD PTR [esp+4]
        shrd    eax, edx, 7
        shr     edx, 7
        ret

位移运算仍然分为2步：第一步处理低32位数据，第二步处理高32位数据。需要注意的是处理低32位数据的指令——SHRD指令。这个指令不仅可以把EAX里的低32位数据右移7位运算，而且还能从EDX寄存器里读取高32位中的低7位、用它填补到低32位数据的高位。如此一来，就可直接使用最普通的SHR指令对高32位进行位移操作，并用零补充位移产生的空位了。

24.4.2　ARM

ARM的指令集比x86的小，没有SHRD之类的指令。因此，Keil把它拆分为位移和或运算，以进行等效处理。

指令清单24.16　Optimizing Keil 6/2013 (ARM mode)

||f|| PROC
         LSR      r0,r0,#7
         ORR      r0,r0,r1,LSL #25
         LSR      r1,r1,#7
         BX       lr
         ENDP

指令清单24.17　Optimizing Keil 6/2013 (Thumb mode)

||f|| PROC
         LSLS     r2,r1,#25
         LSRS     r0,r0,#7
         ORRS     r0,r0,r2
         LSRS     r1,r1,#7
         BX       lr
         ENDP

24.4.3　MIPS

GCC for MIPS采用了Keil for Thumb的编译手段。

指令清单24.18　Optimizing GCC 4.4.5 (IDA)

f:
                sll    $v0, $a0, 25
                srl    $v1, $a1, 7
                or     $v1, $v0, $v1
                jr     $ra
                srl    $v0, $a0, 7

24.5　32位数据转换为64位数据

#include <stdint.h>

int64_t f (int32_t a)
{
         return a;
};

24.5.1　x86

指令清单24.19　Optimizing MSVC 2012

_a$ = 8
_f        PROC
          mov     eax, DWORD PTR _a$[esp-4]
          cdq
          ret     0
_f        ENDP

在这种情况下，我们需要把32位有符号数参量扩展为64位有符号数。无符号数的转换过程较为直接：高位设置为0就万事大吉。但是对于有符号数来说，最高的一位是符号位，不能一概而论地把高位都填零。这里使用了CDQ指令进行有符号数的格式转换。它把EAX的值扩展为64位，将结果存储到EDX:EAX寄存器对中。换句话说，CDQ指令获取EAX寄存器中的符号位（最高位），并按照符号位的不同把高32位都设为0或1。CDQ指令的作用和MOVSX指令相似。

GCC生成的汇编指令使用了inlines乘法运算。其他部分和MSVC的编译结果大体相同。

24.5.2　ARM

指令清单24.20　Optimizing Keil 6/2013 (ARM mode)

||f|| PROC
          ASR     r1,r0,#31
          BX      lr
          ENDP

Keil for ARM的编译方法与MSVC不同：它把参数向右位移31位，只用了算术右移指令。MSB是符号位，而算术位移指令会用符号位填补位移产生的空缺位。所以在执行“ASRr1,r0,#31”指令的时候，如果输入值是负数，那么R1就是0xFFFFFFFF；否则R1的值会是零。在生成64位值的时候，高32位应当存储在R1寄存器里。

换句话说，这个指令用最高数权位填充R1寄存器以形成64位值的高32位。

24.5.3　MIPS

GCC for MIPS的编译方法和Keil编译ARM程序的方法相同。

指令清单24.21　Optimizing GCC 4.4.5 (IDA)

f:
                sra    $v0, $a0, 31
                jr     $ra
                move   $v1, $a0

[1]　16位系统处理32位数据时同样采取了这种拆分数据的处理方法，详情请参见本书53.4节。

第25章 SIMD

SIMD意为“单指令流多数据流”，其全称是“Single Instruction, Multiple Data”。顾名思义，这类指令可以一次处理多个数据。在x86的CPU里，SIMD子系统和FPU都由专用模块实现。

不久之后，x86 CPU通过MMX指令率先整合了SIMD的运算功能。支持这种技术的CPU里都有8个64位寄存器，即MM0～MM7。每个MMX寄存器都是8字节寄存器，可以容纳2个32位数据，或者4个16位数据。使用SIMD指令进行操作数的计算时，它可以把8个8位数据分为4组数据同时运算。

平面图像可视为一种由二维数组构成的数据结构。在美工人员调整图像亮度的时候，图像编辑程序得对每个像素的亮度系数进行加减法的运算。简单地说，图像的每个像素都有灰度系数，而且灰度系统是8位数据（1个字节）。因而，每执行一个SIMD指令就能够同时调整8个像素的灰度（即亮度）。为了满足这种需要，处理器厂商后来专门推出了基于SIMD技术的饱和度调整指令。这种饱和度调整指令甚至有越界保护功能，能够避免亮度调整时可能会产生的因子上溢（overflow）和下溢（underflow）等问题。

在刚刚推出MMX技术的时候，SIMD借用了FPU的寄存器（别名）。这有利于FPU或MMX的混合运算。有人说Intel这样处理是为了节约晶体管，但是其实际原因更为简单：老式操作系统并不会利用新增的CPU寄存器，还是会把数据保存到FPU上。所以说，只有同时满足“支持MMX技术的CPU”＋“老式操作系统”＋“利用MMX指令集的程序”这3个条件，才能充分体现SIMD的优势。

SSE技术是SIMD的扩展技术，它把SIMD寄存器扩展为128位寄存器。支持SSE技术的CPU已经有了单独的SIMD通用寄存器，不再复用FPU的寄存器。

AVX是另外一种SIMD技术，它的通用寄存器都是256位寄存器。

现在转入正题，看看使用SIMD的具体应用。

SIMD技术当然可用于内存复制（memcpy）和内存比较（memcmp）等用途。

此外它还可用于DES的运算。DES（Data Encryption Standard）是分组对称密码算法。它采用了64位的分组和56位的密钥，将64位的输入经过一系列变换得到64位的输出。如果在电路的与、或、非门和导线实现DES模块，电路规模将会是非常庞大。基于并行分组密码算法的Bitslice DES[1]应运而生。这种算法可由单指令并行处理技术/SIMD实现。我们已经知道，x86平台的unsigned int型数据占用32位空间。因此，在进行unsigned int型数据的64位数据和56位密钥的演算时，可以以把64位中间运算结果和运算密钥都分为2个32位数据，再进行分步的并行处理。

在Oracle存储密码和hash的算法就是DES。为了进行有关演示，您还可以研究一下暴力破解Oracle RDBMS密码和hash的小程序。这个程序利用了bitslice DES算法，针对SSE2和AVX指令集进行了优化。对其稍加修改，则可用于128位/256位消息模块的并行加密运算。如需查看这个程序的源代码，请参阅http://conus.info/utils/ops_SIMD/

。

25.1　矢量化

矢量化[2]泛指将多个标量数组计算为（转换成）一个矢量数组的技术。进行这种计算时，循环体从输入数组中取值，进行某种运算后生成最终数组。这种算法只对数组中的单个元素进行一次运算。“（并行）矢量化技术”是矢量化处理的并行计算技术。

矢量化并不是最近才出现的技术。本书的作者在1998年的Cray Y-MP超级计算机系列产品中就见到过“Cray Y-MP EL”的羽量级lite版本。有兴趣的读者可以去他们在线超级计算机博物馆去看看：http://www.cray-cyber.org/

。

举例来说，下述循环就采用了矢量化技术：

for (i = 0; i < 1024; i++)
{
     C[i] = A[i]*B[i];
}

上述代码从数组A和数组B取值，把这两个数组元素进行乘法运算，并把求得的积存储在数组C里。

如果输入数组中的每个元素都是32位int型数据，那么就可以把A的四个元素放在128位的MMX寄存器中，再把数组B的四个元素放在另一个MMX寄存器里，然后通过PMULLD（Multiply Packed Signed Dword Integers and Store Low Result）和PMULHW（Multiply Packed Signed Integers and Store High Result）指令进行并行乘法运算。这可以一次获得4个64位的积。

这样一来，循环体的执行次数不再是1024，而是1024/4。换句话说，程序的性能将提高4倍。

某些编译器具有简单的矢量化自动优化功能。Intel C++编译器就有这样的智能优化功能，详情请参见http://www.intel.com/intelpress/sum_vmmx.htm

。

25.1.1　用于加法计算

本节将使用下述程序作为编译对象。

int f (int sz, int *ar1, int *ar2, int *ar3)
{
         for (int i=0; i<sz; i++)
                  ar3[i]=ar1[i]+ar2[i];
         return 0;
};

Intel C++

我们通过下述指令用win32版本的Intel C++编译器编译上述代码：

icl intel.cpp /QaxSSE2 /Faintel.asm /Ox

利用IDA打开上面生成的可执行文件，可看到：

; int __cdecl f(int, int *, int *, int *)
                   public ?f@@YAHHPAH00@Z
?f@@YAHHPAH00@Z proc near

var_10 = dword ptr -10h
sz     = dword ptr  4
ar1    = dword ptr  8
ar2    = dword ptr  0Ch
ar3    = dword ptr  10h

       push    edi
       push    esi
       push    ebx
       push    esi
       mov     edx, [esp+10h+sz]
       test    edx, edx
       jle     loc_15B
       mov     eax, [esp+10h+ar3]
       cmp     edx, 6
       jle     loc_143
       cmp     eax, [esp+10h+ar2]
       jbe     short loc_36
       mov     esi, [esp+10h+ar2]
       sub     esi, eax
       lea     ecx, ds:0[edx*4]
       neg     esi
       cmp     ecx, esi
       jbe     short loc_55

loc_36: ; CODE XREF: f(int,int *,int *,int *)+21
       cmp     eax, [esp+10h+ar2]
       jnb     loc_143
       mov     esi, [esp+10h+ar2]
       sub     esi, eax
       lea     ecx, ds:0[edx*4]
       cmp     esi, ecx
       jb      loc_143

loc_55: ; CODE XREF: f(int,int *,int *,int *)+34
       cmp     eax, [esp+10h+ar1]
       jbe     short loc_67
       mov     esi, [esp+10h+ar1]
       sub     esi, eax
       neg     esi
       cmp     ecx, esi
       jbe     short loc_7F

loc_67: ; CODE XREF: f(int,int *,int *,int *)+59
       cmp     eax, [esp+10h+ar1]
       jnb     loc_143
       mov     esi, [esp+10h+ar1]
       sub     esi, eax
       cmp     esi, ecx
       jb      loc_143

loc_7F: ; CODE XREF: f(int,int *,int *,int *)+65
       mov     edi, eax        ;edi = ar3
       and     edi, 0Fh        ; is ar3 16-byte aligned?
       jz      short loc_9A    ; yes
       test    edi, 3
       jnz     loc_162
       neg     edi
       add     edi, 10h
       shr     edi, 2

loc_9A: ; CODE XREF: f(int,int *,int *,int *)+84
       lea     ecx, [edi+4]
       cmp     edx, ecx
       jl      loc_162
       mov     ecx, edx
       sub     ecx, edi
       and     ecx, 3
       neg     ecx
       add     ecx, edx
       test    edi, edi
       jbe     short loc_D6
       mov     ebx, [esp+10h+ar2]
       mov     [esp+10h+var_10], ecx
       mov     ecx, [esp+10h+ar1]
       xor     esi, esi

loc_C1: ; CODE XREF: f(int,int *,int *,int *)+CD
       mov     edx, [ecx+esi*4]
       add     edx, [ebx+esi*4]
       mov     [eax+esi*4], edx
       inc     esi
       cmp     esi, edi
       jb      short loc_C1
       mov     ecx, [esp+10h+var_10]
       mov     edx, [esp+10h+sz]

loc_D6: ; CODE XREF: f(int,int *,int *,int *)+B2
       mov     esi, [esp+10h+ar2]
       lea     esi, [esi+edi*4] ; is ar2+i*4 16-byte aligned?
       test    esi, 0Fh
       jz      short loc_109   ; yes!
       mov     ebx, [esp+10h+ar1]
       mov     esi, [esp+10h+ar2]

loc_ED: ; CODE XREF: f(int,int *,int *,int *)+105
       movdqu  xmm1, xmmword ptr [ebx+edi*4]
       movdqu  xmm0, xmmword ptr [esi+edi*4] ; ar2+i*4 is not 16-byte aligned, so load it to XMM0
       paddd   xmm1, xmm0
       movdqa  xmmword ptr [eax+edi*4], xmm1 add edi, 4
       cmp     edi, ecx
       jb      short loc_ED
       jmp     short loc_127

loc_109: ; CODE XREF: f(int,int *,int *,int *)+E3
       mov     ebx, [esp+10h+ar1]
       mov     esi, [esp+10h+ar2]

loc_111: ; CODE XREF: f(int,int *,int *,int *)+125
       movdqu  xmm0, xmmword ptr [ebx+edi*4]
       paddd   xmm0, xmmword ptr [esi+edi*4]
       movdqa  xmmword ptr [eax+edi*4], xmm0
       add     edi, 4
       cmp     edi, ecx
       jb      short loc_111

loc_127: ; CODE XREF: f(int,int *,int *,int *)+107
         ; f(int,int *,int *,int *)+164
       cmp     ecx, edx
       jnb     short loc_15B
       mov     esi, [esp+10h+ar1]
       mov     edi, [esp+10h+ar2]

loc_133: ; CODE XREF: f(int,int *,int *,int *)+13F
       mov     ebx, [esi+ecx*4]
       add     ebx, [edi+ecx*4]
       mov     [eax+ecx*4], ebx
       inc     ecx
       cmp     ecx, edx
       jb      short loc_133
       jmp     short loc_15B

loc_143: ; CODE XREF: f(int,int *,int *,int *)+17
         ; f(int,int *,int *,int *)+3A ...
       mov     esi, [esp+10h+ar1]
       mov     edi, [esp+10h+ar2]
       xor     ecx, ecx

loc_14D: ; CODE XREF: f(int,int *,int *,int *)+159
       mov     ebx, [esi+ecx*4]
       add     ebx, [edi+ecx*4]
       mov     [eax+ecx*4], ebx
       inc     ecx
       cmp     ecx, edx
       jb      short loc_14D

loc_15B: ; CODE XREF: f(int,int *,int *,int *)+A
         ; f(int,int *,int *,int *)+129 ...
       xor     eax, eax
       pop     ecx
       pop     ebx
       pop     esi
       pop     edi
       retn

loc_162: ; CODE XREF: f(int,int *,int *,int *)+8C
         ; f(int,int *,int *,int *)+9F
       xor     ecx, ecx
       jmp     short loc_127
?f@@YAHHPAH00@Z endp

其中，与SSE2有关的指令有：

MOVDQU（Move Unaligned Double Quadword）是从内存加载16字节数据，并复制到XMM寄存器的指令。

PADDD（Add Packed Integers）是对4对32位数进行加法运算，并把运算结果存储到第一个操作符的指令。此外，该指令不会设置任何标志位，在溢出时只保留运算结果的低32位，也不会报错。如果PADDD的某个操作数是内存中的某个值，那么这个值的地址必须与16字节边界对齐，否则将报错。[3]

MOVDQA（Move Aligned Double Quadword）的功能MOVDQU相同，只是要求存储操作数的内存地址必须向16字节边界对齐，否则报错。除了这点区别之外，两个指令完全相同。此外，MOVDQA的运行速度要比MOVDQU快。

所以上述SSE2指令的运行条件有2个：需要进行加法处理的操作数至少有4对；指针ar3的地址已经向16字节边界对齐。

而且，如果ar2的地址也向16字节边界对齐，则会执行下述指令：

movdqu  xmm0, xmmword ptr [ebx+edi*4] ; ar1+i*4
paddd   xmm0, xmmword ptr [esi+edi*4] ; ar2+i*4
movdqa  xmmword ptr [eax+edi*4], xmm0 ; ar3+i*4

另外，MOVDQU指令会把ar2的值加载到XMM0寄存器。虽然这条指令不要求指针地址对齐，但是性能略微慢些：

movdqu  xmm1, xmmword ptr [ebx+edi*4] ; ar1+i*4
movdqu  xmm0, xmmword ptr [esi+edi*4] ; ar2+i*4 is not 16-byte aligned, so load it to xmm0
paddd   xmm1, xmm0
movdqa  xmmword ptr [eax+edi*4], xmm1 ; ar3+i*4

其他代码没有涉及与SSE2有关的指令。

GCC

在指定-O3选项并且启用SSE2支持（-msse2选项）的情况下，GCC编译器也能够进行简单的矢量化智能处理。[4]

我们使用GCC 4.4.1编译上述程序，可得到：

; f(int, int *, int *, int *)
               public _Z1fiPiS_S_
_Z1fiPiS_S_ proc near

var_18      = dword ptr -18h
var_14      = dword ptr -14h
var_10      = dword ptr -10h
arg_0       = dword ptr  8
arg_4       = dword ptr  0Ch
arg_8       = dword ptr  10h
arg_C       = dword ptr  14h

            push    ebp
            mov     ebp, esp
            push    edi
            push    esi
            push    ebx
            sub     esp, 0Ch
            mov     ecx, [ebp+arg_0]
            mov     esi, [ebp+arg_4]
            mov     edi, [ebp+arg_8]
            mov     ebx, [ebp+arg_C]
            test    ecx, ecx
            jle     short loc_80484D8
            cmp     ecx, 6
            lea     eax, [ebx+10h]
            ja      short loc_80484E8

loc_80484C1: ; CODE XREF: f(int,int *,int *,int *)+4B
             ; f(int,int *,int *,int *)+61 ...
            xor     eax, eax
            nop
            lea     esi, [esi+0]

loc_80484C8: ; CODE XREF: f(int,int *,int *,int *)+36
            mov     edx, [edi+eax*4]
            add     edx, [esi+eax*4]
            mov     [ebx+eax*4], edx
            add     eax, 1
            cmp     eax, ecx
            jnz     short loc_80484C8

loc_80484D8: ; CODE XREF: f(int,int *,int *,int *)+17
             ; f(int,int *,int *,int *)+A5
            add     esp, 0Ch
            xor     eax, eax
            pop     ebx
            pop     esi
            pop     edi
            pop     ebp
            retn

            align8

loc_80484E8: ; CODE XREF: f(int,int *,int *,int *)+1F
            test    bl, 0Fh
            jnz     short loc_80484C1
            lea     edx, [esi+10h]
            cmp     ebx, edx
            jbe     loc_8048578

loc_80484F8: ; CODE XREF: f(int,int *,int *,int *)+E0
            lea     edx, [edi+10h]
            cmp     ebx, edx
            ja      short loc_8048503
            cmp     edi, eax
            jbe     short loc_80484C1

loc_8048503: ; CODE XREF: f(int,int *,int *,int *)+5D
            mov     eax, ecx
            shr     eax, 2
            mov     [ebp+var_14], eax
            shl     eax, 2
            test    eax, eax
            mov     [ebp+var_10], eax
            jz      short loc_8048547
            mov     [ebp+var_18], ecx
            mov     ecx, [ebp+var_14]
            xor     eax, eax
            xor     edx, edx
            nop

loc_8048520: ; CODE XREF: f(int,int *,int *,int *)+9B
            movdqu  xmm1, xmmword ptr [edi+eax]
            movdqu  xmm0, xmmword ptr [esi+eax]
            add     edx, 1
            paddd   xmm0, xmm1
            movdqa  xmmword ptr [ebx+eax], xmm0
            add     eax, 10h
            cmp     edx, ecx
            jb      short loc_8048520
            mov     ecx, [ebp+var_18]
            mov     eax, [ebp+var_10]
            cmp     ecx, eax
            jz      short loc_80484D8

loc_8048547: ; CODE XREF: f(int,int *,int *,int *)+73
            lea     edx, ds:0[eax*4]
            add     esi, edx
            add     edi, edx
            add     ebx, edx
            lea     esi, [esi+0]

loc_8048558: ; CODE XREF: f(int,int *,int *,int *)+CC
            mov     edx, [edi]
            add     eax, 1
            add     edi, 4
            add     edx, [esi]
            add     esi, 4
            mov     [ebx], edx
            add     ebx, 4
            cmp     ecx, eax
            jg      short loc_8048558
            add     esp, 0Ch
            xor     eax, eax
            pop     ebx
            pop     esi
            pop     edi
            pop     ebp
            retn

loc_8048578: ; CODE XREF: f(int,int *,int *,int *)+52
            cmp     eax, esi
            jnb     loc_80484C1
            jmp     loc_80484F8
_Z1fiPiS_S_ endp

GCC生成的代码和Intel C++十分相似，只是严谨性略差。

25.1.2　用于内存复制

我们回顾一下本书14.2节的memcpy()函数：

#include <stdio.h>

void my_memcpy (unsigned char* dst, unsigned char* src, size_t cnt)
{
     size_t i;
     for (i=0; i<cnt; i++)
          dst[i]=src[i];
};

用GCC 4.9.1进行优化编译，可得到如下所示的代码。

指令清单25.1　Optimizing GCC 4.9.1 x64

my_memcpy:
; RDI = destination address
; RSI = source address
; RDX = size of block
        test    rdx, rdx
        je      .L41
        lea     rax, [rdi+16]
        cmp     rsi, rax
        lea     rax, [rsi+16]
        setae   cl
        cmp     rdi, rax
        setae   al
        or      cl, al
        je      .L13
        cmp     rdx, 22
        jbe     .L13
        mov     rcx, rsi
        push    rbp
        push    rbx
        neg     rcx
        and     ecx, 15
        cmp     rcx, rdx
        cmova   rcx, rdx
        xor     eax, eax
        test    rcx, rcx
        je      .L4
        movzx   eax, BYTE PTR [rsi]
        cmp     rcx, 1
        mov     BYTE PTR [rdi], al
        je      .L15
        movzx   eax, BYTE PTR [rsi+1]
        cmp     rcx, 2
        mov     BYTE PTR [rdi+1], al
        je      .L16
        movzx   eax, BYTE PTR [rsi+2]
        cmp     rcx, 3
        mov     BYTE PTR [rdi+2], al
        je      .L17
        movzx   eax, BYTE PTR [rsi+3]
        cmp     rcx, 4
        mov     BYTE PTR [rdi+3], al
        je      .L18
        movzx   eax, BYTE PTR [rsi+4]
        cmp     rcx, 5
        mov     BYTE PTR [rdi+4], al
        je      .L19
        movzx   eax, BYTE PTR [rsi+5]
        cmp     rcx, 6
        mov     BYTE PTR [rdi+5], al
        je      .L20
        movzx   eax, BYTE PTR [rsi+6]
        cmp     rcx, 7
        mov     BYTE PTR [rdi+6], al
        je      .L21
        movzx   eax, BYTE PTR [rsi7+]
        cmp     rcx, 8
        mov     BYTE PTR [rdi+7], al
        je      .L22
        movzx   eax, BYTE PTR [rsi+8]
        cmp     rcx, 9
        mov     BYTE PTR [rdi+8], al
        je      .L23
        movzx   eax, BYTE PTR [rsi+9]
        cmp     rcx, 10
        mov     BYTE PTR [rdi+9], al
        je      .L24
        movzx   eax, BYTE PTR [rsi+10]
        cmp     rcx, 11
        mov     BYTE PTR [rdi+10], al
        je      .L25
        movzx   eax, BYTE PTR [rsi+11]
        cmp     rcx, 12
        mov     BYTE PTR [rdi+11], al
        je      .L26
        movzx   eax, BYTE PTR [rsi+12]
        cmp     rcx, 13
        mov     BYTE PTR [rdi+12], al
        je      .L27
        movzx   eax, BYTE PTR [rsi+13]
        cmp     rcx, 15
        mov     BYTE PTR [rdi+13], al
        jne     .L28
        movzx   eax, BYTE PTR [rsi+14]
        mov     BYTE PTR [rdi+14], al
        mov     eax, 15
.L4:
        mov     r10, rdx
        lea     r9, [rdx-1]
        sub     r10, rcx
        lea     r8, [r10-16]
        sub     r9, rcx
        shr     r8, 4
        add     r8, 1
        mov     r11, r8
        sal     r11, 4
        cmp     r9,14
        jbe     .L6
        lea     rbp, [rsi+rcx]
        xor     r9d, r9d
        add     rcx, rdi
        xor     ebx, ebx
.L7:
        movdqa  xmm0, XMMWORD PTR [rbp+0+r9]
        add     rbx, 1
        movups  XMMWORD PTR [rcx+r9], xmm0
        add     r9, 16
        cmp     rbx, r8
        jb      .L7
        add     rax, r11
        cmp     r10, r11
        je      .L1
.L6:
        movzx   ecx, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        lea     rcx, [rax+1]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+1+rax]
        mov     BYTE PTR [rdi+1+rax], cl
        lea     rcx, [rax+2]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+2+rax]
        mov     BYTE PTR [rdi+2+rax], cl
        lea     rcx, [rax+3]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+3+rax]
        mov     BYTE PTR [rdi+3+rax], cl
        lea     rcx, [rax+4]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+4+rax]
        mov     BYTE PTR [rdi+4+rax], cl
        lea     rcx, [rax+5]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+5+rax]
        mov     BYTE PTR [rdi+5+rax], cl
        lea     rcx, [rax+6]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+6+rax]
        mov     BYTE PTR [rdi+6+rax], cl
        lea     rcx, [rax+7]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+7+rax]
        mov     BYTE PTR [rdi+7+rax], cl
        lea     rcx, [rax+8]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+8+rax]
        mov     BYTE PTR [rdi+8+rax], cl
        lea     rcx, [rax+9]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+9+rax]
        mov     BYTE PTR [rdi+9+rax], cl
        lea     rcx, [rax+10]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+10+rax]
        mov     BYTE PTR [rdi+10+rax], cl
        lea     rcx, [rax+11]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+11+rax]
        mov     BYTE PTR [rdi+11+rax], cl
        lea     rcx, [rax+12]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+12+rax]
        mov     BYTE PTR [rdi+12+rax], cl
        lea     rcx, [rax+13]
        cmp     rdx, rcx
        jbe     .L1
        movzx   ecx, BYTE PTR [rsi+13+rax]
        mov     BYTE PTR [rdi+13+rax], cl
        lea     rcx, [rax+14]
        cmp     rdx, rcx
        jbe     .L1
        movzx   edx, BYTE PTR [rsi+14+rax]
        mov     BYTE PTR [rdi+14+rax], dl
.L1:
        pop     rbx
        pop     rbp
.L41:
        rep ret
.L13:
        xor     eax, eax
.L3:
        movzx   ecx, BYTE PTR [rsi+rax]
        mov     BYTE PTR [rdi+rax], cl
        add     rax, 1
        cmp     rax, rdx
        jne     .L3
        rep ret
.L28:
        mov     eax, 14
        jmp     .L4
.L15:
        mov     eax, 1
        jmp     .L4
.L16:
        mov     eax, 2
        jmp     .L4
.L17:
        mov     eax, 3
        jmp     .L4
.L18:
        mov     eax, 4
        jmp     .L4
.L19:
        mov     eax, 5
        jmp     .L4
.L20:
        mov     eax, 6
        jmp     .L4
.L21:
        mov     eax, 7
        jmp     .L4
.L22:
        mov     eax, 8
        jmp     .L4
.L23:
        mov     eax, 9
        jmp     .L4
.L24:
        mov     eax, 10
        jmp     .L4
.L25:
        mov     eax, 11
        jmp     .L4
.L26:
        mov     eax, 12
        jmp     .L4
.L27:
        mov     eax, 13
        jmp     .L4

25.2　SIMD实现strlen()

根据msdn（http://msdn.microsoft.com/en-us/library/y0dh78ez(VS.80).aspx

），在C/C++源程序中插入特定的宏即可调用SIMD指令。在MSVC编译器的库文件之中，intrin.h文件就使用了这种宏。

我们可以把字符串进行分组处理，使用SIMD指令实现strlen()函数。这种算法比常规实现算法快2～2.5倍。它每次把16个字符加载到1个XMM寄存器里，然后检测字符串的结束标志—数值为零的结束符。[5]

size_t strlen_sse2(const char *str)
{
    register size_t len = 0;
    const char *s=str;
    bool str_is_aligned=(((unsigned int)str)&0xFFFFFFF0) == (unsigned int)str;

    if (str_is_aligned==false)
         return strlen (str);

    __m128i xmm0 = _mm_setzero_si128();
    __m128i xmm1;
    int mask = 0;

    for (;;)
    {
         xmm1 = _mm_load_si128((__m128i *)s);
         xmm1 = _mm_cmpeq_epi8(xmm1, xmm0);
         if ((mask = _mm_movemask_epi8(xmm1)) != 0)
         {
             unsigned long pos;
             _BitScanForward(&pos, mask);
             len += (size_t)pos;

             break;
         }
         s += sizeof(__m128i);
         len += sizeof(__m128i);
    };

    return len;
}

使用MSVC 2010（启用/Ox选项）编译上述程序，可得到如下所示的代码。

指令清单25.2　Optimizing MSVC 2010

_pos$75552 = -4             ; size = 4
_str$ = 8                   ; size = 4
?strlen_sse2@@YAIPBD@Z PROC ; strlen_sse2

     pushebp
     mov      ebp, esp
     and      esp, -16      ; fffffff0H
     mov      eax, DWORD PTR _str$[ebp]
     sub      esp, 12       ; 0000000cH
     push     esi
     mov      esi, eax
     and      esi, -16      ; fffffff0H
     xor      edx, edx
     mov      ecx, eax
     cmp      esi, eax
     je       SHORT $LN4@strlen_sse
     lea      edx, DWORD PTR [eax+1]
     npad     3 ; align next label
$LL11@strlen_sse:
     mov      cl, BYTE PTR [eax]
     inc      eax
     test     cl, cl
     jne      SHORT $LL11@strlen_sse
     sub      eax, edx
     pop      esi
     mov      esp, ebp
     pop      ebp
     ret      0
$LN4@strlen_sse:
     movdqa   xmm1, XMMWORD PTR [eax]
     pxor     xmm0, xmm0
     pcmpeqb  xmm1, xmm0
     pmovmskb eax, xmm1
     test     eax, eax
     jne      SHORT $LN9@strlen_sse
$LL3@strlen_sse:
     movdqa   xmm1, XMMWORD PTR [ecx+16]
     add      ecx, 16                ; 00000010H
     pcmpeqb  xmm1, xmm0
     add      edx, 16                ; 00000010H
     pmovmskb eax, xmm1
     test     eax, eax
     je       SHORT $LL3@strlen_sse
$LN9@strlen_sse:
     bsf      eax, eax
     mov      ecx, eax
     mov      DWORD PTR _pos$75552[esp+16], eax
     lea      eax, DWORD PTR [ecx+edx]
     pop      esi
     mov      esp, ebp
     pop      ebp
     ret      0
?strlen_sse2@@YAIPBD@Z ENDP          ; strlen_sse2

程序首先检查str指针是否已经向16字节边界对齐。如果这个地址没有对齐，就调用常规的strlen()函数计算字符串长度。

然后通过MOVDQA指令把16字节数据加载到XMM1寄存器。

部分读者可能会问：为什么不直接使用不要求指针对齐的MOVDQU指令？

这种说法似乎有道理：如果指针已经向16字节边界对齐了，我们可以使用MOVDQA指令；否则，就使用速度慢些的MOVDQU指令。

但是本文意在强调：

在Windows NT系列操作系统，以及其他一些操作系统里，内存的最小分页单位是4KB（4096字节）。虽然每个win32程序表面上都有（虚拟）独立的4GB内存可以使用，但是实际上部分地址空间受到物理内存和分页方法的限制。如果程序试图访问不存在的内存块，将会引发错误。这是虚拟内存的工作方式决定的，详情请参见http://en.wikipedia.org/wiki/Page_(computer_memory)

。

所以，如果函数一次加载16字节数据，可能会跨越虚拟内存的内存块边界。例如说，操作系统可能在地址为0x00c0000的物理内存处给这个程序分配了8192（0x2000）字节的内存块。那么这个程序的内存就在0x008c0000与0x008c1fff之间。

在这个内存块之后，即位于0x008c2000地址以后的物理内存都不可被程序访问。换而言之，操作系统没有给这个程序分配那块内存。如果程序访问这块不可用的内存地址就将引发错误。

我们再来研究一下这个程序。程序可能在内存块的末端存储5个字符。这种分配方法是合情合理的。

0x008c1ff8
0x008c1ff9
0x008c1ffa
0x008c1ffb
0x008c1ffc
0x008c1ffd
0x008c1ffe
0x008c1fff
’h’
’e’
’l’
’l’
’o’
’\x00’
random noise
random noise

当采用常规算法的strlen()函数在处理这个字符串时，首先把指针指向“hello”的第一个字节，即地址0x008c1ff8。strlen()函数会逐字节地读取字符串。在遇到0x008c1ffd的0字节的时候，函数就结束了。

假如我们编写的strlen()函数不考虑字符串的地址对齐问题，无论字符串指针地址是否已经向16字节边界对齐，每次都读取16字节。那么它就可能要从0x008c1ff8一直读取到0x008c2008，这将引发错误。当然，我们应当尽力避免这种错误。

所以仅在字符串地址向16字节边界对齐的情况下，才可以使用基于SIMD技术的算法。另外，操作系统在进行内存分页时也是向16字节边界对齐的，所以在字符串首地址同样向16字节边界对齐时，我们的函数不会访问未分配的内存空间。

下面继续介绍我们的函数。

_mm_setzero_si128()：清空XMM0寄存器的指令，相当于pxor xmm0, xmm0。

_mm_load_si128()：MOVDQA的宏。它从指定地址里加载16字节数据到XMM1寄存器。

_mm_cmpeq_epi8()：PCMPEQB的宏。它以字节为单位，比较两个XMM专用寄存器的值。如果某个字节相等，该字节返回值为0xff，否则返回值为0。举例来说，运行pcmpeqdb xmm1, xmm0指令的情况如下表所示。

运行前 | XMM1 | 11223344556677880000000000000000
运行前 | XMM0 | 11ab3444007877881111111111111111
运行后 | XMM1 | ff0000ff0000ffff0000000000000000

本例先用“pxor xmm0, xmm0”将xmm0寄存器清零，然后把从字符串摘取出的16字节与XMM0寄存器的16个0相比较。

下一条指令是_mm_movemask_epi8()，即PMOVVMSKB指令。这个指令通常与PCMPEQB配合使用：

pmovmskb eax, xmm1

在XMM1的第一个字节的最高位是1的情况下，设置EAX寄存器的最高位为1。也就是说，如果XMM1寄存器的值是0xff，那么EAX寄存器的最高位还会被赋值为1。

它还会在XMM1的第二个字节是0xff的情况下，设置EAX寄存器的次高位为1。

总而言之，这个指令此处的作用是“判断XMM1的每个字节是否是0xFF”。如果XMM1的第n个字节是0xFF，则设置EAX的第n位为1，否则EAX的第n位为0。

另外，不要忘记我们的算法可能会遇到的实际问题：指针指向的字符串可能包含有效值、结束符和脏数据。例如：

15

14

13

12

11

10

9～3

2

1～0

“h”

“e”

“l”

“l”

“o”

0

脏数据

0

脏数据

一次读取16字节到XMM寄存器，并将它与XMM0里的零进行比较，可能会得到这样的计算结果（从左到右是LSB到MSB的顺序）：

XMM1: 0000ff00000000000000ff0000000000

也就是说它找到了2个以上的零。这是情理之中的结果。

在这种情况下PMOVMSKB指令将设置EAX为（二进制）：0010000000100000b。

很明显，我们的函数必须以第一个结束符为准，忽略掉脏数据里的结束符。

下一条BSF（Bit Scan Forward）指令将保留操作符二进制值里的第一个1，将其余位清零，并把结果存储到第一个操作符里。

在EAX=0010000000100000b的情况下，执行“bsf eax, eax”，那么EAX将将保留第5位的1（从0开始数）。

MSVC里这条指令对应的宏是_BitScanForward。

代码的其他部分就容易理解了。如果找到了0字节，就把它的位置编号累加到计数器里，从而获取字符串长度。

应当注意到MSVC编译器优化掉了循环体的部分结构。

Intel Core I7处理器推出了SSE 4.2。它提供的新的指令大大简化了字符串的处理。有兴趣的读者可参见：http://www.strchr.com/strcmp_and_strlen_using_sse_4.2

。

[1]　请参见http://www.darkside.com.au/bitslice/。

[2]　Vectorization，又称“向量化”。请参见http://en.wikipedia.org/wiki/Vectorization_ (computer_science)。

[3]　有关地址对齐的详细信息，请参见：http://en.wikipedia.org/wiki/Data_structure_ alignment。

[4]　https://gcc.gnu.org/projects/tree-ssa/vectorization.html。

[5]　这种算法借鉴了网上的资料http://www.strchr.com/sse2_optimised_strlen。

第26章 64位平台

26.1　x86-64

x86-64框架是一种兼容x86指令集的64位微处理器架构。

从逆向工程的角度来说，这种框架的主要区别在于：

除了位于FPU和SIMD的寄存器之外，几乎所有的寄存器都变为64位寄存器。所有指令都可以通过R-字头的助记符（寄存器名称）调用64位寄存器。而且x86-64框架的CPU要比x86框架的CPU多出8个通用寄存器。这些通用寄存器分别是：RAX、RBX、RCX、RDX、RBP、RSP、RSI、RDI、R8、R9、R10、R11、R12、R13、R14、R15。

它向下兼容，允许程序使用GPR的32位寄存器的助记符、操作该寄存器的低32位数据。例如说，程序可以通过助记符EAX访问RAX寄存器的低32位。

7th（字节号）

6th

5th

4th

3rd

2nd

1st

0th

RAXx64

EAX

AX

AH

AL

64位寄存器特有的R8～R15寄存器不仅有相应的（低）32位助记符，即R8D～R15D（低32位），而且还有相应的（低）16位助记符R8W～R15W。

7th（字节号）

6th

5th

4th

3rd

2nd

1st

0th

R8

R8D

R8W

R8L

x86-64框架的CPU有16个SIMD寄存器，即XMM0～XMM15。它的SIMD寄存器比x86 CPU多出一倍。

Win64系统的函数调用约定发生了相应的变化，采取了与fastcall规范（请参见本书64.3节）相似的参数传递规范。这类程序使用RCX、RDX、R8、R9寄存器传递函数所需的前4个参数，并用栈传递其余参数。为了保证被调用方函数能够使用前4个参数所占的寄存器，调用方函数会单独分配出来32字节的栈空间，以便被调用方函数保存4个参数寄存器的原始状态。虽然小型函数可能不会用到多少寄存器，但是大型函数可能就要把传入的参数保存到栈里，以尽量充分使用寄存器资源。

System V AMD64 ABI（泛指Linux、各种BSD和Mac OS X）（参见Mit13）系统的函数调用约定也与fastcall规范相似。它使用RDI、RSI、RDX、RCX、R8、R9这6个寄存器传递前6个参数，再使用栈来传递其余参数。

本书的第64章详细介绍了各种调用约定。

在C/C++语言里，x86-64平台的int型数据仍然是32位数据。

x86-64平台的指针都是64位指针。

不过实际情况并非那么理想，x64 CPU在外部RAM的寻址能力只有48位。但是存储指针、包括缓存区的开销却高出x86一倍。

因为64位平台的寄存器数量是32位平台的两倍，编译器在调动资源方面的底气也更足一些，所以它们的寄存器分配方案（register allocation）当然会不同。这就是说，64位应用程序中的局部变量（栈空间）会更少一些。

第25章介绍过一个用bitslice DES算法实现并行计算的源程序。我们对其稍加修改，让程序根据DES_type数据类型（uint32、uint64、SSE2或AVX）处理32/64/128/256位数据的S-Box。

/*
 * Generated S-box files.
 *
 * This software may be modified, redistributed, and used for any purpose,
 * so long as its origin is acknowledged.
 *
 * Produced by Matthew Kwan - March 1998
 */

#ifdef _WIN64
#define DES_type unsigned __int64
#else
#define DES_type unsigned int
#endif

void
s1 (
    DES_type    a1,
    DES_type    a2,
    DES_type    a3,
    DES_type    a4,
    DES_type    a5,
    DES_type    a6,
    DES_type    *out1,
    DES_type    *out2,
    DES_type    *out3,
    DES_type    *out4
){
    DES_type    x1, x2, x3, x4, x5, x6, x7, x8;
    DES_type    x9, x10, x11, x12, x13, x14, x15, x16;
    DES_type    x17, x18, x19, x20, x21, x22, x23, x24;
    DES_type    x25, x26, x27, x28, x29, x30, x31, x32;
    DES_type    x33, x34, x35, x36, x37, x38, x39, x40;
    DES_type    x41, x42, x43, x44, x45, x46, x47, x48;
    DES_type    x49, x50, x51, x52, x53, x54, x55, x56;

    x1 = a3 & ~a5;
    x2 = x1 ^ a4;
    x3 = a3 & ~a4;
    x4 = x3 | a5;
    x5 = a6 & x4;
    x6 = x2 ^ x5;
    x7 = a4 & ~a5;
    x8 = a3 ^ a4;
    x9 = a6 & ~x8;
    x10 = x7 ^ x9;
    x11 = a2 | x10;
    x12 = x6 ^ x11;
    x13 = a5 ^ x5;
    x14 = x13 & x8;
    x15 = a5 & ~a4;
    x16 = x3 ^ x14;
    x17 = a6 | x16;
    x18 = x15 ^ x17;
    x19 = a2 | x18;
    x20 = x14 ^ x19;
    x21 = a1 & x20;
    x22 = x12 ^ ~x21;
    *out2 ^= x22;
    x23 = x1 | x5;
    x24 = x23 ^ x8;
    x25 = x18 & ~x2;
    x26 = a2 & ~x25;
    x27 = x24 ^ x26;
    x28 = x6 | x7;
    x29 = x28 ^ x25;
    x30 = x9 ^ x24;
    x31 = x18 & ~x30;
    x32 = a2 & x31;
    x33 = x29 ^ x32;
    x34 = a1 & x33;
    x35 = x27 ^ x34;
    *out4 ^= x35;
    x36 = a3 & x28;
    x37 = x18 & ~x36;
    x38 = a2 | x3;
    x39 = x37 ^ x38;
    x40 = a3 | x31;
    x41 = x24 & ~x37;
    x42 = x41 | x3;
    x43 = x42 & ~a2;
    x44 = x40 ^ x43;
    x45 = a1 & ~x44;
    x46 = x39 ^ ~x45;
    *out1 ^= x46;
    x47 = x33 & ~x9;
    x48 = x47 ^ x39;
    x49 = x4 ^ x36;
    x50 = x49 & ~x5;
    x51 = x42 | x18;
    x52 = x51 ^ a5;
    x53 = a2 & ~x52;
    x54 = x50 ^ x53;
    x55 = a1 | x54;
    x56 = x48 ^ ~x55;
    *out3 ^= x56;
}

这个源程序里有很多局部变量。当然并非所有的局部变量都要存储到栈里。我们使用32位的MSVC 2008（启用/Ox选项）编译它，可得到如下所示的代码。

指令清单26.1　Optimizing MSVC 2008

PUBLIC     _s1
; Function compile flags: /Ogtpy
_TEXT     SEGMENT
_x6$ = -20          ; size = 4
_x3$ = -16          ; size = 4
_x1$ = -12          ; size = 4
_x8$ = -8           ; size = 4
_x4$ = -4           ; size = 4
_a1$ = 8            ; size = 4
_a2$ = 12           ; size = 4
_a3$ = 16           ; size = 4
_x33$ = 20          ; size = 4
_x7$ = 20           ; size = 4
_a4$ = 20           ; size = 4
_a5$ = 24           ; size = 4
tv326 = 28          ; size = 4
_x36$ = 28          ; size = 4
_x28$ = 28          ; size = 4
_a6$ = 28           ; size = 4
_out1$ = 32         ; size = 4
_x24$ = 36          ; size = 4
_out2$ = 36         ; size = 4
_out3$ = 40         ; size = 4
_out4$ = 44         ; size = 4
_s1     PROC
     sub    esp, 20                   ; 00000014H
     mov    edx, DWORD PTR _a5$[esp+16]
     push   ebx
     mov    ebx, DWORD PTR _a4$[esp+20]
     push   ebp
     push   esi
     mov    esi, DWORD PTR _a3$[esp+28]
     push   edi
     mov    edi, ebx
     not    edi
     mov    ebp, edi
     and    edi, DWORD PTR _a5$[esp+32]
     mov    ecx, edx
     not    ecx
     and    ebp, esi
     mov    eax, ecx
     and    eax, esi
     and    ecx, ebx
     mov    DWORD PTR _x1$[esp+36], eax
     xor    eax, ebx
     mov    esi, ebp
     or     esi, edx
     mov    DWORD PTR _x4$[esp+36], esi
     and    esi, DWORD PTR _a6$[esp+32]
     mov    DWORD PTR _x7$[esp+32], ecx
     mov    edx, esi
     xor    edx, eax
     mov    DWORD PTR _x6$[esp+36], edx
     mov    edx, DWORD PTR _a3$[esp+32]
     xor    edx, ebx
     mov    ebx, esi
     xor    ebx, DWORD PTR _a5$[esp+32]
     mov    DWORD PTR _x8$[esp+36], edx
     and    ebx, edx
     mov    ecx, edx
     mov    edx, ebx
     xor    edx, ebp
     or     edx, DWORD PTR _a6$[esp+32]
     not    ecx
     and    ecx, DWORD PTR _a6$[esp+32]
     xor    edx, edi
     mov    edi, edx
     or     edi, DWORD PTR _a2$[esp+32]
     mov    DWORD PTR _x3$[esp+36], ebp
     mov    ebp, DWORD PTR _a2$[esp+32]
     xor    edi, ebx
     and    edi, DWORD PTR _a1$[esp+32]
     mov    ebx, ecx
     xor    ebx, DWORD PTR _x7$[esp+32]
     not    edi
     or     ebx, ebp
     xor    edi, ebx
     mov    ebx, edi
     mov    edi, DWORD PTR _out2$[esp+32]
     xor    ebx, DWORD PTR [edi]
     not    eax
     xor    ebx, DWORD PTR _x6$[esp+36]
     and    eax, edx
     mov    DWORD PTR [edi], ebx
     mov    ebx, DWORD PTR _x7$[esp+32]
     or     ebx, DWORD PTR _x6$[esp+36]
     mov    edi, esi
     or     edi, DWORD PTR _x1$[esp+36]
     mov    DWORD PTR _x28$[esp+32], ebx
     xor    edi, DWORD PTR _x8$[esp+36]
     mov    DWORD PTR _x24$[esp+32], edi
     xor    edi, ecx
     not    edi
     and    edi, edx
     mov    ebx, edi
     and    ebx, ebp
     xor    ebx, DWORD PTR _x28$[esp+32]
     xor    ebx, eax
     not    eax
     mov    DWORD PTR _x33$[esp+32], ebx
     and    ebx, DWORD PTR _a1$[esp+32]
     and    eax, ebp
     xor    eax, ebx
     mov    ebx, DWORD PTR _out4$[esp+32]
     xor    eax, DWORD PTR [ebx]
     xor    eax, DWORD PTR _x24$[esp+32]
     mov    DWORD PTR [ebx], eax
     mov    eax, DWORD PTR _x28$[esp+32]
     and    eax, DWORD PTR _a3$[esp+32]
     mov    ebx, DWORD PTR _x3$[esp+36]
     or     edi, DWORD PTR _a3$[esp+32]
     mov    DWORD PTR _x36$[esp+32], eax
     not    eax
     and    eax, edx
     or     ebx, ebp
     xor    ebx, eax
     not    eax
     and    eax, DWORD PTR _x24$[esp+32]
     not    ebp
     or     eax, DWORD PTR _x3$[esp+36]
     not    esi
     and    ebp, eax
     or     eax, edx
     xor    eax, DWORD PTR _a5$[esp+32]
     mov    edx, DWORD PTR _x36$[esp+32]
     xor    edx, DWORD PTR _x4$[esp+36]
     xor    ebp, edi
     mov    edi, DWORD PTR _out1$[esp+32]
     not    eax
     and    eax, DWORD PTR _a2$[esp+32]
     not    ebp
     and    ebp, DWORD PTR _a1$[esp+32]
     and    edx, esi
     xor    eax, edx
     or     eax, DWORD PTR _a1$[esp+32]
     not    ebp
     xor    ebp, DWORD PTR [edi]
     not    ecx
     and    ecx, DWORD PTR _x33$[esp+32]
     xor    ebp, ebx
     not    eax
     mov    DWORD PTR [edi], ebp
     xor    eax, ecx
     mov    ecx, DWORD PTR _out3$[esp+32]
     xor    eax, DWORD PTR [ecx]
     pop    edi
     pop    esi
     xor    eax, ebx
     pop    ebp
     mov    DWORD PTR [ecx], eax
     pop    ebx
     add    esp, 20             ; 00000014H
     ret    0
_s1    ENDP

编译器只在栈里存储了5个变量。

作为对比，我们再使用64位的MSVC 2008编译它，得到的代码如下所示。

指令清单26.2　Optimizing MSVC 2008

a1$ = 56
a2$ = 64
a3$ = 72
a4$ = 80
x36$1$ = 88
a5$ = 88
a6$ = 96
out1$ = 104
out2$ = 112
out3$ = 120
out4$ = 128
s1    PROC
$LN3:
     mov    QWORD PTR [rsp+24], rbx
     mov    QWORD PTR [rsp+32], rbp
     mov    QWORD PTR [rsp+16], rdx
     mov    QWORD PTR [rsp+8], rcx
     push   rsi
     push   rdi
     push   r12
     push   r13
     push   r14
     push   r15
     mov    r15, QWORD PTR a5$[rsp]
     mov    rcx, QWORD PTR a6$[rsp]
     mov    rbp, r8
     mov    r10, r9
     mov    rax, r15
     mov    rdx, rbp
     not    rax
     xor    rdx, r9
     not    r10
     mov    r11, rax
     and    rax, r9
     mov    rsi, r10
     mov    QWORD PTR x36$1$[rsp], rax
     and    r11, r8
     and    rsi, r8
     and    r10, r15
     mov    r13, rdx
     mov    rbx, r11
     xor    rbx, r9
     mov    r9, QWORD PTR a2$[rsp]
     mov    r12, rsi
     or     r12, r15
     not    r13
     and    r13, rcx
     mov    r14, r12
     and    r14, rcx
     mov    rax, r14
     mov    r8, r14
     xor    r8, rbx
     xor    rax, r15
     not    rbx
     and    rax, rdx
     mov    rdi, rax
     xor    rdi, rsi
     or     rdi, rcx
     xor    rdi, r10
     and    rbx, rdi
     mov    rcx, rdi
     or     rcx, r9
     xor    rcx, rax
     mov    rax, r13
     xor    rax, QWORD PTR x36$1$[rsp]
     and    rcx, QWORD PTR a1$[rsp]
     or     rax, r9
     not    rcx
     xor    rcx, rax
     mov    rax, QWORD PTR out2$[rsp]
     xor    rcx, QWORD PTR [rax]
     xor    rcx, r8
     mov    QWORD PTR [rax], rcx
     mov    rax, QWORD PTR x36$1$[rsp]
     mov    rcx, r14
     or     rax, r8
     or     rcx, r11
     mov    r11, r9
     xor    rcx, rdx
     mov    QWORD PTR x36$1$[rsp], rax
     mov    r8, rsi
     mov    rdx, rcx
     xor    rdx, r13
     not    rdx
     and    rdx, rdi
     mov    r10, rdx
     and    r10, r9
     xor    r10, rax
     xor    r10, rbx
     not    rbx
     and    rbx, r9
     mov    rax, r10
     and    rax, QWORD PTR a1$[rsp]
     xor    rbx, rax
     mov    rax, QWORD PTR out4$[rsp]
     xor    rbx, QWORD PTR [rax]
     xor    rbx, rcx
     mov    QWORD PTR [rax], rbx
     mov    rbx, QWORD PTR x36$1$[rsp]
     and    rbx, rbp
     mov    r9, rbx
     not    r9
     and    r9, rdi
     or     r8, r11
     mov    rax, QWORD PTR out1$[rsp]
     xor    r8, r9
     not    r9
     and    r9, rcx
     or     rdx, rbp
     mov    rbp, QWORD PTR [rsp+80]
     or     r9, rsi
     xor    rbx, r12
     mov    rcx, r11
     not    rcx
     not    r14
     not    r13
     and    rcx, r9
     or     r9, rdi
     and    rbx, r14
     xor    r9, r15
     xor    rcx, rdx
     mov    rdx, QWORD PTR a1$[rsp]
     not    r9
     not    rcx
     and    r13, r10
     and    r9, r11
     and    rcx, rdx
     xor    r9, rbx
     mov    rbx, QWORD PTR [rsp+72]
     not    rcx
     xor    rcx, QWORD PTR [rax]
     or     r9, rdx
     not    r9
     xor    rcx, r8
     mov    QWORD PTR [rax], rcx
     mov    rax, QWORD PTR out3$[rsp]
     xor    r9, r13
     xor    r9, QWORD PTR [rax]
     xor    r9, r8
     mov    QWORD PTR [rax], r9
     pop    r15
     pop    r14
     pop    r13
     pop    r12
     pop    rdi
     pop    rsi
     ret    0
s1     ENDP

编译器根本没有使用栈。上述代码中的x36就是a5。

顺便说一下，x86-64 CPU的GPR还算不上是最多的。例如，Itanium/安腾处理器就有128个GPR。

26.2　ARM

自ARM v8开始，ARM处理器支持64位指令。

26.3　浮点数

有关64位平台的浮点数运算，请参见本书第27章。

第27章 SIMD与浮点数的并行运算

在SIMD功能问世之前，x86兼容的处理器早就集成了FPU。自SSE2开始，SIMD拓展指令集提供了单指令多数据浮点计算的指令。这类指令支持的数据格式同样是IEEE 754。

随着硬件的发展，最近推出的x86/x86-64编译器越来越多地使用SIMD指令，都不再怎么分配FPU指令了。

这不得不说是一种好消息。毕竟SIMD的指令更为简单。

本章将基于第17章的源程序进行讲解。

27.1　样板程序

#include <stdio.h>

double f (double a, double b)
{
          return a/3.14 + b*4.1;
};

int main()
{
          printf ("%f\n", f(1.2, 3.4));
};

27.1.1　x64

指令清单27.1　Optimizing MSVC 2012 x64

__real@4010666666666666 DQ 04010666666666666r   ; 4.1
__real@40091eb851eb851f DQ 040091eb851eb851fr   ; 3.14

a$ = 8
b$ = 16
f        PROC
         divsd   xmm0, QWORD PTR __real@40091eb851eb851f
         mulsd   xmm1, QWORD PTR __real@4010666666666666
         addsd   xmm0, xmm1
         ret     0
f        ENDP

程序使用XMM0～XMM3传递参数中的前几个浮点数，其余参数都通过栈传递。有关浮点数的参数传递规范，请参见MSDN：https://msdn.microsoft.com/en-us/library/zthk2dkh.aspx

。

变量a存储在XMM0寄存器里，变量b存储在XMM1寄存器里。XMM系列寄存器都是128位寄存器，而双精度double型浮点数都是64位数据。所以这个程序只使用了寄存器的低半部分。

DIVSD是SSE指令，它的全称是“Divide Scalar Double-Precision Floating-Point Values”，即标量双精度浮点除法。DIVSD指令以第一个操作数中的低64位中的双精度浮点数做被除数，以第二操作数的低64位中的双精度浮点数做除数进行除法运算，商将保存到目标地址（第一操作数）的低64位中，目标地址的高64位保存不变。

在上述程序中可以看到，编译器以IEEE 754格式封装浮点数常量。

MULSD和ADDSD分别是乘法和加法的运算指令。

上述函数返回的数据类型是double，由XMM0寄存器回传。

由MSVC编译器进行非优化编译，可得如下所示的代码。

指令清单27.2　MSVC 2012 x64

__real@4010666666666666 DQ 04010666666666666r   ; 4.1
__real@40091eb851eb851f DQ 040091eb851eb851fr   ; 3.14

a$ = 8
b$ = 16

f        PROC
         movsdx  QWORD PTR [rsp+16], xmm1
         movsdx  QWORD PTR [rsp+8], xmm0
         movsdx  xmm0, QWORD PTR a$[rsp]
         divsd   xmm0, QWORD PTR __real@40091eb851eb851f
         movsdx  xmm1, QWORD PTR b$[rsp]
         mulsd   xmm1, QWORD PTR __real@4010666666666666
         addsd   xmm0, xmm1
         ret     0
f        ENDP

上述程序还可以进行优化。请注意：传入的参数被保存到了“阴影空间”（可参见本书8.2.1节）。但是只有寄存器的低半部分数据——即double型64位数据会被存在阴影空间里。

GCC编译出的程序与之相同，本文不再介绍。

27.1.2　x86

在使用MSVC 2012编译x86平台的可执行程序时，编译器会分配SSE2指令。

指令清单27.3　Non-optimizing MSVC 2012 x86

tv70 = -8       ;size=8
_a$ = 8         ;size=8
_b$ = 16        ;size=8
_f      PROC
        push    ebp
        mov     ebp, esp
        sub     esp, 8
        movsd   xmm0, QWORD PTR _a$[ebp]
        divsd   xmm0, QWORD PTR __real@40091eb851eb851f
        movsd   xmm1, QWORD PTR _b$[ebp]
        mulsd   xmm1, QWORD PTR __real@4010666666666666
        addsd   xmm0, xmm1
        movsd   QWORD PTR tv70[ebp], xmm0
        fld     QWORD PTR tv70[ebp]
        mov     esp, ebp
        pop     ebp
        ret     0
_f      ENDP

指令清单27.4　Optimizing MSVC 2012 x86

tv67 = 8        ; size = 8
_a$ = 8         ; size = 8
_b$ = 16        ; size = 8
_f      PROC
        movsd   xmm1, QWORD PTR _a$[esp-4]
        divsd   xmm1, QWORD PTR __real@40091eb851eb851f
        movsd   xmm0, QWORD PTR _b$[esp-4]
        mulsd   xmm0, QWORD PTR __real@4010666666666666
        addsd   xmm1, xmm0
        movsd   QWORD PTR tv67[esp-4], xmm1
        fld     QWORD PTR tv67[esp-4]
        ret     0
_f      ENDP

这种x86程序与前面介绍的64位程序十分相似。它们的区别主要体现在参数的调用规范上：

① x86程序会像第17章的FPU例子那样使用栈来传递浮点数，而不是像64位程序那样使用XMM寄存器传递浮点型参数。

② x86程序的浮点型数据的运算结果保存在ST(0)寄存器里——这个值是通过局部变量tv、从XMM寄存器复制到ST(0)寄存器的。

现在，我们使用OllyDbg调试前面那个优化编译的32位程序，如图27.1到图27.4所示。

图27.1　OllyDbg: MOVSD指令把变量a传递给XMM1

图27.2　OllyDbg:DIVSD进行除法运算、把商存储于XMM1

图27.3　OllyDbg:MULSD进行乘法运算、把积存储于XMM0

图27.4　OllyDbg:ADDSD计算XMM0和XMM1的和

从图27.5可以看到：虽然OllyDbg把单个XMM寄存器视为一对double数据的寄存器，但是它只使用了寄存器的低半部分。很明显，OllyDbg根据SSE2指令的后缀“－SD”判断出了数据类型，并据此进行了数据整理。实际上，OllyDbg还能够根据SSE指令进行判断，把XMM寄存器的数据整理为4个float浮点数、或者是16个字节。

图27.5　OllyDbg:FLD把函数返回值传递给ST（0）

27.2　传递浮点型参数

#include <math.h>
#include <stdio.h>

int main ()
{
        printf ("32.01 ^ 1.54 = %lf\n", pow (32.01,1.54));
        return 0;
}

在函数间负责传递浮点数的寄存器是XMM0～XMM3寄存器。

指令清单27.5　Optimizing MSVC 2012 x64

$SG1354 DB       '32.01 ^ 1.54 = %lf', 0aH, 00H

__real@40400147ae147ae1 DQ 040400147ae147ae1r    ; 32.01
__real@3ff8a3d70a3d70a4 DQ 03ff8a3d70a3d70a4r    ; 1.54

Main    PROC
        sub      rsp, 40                                ; 00000028H
        movsdx   xmm1, QWORD PTR __real@3ff8a3d70a3d70a4
        movsdx   xmm0, QWORD PTR __real@40400147ae147ae1
        call     pow
        lea      rcx, OFFSET FLAT:$SG1354
        movaps   xmm1, xmm0
        movd     rdx, xmm1
        call     printf
        xor      eax, eax
        add       rsp, 40                               ; 00000028H
        ret      0
main    ENDP

无论是Intel指令白皮书[1]还是AMD指令白皮书[2]，都没有记载MOVSDX指令。它就是MOVSD指令。这就是说x86指令集的指令可能对应着多个名称（详情请参阅附录A.6.2）。而此处出现了这个指令，说明微软的研发人员希望用指令名称来标明操作符的数据类型，避免产生混淆。它是把浮点值传递给XMM寄存器的低半部分的指令。

pow()函数从XMM0和XMM1中读取参数，再把返回结果存储在XMM0寄存器。函数返回值又刻意通过RDX寄存器传递给printf()函数，不过这是为什么？坦白地讲，我不知道个中缘由。或许这是printf()的特性——可受理不同类型参数——造成的。

指令清单27.6　Optimizing GCC 4.4.6 x64

.LC2:
         .string "32.01 ^ 1.54 = %lf\n"
main:
         sub     rsp, 8
         movsd   xmm1, QWORD PTR .LC0[rip]
         movsd   xmm0, QWORD PTR .LC1[rip]
         call    pow
         ; result is now in XMM0
         mov     edi, OFFSET FLAT:.LC2
         mov     eax, 1 ; number of vector registers passed
         call    printf
         xor     eax, eax
         add     rsp, 8
         ret

.LC0:
         .long   171798692
         .long   1073259479
.LC1:
         .long   2920577761
         .long   1077936455

GCC的编译手法更易懂一些。它使用XMM0寄存器向printf()函数传递浮点型参数。此外，在向printf()函数传递参数的时候，程序将EAX寄存器的值设置为1。这就相当于告诉函数“有1个参数在矢量寄存器里”。这种特例完全遵循了有关规范（请参阅Mit13）。

27.3　浮点数之间的比较

#include <stdio.h>

double d_max (double a, double b)
{
         if (a>b)
                  return a;
         return b;
};

int main()
{
         printf ("%f\n", d_max (1.2, 3.4));
         printf ("%f\n", d_max (5.6, -4));
};

27.3.1　x64

指令清单27.7　Optimizing 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

MSVC的优化编译结果简明易懂。

其中，COMISD的全称是“Compare Scalar Ordered Double-Precision Floating-Point Values and Set EFLAGS”。它是比较双精度标量并设置标志位的指令。

MSVC的非优化编译结果同样不难阅读，只是程序的效率低了一些。

指令清单27.8　MSVC 2012 x64

a$ = 8
b$ = 16
d_max   PROC
        movsdx   QWORD PTR [rsp+16], xmm1
        movsdx   QWORD PTR [rsp+8], xmm0
        movsdx   xmm0, QWORD PTR a$[rsp]
        comisd   xmm0, QWORD PTR b$[rsp]
        jbe      SHORT $LN1@d_max
        movsdx   xmm0, QWORD PTR a$[rsp]
        jmp      SHORT $LN2@d_max
$LN1@d_max:
        movsdx   xmm0, QWORD PTR b$[rsp]
$LN2@d_max:
        fatret   0
d_max   ENDP

然而GCC 4.4.6优化得更为彻底。它直接使用MAXSD指令（Return Maximum Scalar Double-Precision Floating-Point Value），一步就可完成任务。

指令清单27.9　Optimizing GCC 4.4.6 x64

d_max:
          maxsd   xmm0, xmm1
          ret

27.3.2　x86

使用MSVC 2012对上述代码进行优化编译，可得到如下所示的代码。

指令清单27.10　Optimizing MSVC 2012 x86

_a$ = 8          ; size = 8
_b$ = 16         ; size = 8
_d_max  PROC
        movsd    xmm0, QWORD PTR _a$[esp-4]
        comisd   xmm0, QWORD PTR _b$[esp-4]
        jbe      SHORT $LN1@d_max
        fld      QWORD PTR _a$[esp-4]
        ret      0
$LN1@d_max:
        fld      QWORD PTR _b$[esp-4]
        ret      0
_d_max  ENDP

这个程序用栈向函数传递变量a和b，并且把函数返回值存储在ST(0)寄存器里。除此之外它和64位的程序没有区别。

使用OllyDbg调试这个程序，则可以观察COMISD指令比较数值、设置/清零CF/PF标识位的具体过程，如图27.6所示。

图27.6　OllyDbg：COMISD指令调整CF和PF标识位

27.4　机器精度

本书在22.2.2节就介绍过机器精度的具体计算算法。在这一节里，我们把它编译为如下所示的64位应用程序。

指令清单27.11　Optimizing MSVC 2012 x64

v$ = 8
calculate_machine_epsilon PROC
         movsdx   QWORD PTR v$[rsp], xmm0
         movaps   xmm1, xmm0
         inc      QWORD PTR v$[rsp]
         movsdx   xmm0, QWORD PTR v$[rsp]
         subsd    xmm0, xmm1
         ret      0
calculate_machine_epsilon ENDP

由于INC指令无法对128位的XMM寄存器里的值进行操作，程序首先把寄存器的值传递到了内存中的某个地址。

然而这也不是必须如此。如果在此处直接使用 ADDSD（Add Scalar Double-Precision Floating-Point Values）指令，就能够直接对XMM寄存器的低64位进行加法运算，同时保持寄存器的高64位不变。或许，我们不得不说MSVC 2012还有改善的余地。

我们再看这个程序。在完成递增运算之后，程序把返回值再次回传给了XMM寄存器，然后对XMM寄存器进行减法（比较）操作。SUBSD的全称是“Substract Scalar Double-Precision Floating-Point Values”。它只对128位XMM寄存器的低64位进行操作。最后，减法运算的返回值保存在XMM0寄存器中。

27.5　伪随机数生成程序（续）

现在，我们再次利用MSVC 2012编译指令清单22.1的源程序。这个版本的MSVC编译器能够直接分配FPU的SIMD指令。

指令清单27.12　Optimizing MSVC 2012

__real@3f800000 DD 03f800000r              ; 1

tv128 = -4
_tmp$ = -4
?float_rand@@YAMXZ PROC
         push   ecx
         call   ?my_rand@@YAIXZ
; EAX=pseudorandom value
         and    eax, 8388607               ; 007fffffH
         or     eax, 1065353216            ; 3f800000H
; EAX=pseudorandom value & 0x007fffff | 0x3f800000
; store it into local stack:
         mov    DWORD PTR _tmp$[esp+4], eax
; reload it as float point number:
         movss  xmm0, DWORD PTR _tmp$[esp+4]
; subtract 1.0:
         subss  xmm0, DWORD PTR __real@3f800000
; move value to ST0 by placing it in temporary variable...
         movss  DWORD PTR tv128[esp+4], xmm0
; ... and reloading it into ST0:
         fld    DWORD PTR tv128[esp+4]
         pop    ecx
         ret    0
?float_rand@@YAMXZ ENDP

编译器大量使用了带有“-SS”后缀的指令。这个后缀是“Scalar Single”的缩写。“Scalar（标量）”表明寄存器里只有一个值，“Single”则表明该值是单精度float型的数据。

27.6　总结

在XMM寄存器里存储IEEE 754格式格式浮点数的时候，本章的所有程序都只用到了128位寄存器空间的低64位。

这是由操作指令决定的。本章中的指令都带有-SD（Scalar Double-Precision）后缀。因此，所有指令操作的数据类型都是IEEE 754格式的双精度浮点数。这种数据只会占用XMM寄存器的低64位。

SIMD指令集比FPU的指令更易理解。过去的那些FPU指令距离人类语言较远，而且它们还往往涉及FPU寄存器的栈结构，远不如SIMD指令直观。

如果把本章源程序的数据类型都从double改为float，那么操作浮点数的指令都将改为-SS后缀（Scalar Single-Precision）。您将看到MOVSS/COMISS/ADDSS等指令。

“Scalar”就是“标量”的意思，它表示寄存器里只含有一个值。如果一个寄存器里存有多个值，那么对这些寄存器进行操作的指令的名字里都会带有“packed”字样。

还需要强调的是，SSE2指令的操作数都是IEEE 754格式的64位数据，而FPU内部则使用80位的数据格式存储数据。所以FPU的误差较小、精度较高。

[1]　请参阅Int13。

[2]　请参阅AMD13a。

第28章 ARM指令详解

28.1　立即数标识（#）

在Keil编译器的链接文件里，以及在IDA和objdump程序显示ARM的汇编指令时，立即数（汇编指令中的常量）前面都有“#”标识。然而GCC 4.9的编译文件中没有这种立即数标识。请对照一下 14.1.4节和39.3节中的指令清单。

本章列举了很多例子，它们是由多种编译器生成的汇编代码。有些汇编指令的立即数确实没有井号标识，因为它们是GCC编译出来的文件。

这种问题毕竟谈不上谁比谁更为正宗。我们也建议读者不必深究这种格式上的问题。

28.2　变址寻址

以64位的寻址指令为例：

ldr      x0, [x29,24]

上述指令从X29寄存器中取值，把这个值与24相加，再将算术和对应地址的值复制给X0寄存器。请注意，X29和24都在方括号之中。如果24不在方括号里，指令的涵义就完全不一样了。我们来看：

ldr      w4, [x1],28

上述指令在X1寄存器所表示的地址取值，然后进行“指针X1所对应的值+28”的运算。

ARM可以在取值过程中进行立即数的加减运算。它即可以在取值前进行地址运算，还可以在取值后进行数值运算。

x86的汇编指令不支持这种操作。但是包括旧式的PDP-11平台在内，许多其他处理器平台都支持这种运算。PDP-11平台的指令集支持前增量（pre-increment）、前减量（pre-decrement）、后增量（post- increment）、后减量（post-decrement）运算。这大体可以归咎于C语言（基于PDP-11平台开发的）里的*ptr++、*++ptr、*ptr--、*--ptr一类的指令。在C语言中，这些指令确实属于不易掌握的指令。本文将之整理如下。

C术语

ARM术语

C语句

工作原理

后增量

后变址寻址

*ptr++

使用*ptr的值后，再进行递增运算

后减量

后变址寻址

*ptr--

使用*ptr的值后，再进行递减运算

前增量

前变址寻址

*++ptr

ptr先递增，后取*ptr的值

前减量

前变址寻址

*--ptr

ptr先递减，后取*ptr的值

前变址寻址的指令带感叹号标识。例如，指令清单3.15的第二条指令“stp x29, x30, [sp,#-16]!”。

Dennis Ritchie（C语言的作者之一）曾经提到过，变址寻址是Ken Thompson（另一位C语言的作者）根据PDP-7平台的特性开发的指令（请参阅［Rit86］[Rit93]）。现在C语言编译器仍然保持着这种兼容性，只要硬件处理器支持，编译器就可以进行相应的优化编译。

变址寻址的优越性集中体现在数组的操作方面。

28.3　常量赋值

28.3.1　32位ARM

所有的Thumb模式的汇编指令都是2字节指令，所有的ARM模式的汇编指令都是4字节指令。ARM模式指令和32位的立即数都占用4字节，又如何进行32位立即数赋值呢？

我们来看：

unsigned int f()
{
         return 0x12345678;
};

指令清单28.1　GCC 4.6.3 -O3 ARM mode

f:
         ldr      r0, .L2
         bx       lr
.L2:
         .word    305419896 ; 0x12345678

可见，0x12345678存储于内存之中，供其他指令调用。但是这种指令增加了CPU访问内存的次数。我们可以改善这一状况。

指令清单28.2　GCC 4.6.3 -O3 -march=armv7-a (ARM mode)

movw    r0, #22136      ; 0x5678
movt    r0, #4660       ; 0x1234
bx      lr

上述指令把一个立即数分为2个部分，并依次存储到同一个寄存器里。它使用MOVW指令先把立即数的低位部分存储到寄存器里，然后再使用MOVT存储这个数的高位部分。

这也就是说，ARM模式的程序需要2个指令才能加载一个32位的立即数。除了0和1之外，程序很少用到常量，所以这种分步赋值的方法不会造成实际问题。有人会问，这种一分为二的做法会不会造成性能的下降呢？虽然肯定比单条指令的效率要低，但是现在的ARM处理器能够检测到这种指令序列并能够对这种指令进行优化。

另外，IDA这样的工具能够识别出这种一分为二指令，并在显示的时候把它们合二为一。

MOV    R0, 0x12345678
BX     LR

28.3.2　ARM64

uint64_t f()
{
         return 0x12345678ABCDEF01;
};

指令清单28.3　GCC 4.9.1 -O3

mov     x0, 61185    ; 0xef01
movk    x0, 0xabcd, lsl 16
movk    x0, 0x5678, lsl 32
movk    x0, 0x1234, lsl 48
ret

其中，MOVK是“MOV Keep”的缩写。它把16位数值存储到寄存器里，而保留寄存器里的其他比特位。实际上这几个MOVK指令先使用LSL指令依次位移了16、32、48位，然后再进行的赋值操作。即，程序通过4条指令把一个64位数值存储到寄存器里。

浮点数

一条指令就可把浮点型数据存储到D-存储器里。我们来看：

double a()
{
         return 1.5;
};

指令清单28.4　GCC 4.9.1 -O3 + objdump

0000000000000000 <a>:
   0:    1e6f1000         fmov    d0, #1.500000000000000000e+000
   4:    d65f03c0         ret

上面这个单条32位指令如何封装浮点数1.5呢？在ARM64的FMOV指令里，有8个特殊的比特位。这8位空间用于编排浮点型数据。通过VFPExpandImm()函数的算法，编译器把浮点数封装在FMOV指令的8位空间里。这种算法又叫作minifloat[1]，实现方法请参见[ARM13a]。我测试了几个浮点数，发现编译器能够通过该函数把30.0和31.0编排在8位指令空间里。但是这8位空间无法封装32.0。根据IEEE 754规范，32.0要占用8个字节的空间。

double a()
{
         return 32;
};

上述源程序的汇编指令如下所示。

指令清单28.5　GCC 4.9.1 -O3

a:
         ldr     d0, .LC0
         ret
.LC0:
         .word   0
         .word   1077936128

28.4　重定位

我们已经知道ARM64的指令都是4字节（汇编）指令。4个字节的容量有限，无法封装很大的数。尽管如此，程序镜像可能会被操作系统加载内存中的任意地址，这就需要（基址）重定位（relocations/relocs）来进行修正。有关重定位的详细介绍，请参见本书的68.2.6节。

ARM64成对使用ADRP和ADD指令来传递64位指针地址。ADRP指令用来获取标签所在处（本例是main）的4KB分页地址，而ADD指令则负责存储偏移量的其余部分。在Win32下使用GCC(Linaro) 4.9编译了“Hello, word!”程序（即第6章的第一个程序），然后使用objdump工具分析它的目标文件。

指令清单28.6　GCC (Linaro) 4.9 and objdump of object file

...>aarch64-linux-gnu-gcc.exe hw.c –c

...>aarch64-linux-gnu-objdump.exe -d hw.o

...

0000000000000000 <main>:
   0:   a9bf7bfd        stp    x29, x30, [sp,#-16]!
   4:   910003fd        mov    x29, sp
   8:   90000000        adrp   x0, 0 <main>
   c:   91000000        add    x0, x0, #0x0
  10:   94000000        bl     0 <printf>
  14:   52800000        mov    w0, #0x0              // #0
  18:   a8c17bfd        ldp    x29, x30, [sp],#16
  1c:   d65f03c0        ret

...>aarch64-linux-gnu-objdump.exe -r hw.o

...

RELOCATION RECORDS FOR [.text]:
OFFSET           TYPE                  VALUE
0000000000000008 R_AARCH64_ADR_PREL_PG_HI21  .rodata
000000000000000c R_AARCH64_ADD_ABS_LO12_NC .rodata
0000000000000010 R_AARCH64_CALL26 printf

这个目标文件有3个地方涉及重定位：

第一处首先获取页面地址，把地址的低12位舍去，以便在ADRP指令里封装地址的高21位。ADRP获取的偏移量的4KB地址，其最后12位肯定是零，所以可以被忽略；另一方面，ADRP指令也只有21位空间可以封装数据。

其后的ADD指令则用于保存偏移量的低12位。

跳转到printf()函数的BL指令。若把B/BL指令逐位的展开，会看到其中只有26比特可供存储偏移量。实际上ARM模式和ARM64模式的转移指令只可能跳转到4的整数倍的偏移量地址，所以在指令中省略了最后的2位（相当于右移2位）。在执行转移指令时，相当于先对文件中的偏移量左移两位后再进行相应跳转。如此一来，转移指令的偏移量空间是28位，而不是26位；即可跳转至PC±128MB的地址（即偏移量的取值范围）。

最后生成的可执行文件里并没有再出现重定位。因为在编译过程的后续阶段，“Hello！”字符串的相对地址、分页地址，以及puts()函数的相对地址都是可被确定的已知数。链接器linker可依次计算出ADRP、ADD和BL指令所需的实际偏移量。

使用objdump分析最终的可执行文件，可以看到如下所示的代码。

指令清单28.7　objdump of executable file

0000000000400590 <main>:
  400590:      a9bf7bfd         stp         x29, x30, [sp,#-16]!
  400594:      910003fd         mov         x29, sp
  400598:      90000000         adrp        x0, 400000 <_init-0x3b8>
  40059c:      91192000         add         x0, x0, #0x648
  4005a0:      97ffffa0         bl          400420 <puts@plt>
  4005a4:      52800000         mov         w0, #0x0      // #0
  4005a8:      a8c17bfd         ldp         x29, x30, [sp],#16
  4005ac:      d65f03c0         ret

...

Contents of section .rodata:
 400640 01000200 00000000 48656c6c 6f210000 ........Hello!..

BL指令要跳转到的地址可以推算出来。

假如BL那条指令的opcode是0x97ffffa0，即二进制的10010111111111111111111110100000。依据［ARM13a］C5.2.26描述的有关技术规范，opcode的最后26位是imm26。因此imm26的二进制数值为11111111111111111110100000，即imm26 = 0x3FFFFA0。但是imm26的最高数权位即符号位是1，所以它是负数的补码。要把补码转换为原码，则要进行就取非再加一的运算。由此可得原码为负的0x5F+1=0x60，即−0x60。ARM指令里的偏移量是实际偏移量除以4，所以实际偏移量是其4倍，即−0x180。综上，BL将要跳转到的目标地址是0x4005a0−0x180 = 0x400420。请注意：要以BL指令的偏移量为基数进行计算。如果要对PC指针进行计算，那么整个推算过程将完全不同。

如需深入了解AM64的重定位，请参见《ELF for the ARM 64-bit Architecture (AArch64)》（2013）。其官方下载地址是http://infocenter.arm.com/help/topic/com.arm.doc.ihi0056b/ IHI0056B_aaelf64.pdf

。

[1]　https://en.wikipedia.org/wiki/Minifloat。

第29章 MIPS的特点

29.1　加载常量

unsigned int f()
{
         return 0x12345678;
};

MIPS和ARM平台的指令都是定长指令。MIPS程序的指令都是32位opcode。在MIPS平台上，我们不可能只使用单条指令就完成对32位常量的赋值操作。所以此类操作至少分两步进行：首先传递32位数据的高16位，然后再通过ORI操作向寄存器传递立即数的低16位。

指令清单29.1　GCC 4.4.5 -O3 (assembly output)

        li        $2,305397760                         # 0x12340000
        j         $31
        ori       $2,$2,0x5678 ; branch delay slot

IDA能够识别此类组合指令。为了方便阅读，它对后面的ORi指令进行了处理：用伪指令LI替换了Ori指令，而且用完整的32位值替换了值的低16位。

指令清单29.2　GCC 4.4.5 -O3 (IDA)

        lui       $v0, 0x1234
        jr        $ra
        li        $v0, 0x12345678 ; branch delay slot

GCC直接生成的汇编输出文件（assembly output）同样使用了LI伪指令。不过GCC的LI实际上是LUI（Load Upper Immediate）指令，把数据的高16位传递给寄存器的高半部分。

29.2　阅读推荐

Dominc Sweetman撰写的《See MIPS Run（第二版）》，2010年印刷。

第二部分
硬件基础

第30章 有符号数的表示方法

有符号数通常以二进制补码[1]的形式存储于应用程序里。维基百科介绍了各种表示方法，有兴趣的读者可参见：http://en.wikipedia.org/wiki/Signed_number_representations

。本节摘录了一些典型的单字节数值。

二进制
十六进制
无符号值
有符号值（补码）

01111111
0x7f
127
127

01111110
0x7e
126
126

...

00000010
0x2
2
2

00000001
0x1
1
1

00000000
0x0
0
0

11111111
0xff
253
−1

11111110
0xfe
254
−2

...

10000010
0x82
130
−126

10000001
0x81
129
−127

10000000
0x80
128
−128

如果0xFFFFFFFE和0x0000002都是无符号数，则第一个数（4294967294）就比第二个数（2）大。如果这两个值表示的是有符号数，那么第一个数（−2）反而比第二个数（2）小了。所以，为了正确操作有符号数和无符号数，条件转移指令（参见本书第12章）特地分为JG/JL系列（signed型）和JA/JBE系列（unsigned型）两套指令。

为了便于记忆，我们总结其特点如下。

同一个数值即可以表示有符号数，也可以表示无符号数。

C/C++的有符号型数据有：

int64_t（从−9223372036854775806至9223372036854775807），即从0x8000000000000000至0x7FFFFFFFFFFFFFFF）

int（取值范围从−2147483646至2147483647，即0x80000000至0x7FFFFFFF）。

char（取值范围从−127至128，即从0x7F至0x80）。

ssize_t。

无符号型数据有：

uint64_t（从0至18446744073709551615/0xFFFFFFFFFFFFFFFF）：

unsigned int（取值范围从0至4294967295，即从0至0xFFFFFFFF）。

unsigned char（取值范围从0至255，即从0至0xFF）。

size_t。

有符号数的最高数权位是符号位：1代表负数，0代表正数。

从数据宽度较小的数据转换为数据宽度较大的数据是可行的。可参见前面24.5节。

负数的补码和原码的双向转换过程是相同的，都是逐位求非再加1。这种运算过程简单易记：同一个值的正负数是相反的值，所以要求非；求非之后再加1则是因为中间的“零”占了一个数的位置。

加减运算不区分有符号数和无符号数，它们的加减运算指令完全相同。但是乘除法运算还是有区别的：在x86指令集里，有符号数的乘除指令是IMUL/IDIV，而无符号数的指令是MUL/DIV。

此外，有符号数的操作指令更多一些。例如CBW/CWD/CWDE/CDQ/CDQE（附录A.6.3）、MOVSX（15.1.1节），SAR（附录A.6.3）。

[1]　two’s complement；简称为“补码”。

第31章 字节序

字节序是指多字节类型的数据在内存中的存放顺序，通常可分为小端、大端两种字节序。小端字节序（Little-endian）指低数权字节数据存放在内存低地址处，高数权字节数据存放在内存高地址处的内存分布方式；大端字节序（Big-endian）是高数权字节数据存放在低地址处，低字节数据存放在高地址处的内存分布方式。

31.1　大端字节序

在采用大端字节序时，0x12345678在内存中的存储方式如下所示。

内存地址

字节值

+0

0x12

+1

0x34

+2

0x56

+3

0x78

Motorola 68k、IBM POWER系列CPU采用大端字节序。

31.2　小端字节序

在采用小端字节序时，0x12345678在内存中的存储方式如下所示。

内存地址

字节值

+0

0x78

+1

0x56

+2

0x34

+3

0x12

Intel x86系列CPU采用小端字节序。

31.3　举例说明

为了进行演示，我们可以在QEMU的虚拟化环境中安装MIPS Linux。[1]

接下来在MIPS Linux里编译下述程序：

#include <stdio.h>

int main()
{
         int v, i;

         v=123;

         printf ("%02X %02X %02X %02X\n",
                   *(char*)&v,
                   *(((char*)&v)+1),
                   *(((char*)&v)+2),
                   *(((char*)&v)+3));
};

然后运行下述指令：

root@debian-mips:~# ./a.out
00 00 00 7B

其中，0x7B就是十进制的123。在采用小端字节序的平台上，例如x86或x86-64的系统上，第一个字节就是0x7B。但是MIPS采用的是大端字节序，所以数权最高的这个字节排列在最后。

正是因为MIPS的硬件平台可能采用两种不同的字节序，所以MIPS Linux又分为采用大端字节序的MIPS Linux和采用小端字节序的mipsel Linux。在采取一种字节序的平台上编译出来的程序，不可能在另一种字节序的平台上运行。

本书的21.4.3节就介绍过MIPS大端字节序的特征。

31.4　双模二元数据格式

ARM、PowerPC、SPARC、MIPS、IA64等CPU采用双模二元数据格式（Bi-endian），它们即可以工作于小端字节序也可以切换到大端字节序。

31.5　转换字节序

BSWAP指令可在汇编层面转换数据的字节序。

TCP/IP数据序的封装规范采用大端字节序，所以采用小端字节序平台的系统就需要使用专门的转换字节序的函数。

常用的字节序转换函数是htonl()和htons()。

在TCP/IP的术语里，大端字节序又称为“网络字节顺序（Network Byte Order）”，网络主机采用的字节序叫作“主机字节顺序”。x86和其他一些平台的主机字节序是小端字节序，但是IBM POWER等著名服务器系列均采用大端字节序。因此，在主机字节顺序为大端字节序的平台上使用htonl()或htons()函数转换字节序，其实不会进行真正意义上的字节重排。

[1]　Debian网站提供虚拟机下载：https://people.debian.org/~aurel32/qemu/mips/。

第32章 内存布局

C/C++把内存划分为许多区域，主要的内存区域有：

全局内存空间，又称为“（全局）静态存储区static memory allocation”。编程人员不需要为全局变量和静态变量明确划分存储空间凡是由源程序声明的全局变量、全局数组，编译器都能够在数据段或常量段为其分配适当的存储空间。由于整个程序都可以访问这个区域的数据，所以人们认为使用这种存储空间数据会破坏程序的结构化体系。此外，在全局内存区存储数据之前，必须事先声明其确切的容量。因而这个空间不适用于存储缓存或动态数组。在全局内存空间出现的缓冲区溢出问题，往往将覆盖在内存中相邻位置的变量或缓存（请参阅本书7.2节的案例）。

栈空间，即分配给栈的存储区域。它是由编译器自动分配、释放的存储区域，常用于存放函数的参数和局部变量。在特定情况下，局部变量可被其他函数访问（局部变量的指针作为参数传递给被调用方函数）。在指令层面，“分配和释放栈空间的实质就是调整SP寄存器的值，因而分配和释放栈空间的操作速度非常快。编译器只能为那些在编译阶段确定存储空间的局部变量分配栈空间。因此，无法事先预判存储空间的缓冲型数据类型，即缓冲区和动态数组[1]不会被分配到栈空间里。在栈空间发生的缓冲区溢出问题，通常会篡改栈里的重要数据（请参阅本书18.2节的案例）案例）。

堆空间，即“动态内存分配区。C语言的malloc()/free()函数或C++的new/delete语句即可分配、释放堆空间）。“不必事先声明堆空间的大小”即“可在程序启动以后再确定数据块的容量”这一特征构成了其独特的便利性。另外，程序员还可以动态调整（realloc()函数）内存块的大小，只是调整堆空间的操作速度不很理想。在内存分配操作中，申请、释放堆空间的操作是速度最慢的操作：在分配、释放堆空间时，进行这种操作的程序必须支持并且更新所有控制结构。在这个区域发生的缓冲区溢出经常会覆盖堆空间的数据结构体。堆空间管理不当还会发生内存泄露问题：所有被分配的堆空间都应当被明确地释放，否则就会出现内存泄露问题。但是程序员可能会出现“忘记释放堆空间”的问题，还有发生释放不彻底的问题。另外，“在调用free()函数释放空间之后再次直接使用这块内存区域”的指令同样会带来非常严重的安全问题（请参阅本书21.2节的案例）。

[1]　除非像5.2.4节那样使用alloca()函数。

第33章 CPU

33.1　分支预测

现在的主流编译器基本都不怎么分配条件转移指令了。本书的12.1.2节、12.3节和19.5.2节的编译结果都体现了这一特性。

虽然目前的分支预测功能并不完美，但是编译器还是在向这一方向发展。

ARM平台出现的条件执行指令（例如ADRcc）及x86平台出现的CMOVcc指令，都是这一趋势的明证。

33.2　数据相关性

当代的CPU多数都能并行执行指令（OOE/乱序执行技术）。但是，要充分利用CPV的乱序执行功能、尽可能频繁地同期执行多条指令，首先就要降低各指令之间的数据相关性。所以，编译器尽可能地分配那些不怎么影响CPU标识的指令。

因为LEA指令并不像其他数学运算指令那样影响标识位，所以编译器越来越多地使用这种指令。

第34章 哈希函数

哈希（hash）函数能够生成可靠程度较高的校验和（Checksum），可充分满足数据检验的需要。CRC32算法就是一种不太复杂的哈希算法。哈希值是一种固定长度的信息摘要，不可能根据哈希值逆向“推测”出信息原文。所以，无论CRC32算法计算的信息原文有多长，它只能生成32位的校验和。但是从加密学的角度看，我们可以轻易地伪造出满足同一CRC32哈希值的多个信息原文。当然，防止伪造就是加密哈希函数（Cryptographic hash function）的任务了。

此外，人们普遍使用MD5、SHA1等哈希算法生成用户密码的摘要（哈希值），然后再把密码摘要存储在数据库里。实际上网上论坛等涉及用户密码的数据库，存储的密码信息差不多都是用户密码的哈希值；否则一旦发生数据库泄露等问题，入侵人员将能够轻易地获取密码原文。不仅如此，当用户登录网站的时候，网络论坛等应用程序检验的也不是密码原文，它们检验的还是密码哈希值：如果用户名和密码哈希值与数据库里的记录匹配，它将授予登录用户相应的访问权限。另外，常见的密码破解工具通常都是通过穷举密码的方法，查找符合密码哈希值的密码原文而已。其他类型的密码破解工具就要复杂得多。

单向函数与不可逆算法

单向函数（one-way function）)是一种具有下述特点的单射函数：对于每一个输入，函数值都容易计算（多项式时间）；但是根据函数值对原始输入进行逆向推算却比较困难（无法在多项式时间内使用确定性图灵机计算）。本节着重讲解它的不可逆性。

假设函数的输入值是由10个介于0～9之间的数值构成的一组矢量，且矢量中的标量仅出现一次。例如：

4  6  0  1  3  5  7  8  9  2

下列算法即可实现最简单的单项函数：

取第0位的值作为参数1（本例而言是4）。

取第1位的值作为参数2（本例而言是6）。

交换位于参数1、2位置处（第4、6位）的值。

本例中的第4位和第6位数字分别是：

4  6  0  1  3  5  7  8  9  2
            ^     ^

进行最终变换可得：

4  6  0  1  7  5  3  8  9  2

即使我们知道具体的算法和最终的函数值，我们也无法确定最初的输入值是什么。因为最初的位置参数值可能是0也可能是1，这两个参数可能会被交换位置。

以上只是对单向函数的一种简单说明。实际应用中的单向函数远比本例复杂。

第三部分
一些高级的例子

第35章 温度转换

入门级的编程书籍一般都介绍“华氏温度转换为摄氏温度”的例子。

从华氏度转换成摄氏度的计算公式为

笔者对程序添加了简单的出错处理：

① 输入的温度数必须正确。

② 核查最终结果，确保不会出现绝对零度（−273℃）以下的摄氏温度值。这是中学物理学课本介绍过的一个常识。

说明：调用函数exit()会立即退出本程序，而且不会向调用方函数返回任何值。

35.1　整数值

#include <stdio.h>
#include <stdlib.h>

int main()
{
          int celsius, fahr;
          printf ("Enter temperature in Fahrenheit:\n");
          if (scanf ("%d", &fahr)!=1)
          {
                    printf ("Error while parsing your input\n");
                    exit(0);
          };

          celsius = 5 * (fahr-32) / 9;

          if (celsius<-273)
          {
                    printf ("Error: incorrect temperature!\n");
                    exit(0);
          };
          printf ("Celsius: %d\n", celsius);
};

35.1.1　x86构架下MSVC 2012优化

指令清单35.1　x86构架下MSVC 2012优化

$SG4228 DB      'Enter temperature in Fahrenheit:', 0aH, 00H
$SG4230 DB      '%d', 00H
$SG4231 DB      'Error while parsing your input', 0aH, 00H
$SG4233 DB      'Error: incorrect temperature!', 0aH, 00H
$SG4234 DB      'Celsius: %d', 0aH, 00H

_fahr$ = -4                                            ; size = 4
_main   PROC
        push    ecx
        push    esi
        mov     esi, DWORD PTR __imp__printf
        push    OFFSET $SG4228            ; 'Enter temperature in Fahrenheit:'
        call    esi                       ; call printf()
        lea     eax, DWORD PTR _fahr$[esp+12]
        push    eax
        push    OFFSET $SG4230            ; '%d'
        call    DWORD PTR __imp__scanf
        add     esp, 12                                ; 0000000cH
        cmp     eax, 1
        je      SHORT $LN2@main
        push    OFFSET $SG4231             ; 'Error while parsing your input'
        call    esi                        ; call printf()
        add     esp, 4
        push    0
        call    DWORD PTR __imp__exit
$LN9@main:
$LN2@main:
        mov    eax, DWORD PTR _fahr$[esp+8]
        add    eax, -32                                 ; ffffffe0H
        lea    ecx, DWORD PTR [eax+eax*4]
        mov    eax, 954437177                           ; 38e38e39H
        imul   ecx
        sar    edx, 1
        mov    eax, edx
        shr    eax, 31                                  ; 0000001fH
        add    eax, edx
        cmp    eax, -273                                ; fffffeefH
        jge    SHORT $LN1@main
        push   OFFSET $SG4233             ; 'Error: incorrect temperature!'
        call   esi                        ; call printf()
        add    esp, 4
        push   0
        call   DWORD PTR __imp__exit
$LN10@main:
$LN1@main:
        push   eax
        push   OFFSET $SG4234             ; 'Celsius: %d'
        call   esi                        ; call printf()
        add    esp, 8
        ; return 0 - by C99 standard
        xor    eax, eax
        pop    esi
        pop    ecx
        ret    0
$LN8@main:
_main   ENDP

必须说明的是：

程序首先把printf()函数的内存地址保存到ESI寄存器。在此之后，只要调用“CALL ESI”指令即可调用prinf()函数了。这是非常常见的编译技术，或许是为了方便后续程序频繁调用这个函数，或许是因为还有“不用白不用”的空闲寄存器。

使用加法ADD而不使用减法SUB。我们注意到程序中有一行指令是“ADD EAX,−32”，它用来实现从EAX寄存器中减去32的目的。程序没有采用指令“SUB EAX,32”，也就是说程序使用了EAX=EAX+(−32)的算法，而没采用EAX=EAX−32的算法。是不是值得这么做，笔者并不能完全确定。

为了实现“乘以5”的运算而使用了LEA指令：“lea ecx,DWORD PTR[eax+eax*4]”使得“i+i*4”和“i*5”是相等的。但是指令LEA运行速度比IMUL快。另外还可以使用指令对——SHL EAX,2和ADD EAX,EAX来代替。确实有些编译器是这样做的。

这里也用到了用乘法来代替除法的技巧。参见第41章。

如果主函数main()没有明确的返回值，在程序退出时，它的返回值为0。C99标准中标明“如果main()函数没有通过明确的return指令声明其返回值，那么它将默认返回0”。当然这项规则仅仅适用于主函数main()。虽然MSVC并未声称它完全遵循C99标准，但是或许它部分遵循了这一标准吧。这里指的C99标准是ISO07,P.5.1.2.2.3。

35.1.2　x64构架下的MSVC 2012优化

x64的代码和x86的代码大体相同。只是每次调用exit()函数之后，都有一个INT 3指令。

        xor      ecx, ecx
        call     QWORD PTR __imp_exit
        int      3

INT 3是调试器debugger的断点设置指令。

当程序执行exit()函数之后，它就不会再返回到原程序，而是直接退出了。编译器大概认为，在发生异常退出的情况下，通常人们应当使用调试器分析异常情况吧。

35.2　浮点数运算

#include <stdio.h>
#include <stdlib.h>

int main()
{
         double celsius, fahr;
         printf ("Enter temperature in Fahrenheit:\n");
         if (scanf ("%lf", &fahr)!=1)
         {
                  printf ("Error while parsing your input\n");
                  exit(0);
         };

         celsius = 5 * (fahr-32) / 9;

         if (celsius<-273)
         {
                  printf ("Error: incorrect temperature!\n");
                  exit(0);
         };
         printf ("Celsius: %lf\n", celsius);
};

MSVC 2010 x86采用的是FPU指令。

指令清单35.2　MSVC 2010 x86优化

$SG4038 DB     'Enter temperature in Fahrenheit:', 0aH, 00H
$SG4040 DB     '%lf', 00H
$SG4041 DB     'Error while parsing your input', 0aH, 00H
$SG4043 DB     'Error: incorrect temperature!', 0aH, 00H
$SG4044 DB     'Celsius: %lf', 0aH, 00H

__real@c071100000000000 DQ 0c071100000000000r     ; -273
__real@4022000000000000 DQ 04022000000000000r     ; 9
__real@4014000000000000 DQ 04014000000000000r     ; 5
__real@4040000000000000 DQ 04040000000000000r     ; 32

_fahr$ = -8                                       ; size = 8
_main PROC
      sub     esp, 8
      push    esi
      mov     esi, DWORD PTR __imp__printf
      push    OFFSET $SG4038          ; 'Enter temperature in Fahrenheit:'
      call    esi                     ; call printf()
      lea     eax, DWORD PTR _fahr$[esp+16]
      push    eax
      push    OFFSET $SG4040          ; '%lf'
      call    DWORD PTR __imp__scanf
      add     esp, 12                                    ; 0000000cH
      cmp     eax, 1
      je      SHORT $LN2@main
      push    OFFSET $SG4041          ; 'Error while parsing your input'
      call    esi                     ; call printf()
      add     esp, 4
      push    0
      call    DWORD PTR __imp__exit
$LN2@main:
      fld     QWORD PTR _fahr$[esp+12]
      fsub    QWORD PTR __real@4040000000000000 ; 32
      fmul    QWORD PTR __real@4014000000000000 ; 5
      fdiv    QWORD PTR __real@4022000000000000 ; 9
      fld     QWORD PTR __real@c071100000000000 ; -273
      fcomp   ST(1)
      fnstsw  ax
      test    ah, 65                                     ; 00000041H
      jne     SHORT $LN1@main
      push    OFFSET $SG4043         ; 'Error: incorrect temperature!'
      fstp    ST(0)
      call    esi                    ; call printf()
      add     esp, 4
      push    0
      call    DWORD PTR __imp__exit
$LN1@main:
      sub     esp, 8
      fstp    QWORD PTR [esp]
      push    OFFSET $SG4044         ; 'Celsius: %lf'
      call    esi
      add     esp, 12                                    ; 0000000cH
      ; return 0 - by C99 standard
      xor     eax, eax
      pop     esi
      add     esp, 8
      ret     0
$LN10@main:
_main ENDP

但MSVC 2012分配的却是SIMD指令。

指令清单35.3　MSVC 2012 x86优化

$SG4228 DB      'Enter temperature in Fahrenheit:', 0aH, 00H
$SG4230 DB      '%lf', 00H
$SG4231 DB      'Error while parsing your input', 0aH, 00H
$SG4233 DB      'Error: incorrect temperature!', 0aH, 00H
$SG4234 DB      'Celsius: %lf', 0aH, 00H
__real@c071100000000000 DQ 0c071100000000000r   ; -273
__real@4040000000000000 DQ 04040000000000000r   ; 32
__real@4022000000000000 DQ 04022000000000000r   ; 9
__real@4014000000000000 DQ 04014000000000000r   ; 5

_fahr$ = -8                                         ; size = 8
_main    PROC
         sub     esp, 8
         push    esi
         mov     esi, DWORD PTR __imp__printf
         push    OFFSET $SG4228           ; 'Enter temperature in Fahrenheit:'
         call    esi                      ; call printf()
         lea     eax, DWORD PTR _fahr$[esp+16]
         push    eax
         push    OFFSET $SG4230           ; '%lf'
         call    DWORD PTR __imp__scanf
         add     esp, 12                             ; 0000000cH
         cmp     eax, 1
         je      SHORT $LN2@main
         push    OFFSET $SG4231           ; 'Error while parsing your input'
         call    esi                      ; call printf()
         add     esp, 4
         push    0
         call    DWORD PTR __imp__exit
$LN9@main:
$LN2@main:
         movsd   xmm1, QWORD PTR _fahr$[esp+12]
         subsd   xmm1, QWORD PTR __real@4040000000000000 ; 32
         movsd   xmm0, QWORD PTR __real@c071100000000000 ; -273
         mulsd   xmm1, QWORD PTR __real@4014000000000000 ; 5
         divsd   xmm1, QWORD PTR __real@4022000000000000 ; 9
         comisd  xmm0, xmm1
         jbe     SHORT $LN1@main
         push    OFFSET $SG4233           ; 'Error: incorrect temperature!'
         call    esi                      ; call printf()
         add     esp, 4
         push    0
         call    DWORD PTR __imp__exit
$LN10@main:
$LN1@main:
         sub     esp, 8
         movsd   QWORD PTR [esp], xmm1
         push    OFFSET $SG4234           ; 'Celsius: %lf'
         call    esi                      ; call printf()
         add     esp, 12                                ; 0000000cH
         ; return 0 - by C99 standard
         xor     eax, eax
         pop     esi
         add     esp, 8
         ret     0
$LN8@main:
_main ENDP

当然，x86的指令集确实支持SIMD指令，浮点数运算也毫无问题。大概是这种方式的计算指令比较简单，所以微软的编译器分配了SIMD指令。

我们还注意到绝对零度−273，早早地就导入了寄存器XMM0。这也没关系，编译器不是按照源代码的书写顺序分配的汇编指令。

第36章 斐波拉契数列

在计算机编程方面的教科书里，我们通常都能找到Fibonacci数列（斐波拉契数列）（以下简称为“Fibonacci”）的生成函数。其实这种数列编排的规则非常简单：从第三项开始，后续数字为前两项数字之和。头两项一般为0和1；也有头两项都是1的情况。因此，常见的Fibonacci数列大致如下：

0; 1; 1; 2; 3; 5; 8; 13; 21; 34; 55; 89; 144; 233; 377; 610; 987; 1597; 2584; 4181…

36.1　例子1

Fibonacci的实现方法比较简单。举例来说，下面这个程序可以生成数值不超过21的数列各项：

#include <stdio.h>

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);
};

指令清单36.1　MSVC 2010 x86

_TEXT    SEGMENT
_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 $SG2750 ; "%d"
        call    DWORD PTR __imp__printf
        add     esp, 8
        mov     ecx, DWORD PTR _limit$[ebp]
        push    ecx
        mov     edx, DWORD PTR _a$[ebp]
        add     edx, DWORD PTR _b$[ebp]
        push    edx
        mov     eax, DWORD PTR _b$[ebp]
        push    eax
        call    _fib
        add     esp, 12
        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

我们重点分析这个程序的栈结构。

我们在OllyDbg中加载本例生成的可执行程序，并跟踪到调用f()函数的那条指令，如图36.1所示。

图36.1　OllyDbg最后一个函数f()

让我们来仔细分析栈里的内容。在下面的程序行中，笔者用打括弧的方法加了两个注释。一个是main()为f1()做准备，另外一个是CRT为main()做准备[1]。

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 for main()
0035F9C4    |00682940   main() 3rd argument: envp  /

本例属于递归函数[2]。递归函数的栈一般都是这样的“三明治”结构。在上述程序中，limit（阈值）参数总是保持不变（十六进制的14，也就是十进制的20），而两个参数a和b在每次调用函数的时候都是不同的值。此外栈也存储了RA和保存EBP（扩展堆栈指针）的值。OllyDbg能基于EBP的值判断栈的存储结构，因此它能够用括弧标注栈帧。换而言之，在每个括弧里的一组数值都形成了一个相对独立的栈结构，即栈帧（stack frame）。栈帧就是每次函数调用期间的数据实体。另一方面，即使从纯萃技术方面看每个被调用方函数确实可以访问栈帧之外的栈存储空间，但是正常情况下不应当访问栈帧之外的数据（当然除了获取函数参数的操作以外）。对于没有bug（缺陷）的函数来说，上述命题的确成立。每一个 EBP值都是前一个栈帧的地址。因此调试程序能够把数据栈识别为栈帧，并能识别出每次调用函数时传递的参数值。

结合上述指令可知，递归函数应当为下一轮的自身调用制备各项参数。

在程序的最后部分，main()有3个参数。其中argc（参数总数）为1。确实如此，笔者的确未带参数直接运行并调试本程序。

另外，调整本程序、引发栈溢出的过程并不复杂：我们只需要删除或者注释掉阈值limit判断语句，即可导致栈溢出（即错误编号为0xC00000FD的异常错误）。

36.2　例子2

上一节的函数有些冗余。接下来，我们增加一个新的局部变量next，并用它来代替所有程序中的a+b:

#include <stdio.h>

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变量确实是在本地栈中分配存储空间。

指令清单36.2　MSVC 2010 x86

_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

我们再来调用OllyDbg，如图36.2所示。

图36.2　OllyDbg: 最后调用f()

现在，每个栈帧里都有一个变量next。

我们来仔细看看堆栈。笔者依然给其中增加了注释。

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 for main()
0029FCAC   |00082940   main() 3rd argument: envp   /

这里我们看到：递归函数在每次调用期间都会计算并传递下一轮调用所需的函数参数。

36.3　总结

递归函数只是看起来很帅而已。从技术上讲，递归函数在栈方面的开销过大，因而性能不怎么理想。注重性能指标的应用程序，应当避免使用递归函数。

笔者曾经编写过一个遍历二叉树、搜索既定节点的应用程序。把它写成递归函数的时候，整个程序确实又清爽又有条理性。但是每次函数调用都得进行赋值、回调，这使得递归函数比其他类型的函数慢了数倍。

另外，部分PL[3]编译器会对递归调用采取“尾部调用”的优化方法，以减轻栈的各种开销。

[1]　在OllyDbg中，可以选择多项指令，再使用Ctrl+C组合键把它们复制到剪切板中。本例就是这样做的。

[2]　也就是自己调用自己。

[3]　PL：Program Language（编程语言）。LISP、Python、Lua等编程语言的编译器能够进行尾部调用优化。详情请参阅https://en. wikipedia.org/wiki/Tail_call。

第37章 CRC32计算的例子

本章介绍一个基于表查询技术实现的CRC32校验值的计算程序：[1]

/* By Bob Jenkins, (c) 2006, Public Domain */

#include <stdio.h>
#include <stddef.h>
#include <string.h>

typedef  unsigned long ub4;
typedef  unsigned char ub1;

static const ub4 crctab[256] = {
  0x00000000, 0x77073096, 0xee0e612c, 0x990951ba, 0x076dc419, 0x706af48f,
  0xe963a535, 0x9e6495a3, 0x0edb8832, 0x79dcb8a4, 0xe0d5e91e, 0x97d2d988,
  0x09b64c2b, 0x7eb17cbd, 0xe7b82d07, 0x90bf1d91, 0x1db71064, 0x6ab020f2,
  0xf3b97148, 0x84be41de, 0x1adad47d, 0x6ddde4eb, 0xf4d4b551, 0x83d385c7,
  0x136c9856, 0x646ba8c0, 0xfd62f97a, 0x8a65c9ec, 0x14015c4f, 0x63066cd9,
  0xfa0f3d63, 0x8d080df5, 0x3b6e20c8, 0x4c69105e, 0xd56041e4, 0xa2677172,
  0x3c03e4d1, 0x4b04d447, 0xd20d85fd, 0xa50ab56b, 0x35b5a8fa, 0x42b2986c,
  0xdbbbc9d6, 0xacbcf940, 0x32d86ce3, 0x45df5c75, 0xdcd60dcf, 0xabd13d59,
  0x26d930ac, 0x51de003a, 0xc8d75180, 0xbfd06116, 0x21b4f4b5, 0x56b3c423,
  0xcfba9599, 0xb8bda50f, 0x2802b89e, 0x5f058808, 0xc60cd9b2, 0xb10be924,
  0x2f6f7c87, 0x58684c11, 0xc1611dab, 0xb6662d3d, 0x76dc4190, 0x01db7106,
  0x98d220bc, 0xefd5102a, 0x71b18589, 0x06b6b51f, 0x9fbfe4a5, 0xe8b8d433,
  0x7807c9a2, 0x0f00f934, 0x9609a88e, 0xe10e9818, 0x7f6a0dbb, 0x086d3d2d,
  0x91646c97, 0xe6635c01, 0x6b6b51f4, 0x1c6c6162, 0x856530d8, 0xf262004e,
  0x6c0695ed, 0x1b01a57b, 0x8208f4c1, 0xf50fc457, 0x65b0d9c6, 0x12b7e950,
  0x8bbeb8ea, 0xfcb9887c, 0x62dd1ddf, 0x15da2d49, 0x8cd37cf3, 0xfbd44c65,
  0x4db26158, 0x3ab551ce, 0xa3bc0074, 0xd4bb30e2, 0x4adfa541, 0x3dd895d7,
  0xa4d1c46d, 0xd3d6f4fb, 0x4369e96a, 0x346ed9fc, 0xad678846, 0xda60b8d0,
  0x44042d73, 0x33031de5, 0xaa0a4c5f, 0xdd0d7cc9, 0x5005713c, 0x270241aa,
  0xbe0b1010, 0xc90c2086, 0x5768b525, 0x206f85b3, 0xb966d409, 0xce61e49f,
  0x5edef90e, 0x29d9c998, 0xb0d09822, 0xc7d7a8b4, 0x59b33d17, 0x2eb40d81,
  0xb7bd5c3b, 0xc0ba6cad, 0xedb88320, 0x9abfb3b6, 0x03b6e20c, 0x74b1d29a,
  0xead54739, 0x9dd277af, 0x04db2615, 0x73dc1683, 0xe3630b12, 0x94643b84,
  0x0d6d6a3e, 0x7a6a5aa8, 0xe40ecf0b, 0x9309ff9d, 0x0a00ae27, 0x7d079eb1,
  0xf00f9344, 0x8708a3d2, 0x1e01f268, 0x6906c2fe, 0xf762575d, 0x806567cb,
  0x196c3671, 0x6e6b06e7, 0xfed41b76, 0x89d32be0, 0x10da7a5a, 0x67dd4acc,
  0xf9b9df6f, 0x8ebeeff9, 0x17b7be43, 0x60b08ed5, 0xd6d6a3e8, 0xa1d1937e,
  0x38d8c2c4, 0x4fdff252, 0xd1bb67f1, 0xa6bc5767, 0x3fb506dd, 0x48b2364b,
  0xd80d2bda, 0xaf0a1b4c, 0x36034af6, 0x41047a60, 0xdf60efc3, 0xa867df55,
  0x316e8eef, 0x4669be79, 0xcb61b38c, 0xbc66831a, 0x256fd2a0, 0x5268e236,
  0xcc0c7795, 0xbb0b4703, 0x220216b9, 0x5505262f, 0xc5ba3bbe, 0xb2bd0b28,
  0x2bb45a92, 0x5cb36a04, 0xc2d7ffa7, 0xb5d0cf31, 0x2cd99e8b, 0x5bdeae1d,
  0x9b64c2b0, 0xec63f226, 0x756aa39c, 0x026d930a, 0x9c0906a9, 0xeb0e363f,
  0x72076785, 0x05005713, 0x95bf4a82, 0xe2b87a14, 0x7bb12bae, 0x0cb61b38,
  0x92d28e9b, 0xe5d5be0d, 0x7cdcefb7, 0x0bdbdf21, 0x86d3d2d4, 0xf1d4e242,
  0x68ddb3f8, 0x1fda836e, 0x81be16cd, 0xf6b9265b, 0x6fb077e1, 0x18b74777,
  0x88085ae6, 0xff0f6a70, 0x66063bca, 0x11010b5c, 0x8f659eff, 0xf862ae69,
  0x616bffd3, 0x166ccf45, 0xa00ae278, 0xd70dd2ee, 0x4e048354, 0x3903b3c2,
  0xa7672661, 0xd06016f7, 0x4969474d, 0x3e6e77db, 0xaed16a4a, 0xd9d65adc,
  0x40df0b66, 0x37d83bf0, 0xa9bcae53, 0xdebb9ec5, 0x47b2cf7f, 0x30b5ffe9,
  0xbdbdf21c, 0xcabac28a, 0x53b39330, 0x24b4a3a6, 0xbad03605, 0xcdd70693,
  0x54de5729, 0x23d967bf, 0xb3667a2e, 0xc4614ab8, 0x5d681b02, 0x2a6f2b94,
  0xb40bbe37, 0xc30c8ea1, 0x5a05df1b, 0x2d02ef8d,
};

/* how to derive the values in crctab[] from polynomial 0xedb88320 */
void build_table()
{
  ub4 i, j;
  for (i=0; i<256; ++i) {
    j = i;
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    j = (j>>1) ^ ((j&1) ? 0xedb88320 : 0);
    printf("0x%.8lx, ", j);
    if (i%6 == 5) printf("\n");
  }
}

/* the hash function */
ub4 crc(const void *key, ub4 len, ub4 hash)
{
  ub4 i;
  const ub1 *k = key;
  for (hash=len, i=0; i<len; ++i)
    hash = (hash >> 8) ^ crctab[(hash & 0xff) ^ k[i]];
  return hash;
}

/* To use, try "gcc -O crc.c -o crc; crc < crc.c" */
int main()
{
  char s[1000];
  while (gets(s)) printf("%.8lx\n", crc(s, strlen(s), 0));
  return 0;
}

我们这里只关心校验函数crc()的详细细节。另外，请注意for()语句的两个初始指令“hash=len”和 “i=0”。当然，在C/C++语言里，我们可以一次指定两条循环初始指令。在最终的汇编指令层面，也就会出现两条初始化指令，而不会是一条指令。

下面我们采用优化的方式（/0x）来编译。为简化起见，这里只列出crc()函数，同时还增加了笔者的注释。

_key$ = 8             ; size = 4
_len$ = 12            ; size = 4
_hash$ = 16           ; size = 4
_crc    PROC
    mov    edx, DWORD PTR _len$[esp-4]
    xor    ecx, ecx ; i will be stored in ECX
    mov    eax, edx
    test   edx, edx
    jbe    SHORT $LN1@crc
    push   ebx
    push   esi
    mov    esi, DWORD PTR _key$[esp+4] ; ESI = key
    push   edi
$LL3@crc:

; work with bytes using only 32-bit registers. byte from address key+i we store into EDI

    movzx  edi, BYTE PTR [ecx+esi]
    mov    ebx, eax ; EBX = (hash = len)
    and    ebx, 255 ; EBX = hash & 0xff

; XOR EDI, EBX (EDI=EDI^EBX) - this operation uses all 32 bits of each register
; but other bits (8-31) are cleared all time, so its OK'
; these are cleared because, as for EDI, it was done by MOVZX instruction above
; high bits of EBX was cleared by AND EBX, 255 instruction above (255 = 0xff)

    xor    edi, ebx

; EAX=EAX>>8; bits 24-31 taken "from nowhere" will be cleared
    shr    eax, 8

; EAX=EAX^crctab[EDI*4] - choose EDI-th element from crctab[] table
    xor    eax, DWORD PTR _crctab[edi*4]
    inc    ecx            ; i++
    cmp    ecx, edx       ; i<len ?
    jb     SHORT $LL3@crc ; yes
    pop    edi
    pop    esi
    pop    ebx
$LN1@crc:
    ret    0
_crc    ENDP

在GCC 4.4.1环境下，启用优化选项-03编译，可得到的下述代码。

                 public crc
crc              proc near

key              = dword ptr 8
hash             = dword ptr 0Ch

                 push    ebp
                 xor     edx, edx
                 mov     ebp, esp
                 push    esi
                 mov     esi, [ebp+key]
                 push    ebx
                 mov     ebx, [ebp+hash]
                 test    ebx, ebx
                 mov     eax, ebx
                 jz      short loc_80484D3
                 nop                       ; padding
                 lea     esi, [esi+0]      ; padding; works as NOP (ESI does not changing here)

loc_80484B8:
                 mov     ecx, eax          ; save previous state of hash to ECX
                 xor     al, [esi+edx]     ; AL=*(key+i)
                 add     edx, 1            ; i++
                 shr     ecx, 8            ; ECX=hash>>8
                 movzx   eax, al           ; EAX=*(key+i)
                 mov     eax, dword ptr ds:crctab[eax*4] ; EAX=crctab[EAX]
                 xor     eax, ecx          ; hash=EAX^ECX
                 cmp     ebx, edx
                 ja      short loc_80484B8
loc_80484D3:
                 pop     ebx
                 pop     esi
                 pop     ebp
                 retn
crc              endp
\

GCC增加了空指令NOP以及LEA esi,[esi+0]（这实际也是一个空指令），以此使得循环语句向8字节对齐。另外，编译器通常还会采用npad指令进行边界对齐。有关详情请参阅本书第88章。

[1]　源代码来源于http://go.yurichev.com/17327。

第38章 网络地址计算实例

众所周知，IPv4下的TCP/IP地址由4个数字组成，每个数字都在0～255（十进制）之间。所以IPv4的地址可以表示为4字节数据。4字节数据就是一个32位数据。因此IPv4的主机地址、子网掩码和网络地址都可以表示为一个32位的整数。

从使用者的角度来看，子网掩码由4位数字组成，写出来大致就是255.255.255.0这类形式的数字。但是网络工程师或者系统管理员更喜欢使用更为紧凑的表示方法，也就是CIDR[1]规范的“/8”“/16”一类的表示方法。CIDR格式的子网掩码从子网掩码的MSB（最高数权位）开始计数，统计子网掩码里面有多少个1并将统计数字转换为10进制数。

CIDK规范的掩码

数字空间

可用地址（个）[2]

十进制子网掩码

十六进制子网掩码

/30

4

2

255.255.255.252

fffffffc

/29

8

6

255.255.255.248

fffffff8

/28

16

14

255.255.255.240

fffffff0

/27

32

30

255.255.255.224

ffffffe0

/26

64

62

255.255.255.192

ffffffc0

/24

256

254

255.255.255.0

ffffff00

C类网段

/23

512

510

255.255.254.0

fffffe00

/22

1024

1022

255.255.252.0

fffffc00

/21

2048

2046

255.255.248.0

fffff800

/20

4096

4094

255.255.240.0

fffff000

/19

8192

8190

255.255.224.0

ffffe000

/18

16384

16382

255.255.192.0

ffffc000

/17

32768

32766

255.255.128.0

ffff8000

/16

65536

65534

255.255.0.0

ffff0000

B类网段

/8

16777216

16777214

255.0.0.0

ff000000

A类网段

这里举一个简单的例子：将子网掩码应用到主机地址，从而计算的网络地址。

#include <stdio.h>
#include <stdint.h>

uint32_t form_IP (uint8_t ip1, uint8_t ip2, uint8_t ip3, uint8_t ip4)
{
          return (ip1<<24) | (ip2<<16) | (ip3<<8) | ip4;
};

void print_as_IP (uint32_t a)
{
          printf ("%d.%d.%d.%d\n",
                   (a>>24)&0xFF,
                   (a>>16)&0xFF,
                   (a>>8)&0xFF,
                   (a)&0xFF);
};

// bit=31..0
uint32_t set_bit (uint32_t input, int bit)
{
          return input=input|(1<<bit);
};

uint32_t form_netmask (uint8_t netmask_bits)
{
          uint32_t netmask=0;
          uint8_t i;

          for (i=0; i<netmask_bits; i++)
                    netmask=set_bit(netmask, 31-i);

          return netmask;
};

void calc_network_address (uint8_t ip1, uint8_t ip2, uint8_t ip3, uint8_t  ip4, uint8_t netmask_bits)
{
          uint32_t netmask=form_netmask(netmask_bits);
          uint32_t ip=form_IP(ip1, ip2, ip3, ip4);
          uint32_t netw_adr;

          printf ("netmask=");
          print_as_IP (netmask);

          netw_adr=ip&netmask;

          printf ("network address=");
          print_as_IP (netw_adr);
};

int main()
{
          calc_network_address (10, 1, 2, 4, 24);       // 10.1.2.4, /24
          calc_network_address (10, 1, 2, 4, 8);       // 10.1.2.4, /8
          calc_network_address (10, 1, 2, 4, 25);      // 10.1.2.4, /25
          calc_network_address (10, 1, 2, 64, 26);     // 10.1.2.4, /26
};

38.1　计算网络地址函数calc_network_address()

计算网络地址函数calc_network_address()实现起来非常简单：它将主机地址和网络子网掩码进行AND与运算，得到的结果就是网络的实际地址。

指令清单38.1　MSVC 2012采用参数/Ob0优化

 1  _ip1$ = 8              ; size = 1
 2  _ip2$ = 12             ; size = 1
 3  _ip3$ = 16             ; size = 1
 4  _ip4$ = 20             ; size = 1
 5  _netmask_bits$ = 24    ; size = 1
 6  _calc_network_address PROC
 7         push     edi
 8         push     DWORD PTR _netmask_bits$[esp]
 9         call     _form_netmask
10         push     OFFSET $SG3045   ; 'netmask='
11         mov      edi, eax
12         call     DWORD PTR __imp__printf
13         push     edi
14         call     _print_as_IP
15         push     OFFSET $SG3046   ; 'network address='
16         call     DWORD PTR __imp__printf
17         push     DWORD PTR _ip4$[esp+16]
18         push     DWORD PTR _ip3$[esp+20]
19         push     DWORD PTR _ip2$[esp+24]
20         push     DWORD PTR _ip1$[esp+28]
21         call     _form_IP
22         and      eax, edi         ; network address = host address & netmask
23         push     eax
24         call     _print_as_IP
25         add      esp, 36
26         pop      edi
27         ret      0
28  _calc_network_address ENDP

在第22行，我们可以看到最为重要的运算指令AND。就是它计算出了网络地址，实现了核心功能。

38.2　函数form_IP()

form_IP()函数将IP的4个字节转换成一个32位数值。

它的运算流程如下：

给返回值分配一个变量，并赋值为0。

取数权最低的第4个字节，与返回值0进行OR/或操作，即可得到含有第4字节信息的32位值。

取第3个字节，左移8位，以生成0x0000bb00（其中bb就是这步读取的第三字节）这种形式的数值。此后与返回值进行OR/或运算。如果上一步的值如果是0x000000aa的话，在执行OR或操作后，就会得到0x0000bbaa这样的返回值。

依此类推。取第2个字节，左移16位，生成0x00cc0000这样一个含有第2字节的32位值，再进行OR/或运算。由于以上一步的返回值应当是0x0000bbaa，因此本次运算的结果会是0x00ccbbaa。

同理。取最高位，左移24位，以生成0xdd000000这样一个含有第一字节信息的32位值，再进行OR/或运算。由于上一步的返回值是0x00ccbbaa，因此最终的结果的值就是0xddccbbaa这样的32位值了。

经MSVC 2012进行非优化编译，可得到下述指令：

指令清单38.2　非优化的MSVC2012的实现

; denote ip1 as "dd", ip2 as "cc", ip3 as "bb", ip4 as "aa".
_ip1$ = 8        ; size = 1
_ip2$ = 12       ; size = 1
_ip3$ = 16       ; size = 1
_ip4$ = 20       ; size = 1
_form_IP PROC
         push    ebp
         mov     ebp, esp
         movzx   eax, BYTE PTR _ip1$[ebp]
         ; EAX=000000dd
         shl     eax, 24
         ; EAX=dd000000
         movzx   ecx, BYTE PTR _ip2$[ebp]
         ; ECX=000000cc
         shl     ecx, 16
         ; ECX=00cc0000
         or      eax, ecx
         ; EAX=ddcc0000
         movzx   edx, BYTE PTR _ip3$[ebp]
         ; EDX=000000bb
         shl     edx, 8
         ; EDX=0000bb00
         or      eax, edx
         ; EAX=ddccbb00
         movzx   ecx, BYTE PTR _ip4$[ebp]
         ; ECX=000000aa
         or      eax, ecx
         ; EAX=ddccbbaa
         pop     ebp
         ret     0
_form_IP ENDP

这里操作的顺序不同。当然这不会影响最后的运算结果。

在启用优化选项之后，MSVC 2012会生成另一种算法的应用程序：

指令清单38.3　优化的MSVC2012带参数/Ob0的实现

; denote ip1 as "dd", ip2 as "cc", ip3 as "bb", ip4 as "aa".
_ip1$ = 8        ; size = 1
_ip2$ = 12       ; size = 1
_ip3$ = 16       ; size = 1
_ip4$ = 20       ; size = 1
_form_IP PROC
        movzx   eax, BYTE PTR _ip1$[esp-4]
        ; EAX=000000dd
        movzx   ecx, BYTE PTR _ip2$[esp-4]
        ; ECX=000000cc
        shl     eax, 8
        ; EAX=0000dd00
        or      eax, ecx
        ; EAX=0000ddcc
        movzx   ecx, BYTE PTR _ip3$[esp-4]
        ; ECX=000000bb
        shl     eax, 8
        ; EAX=00ddcc00
        or      eax, ecx
        ; EAX=00ddccbb
        movzx   ecx, BYTE PTR _ip4$[esp-4]
        ; ECX=000000aa
        shl     eax, 8
        ; EAX=ddccbb00
        or      eax, ecx
        ; EAX=ddccbbaa
        ret     0
_form_IP ENDP

这个实现过程还可以描述为：每个字节都写入到其返回值的最低8个比特位，并且每次左移一个字节，并将其与返回值做或操作。重复四次，就能完成函数功能。

就这样了。然而遗憾的是，可能没其他的办法来实现以上的逻辑了。据笔者所知，目前的CPU及其ISA还不能把既定比特位或者字节直接复制到其他类型数据里。所以一般都是通过位移和OR或运算才能把IP地址转换为32位数据。

38.3　函数print_as_IP()

函数print_as_IP()实现的功能与上面函数完全相反，它将一个32位的数值切分成4个字节。切分过程比较简单：只需要将输入的数值分别位移24位、16位、8位或者0位，取最低字节的0到7位即可。

指令清单38.4　非优化MSVC 2012

_a$ = 8                   ; size = 4
_print_as_IP PROC
         push    ebp
         mov     ebp, esp
         mov     eax, DWORD PTR _a$[ebp]
         ; EAX=ddccbbaa
         and     eax, 255
         ; EAX=000000aa
         push    eax
         mov     ecx, DWORD PTR _a$[ebp]
         ; ECX=ddccbbaa
         shr     ecx, 8
         ; ECX=00ddccbb
         and     ecx, 255
         ; ECX=000000bb
         push    ecx
         mov     edx, DWORD PTR _a$[ebp]
         ; EDX=ddccbbaa
         shr     edx, 16
         ; EDX=0000ddcc
         and     edx, 255
         ; EDX=000000cc
         push    edx
         mov     eax, DWORD PTR _a$[ebp]
         ; EAX=ddccbbaa
         shr     eax, 24
         ; EAX=000000dd
         and     eax, 255 ; probably redundant instruction
         ; EAX=000000dd
         push    eax
         push    OFFSET $SG2973 ; '%d.%d.%d.%d'
         call    DWORD PTR __imp__printf
         add     esp, 20
         pop     ebp
         ret     0
_print_as_IP ENDP

优化MSVC 2012程序做的和上面的一样，但是它不会重新加载输入值。

指令清单38.5　优化MSVC 2012 /Ob0

_a$ = 8          ; size = 4
_print_as_IP PROC
         mov     ecx, DWORD PTR _a$[esp-4]
         ; ECX=ddccbbaa
         movzx   eax, cl
         ; EAX=000000aa
         push    eax
         mov     eax, ecx
         ; EAX=ddccbbaa
         shr     eax, 8
         ; EAX=00ddccbb
         and     eax, 255
         ; EAX=000000bb
         push    eax
         mov     eax, ecx
         ; EAX=ddccbbaa
         shr     eax, 16
         ; EAX=0000ddcc
         and     eax, 255
         ; EAX=000000cc
         push    eax
         ; ECX=ddccbbaa
         shr     ecx, 24
         ; ECX=000000dd
         push    ecx
         push    OFFSET $SG3020 ; '%d.%d.%d.%d'
         call    DWORD PTR __imp__printf
         add     esp, 20
         ret     0
_print_as_IP ENDP

38.4　form_netmask()函数和set_bit()函数

form_netmask()函数从网络短地址CIDR中获取网络子网掩码。当然，或许事先计算出一个查询表、转换的时候进行表查询的速度可能会更快。但是为了演示位移运算的特征，本节特意采用了这种现场计算的转换方法。我们这里还编写了一个函数set_bit()。虽说格式转换这种底层运算本来不应当调用其他函数了，但是笔者相信set_bit()函数可以提高代码的可读性。

指令清单38.6　优化MSVC 2012 /Ob0

_input$ = 8          ; size = 4
_bit$ = 12           ; size = 4
_set_bit PROC
        mov     ecx, DWORD PTR _bit$[esp-4]
        mov     eax, 1
        shl     eax, cl
        or      eax, DWORD PTR _input$[esp-4]
        ret     0
_set_bit ENDP

_netmask_bits$ = 8                ; size = 1
_form_netmask PROC
        push    ebx
        push    esi
        movzx   esi, BYTE PTR _netmask_bits$[esp+4]
        xor     ecx, ecx
        xor     bl, bl
        test    esi, esi
        jle     SHORT $LN9@form_netma
        xor     edx, edx
$LL3@form_netma:
        mov     eax, 31
        sub     eax, edx
        push    eax
        push    ecx
        call    _set_bit
        inc     bl
        movzx   edx, bl
        add     esp, 8
        mov     ecx, eax
        cmp     edx, esi
        jl      SHORT $LL3@form_netma
$LN9@form_netma:
        pop     esi
        mov     eax, ecx
        pop     ebx
        ret     0
_form_netmask ENDP

set_bit()函数的功能十分单一。它将输入值左移既定的比特位，接着将位移运算的结果与输入值进行或OR运算。而后form_mask()函数通过循环语句重复调用set_bit()函数，借助循环控制变量netmask_bits设置子网掩码里数值为1的各个比特位。

38.5　总结

上述程序的结果如下所示。

netmask=255.255.255.0
network address=10.1.2.0
netmask=255.0.0.0
network address=10.0.0.0
netmask=255.255.255.128
network address=10.1.2.0
netmask=255.255.255.192
network address=10.1.2.64

[1]　CIDR是Classless Inter-Domain Routing的缩写，即无类域间路由。

[2]　可用地址二数字空间-2。

第39章 循环：几个迭代

多数循环语句只有一个迭代器。但是在汇编层面，一个迭代器也可能对应多个数据实体。

下面所示的是一个简单的例子。

#include <stdio.h>

void f(int *a1, int *a2, size_t cnt)
{
       size_t i;

       // copy from one array to another in some weird scheme
       for (i=0; i<cnt; i++)
              a1[i*3]=a2[i*7];
};

我们可以看到，每次迭代都有两次乘法运算，这是很耗费时间的操作。能不能优化一下呢？答案是肯定的。如果仔细看看程序代码，就会发现这个程序中的矩阵的参数是跳跃的，我们能比较容易地不用乘法就将它计算出来。

39.1　三个迭代器

指令清单39.1　采用MSVC 2013 x64优化的代码

f        PROC
; RDX=a1
; RCX=a2
; R8=cnt
         test      r8, r8         ; cnt==0? exit then
         je        SHORT $LN1@f
         npad      11
$LL3@f:
         mov       eax, DWORD PTR [rdx]
         lea       rcx, QWORD PTR [rcx+12]
         lea       rdx, QWORD PTR [rdx+28]
         mov       DWORD PTR [rcx-12], eax
         dec       r8
         jne       SHORT $LL3@f
$LN1@f:
         ret       0
f        ENDP

这里有三个迭代变量，它们是cnt变量以及2个数列参数（索引游标）。数列参数每次迭代都增加12或者28（其实这就是采用加法代替了源程序中的乘法）。因此我们可以采用C/C++语言重写代码如下。

#include <stdio.h>

void f(int *a1, int *a2, size_t cnt)
{
        size_t i;
        size_t idx1=0; idx2=0;

        // copy from one array to another in some weird scheme
        for (i=0; i<cnt; i++)
        {
               a1[idx1]=a2[idx2];
               idx1+=3;
               idx2+=7;
        };
};

这个程序可在每次迭代更新3个迭代参数，而不是1个。另外，编译器变相实现了两个乘法操作。

39.2　两个迭代器

GCC 4.9可以做得更好，只有两个迭代。

指令清单39.2　采用GCC 4.9 x64优化

; RDI=a1
; RSI=a2
; RDX=cnt
f:
        test     rdx, rdx ; cnt==0? exit then
        je       .L1
; calculate last element address in "a2" and leave it in RDX
        lea      rax, [0+rdx*4]
; RAX=RDX*4=cnt*4
        sal      rdx, 5
; RDX=RDX<<5=cnt*32
        sub      rdx, rax
; RDX=RDX-RAX=cnt*32-cnt*4=cnt*28
        add      rdx, rsi
; RDX=RDX+RSI=a2+cnt*28
.L3:
        mov      eax, DWORD PTR [rsi]
        add      rsi, 28
        add      rdi, 12
        mov      DWORD PTR [rdi-12], eax
        cmp      rsi, rdx
        jne      .L3
.L1:
        rep ret

这里没有计数器变量counter了，这是因为GCC编译器认为它没有必要。数组a2的最后一个参数在循环开始前就已经计算好了（很容易，其实就是cnt乘以7）。而且循环的结束条件也不复杂：第二个索引号指数达到那个可预先计算出来的临界值时，循环语句随即终止迭代。

其中涉及一些采用加/减/位移等办法来替代乘法运算的知识，有兴趣的读者请参阅本书16.1.3节。

上述汇编代码与下述C/C++程序相对应：

#include <stdio.h>

void f(int *a1, int *a2, size_t cnt)
{
        size_t i;
        size_t idx1=0; idx2=0;
        size_t last_idx2=cnt*7;

        // copy from one array to another in some weird scheme
        for (;;)
        {
                a1[idx1]=a2[idx2];
                idx1+=3;
                idx2+=7;
                if (idx2==last_idx2)
                        break;
        };
};

ARM64下的GCC(Linaro) 4.9采用了同种类型的编译方法。但是它计算的是数组a1的最后一个索引值，而不是像上面的程序那样以数组a2为边界条件。当然，程序的功能最终还是一样的。

指令清单39.3　ARM64下的GCC(Linaro) 4.9优化

; X0=a1
; X1=a2
; X2=cnt
f:
        cbz     x2, .L1             ; cnt==0? exit then
; calculate last element of "a1" array
        add     x2, x2, x2, lsl 1
; X2=X2+X2<<1=X2+X2*2=X2*3
        mov     x3, 0
        lsl     x2, x2, 2
; X2=X2<<2=X2*4=X2*3*4=X2*12
.L3:
        ldr     w4, [x1],28         ; load at X1, add 28 to X1 (post-increment)
        str     w4, [x0,x3]         ; store at X0+X3=a1+X3
        add     x3, x3, 12          ; shift X3
        cmp     x3, x2              ; end?
        bne     .L3
.L1:
        ret

MIPS下的GCC 4.4.5也差不多如此。

指令清单39.4　MIPS(IDA)下的GCC 4.4.5优化

; $a0=a1
; $a1=a2
; $a2=cnt
f:
; jump to loop check code:
               beqz     $a2, locret_24
; initialize counter (i) at 0:
               move     $v0, $zero ; branch delay slot, NOP
loc_8:
; load 32-bit word at $a1
               lw       $a3, 0($a1)
; increment counter (i):
               addiu    $v0, 1
; check for finish (compare "i" in $v0 and "cnt" in $a2):
               sltu     $v1, $v0, $a2
; store 32-bit word at $a0:
               sw        $a3, 0($a0)
; add 0x1C (28) to \$a1 at each iteration:
               addiu    $a1, 0x1C
; jump to loop body if i<cnt:
               bnez     $v1, loc_8
; add 0xC (12) to \$a0 at each iteration:
               addiu    $a0, 0xC ; branch delay slot
locret_24:
               jr       $ra
               or       $at, $zero ; branch delay slot, NOP

39.3　Intel C++ 2011实例

编译器的优化操作有时候会非常奇怪。但是无论它们采用了何种优化方式，程序的功能肯定忠于源程序。这里列出的是Intel C++ 2011编译器如何操作的例子。

指令清单39.5　Intel C++ 2011 (x64)优化

f        PROC
; parameter 1: rcx = a1
; parameter 2: rdx = a2
; parameter 3: r8 = cnt
.B1.1::                               ; Preds .B1.0
         test        r8, r8                                       ;8.14
         jbe         exit             ; Prob 50%                  ;8.14
                                      ; LOE rdx rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm6 xmm7 xmm8 ↙
    ↘ xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
.B1.2::                               ; Preds .B1.1
         cmp         r8, 6                                        ;8.2
         jbe         just_copy        ; Prob 50%                  ;8.2
                                      ; LOE rdx rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm6 xmm7 xmm8 ↙
    ↘ xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
.B1.3::                              ; Preds .B1.2
         cmp         rcx, rdx                                     ;9.11
         jbe         .B1.5　         ; Prob 50%                   ;9.11
                                     ; LOE rdx rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm6 xmm7 xmm8 ↙
    ↘ xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
.B1.4::                              ; Preds .B1.3
         mov         r10, r8                                      ;9.11
         mov         r9, rcx                                      ;9.11
         shl         r10, 5                                       ;9.11
         lea         rax, QWORD PTR [r8*4]                        ;9.11
         sub         r9, rdx                                      ;9.11
         sub         r10, rax                                     ;9.11
         cmp         r9, r10                                      ;9.11
         jge         just_copy2      ; Prob 50%                   ;9.11
                                     ; LOE rdx rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm6 xmm7 xmm8 ↙
    ↘ xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
.B1.5::                              ; Preds .B1.3 .B1.4
         cmp         rdx, rcx                                     ;9.11
         jbe         just_copy       ; Prob 50%                   ;9.11
                                     ; LOE rdx rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm6 xmm7 xmm8 ↙
    ↘ xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
.B1.6::                              ; Preds .B1.5
         mov         r9, rdx                                      ;9.11
         lea         rax, QWORD PTR [r8*8]                        ;9.11
         sub         r9, rcx                                      ;9.11
         lea         r10, QWORD PTR [rax+r8*4]                    ;9.11
         cmp         r9, r10                                      ;9.11
         jl          just_copy       ; Prob 50%                   ;9.11
                                     ; LOE rdx rcx rbx rbp rsi rdi r8 r12 r13 r14 r15 xmm6 xmm7 xmm8 ↙
    ↘ xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
just_copy2::                         ; Preds .B1.4 .B1.6
; R8 = cnt
; RDX = a2
; RCX = a1
         xor        r10d, r10d                                    ;8.2
         xor        r9d, r9d                                      ;
         xor        eax, eax                                      ;
                                     ; LOE rax rdx rcx rbx rbp rsi rdi r8 r9 r10 r12 r13 r14 r15 xmm6 xmm7 ↙
    ↘ xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
.B1.8::                              ; Preds .B1.8 just_copy2
         mov        r11d, DWORD PTR [rax+rdx]                     ;3.6
         inc        r10                                           ;8.2
         mov        DWORD PTR [r9+rcx], r11d                      ;3.6
         add        r9, 12                                        ;8.2
         add        rax, 28                                       ;8.2
         cmp        r10, r8                                       ;8.2
         jb         .B1.8             ; Prob 82%                  ;8.2
         jmp        exit              ; Prob 100%                 ;8.2
                                      ; LOE rax rdx rcx rbx rbp rsi rdi r8 r9 r10 r12 r13 r14 r15 xmm6 xmm7 ↙
    ↘ xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
just_copy::                           ; Preds .B1.2 .B1.5 .B1.6
; R8 = cnt
; RDX = a2
; RCX = a1
         xor        r10d, r10d                                    ;8.2
         xor        r9d, r9d                                      ;
         xor        eax, eax                                      ;
                                      ; LOE rax rdx rcx rbx rbp rsi rdi r8 r9 r10 r12 r13 r14 r15 xmm6 ↙
    ↘ xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
.B1.11::                              ; Preds .B1.11 just_copy
         mov        r11d, DWORD PTR [rax+rdx]                     ;3.6
         inc        r10                                           ;8.2
         mov        DWORD PTR [r9+rcx], r11d                      ;3.6
         add        r9, 12                                        ;8.2
         add        rax, 28                                       ;8.2
         cmp        r10, r8                                       ;8.2
         jb         .B1.11             ; Prob 82%                 ;8.2
                                       ; LOE rax rdx rcx rbx rbp rsi rdi r8 r9 r10 r12 r13 r14 r15 xmm6 ↙
    ↘ xmm7 xmm8 xmm9 xmm10 xmm11 xmm12 xmm13 xmm14 xmm15
exit::                                 ; Preds .B1.11 .B1.8 .B1.1
         ret                                                      ;10.1

上述程序首先根据输入参数确定分支，接着执行相应的例程。虽然它看起来就像是检查数组交叉分叉，但是编译器采用的是一种非常著名的内存块复制例程的优化方法。仔细分析就会发现，它所复制的例程居然完全相同。这或许是Intel C++编译器的某种不足吧。然而无论怎样，最后生成的程序功能正常。

本书刻意介绍这个例子，旨在让读者认识到：有些时候编译器的输出指令确实会让人感到摸不到头脑。然而只要程序功能正常，我们就不必进行深究了吧。

第40章 达夫装置

达夫装置（Duff’s Device）是一种综合了多种控制语句的循环展开技术，可大幅度地减少代码的分支总数。这种循环展开技术巧妙地利用了swtich语句的滑梯（fallthrough）效应。

本章对Tom Duff的原始程序进行了轻度简化。

现在假设我们要编写一个清除一块连续内存的函数。当然，我们可以使用循环语句、逐个字节的复写数据。但是现代的计算机内存总线都很宽，以字节为单位地清除效率会非常低。若以4字节或8字节为操作单位进行io操作，那么操作效率会高一些。由于本例演示的是64位应用程序，所以我们就以8字节为单位进行操作。不过，我们又当如何应对那些不足8字节的内存空间？毕竟我们的函数也可能清除容量不足8字节的内存空间。

因此，合理的算法应当是：

首先统计目标空间含有多少个连续的8字节空间，继而以8字节（64位）为操作单位将其清除。

然后统计那些大小不足8字节的尾数、即上一步除法计算的余数，然后逐字节地将之清零。

简单的循环语句即可完成第二步的任务。然而我们更希望把这个循环分解、展开：

#include <stdint.h>
#include <stdio.h>

void bzero(uint8_t* dst, size_t count)
{
         int i;

         if (count&(~7))
                  // work out 8-byte blocks
                  for (i=0; i<count>>3; i++)
                  {
                           *(uint64_t*)dst=0;
                           dst=dst+8;
                  };

         // work out the tail
         switch(count & 7)
         {
         case 7: *dst++ = 0;
         case 6: *dst++ = 0;
         case 5: *dst++ = 0;
         case 4: *dst++ = 0;
         case 3: *dst++ = 0;
         case 2: *dst++ = 0;
         case 1: *dst++ = 0;
         case 0: // do nothing
                  break;
         }
}

我们来看看这个计算是如何完成的。待处理内存区域的大小是64位数据，它分为如下两个部分。

| 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0
… | B | B | B | B | B | S | S | S

备注：B代表大小为8字节的内存块；S代表大小不足8字节的尾部内存块。

当我们将输入的内存块的大小除以8，其实就是将该值右移3位。然而不足8字节的内存块总数，即这步除法计算的余数，恰恰是刚刚位移出去的那最后的三位。可见，把目标空间大小/即变量count右移3位，可求得它含有多少个8字节的内存块；令变量counter与数字7进行逻辑与运算可求得它有多少字节的尾部内存块。

当然，我们也得首先看看这块目标空间是否足够进行一次8字节的清除操作。因此首先就要检查变量count是否大于7。为此，我们就要将变量count的最低三位清零，并将结果和零比较。如果该数大于零，则表示这个数量count大于7，我们就可以进行8字节的块操作。当然我们不需要知道它到底比7大多少，只需知道它是否比7大，即count的高位是否为零。

当然之所以能这样做，主要是因为8是2的3次方，而且“某数除以2的n次方”通过位移计算即可实现，其他类型的数字就不能通过位移运算进行判断了。

很难说这些技术是不是值得采用，毕竟这种技巧会明显降低源程序的可读性。然而这已经属于常见技术了。凡是资深的编程人员，不论他愿否使用达夫装置，他都应当能够理解使用了这种技巧的程序。

第一部分其实很简单，用64位的零填充所有8字节内存块。

第二部分的难点在于其循环展开技术。达夫装置利用的是switch()函数的滑梯效应。用人类的语言来讲，这段代码的功能就是将变量count与数字7进行逻辑与运算，得到尾数，然后把它们逐个清零。如果尾数为0，那么直接跳转至函数尾声，不做任何操作。如果尾数是1的话，跳转到switch()语句中清除单字节的那条语句。如果尾数是2的话，跳转到swith()语句中相应的位置执行清零操作；由于滑梯效应的存在，函数会清除2个字节的空间，以此类推。当尾数的值为最大值7的时候，它会执行7次相同操作。 在这个算法中，不会出现尾数大于7，也就是8的情况，因为第一部分的指令已经清除了所有8字节的内存块。

换而言之，达夫装置就是循环展开技术的一种特例。在老式设备上，它显然比普通循环的运行速度更高。然而，对于现代的大多数CPU来说，一些体积短小的循环语句反而会比循环展开体的执行速度更快。也许对于目前低成本的嵌入式MCU（微控单元，例如单片机）处理器而言，达夫设备更有意义一些。

我们下面来看看优化后的MSVC 2012的一些行为。

dst$ = 8
count$ = 16
bzero    PROC
         test    rdx, -8
         je      SHORT $LN11@bzero
; work out 8-byte blocks
         xor     r10d, r10d
         mov     r9, rdx
         shr     r9, 3
         mov     r8d, r10d
         test    r9, r9
         je      SHORT $LN11@bzero
         npad    5
$LL19@bzero:
         inc     r8d
         mov     QWORD PTR [rcx], r10
         add     rcx, 8
         movsxd  rax, r8d
         cmp     rax, r9
         jb      SHORT $LL19@bzero
$LN11@bzero:
; work out the tail
         and     edx, 7
         dec     rdx
         cmp     rdx, 6
         ja      SHORT $LN9@bzero
         lea     r8, OFFSET FLAT:__ImageBase
         mov     eax, DWORD PTR $LN22@bzero[r8+rdx*4]
         add     rax, r8
         jmp     rax
$LN8@bzero:
         mov     BYTE PTR [rcx], 0
         inc     rcx
$LN7@bzero:
         mov     BYTE PTR [rcx], 0
         inc     rcx
$LN6@bzero:
         mov     BYTE PTR [rcx], 0
         inc     rcx
$LN5@bzero:
         mov     BYTE PTR [rcx], 0
         inc     rcx
$LN4@bzero:
         mov     BYTE PTR [rcx], 0
         inc     rcx
$LN3@bzero:
         mov     BYTE PTR [rcx], 0
         inc     rcx
$LN2@bzero:
         mov     BYTE PTR [rcx], 0
$LN9@bzero:
         fatret  0
         npad    1
$LN22@bzero:
         DD      $LN2@bzero
         DD      $LN3@bzero
         DD      $LN4@bzero
         DD      $LN5@bzero
         DD      $LN6@bzero
         DD      $LN7@bzero
         DD      $LN8@bzero
bzero    ENDP

这个函数的第一部分与源程序一一对应。第二部分的循环展开体也不难理解：switch语句通过转移指令直接跳到合适的位置。由于MOV/INC指令对之间没有其他代码，所以swtich语句在转移到指定标签后不会越过后续的转移标签，它会执行完所需的所有指令。

另外，我们注意到MOV/INC指令对占用固定的字节数（3+3=6字节）。在注意到这个问题之后，我们就可以舍去switch()语句的转移表结构，将输入值乘以6，并直接跳转到如下地址：目前的RIP地址+输入值*6。这样一来，由于省掉了从转移表的查询操作，执行速度会更快。对于乘法来说，数字6是一个计算效率不高的乘数因子，或许乘法运算的速度比表查询的转移指令更慢；不过所谓“深度优化”就是这个思路[1]。在介绍循环展开技术时，过去的教科书就是这样介绍“达夫设备”的。

[1]　作为一个练习，为了去掉跳转表，读者可以尝试重新编写代码。上述MOV/INC指令对可以重写成4字节或者8字节，当然一个字节也可以，比如STOSB指令。

第41章 除以9

我们来看一个非常简单的函数：

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

41.1　x86

x86平台编译器的编译方法十分直白：

指令清单41.1　MSVC

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

IDIV指令是除法指令。它会从寄存器对EDX:EAX中提取被除数、从ECX寄存器中提取除数。计算结束以后，它把计算结果/商存储在EAX寄存器里，把余数存储在EDX寄存器。除法计算之后，商就位于EAX寄存器里，直接成为f()函数的返回值；因此没有其他值传递的操作。为了通知IDIV指令从EDX：EAX寄存器对中提取64位被除数，编译器在IDIV指令之前分派了CDQ指令。IDIV指令就会进行MOVSX那样的符号位处理和数据扩展处理。

启用编译器的优化选项之后，可得到下述程序：

指令清单41.2　采用MSVC 优化

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

编译器用乘法指令来变相实现除法运算。大家知道乘法会比除法运行快很多。使用这里介绍[1]的方法可以有效提高程序效率、节省时间开销。

在编译优化中，我们常常将其称为“强度减轻”的办法。

若使用GCC 4.4.1编译此程序，即使我们刻意不启用其优化选项，GCC的非优化编译结果也足以和MSVC的优化编译结果媲美。

指令清单41.3　不带优化的GCC 4.4.1

      public f
f     proc near

arg_0 = dword ptr  8

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

41.2　ARM

ARM处理器和其他的RISC处理器一样，“纯洁”得不支持硬件级别的除法指令。此外，这种CPU还不难“直接”进行32位常量的乘法运算（32位opcode容纳不下32位常量）。因此，在进行除法运算时，编译器会混合加减法运算和位移运算、变相实现除法运算（详情请参阅第19章）。

本节引用参考书目Ltd94（第3.3节）的一个例子，介绍一个“32位数除以10”的例子，分别计算商和余数。

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

41.2.1　ARM模式下，采用Xcode 4.6.3（LLVM）优化

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

这里的代码和采用优化算法时的MSVC与GCC基本相同。很明显，LLVM采用了相同的算法来处理常数。

细心的读者可能会问：既然ARM模式的单条指令不能把32位立即数赋值给寄存器，那么这个程序又是怎样做到单条MOV指令赋值的呢？实际上，原始指令并非是IDA显示的那种单条MOV指令。仔细观察您就会发现，那“条”指令占用了8个字节，而标准的ARM指令只有4个字节。原始的指令分两步进行32位赋值：首先用MOV指令将低16位（本例是常量0x8E39）复制到寄存器的低16位，再用MOVT指令把立即数的高16位复制到寄存器的高16位。IDA能够识别出这种指令组合，为了便于读者理解，把两条指令“排版”为一条“伪指令”。这8字节的MOV指令实际上是2条指令。

SMMUL是Signed Most Significant Word Multiply的简称。它是2个32位有符号数的乘法运算指令，会把64位结果的高32位保存在寄存器R0中，舍弃结果中的低32位。

MOV R1,R0,ASR#1是算术右移1位的运算指令。

而指令ADD R0,R1,R0,LSR#31的执行结果相当于将R0的值右移31位，并与R1的值相加，其结果保存在R0中。也就是：R0=R1+R0>>31。

在ARM模式的指令中没有单独的位移指令。不过，它可以在MOV、ADD、SUB以及RSB[2]指令中，使用“后缀”形式的参数调节符对第二个操作数进行位移运算。在使用位移调节符的时候，应当指定位移的确切位数。

ASR是算术右移Arithmetic Shift Right的简称。算术右移需要考虑符号位。

LSR是逻辑右移Logical Shift Right的简称。逻辑右移不考虑符号位。

41.2.2　Thumb-2模式下的Xcode 4.6.3优化（LLVM）

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

Thumb模式的指令集里有单独的位移运算指令。本例中的ASRS就是算术右移指令。

41.2.3　非优化的Xcode 4.6.3(LLVM) 以及Keil 6/2013

在没有启用优化选项的情况下，LLVM编译器不会生成上面那种混合运算指令，它会调用仿真库里的模拟运算函数__divsi3。

然而，无论是否启用优化选项，Keil编译器都只会调用库函数__aeabi_idivmod。

41.3　MIPS

出于某些原因，优化的GCC 4.4.5有除法指令。

指令清单41.4　优化的GCC 4.4.5（IDA）

f:
                li      $v0, 9
                bnez    $v0, loc_10
                div     $a0, $v0 ; branch delay slot
                break   0x1C00    ; "break 7" in assembly output and objdump

loc_10:
                mflo    $v0
                jr      $ra
                or      $at, $zero ; branch delay slot, NOP

本例出现了新指令BREAK。它是由编译器产生的异常处理指令，会在除数为零的情况下抛出错误信息。毕竟在正规的数学概念里，除数不可以是零。即使在启用优化选项的情况下，GCC也未能判断出本例“除数$V0用于不会是零”的切实情况，机械性地分派了异常处理的检测指令。BREAK指令只是一个在除数为零的情况下通知操作系统进行异常处理的指令。只要除数不是零，程序将会执行MFLO指令，把LO寄存器里的商复制到$V0寄存器。

这里顺便说明一下，乘法指令MUL会把积的高32位保存在HI寄存器中，把积的低32位保存在LO寄存器中。而除法指令DIV则会把商保存在LO寄存器，把余数在保存在HI寄存器中。

如果把源程序的除法指令修改为“a % 9”、计算余数，那么编译之后的程序就会用MFHI指令替换本例中的MFLO指令。

41.4　它是如何工作的

在引入2的n次方之后，除法运算可转换为乘法运算：

这里的M是魔术因子（Magic coefficient）。其计算公式是

最终，除法运算就转换成了

“除以2的n次方”的运算可以直接通过右移操作实现。如果n小于32，那么中间运算的积的低n位（通常位于EAX或者RAX寄存器）就会被位移运算直接抹去；如果n大于或等于32，那么积的高半部分（通常位于EDX或者RDX寄存器）的数值都会受到影响。

可见，参数n的取值直接决定了转换运算的计算精度。

在进行有符号数的除法运算时，符号位也对计算精度及n的取值产生了显著影响。

下面这个例子将验证符号位的影响。

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

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

在无符号数的计算过程中，魔术因子是0xaaaaaaab。乘法的中间结果要除以2的33次方。

而在有符号数的计算过程中，魔术因子则是0x55555556，乘法的中间结果要除以2的32次方。虽然这里没有进行除法运算，但是根据前面的讨论可知：商的有效位取自于EDX寄存器。

请别忘记中间一步的乘法计算同样存在符号位的问题：积的高32位右移31位，将在EAX寄存器的最低数权位保留有符号数的符号位（正数为0，负数为1）。将符号位加入积的高32位值，可实现负数补码的“＋1”修正

指令清单41.5　带优化的MSVC 2012

_f3_32_unsigned PROC
         mov      eax, -1431655765     ; aaaaaaabH
         mul      DWORD PTR _a$[esp-4] ; unsigned multiply
; EDX=(input*0xaaaaaaab)/2^32
         shr      edx, 1
; EDX=(input*0xaaaaaaab)/2^33
         mov      eax, edx
         ret      0
_f3_32_unsigned ENDP

_f3_32_signed PROC
         mov      eax, 1431655766              ; 55555556H
         imul     DWORD PTR _a$[esp-4] ; signed multiply
; take high part of product
; it is just the same as if to shift product by 32 bits right or to divide it by 2^32
         mov      eax, edx       ; EAX=EDX=(input*0x55555556)/2^32
         shr      eax, 31        ; 0000001fH
         add      eax, edx       ; add 1 if sign is negative
         ret      0
_f3_32_signed ENDP

41.4.1　更多的理论

其实，我们都知道，乘法和除法互为逆运算。因此下面的除法可以用乘法来代替。我们可以表述为

1/c可以称为乘法的逆运算，是c的倒数。可以用编译器来做提前运算。

但是这是为浮点计算用的，有没有整数呢？在模算术计算环境下，是可能会有的。CPU寄存器的长度是很规整的，要么32位要么64位，因此几乎所有的寄存器的算术操作都是对2的32次方或者2的64次方进行操作。

可以查阅War02的第10章第3节。

41.5　计算除数

41.5.1　变位系数#1

通常我们看到的代码可能就像这样：

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

我们这里用大写字母M来代表32位的魔术因子，把变位系数记为C，把除数记为D。

因此除数可以表示为

指令清单41.6　优化的MSVC 2012代码

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

用公式可以表示为

这个数字超过了32位的表达范围。我们可使用Mathematica程序计算除数：

指令清单41.7　Wolfram Mathematica的计算结果

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

它可算出本例采用的除数是17。

在64位数据的除法运算中，计算除数的方法完全相同。只是不再采用2的32次方，而采用了2的64次方。

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

指令清单41.8　64位下的优化MSVC2012

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

指令清单41.9　WolframMathematica的计算结果

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

41.5.2　变位系数#2

变位系数确实可能为零：

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

这样，计算除数的方法就更简单一些了：

就本例而言，除数的计算方法是：

再次使用Mathematica程序计算除数

指令清单41.10 Wolfram Mathematica的计算结果

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

最终求得除数为3。

41.6　练习题

请描述下述代码的功能。

指令清单41.11　采用MSVC 2010优化的代码

_a$ = 8
_f        PROC
          mov    ecx, DWORD PTR _a$[esp-4]
          mov    eax, -968154503 ; c64b2279H
          imul   ecx
          add    edx, ecx
          sar    edx, 9
          mov    eax, edx
          shr    eax, 31           ; 0000001fH
          add    eax, edx
          ret    0
_f        ENDP

指令清单41.12　ARM64位下采用GCC 4.9优化

f:
          mov    w1, 8825
          movk   w1, 0xc64b, lsl 16
          smull  x1, w0, w1
          lsr    x1, x1, 32
          add    w1, w0, w1
          asr    w1, w1, 9
          sub    w0, w1, w0, asr 31
          ret

答案请参见G.1.14。

[1]　可以看War02 pp.10-3中介绍的用乘法来代替除法的部分。

[2]　这些指令也称为“数据处理指令”。

第42章 字符串转换成数字，函数atoi()

我们来重新实现一下标准的C函数atoi()。这是一个将字符串转换成整数的函数。

42.1　例1

本例可按照ASCII表把数字字符转换为数字。代码没有做容错处理，因此当输入值为非数字型字符时，返回值也就不会正确。

#include <stdio.h>

int my_atoi (char *s)
{
         int rt=0;

         while (*s)
         {
                  rt=rt*10 + (*s-'0');
                  s++;
         };

         return rt;
};

         int main()
         {
                  printf ("%d\n", my_atoi ("1234"));
                  printf ("%d\n", my_atoi ("1234567890"));
};

这个算法实现的是从左到右读取数字，并将每个读取的字符减去数字0的ASCII值。我们知道，在ASCII表中，数字0～9是按照递增顺序连续存放的。因此无论字符“0”对应的数值是多少，“0”的值减去“0”的值为0，“9”的值减去“0”的值为9。按照这种模式，函数就可把单字节字符转换为相应数值。因此，如果函数读取的字符不是数字型字符的话，计算结果就不会正确。在处理多字符字符串时还要考虑数权问题，本例会把前一步的结果（即变量rt）乘以10，再累加本次转换出来的数字。换句话说，在转换单个字符的每次迭代过程中，转换函数都应当给上一次迭代的转换结果分配一次合理的数权。最后一个被转换的字符不会被提高数权。

42.1.1　64位下的MSVC 2013优化

指令清单42.1　64位下的MSVC2013优化

s$ = 8
my_atoi PROC
; load first character
         movzx   r8d, BYTE PTR [rcx]
; EAX is allocated for "rt" variable
; its 0 at start'
         xor     eax, eax
; first character is zero-byte, i.e., string terminator?
; exit then.
         test    r8b, r8b
         je      SHORT $LN9@my_atoi
$LL2@my_atoi:
         lea     edx, DWORD PTR [rax+rax*4]
; EDX=RAX+RAX*4=rt+rt*4=rt*5
         movsx   eax, r8b
; EAX=input character
; load next character to R8D
         movzx   r8d, BYTE PTR [rcx+1]
; shift pointer in RCX to the next character:
         lea     rcx, QWORD PTR [rcx+1]
         lea     eax, DWORD PTR [rax+rdx*2]
; EAX=RAX+RDX*2=input character + rt*5*2=input character + rt*10
; correct digit  by subtracting 48 (0x30 or '0')
         add     eax, -48           ; ffffffffffffffd0H
; was last character zero?
         Test    r8b, r8b
; jump to loop begin, if not
         jne     SHORT $LL2@my_atoi
$LN9@my_atoi:
         ret     0
my_atoi ENDP

字符串的第一个字符可能就是终止符。在这种情况下，函数不应当进入字符转换的迭代过程。此外，编译器没有分配“乘以10”的乘法指令，而是使用了第一条、第三条LEA指令分步实现了“上次迭代的rt值乘以5”、“输入字符＋5倍原始rt值×2”的运算。某些情况下，MSVC编译器会刻意避开减法运算的SUB指令、转而分配“ADD某个负数”的指令来实现减法运算。本例就发生了这种情况。虽然笔者也不明白ADD指令的优越性到底在哪，但是MSVC编译器经常会做这种指令替换。

42.1.2　64位下的GCC 4.9.1优化

GCC 4.9.1优化更精确，但是它会在代码的最后添加一个多余的返回指令RET。其实一个RET就足够了。

指令清单42.2　64位下的GCC 4.9.1优化

my_atoi:
; load input character into EDX
          movsx   edx, BYTE PTR [rdi]
; EAX is allocated for "rt" variable
          xor     eax, eax
; exit, if loaded character is null byte
          test    dl, dl
          je      .L4
.L3:
          lea     eax, [rax+rax*4]
; EAX=RAX*5=rt*5
; shift pointer to the next character:
          add     rdi, 1
          lea     eax, [rdx-48+rax*2]
; EAX=input character - 48 + RAX*2 = input character - '0' + rt*10
; load next character:
          movsx   edx, BYTE PTR [rdi]
; goto loop begin, if loaded character is not null byte
          test    dl, dl
          jne     .L3
          rep ret
.L4:
          rep ret

42.1.3　ARM模式下Keil 6/2013优化

指令清单42.3　ARM模式下Keil 6/2013 优化

my_atoi PROC
; R1 will contain pointer to character
          MOV       r1,r0
; R0 will contain "rt" variable
          MOV       r0,#0
          B         |L0.28|
|L0.12|
          ADD       r0,r0,r0,LSL #2
; R0=R0+R0<<2=rt*5
          ADD       r0,r2,r0,LSL #1
; R0=input character + rt*5<<1 = input character + rt*10
; correct whole thing by subtracting '0' from rt:
          SUB       r0,r0,#0x30
; shift pointer to the next character:
          ADD       r1,r1,#1
|L0.28|
; load input character to R2
          LDRB      r2,[r1,#0]
; is it null byte? if no, jump to loop body.
          CMP       r2,#0
          BNE       |L0.12|
; exit if null byte.
; "rt" variable is still in R0 register, ready to be used in caller function
          BX        lr
          ENDP

42.1.4　Thumb模式下Keil 6/2013优化

指令清单42.4　Thumb模式下Keil 6/2013优化

my_atoi PROC
; R1 will be pointer to the input character
          MOVS      r1,r0
; R0 is allocated to "rt" variable
          MOVS      r0,#0
          B         |L0.16|
|L0.6|
          MOVS      r3,#0xa
; R3=10
          MULS      r0,r3,r0
; R0=R3*R0=rt*10
; shift pointer to the next character:
          ADDS      r1,r1,#1
; correct whole thing by subtracting 0' character from it':
          SUBS      r0,r0,#0x30
          ADDS      r0,r2,r0
; rt=R2+R0=input character + (rt*10 - '0')
|L0.16|
; load input character to R2
          LDRB      r2,[r1,#0]
; is it zero?
          CMP       r2,#0
; jump to loop body if it is not
          BNE       |L0.6|
; rt variable in R0 now, ready to be used in caller function
          BX        lr
          ENDP

很有趣的是，我们以前上学时学习到的规则是：加法和减法的运算顺序无所谓先后，完全可以重排。本例就活用了这一原则：编译器首先计算乘/减法的混合运算表达式“rt*10−‘0’”、求得中间差，然后计算输入字符与中间差的和。虽然这种算法的计算结果必定和源代码的计算结果相同，但是请不要忽视编译器的算式重排功能。

42.1.5　ARM64下的GCC 4.9.1优化

ARM64的编译器可以使用预增量指令后缀。

指令清单42.5　ARM64的GCC 4.9.1指令优化

my_atoi:
; load input character into W1
          ldrb     w1, [x0]
          mov      x2, x0
; X2=address of input string
; is loaded character zero?
; jump to exit if its so'
; W1 will contain 0 in this case.
; it will be reloaded into W0 at L4.
          cbz      w1, .L4
; W0 will contain "rt" variable
; initialize it at zero:
          mov      w0, 0
.L3:
; subtract 48 or '0' from input variable and put result into W3:
          sub      w3, w1, #48
; load next character at address X2+1 into W1 with pre-increment:
          ldrb     w1, [x2,1]!
          add      w0, w0, w0, lsl 2
; W0=W0+W0<<2=W0+W0*4=rt*5
          add      w0, w3, w0, lsl 1
; W0=input digit + W0<<1 = input digit + rt*5*2 = input digit + rt*10
; if the character we just loaded is not null byte, jump to the loop begin
          cbnz     w1, .L3
; variable to be returned (rt) is in W0, ready to be used in caller function
          ret
.L4:
          mov      w0, w1
          ret

42.2　例2

现在，我们把例1的源代码改得更高级一些：令其检测第一个字符是否为负数符号“−”，并且检测输入值里有没有非数字型字符；当输入字符串含有非数字字符的时候，要令程序提示错误信息。

#include <stdio.h>

int my_atoi (char *s)
{
          int negative=0;
          int rt=0;

          if (*s=='-')
          {
                    negative=1;
                    s++;
          };

          while (*s)
          {
                    if (*s<'0' || *s>'9')
                    {
                              printf ("Error! Unexpected char: '%c'\n", *s);
                              exit(0);
                    };
                    rt=rt*10 + (*s-'0');
                    s++;
          };

          if (negative)
                    return -rt;
          return rt;
};

int main()
{
          printf ("%d\n", my_atoi ("1234"));
          printf ("%d\n", my_atoi ("1234567890"));
          printf ("%d\n", my_atoi ("-1234"));
          printf ("%d\n", my_atoi ("-1234567890"));
          printf ("%d\n", my_atoi ("-a1234567890")); // error
};

42.2.1　64位下的GCC 4.9.1优化

指令清单42.6　64位下的GCC 4.9.1优化

.LC0:
          .string "Error! Unexpected char: '%c'\n"

my_atoi:
          sub     rsp, 8
          movsx   edx, BYTE PTR [rdi]
; check for minus sign
          cmp     dl, 45 ; '-'
          je      .L22
          xor     esi, esi
          test    dl, dl
          je      .L20
.L10:
; ESI=0 here if there was no minus sign and 1 if it was
          lea     eax, [rdx-48]
; any character other than digit will result unsigned number greater than 9 after subtraction
; so if it is not digit, jump to L4, where error will be reported:
          cmp     al, 9
          ja      .L4
          xor     eax, eax
          jmp     .L6
.L7:
          lea     ecx, [rdx-48]
          cmp     cl, 9
          ja      .L4
.L6:
          lea     eax, [rax+rax*4]
          add     rdi, 1
          lea     eax, [rdx-48+rax*2]
          movsx   edx, BYTE PTR [rdi]
          test    dl, dl
          jne     .L7
; if there was no minus sign, skip NEG instruction
; if it was, execute it.
          test    esi, esi
          je      .L18
          neg     eax
.L18:
          add     rsp, 8
          ret
.L22:
          movsx   edx, BYTE PTR [rdi+1]
          lea     rax, [rdi+1]
          test    dl, dl
          je      .L20
          mov     rdi, rax
          mov     esi, 1
          jmp     .L10
.L20:
          xor     eax, eax
          jmp     .L18
.L4:
; report error. character is in EDX
          mov     edi, 1
          mov     esi, OFFSET FLAT:.LC0 ; "Error! Unexpected char: '%c'\n"
          xor     eax, eax
          call    __printf_chk
          xor     edi, edi
          call    exit

如果字符串的第一个字符是负号，那么就要在转换的最后阶段执行NEG指令，把结果转换为负数。

另外值得一提的是“检测字符是否是数字字符”的判断表达式。我们在程序中可以看到代码为：

if (*s<'0' || *s>'9')
      ...

这是两个比较操作。比较有意思的是，我们完全可以只用一个比较指令就完成这两步比较运算：将输入字符的值减去“0”字符的值，将结果视为无符号数（这是关键）与9进行比较。若最后的无符号数比9还大，那么输入字符就不是数字字符。

比如说，如果我们在输入的字符串中含有小数点(“.”)，这个符号在ASCII表中排在字符零“0”的前2位。因此判断语句的减法运算表达式是：46−48=−2。有符号数的减法运算当然会求得有符号数。被减数比减数小，计算的结果肯定是负数。但是，如果我们把这个结果当作无符号数来处理的话，它将是0xfffffffe（对应的十进制数是42949672294），显然它比9大。举这个例子是想说明按照无符号数处理的重要性。

编译器经常会这样做，因此我们应该重新认识这种技巧。

我们可以本书的其他地方看到这个例子：比如48.1.2节。

在编译64位应用程序时，MSVC 2013还好使用这种优化技巧。

42.2.2　ARM模式下的Keil6/2013优化

指令清单42.7　ARM模式下的Keil6/2013优化

1  my_atoi PROC
2          PUSH      {r4-r6,lr}
3          MOV       r4,r0
4          LDRB      r0,[r0,#0]
5          MOV       r6,#0
6          MOV       r5,r6
7          CMP       r0,#0x2d '-'
8  ; R6 will contain 1 if minus was encountered, 0 if otherwise
9          MOVEQ     r6,#1
10         ADDEQ     r4,r4,#1
11         B         |L0.80|
12  |L0.36|
13         SUB       r0,r1,#0x30
14         CMP       r0,#0xa
15         BCC       |L0.64|
16         ADR       r0,|L0.220|
17         BL        __2printf
18         MOV       r0,#0
19         BL        exit
20  |L0.64|
21         LDRB      r0,[r4],#1
22         ADD       r1,r5,r5,LSL #2
23         ADD       r0,r0,r1,LSL #1
24         SUB       r5,r0,#0x30
25  |L0.80|
26         LDRB      r1,[r4,#0]
27         CMP       r1,#0
28         BNE       |L0.36|
29         CMP       r6,#0
30  ; negate result
31         RSBNE     r0,r5,#0
32         MOVEQ     r0,r5
33         POP       {r4-r6,pc}
34         ENDP
35
36  |L0.220|
37         DCB       "Error! Unexpected char: '%c'\n",0

32位ARM的指令集里没有NEG（取负）指令，因此编译器分配了第31行的“反向减法”指令。当第29行指令——即CMP的比较结果为“不相等”时，才会执行第31行的条件执行指令（我们可以看到NE后缀，也就是“（运行条件为）Not Equal”的意思）。而后，RSBNE指令用0减去上一步的计算结果。这条指令确实就是减法运算指令，只是被减数和减数的操作符排列位置对调了一下。用0来减去任何数，实际的效果就是对该数取负。实现的结果可以用这个来表示：0−x=−x。

Thumb程序的运算模式几乎完全相同。

在编译ARM64平台的应用程序时，GCC 4.9能够分配NEG（取反）指令。

42.3　练习

这里顺便提一下，安全研究人员经常研究各种异常情况。他们重点关注不合乎预期的输入值，通过这种数据诱使程序进行某种与设计思路相左的行为。因此，他们也会关注模糊测试方法。作为练习，我们可以试图输入非数字字符，看看会发生什么。自己可以尝试着解释一下背后的成因是什么。

第43章 内联函数

在编译阶段，将会被编译器把函数体展开并嵌入到每一个调用点的函数，就是内联函数。

指令清单43.1　一个简单的例子

#include <stdio.h>

int celsius_to_fahrenheit (int celsius)
{
        return celsius * 9 / 5 + 32;
};

int main(int argc, char *argv[])
{
        int celsius=atol(argv[1]);
        printf ("%d\n", celsius_to_fahrenheit (celsius));
};

此函数在汇编层面的具体指令与源代码几乎一一对应。然而，如果在GCC编译环境下，我们采用-03参数优化的话，我们会看到如下所示的代码。

指令清单43.2　GCC 4.8.1优化

_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

值得注意的是，编译器用乘法指令变相地实现了除法运算（可以参见第41章）。

您没看错，温度转换函数celsius_to_fahrenheit()（摄氏温度转换成华氏温度）的函数体被直接展开，放在了函数printf()（显示字符串）的前面。为什么呢？因为这样运行速度会更快一些。这种代码不需要在调用函数时处理多余的函数调用和返回指令。

现在具有优化功能的编译器一般都能自动的把小型函数的函数体直接“嵌入”到调用方函数的代码里。当然我们也可以借助关键字“inline”强制编译器进行这种“嵌入”处理。

43.1　字符串和内存操作函数

在处理字符串和内存操作的常见函数时，例如：strycpy()、strcmp()、strlen、memset()、memcpy()、memcmp()函数，编译器通常会把这些函数当作内联函数处理。

多数情况下，以内联函数编译的函数会比那些被单独调用的函数运行得更快。

本节将演示一些非常具有特征的内联代码，以供读者研究。

43.1.1　字符串比较函数strcmp()

指令清单43.3　字符串比较函数strcmp()

bool is_bool (char *s)
{
          if (strcmp (s, "true")==0)
                   return true;
          if (strcmp (s, "false")==0)
                   return false;
          assert(0);
};

指令清单43.4　采用GCC 4.8.1优化的例子

.LC0:
          .string "true"
.LC1:
          .string "false"
is_bool:
.LFB0:
          push    edi
          mov     ecx, 5
          push    esi
          mov     edi, OFFSET FLAT:.LC0
          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
          repz cmpsb
          seta    cl
          setb    dl
          xor     eax, eax
          cmp     cl, dl
          jne     .L8
          add     esp, 20
          pop     esi
          pop     edi
          ret
.L8:
          mov     DWORD PTR [esp], 0
          call    assert
          add     esp, 20
          pop     esi
          pop     edi
          ret
.L3:
          add     esp, 20
          mov     eax, 1
          pop     esi
          pop     edi
          ret

指令清单43.5　采用MSVC 2010优化的例子

$SG3454 DB       'true', 00H
$SG3456 DB       'false', 00H

_s$ = 8          ; size = 4
?is_bool@@YA_NPAD@Z PROC ; is_bool
          push   esi
          mov    esi, DWORD PTR _s$[esp]
          mov    ecx, OFFSET $SG3454 ; 'true'
          mov    eax, esi
          npad   4 ; align next label
$LL6@is_bool:
          mov    dl, BYTE PTR [eax]
          cmp    dl, BYTE PTR [ecx]
          jne    SHORT $LN7@is_bool
          test   dl, dl
          je     SHORT $LN8@is_bool
          mov    dl, BYTE PTR [eax+1]
          cmp    dl, BYTE PTR [ecx+1]
          jne    SHORT $LN7@is_bool
          add    eax, 2
          add    ecx, 2
          test   dl, dl
          jne    SHORT $LL6@is_bool
$LN8@is_bool:
          xor    eax, eax
          jmp    SHORT $LN9@is_bool
$LN7@is_bool:
          sbb    eax, eax
          sbb    eax, -1
$LN9@is_bool:
          test   eax, eax
          jne    SHORT $LN2@is_bool

          mov    al, 1
          pop    esi

          ret    0
$LN2@is_bool:

          mov    ecx, OFFSET $SG3456 ; 'false'
          mov    eax, esi
$LL10@is_bool:
          mov    dl, BYTE PTR [eax]
          cmp    dl, BYTE PTR [ecx]
          jne    SHORT $LN11@is_bool
          test   dl, dl
          je     SHORT $LN12@is_bool
          mov    dl, BYTE PTR [eax+1]
          cmp    dl, BYTE PTR [ecx+1]
          jne    SHORT $LN11@is_bool
          add    eax, 2
          add    ecx, 2
          test   dl, dl
          jne    SHORT $LL10@is_bool
$LN12@is_bool:
          xor    eax, eax
          jmp    SHORT $LN13@is_bool
$LN11@is_bool:
          sbb    eax, eax
          sbb    eax, -1
$LN13@is_bool:
          test   eax, eax
          jne    SHORT $LN1@is_bool

          xor    al, al
          pop    esi

          ret    0
$LN1@is_bool:

          push   11
          push   OFFSET $SG3458
          push   OFFSET $SG3459
          call   DWORD PTR __imp___wassert
          add    esp, 12
          pop    esi

          ret    0
?is_bool@@YA_NPAD@Z ENDP ; is_bool

43.1.2　字符串长度函数strlen()

指令清单43.6　字符串长度函数strlen()的例子

int strlen_test(char *s1)
{
          return strlen(s1);
};

指令清单43.7　采用MSVC 2010优化的例子

_s1$ = 8 ; size = 4
_strlen_test PROC
          mov    eax, DWORD PTR _s1$[esp-4]
          lea    edx, DWORD PTR [eax+1]
$LL3@strlen_tes:
          mov    cl, BYTE PTR [eax]
          inc    eax
          test   cl, cl
          jne    SHORT $LL3@strlen_tes
          sub    eax, edx
          ret    0
_strlen_test ENDP

43.1.3　字符串复制函数strcpy()

指令清单43.8　字符串复制函数strcpy()的例子

void strcpy_test(char *s1, char *outbuf)
{
          strcpy(outbuf, s1);
};

指令清单43.9　采用MSVC 2010优化的例子

_s1$ = 8         ; size = 4
_outbuf$ = 12    ; size = 4
_strcpy_test PROC
          mov    eax, DWORD PTR _s1$[esp-4]
          mov    edx, DWORD PTR _outbuf$[esp-4]
          sub    edx, eax
          npad   6 ; align next label
$LL3@strcpy_tes:
          mov    cl, BYTE PTR [eax]
          mov    BYTE PTR [edx+eax], cl
          inc    eax
          test   cl, cl
          jne    SHORT $LL3@strcpy_tes
          ret    0
_strcpy_test ENDP

43.1.4　内存设置函数memset()

例子#1如下所示。

指令清单43.10　32字节的操作

#include <stdio.h>

void f(char *out)
{
         memset(out, 0, 32);
};

在编译那些操作小体积内存块的memset()函数时，多数编译器不会分配标准的函数调用指令（call），反而会分配一堆的MOV指令，直接赋值。

指令清单43.11　64位下的GCC 4.9.1优化

f:
          mov    QWORD PTR [rdi], 0
          mov    QWORD PTR [rdi+8], 0
          mov    QWORD PTR [rdi+16], 0
          mov    QWORD PTR [rdi+24], 0
          ret

这让我们想起了本书14.1.4节介绍的循环展开技术。

例子#2如下所示。

指令清单43.12　67字节内存的操作

#include <stdio.h>

void f(char *out)
{
         memset(out, 0, 67);
};

当内存块的大小不是4或者8的整数倍时，不同的编译器会有不同的处理方法。

比如说，MSVC 2012依旧会分配一串MOV指令。

指令清单43.13　64位下的MSVC 2012优化

out$ = 8
f         PROC
          xor    eax, eax
          mov    QWORD PTR [rcx], rax
          mov    QWORD PTR [rcx+8], rax
          mov    QWORD PTR [rcx+16], rax
          mov    QWORD PTR [rcx+24], rax
          mov    QWORD PTR [rcx+32], rax
          mov    QWORD PTR [rcx+40], rax
          mov    QWORD PTR [rcx+48], rax
          mov    QWORD PTR [rcx+56], rax
          mov    WORD PTR [rcx+64], ax
          mov    BYTE PTR [rcx+66], al
          ret    0
f         ENDP

GCC还会分配REP STOSQ指令。这可能比一堆的MOV赋值指令更短，效率更高。

指令清单43.14　64位下的GCC 4.9.1优化

f:
          mov    QWORD PTR [rdi], 0
          mov    QWORD PTR [rdi+59], 0
          mov    rcx, rdi
          lea    rdi, [rdi+8]
          xor    eax, eax
          and    rdi, -8
          sub    rcx, rdi
          add    ecx, 67
          shr    ecx, 3
          rep stosq
          ret

43.1.5　内存复制函数memcpy()

在编译那些复制小尺寸内存块的memcpy()函数时，多数编译器会分配一系列的MOV指令。

指令清单43.15　内存复制函数memcpy()

void memcpy_7(char *inbuf, char *outbuf)
{
          memcpy(outbuf+10, inbuf, 7);
};

指令清单43.16　采用MSVC 2010优化

_inbuf$ = 8      ; size = 4
_outbuf$ = 12    ; size = 4
_memcpy_7 PROC
         mov     ecx, DWORD PTR _inbuf$[esp-4]
         mov     edx, DWORD PTR [ecx]
         mov     eax, DWORD PTR _outbuf$[esp-4]
         mov     DWORD PTR [eax+10], edx
         mov     dx, WORD PTR [ecx+4]
         mov     WORD PTR [eax+14], dx
         mov     cl, BYTE PTR [ecx+6]
         mov     BYTE PTR [eax+16], cl
         ret     0
_memcpy_7 ENDP

指令清单43.17　采用GCC 4.8.1优化

memcpy_7:
          push   ebx
          mov    eax, DWORD PTR [esp+8]
          mov    ecx, DWORD PTR [esp+12]
          mov    ebx, DWORD PTR [eax]
          lea    edx, [ecx+10]
          mov    DWORD PTR [ecx+10], ebx
          movzx  ecx, WORD PTR [eax+4]
          mov    WORD PTR [edx+4], cx
          movzx  eax, BYTE PTR [eax+6]
          mov    BYTE PTR [edx+6], al
          pop    ebx
          ret

上述指令的操作流程是：首先复制4个字节，然后复制一个字（也就是2个字节），接着复制最后一个字节。

此外，编译器还会通过赋值指令MOV复制结构体（structure）型数据。详情请参见本书的21.4.1节。

大尺寸内存块的操作：

不同的编译器会有不同的指令分配方案。

指令清单43.18　memcpy()内存复制的例子（这里列出了2个不同的例子，
一个是128字节的操作，另外一个则是123字节的内存操作）

void memcpy_128(char *inbuf, char *outbuf)
{
          memcpy(outbuf+10, inbuf, 128);
};

void memcpy_123(char *inbuf, char *outbuf)
{
          memcpy(outbuf+10, inbuf, 123);
};

MSVC分配了单条MOVSD指令。在循环控制变量ECX的配合下，MOVSD可一步完成128个字节的数据复制。其原因显然是128能被4整除。

指令清单43.19　MSVC 2010优化

_inbuf$ = 8            ; size = 4
_outbuf$ = 12          ; size = 4
_memcpy_128 PROC
          push   esi
          mov    esi, DWORD PTR _inbuf$[esp]
          push   edi
          mov    edi, DWORD PTR _outbuf$[esp+4]
          add    edi, 10
          mov    ecx, 32
          rep movsd
          pop    edi
          pop    esi
          ret    0
_memcpy_128 ENDP

在复制123个字节的程序里，编译器首先通过MOVSD指令复制30个32[1]字节（也就是120字节），然后依次通过MOVSW指令和MOVSB指令复制2个字节和1个字节。

指令清单43.20　采用MSVC 2010优化指令

_inbuf$ = 8            ; size = 4
_outbuf$ = 12          ; size = 4
_memcpy_123 PROC
          push   esi
          mov    esi, DWORD PTR _inbuf$[esp]
          push   edi
          mov    edi, DWORD PTR _outbuf$[esp+4]
          add    edi, 10
          mov    ecx, 30
          rep movsd
          movsw
          movsb
          pop    edi
          pop    esi
          ret    0
_memcpy_123 ENDP

GCC则分配了一个大型的通用的函数。这个函数适用于任意大小的内存块复制操作。

指令清单43.21　采用GCC 4.8.1优化

memcpy_123:
.LFB3:
         push   edi
         mov    eax, 123
         push   esi
         mov    edx, DWORD PTR [esp+16]
         mov    esi, DWORD PTR [esp+12]
         lea    edi, [edx+10]
         test   edi, 1
         jne    .L24
         test   edi, 2
         jne    .L25
.L7:
         mov    ecx, eax
         xor    edx, edx
         shr    ecx, 2
         test   al, 2
         rep movsd
         je     .L8
         movzx  edx, WORD PTR [esi]
         mov    WORD PTR [edi], dx
         mov    edx, 2
.L8:
         test   al, 1
         je     .L5
         movzx  eax, BYTE PTR [esi+edx]
         mov    BYTE PTR [edi+edx], al
.L5:
         pop    esi
         pop    edi
         ret
.L24:
         movzx  eax, BYTE PTR [esi]
         lea    edi, [edx+11]
         add    esi, 1
         test   edi, 2
         mov    BYTE PTR [edx+10], al
         mov    eax, 122
         je     .L7
.L25:
         movzx  edx, WORD PTR [esi]
         add    edi, 2
         add    esi, 2
         sub    eax, 2
         mov    WORD PTR [edi-2], dx
         jmp    .L7
.LFE3:

通用内存复制函数通常的工作原理如下：首先计算块有多少个字（32位），然后用MOVSD指令复制这些内存块，然后逐一复制剩余的字节。

更为复杂的内存复制函数则会利用SIMD指令集进行复制，这种复制还会涉及内存地址对齐的问题。有兴趣的读者可以参阅本书第25章的第2节。

43.1.6　内存对比函数 memcmp()

指令清单43.22　memcmp()函数的例子

void memcpy_1235(char *inbuf, char *outbuf)
{
          memcpy(outbuf+10, inbuf, 1235);
};

无论内存块的尺寸是多大，MSVC 2010都会插入相同的通用比较函数。

指令清单43.23　MSVC 2010优化程序

_buf1$ = 8       ; size = 4
_buf2$ = 12      ; size = 4
_memcmp_1235 PROC
          mov    edx, DWORD PTR _buf2$[esp-4]
          mov    ecx, DWORD PTR _buf1$[esp-4]
          push   esi
          push   edi
          mov    esi, 1235
          add    edx, 10
$LL4@memcmp_123:
          mov    eax, DWORD PTR [edx]
          cmp    eax, DWORD PTR [ecx]
          jne    SHORT $LN10@memcmp_123
          sub    esi, 4
          add    ecx, 4
          add    edx, 4
          cmp    esi, 4
          jae    SHORT $LL4@memcmp_123
$LN10@memcmp_123:
          movzx  edi, BYTE PTR [ecx]
          movzx  eax, BYTE PTR [edx]
          sub    eax, edi
          jne    SHORT $LN7@memcmp_123
          movzx  eax, BYTE PTR [edx+1]
          movzx  edi, BYTE PTR [ecx+1]
          sub    eax, edi
          jne    SHORT $LN7@memcmp_123
          movzx  eax, BYTE PTR [edx+2]
          movzx  edi, BYTE PTR [ecx+2]
          sub    eax, edi
          jne    SHORT $LN7@memcmp_123
          cmp    esi, 3
          jbe    SHORT $LN6@memcmp_123
          movzx  eax, BYTE PTR [edx+3]
          movzx  ecx, BYTE PTR [ecx+3]
          sub    eax, ecx
$LN7@memcmp_123:
          sar    eax, 31
          pop    edi
          or     eax, 1
          pop    esi
          ret    0
$LN6@memcmp_123:
          pop    edi
          xor    eax, eax
          pop    esi
          ret    0
_memcmp_1235 ENDP

43.1.7　IDA脚本

笔者编写了一个检索、收缩（folding）常见内联函数的IDA脚本。有兴趣的读者请访问：

https://github.com/yurichev/IDA_scripts

[1]　应该为4个字节。

第44章 C99标准的受限指针

在某些情况下，用FORTRAN系统编译出来的程序会比用C/C++系统编译出来的程序运行得更快。例如，下面这个例子就是如此：

void f1 (int* x, int* y, int* sum, int* product, int* sum_product, int* update_me, size_t s)
{
          for (int i=0; i<s; i++)
          {
                   sum[i]=x[i]+y[i];
                   product[i]=x[i]*y[i];
                   update_me[i]=i*123; // some dummy value
                   sum_product[i]=sum[i]+product[i];
          };
};

这个程序的功能十分简单，但是里面的指针问题却发人深思：同一块内存可以由多个指针来访问，因此同一个地址的数据可能会被多个指针轮番复写。至少现行标准并不禁止这种情况。

C语言编译器完全允许上述情况。因此，它分四个阶段处理每次迭代的各类数组：

制备sum[i];

制备product[i];

制备update_me[i];

制备sum_product[i]。在这个阶段，计算机将从内存里重新加载sum[i]和product[i]。

第四个阶段是否存在进一步优化的空间呢？既然前面已经计算好了sum[i]和product[i]，那么后面我们应该就不必再从内存中读取它们的值了。

答案是肯定的。

只是编译器本身并不能在第三个阶段确定前两个阶段的赋值没有被其他指令覆盖。换而言之，因为编译器不能判断该程序里是否存在指向相同内存区域的指针——即“指针别名（pointer aliasing）”，所以编译器不能确保该指针指向的内存没被改写。

C99标准中的受限指针［ISO07，6.7.3节］（部分文献又称“严格别名”）的应运而生。编程人员可通过受限指针的strict修饰符向编译器承诺：被该关键字标记的指针是操作相关内存区域的唯一指针，没有其他指针重复指向这个指针所操作的内存区域。

用更为确切、更为正式的语言来说，关键字“restrict”表示该指针是访问既定对象的唯一指针，其他指针都不会重复操作既定对象。从另一个角度来看，一旦某个指针被标记为受限指针，那么编译器就认定既定对象只会被指定的受限指针操作。

下面我们将为每个指针都增加上restrict修饰符：

void f2 (int* restrict x, int* restrict y, int* restrict sum, int* restrict product, int* ↙
    ↘ restrict sum_product,
          int* restrict update_me, size_t s)
{
          for (int i=0; i<s; i++)
          {
                   sum[i]=x[i]+y[i];
                   product[i]=x[i]*y[i];
                   update_me[i]=i*123; // some dummy value
                   sum_product[i]=sum[i]+product[i];
          };
};

我们看到的结果如下所示。

指令清单44.1　x64下的GCC函数f1()

f1:
         push   r15 r14 r13 r12 rbp rdi rsi rbx
         mov    r13, QWORD PTR 120[rsp]
         mov    rbp, QWORD PTR 104[rsp]
         mov    r12, QWORD PTR 112[rsp]
         test   r13, r13
         je     .L1
         add    r13, 1
         xor    ebx, ebx
         mov    edi, 1
         xor    r11d, r11d
         jmp    .L4
.L6:
          mov   r11, rdi
          mov   rdi, rax
.L4:
         lea    rax, 0[0+r11*4]
         lea    r10, [rcx+rax]
         lea    r14, [rdx+rax]
         lea    rsi, [r8+rax]
         add    rax, r9
         mov    r15d, DWORD PTR [r10]
         add    r15d, DWORD PTR [r14]
         mov    DWORD PTR [rsi], r15d         ; store to sum[]
         mov    r10d, DWORD PTR [r10]
         imul   r10d, DWORD PTR [r14]
         mov    DWORD PTR [rax], r10d         ; store to product[]
         mov    DWORD PTR [r12+r11*4], ebx    ; store to update_me[]
         add    ebx, 123
         mov    r10d, DWORD PTR [rsi]         ; reload sum[i]
         add    r10d, DWORD PTR [rax]         ; reload product[i]
         lea    rax, 1[rdi]
         cmp    rax, r13
         mov    DWORD PTR 0[rbp+r11*4], r10d  ; store to sum_product[]
         jne    .L6
.L1:
         pop    rbx rsi rdi rbp r12 r13 r14 r15
         ret

指令清单44.2　x64下的GCC函数f2()

f2:
         push   r13 r12 rbp rdi rsi rbx
         mov    r13, QWORD PTR 104[rsp]
         mov    rbp, QWORD PTR 88[rsp]
         mov    r12, QWORD PTR 96[rsp]
         test   r13, r13
         je     .L7
         add    r13, 1
         xor    r10d, r10d
         mov    edi, 1
         xor    eax, eax
         jmp    .L10
.L11:
         mov    rax, rdi
         mov    rdi, r11
.L10:
         mov    esi, DWORD PTR [rcx+rax*4]
         mov    r11d, DWORD PTR [rdx+rax*4]
         mov    DWORD PTR [r12+rax*4], r10d ; store to update_me[]
         add    r10d, 123
         lea    ebx, [rsi+r11]
         imul   r11d, esi
         mov    DWORD PTR [r8+rax*4], ebx ; store to sum[]
         mov    DWORD PTR [r9+rax*4], r11d ; store to product[]
         add    r11d, ebx
         mov    DWORD PTR 0[rbp+rax*4], r11d ; store to sum_product[]
         lea    r11, 1[rdi]
         cmp    r11, r13
         jne    .L11
.L7:
         pop    rbx rsi rdi rbp r12 r13
         ret

f1()函数和f2()函数的不同之处在于：在f1()函数中，sum[i]和product[i]数组在循环中会再次加载；而函数f2()则没有这种重新加载内存数值的操作。在改动后的程序里，因为我们向编译器“承诺”sum[i]和product[i]的值不会被其他指针复写，所以计算机会重复利用前几个阶段制备好的各项数据，不再从内存加载它们的值了。很明显，改进后的程序运行速度更快一些。

如果我们声明了某个指针是受限指针，而实际的程序又有其他指针操作这个受限指针操作的内存区域，将会发生什么情况？这真的就是程序员的事了，不过程序运行的结果肯定是错误的。

FORTRAN语言的编译器把所有指针都视为受限指针。因此，在C语言不支持C99标准的restrict修饰符而实际指针属于受限指针的时候，用FORTRAN语言编译出来的应用程序会比用C语言编译出来的程序运行得更快。

受限指针主要用于哪些领域？它主要用于操作多个大尺寸内存块的应用方面。例如，在超级计算机/HPC平台上经常进行的线性方程组求解就属于这种类型的应用。或许，这正是这种平台普遍采用FORTRAN语言的原因之一吧。

另一方面，在循环语句的迭代次数不是非常高的情况下，受限指针带来的性能提升就不会十分明显。

第45章 打造无分支的abs()函数

请回顾前文第12章第2节的那个函数。请设想一下，能否用x86的汇编指令打造一个无分支的版本？

int my_abs (int i)
{
          if (i<0)
                   return -i;
          else
                   return i;
};

答案当然是肯定的。

45.1　x64下的GCC 4.9.1优化

首先我们来看看，如果我们采用GCC 4.9来编译的话，会出现什么情况。

指令清单45.1　x64下的GCC 4.9优化

my_abs:
         mov    edx, edi
         mov    eax, edi
         sar    edx, 31
; EDX is 0xFFFFFFFF here if sign of input value is minus
; EDX is 0 if sign of input value is plus (including 0)
; the following two instructions have effect only if EDX is 0xFFFFFFFF
; or idle if EDX is 0
         xor    eax, edx
         sub    eax, edx
         ret

以下是其实现过程的描述：

SAR算术右[1]移：算术右移31位，我们知道算术右移是带符号的操作。有符号数的最高有效位MSB就是其符号位。也就是说，如果MSB的值为1（原操作数是负数），那么右移31位后，不管原来的数是什么，得到的结果都会是0xffffffff。当然如果MSB的值为0（原操作数是正数或零），那么最后的结果就会是零。执行完SAR后，返回值被存放在EDX寄存器中。后面就是XOR指令。当原操作数是负数时，这条指令就变成了“XOR｛寄存器｝,0xffffffff”，相当于逐位求非的逻辑非运算；其后的SUB指令会完成补码运算的“减去负一（加一）”运算、求得绝对值，因为此时EDX寄存器的值是0xffffffff——十进制的“－1”。有关两个数的逻辑运算和递增运算详解，请参阅本书第30章。

因此，当源操作数是负数的时候，最后两条运算指令会对源操作数进行处理。在符号位的值为零，即零和正数的情况下，最后两条运算指令不会调整源操作数的值。

有关本例的详细算法，请查阅参考书目［War02，pp.2-4］的相关介绍。GCC或许借鉴了现有的成熟算法，或许这正是它自己推演的计算结果。

45.2　ARM64下的GCC 4.9优化

GCC 4.9 for ARM64的编译方式和GCC for x64的编译方式几乎相同，只是它使用的是自己CPU平台的64位寄存器而已。由于ARM64可以通过参数调节符ASR在别的指令里完成位移运算，因此GCC 4.9 for ARM64比GCC for x64分配的指令少。

指令清单45.2　ARM64下的GCC 4.9优化

my_abs:
; sign-extend input 32-bit value to X0 64-bit register:
          sxtw   x0, w0
          eor    x1, x0, x0, asr 63
; X1=X0^(X0>>63) (shift is arithmetical)
          sub    x0, x1, x0, asr 63
; X0=X1-(X0>>63)=X0^(X0>>63)-(X0>>63) (all shifts are arithmetical)
          ret

[1]　原文为左移。

第46章 变长参数函数

像printf()和scanf()一类的函数可以处理不同数量的输入参数。这种函数又是如何访问参数的呢？

46.1　计算算术平均值

如果要编写一个计算算术平均值的函数，那么就需要在函数的参数声明部分指定所有的外来参数。但是C/C++的变长参数函数却无法事先知道外来参数的数量。为了方便起见，我们用“-1”作为最后一个参数兼其他参数的终止符。

C语言标准函数库的头文件stdarg.h定义了变长参数的处理方法（宏）。刚才提到的printf()函数和scanf()函数都使用了这个文件提供的宏。

#include <stdio.h>
#include <stdarg.h>

int arith_mean(int v, ...)
{
         va_list args;
         int sum=v, count=1, i;
         va_start(args, v);

         while(1)
         {
                  i=va_arg(args, int);
                  if (i==-1) // terminator
                           break;
                  sum=sum+i;
                  count++;
         }

         va_end(args);
         return sum/count;
};

int main()
{
         printf ("%d\n", arith_mean (1, 2, 7, 10, 15, -1 /* terminator */));
};

变长参数函数按照常规函数参数的方法访问外部传来的第一个参数。而后，程序借助stdarg.h提供的宏var_arg调用其余参数，依次求得各参数之和，最终计算其平均值。

46.1.1　cdecl调用规范

指令清单46.1　MSVC 6.0优化

_v$ = 8
_arith_mean PROC NEAR
        mov     eax, DWORD PTR _v$[esp-4] ; load 1st argument into sum
        push    esi
        mov     esi, 1                    ; count=1
        lea     edx, DWORD PTR _v$[esp]   ; address of the 1st argument
$L838:
        mov     ecx, DWORD PTR [edx+4]    ; load next argument
        add     edx, 4                    ; shift pointer to the next argument
        cmp     ecx, -1                   ; is it -1?
        je      SHORT $L856               ; exit if so
        add     eax, ecx                  ; sum = sum + loaded argument
        inc     esi                       ; count++
        jmp     SHORT $L838
$L856:
; calculate quotient

        cdq
        idiv    esi
        pop     esi
        ret     0
_arith_mean ENDP

$SG851  DB      '%d', 0aH, 00H

_main   PROC NEAR
        push    -1
        push    15
        push    10
        push    7
        push    2
        push    1
        call    _arith_mean
        push    eax
        push    OFFSET FLAT:$SG851 ; '%d'
        call    _printf
        add     esp, 32
        ret     0
_main   ENDP

在main()函数里，各项参数从右向左依次逆序传递入栈。第一个入栈的是最后一项参数“-1”，而最后入栈的是第一项参数——格式化字符串。

函数arith_mean()取出第一个参数的值，并将其保存在变量sum中。接着，将第二个参数的地址保存在寄存器EDX中，并取出其值，与前面的sum相加。如此循环往复，直到参数的终止符−1。

当找到了参数串的结尾后，程序再将所有数的算术和sum除以参数的个数（当然不包括终止符−1）。按照这种算法计算出来的商就是各参数的算术平均值。

换句话说，在调用变长参数函数时，调用方函数先把不确定长度的参数堆积为数组，再通过栈把这个数组传递给变长函数参数。这就解释了为什么cdecl调用规范会要求将第一个参数最后一个推送入栈了。因为如果不这样的话，被调用方函数会找不到第一个参数，这会导致printf()这样的函数因为找不到格式化字符串的地址而无法运行。

46.1.2　基于寄存器的调用规范

细心的读者也可能会问，那些优先利用寄存器传递参数的调用规范是什么情况？下面我们就来看看。

指令清单46.2　x64下的MSVC 2012优化

$SG3013 DB      '%d', 0aH, 00H

v$ = 8
arith_mean PROC
         mov    DWORD PTR [rsp+8], ecx     ; 1st argument
         mov    QWORD PTR [rsp+16], rdx    ; 2nd argument
         mov    QWORD PTR [rsp+24], r8     ; 3rd argument
         mov    eax, ecx                   ; sum = 1st argument
         lea    rcx, QWORD PTR v$[rsp+8]   ; pointer to the 2nd argument
         mov    QWORD PTR [rsp+32], r9     ; 4th argument
         mov    edx, DWORD PTR [rcx]       ; load 2nd argument
         mov    r8d, 1                     ; count=1
         cmp    edx, -1                    ; 2nd argument is -1?
         je     SHORT $LN8@arith_mean      ; exit if so
$LL3@arith_mean:
         add    eax, edx                   ; sum = sum + loaded argument
         mov    edx, DWORD PTR [rcx+8]     ; load next argument
         lea    rcx, QWORD PTR [rcx+8]     ; shift pointer to point to the argument after next
         inc    r8d                        ; count++
         cmp    edx, -1                    ; is loaded argument -1?
         jne    SHORT $LL3@arith_mean      ; go to loop begin if its not'
$LN8@arith_mean:
; calculate quotient
         cdq
         idiv   r8d
         ret    0
arith_mean ENDP

main     PROC
         sub    rsp, 56
         mov    edx, 2
         mov    DWORD PTR [rsp+40], -1
         mov    DWORD PTR [rsp+32], 15
         lea    r9d, QWORD PTR [rdx+8]
         lea    r8d, QWORD PTR [rdx+5]
         lea    ecx, QWORD PTR [rdx-1]
         call   arith_mean
         lea    rcx, OFFSET FLAT:$SG3013
         mov    edx, eax
         call   printf
         xor    eax, eax
         add    rsp, 56
         ret    0
main     ENDP

在这个程序里，寄存器负责传递函数的前4个参数，栈用来传递其余的2个参数。函数arith_mean()首先将寄存器传递的4个参数存放在阴影空间里，把阴影空间和传递参数的栈合并成了统一而连续的参数数组！

GCC会如何处理参数呢？与MSVC相比，GCC在编译的时候略显画蛇添足。它会把函数分为两部分：第一部分的指令将寄存器的值保存在“红色地带”，并在那里进行处理；而第二部分指令再处理栈的数据。

指令清单46.3　x64下的GCC 4.9.1的优化

arith_mean:
         lea    rax, [rsp+8]
         ; save 6 input registers in "red zone" in the local stack
         mov    QWORD PTR [rsp-40], rsi
         mov    QWORD PTR [rsp-32], rdx
         mov    QWORD PTR [rsp-16], r8
         mov    QWORD PTR [rsp-24], rcx
         mov    esi, 8
         mov    QWORD PTR [rsp-64], rax
         lea    rax, [rsp-48]
         mov    QWORD PTR [rsp-8], r9
         mov    DWORD PTR [rsp-72], 8
         lea    rdx, [rsp+8]
         mov    r8d, 1
         mov    QWORD PTR [rsp-56], rax
         jmp    .L5
.L7:
         ; work out saved arguments
         lea    rax, [rsp-48]
         mov    ecx, esi
         add    esi, 8
         add    rcx, rax
         mov    ecx, DWORD PTR [rcx]
         cmp    ecx, -1
         je     .L4
.L8:
         add    edi, ecx
         add    r8d, 1
.L5:
         ; decide, which part we will work out now.
         ; is current argument number less or equal 6?
         cmp    esi, 47
         jbe    .L7                 ; no, process saved arguments then
         ; work out arguments from stack
         mov    rcx, rdx
         add    rdx, 8
         mov    ecx, DWORD PTR [rcx]
         cmp    ecx, -1
         jne    .L8
.L4:
         mov    eax, edi
         cdq
         idiv   r8d
         ret
.LC1:
         .string "%d\n"
main:
         sub    rsp, 8
         mov    edx, 7
         mov    esi, 2
         mov    edi, 1
         mov    r9d, -1
         mov    r8d, 15
         mov    ecx, 10
         xor    eax, eax
         call   arith_mean
         mov    esi, OFFSET FLAT:.LC1
         mov    edx, eax
         mov    edi, 1
         xor    eax, eax
         add    rsp, 8
         jmp    __printf_chk

另外，本书第64章第8节介绍了阴影空间的另外一个案例。

46.2　vprintf()函数例子

在编写日志(logging)函数的时候，多数人都会自己构造一种与printf类似的、处理“格式化字符串＋一系列（但是数量可变）的内容参数”的变长参数函数。

另外一种常见的变长参数函数就是下文的这种die()函数。这是一种在显示提示信息之后随即退出整个程序的异常处理函数。它需要把不确定数量的输入参数打包、封装并传递给printf()函数。如何实现呢？这些函数名称前面有一个字母v的，这是因为它应当能够处理不确定数量（variable.可变的）的参数。以die()函数调用的vprintf()函数为例，它的输入变量就可分为两部分：一部分是格式化字符串，另一部分是带有多种类型数据变量列表va_list的指针。

#include <stdlib.h>
#include <stdarg.h>

void die (const char * fmt, ...)
{
         va_list va;
         va_start (va, fmt);

         vprintf (fmt, va);
         exit(0);
};

仔细观察就会发现，va_list是一个指向数组的指针。在编译后，这个特征非常明显：

指令清单46.4　MSVC 2010下的程序优化

_fmt$ = 8
_die     PROC
         ; load 1st argument (format-string)
         mov    ecx, DWORD PTR _fmt$[esp-4]
         ; get pointer to the 2nd argument
         lea    eax, DWORD PTR _fmt$[esp]
         push   eax          ; pass pointer
         push   ecx
         call   _vprintf
         add    esp, 8
         push   0
         call   _exit
$LN3@die:
         int    3
_die     ENDP

由此可知，die()函数实现的功能就是：取一个指向参数的指针，再将其传送给vprintf()函数。变长参数（序列）像数组那样被来回传递。

指令清单46.5　x64下的MSVC 2012优化

fmt$ = 48
die      PROC
         ; save first 4 arguments in Shadow Space
         mov    QWORD PTR [rsp+8], rcx
         mov    QWORD PTR [rsp+16], rdx
         mov    QWORD PTR [rsp+24], r8
         mov    QWORD PTR [rsp+32], r9
         sub    rsp, 40
         lea    rdx, QWORD PTR fmt$[rsp+8] ; pass pointer to the 1st argument
         ; RCX here is still points to the 1st argument (format-string) of die()
         ; so vprintf() will take it right from RCX
         call   vprintf
         xor    ecx, ecx
         call   exit
         int    3
die      ENDP

第47章 字符串剪切

我们经常需要去除字符串里的开始符号或者结束符号。

本章介绍的程序，专门用于去除字符串的结尾部分的回车和换行字符CR/LF。

#include <stdio.h>
#include <string.h>

char* str_trim (char *s)
{
         char c;
         size_t str_len;

         // work as long as \r or \n is at the end of string
         // stop if some other character there or its an empty string'
         // (at start or due to our operation)
         for (str_len=strlen(s); str_len>0 && (c=s[str_len-1]); str_len--)
         {
                   if (c=='\r' || c=='\n')
                             s[str_len-1]=0;
                   else
                             break;
         };
         return s;
};

int main()
{
         // test

         // strdup() is used to copy text string into data segment,
         // because it will crash on Linux otherwise,
         // where text strings are allocated in constant data segment,
         // and not modifiable.

         printf ("[%s]\n", str_trim (strdup("")));
         printf ("[%s]\n", str_trim (strdup("\n")));
         printf ("[%s]\n", str_trim (strdup("\r")));
         printf ("[%s]\n", str_trim (strdup("\n\r")));
         printf ("[%s]\n", str_trim (strdup("\r\n")));
         printf ("[%s]\n", str_trim (strdup("test1\r\n")));
         printf ("[%s]\n", str_trim (strdup("test2\n\r")));
         printf ("[%s]\n", str_trim (strdup("test3\n\r\n\r")));
         printf ("[%s]\n", str_trim (strdup("test4\n")));
         printf ("[%s]\n", str_trim (strdup("test5\r")));
         printf ("[%s]\n", str_trim (strdup("test6\r\r\r")));
};

输入参数总能正常返回并退出。当你需要对串进行批量处理时会非常方便，就像这里的main()函数一样。

在该程序循环体的for()语句里有两个判断条件表达式：一个条件表达式是str_len>0（字符串的长度大于零）；另外一个是c=s[str_len-1]（意思是取出的值不为0、不是终止符）。循环判断语句“str_len>0 && (c=s[str_len-1])”实际上利用了所谓“逻辑短路”的执行特性，因此可以这样书写第二个判断表达式（可以参考Yur13:p.1.3.8）。C/C++编译器自左至右的逐一检测判断条件。因为逻辑操作符是“&&”（与），所以一旦第一个条件表达式的值为假，计算机就不用再判断（执行）第二个条件判断表达式。实际上，第二个条件表达式是一种只能在相应的条件下才可以运行的语句。笔者综合利用了第一个条件表达式、逻辑操作符“&&”的逻辑含义以及“逻辑短路”的特性，为第二个条件判断表达式限定了执行条件。

47.1　x64下的MSVC 2013优化

指令清单47.1　x64下的MSVC 2013优化

s$ = 8
str_trim PROC

; RCX is the first function argument and it always holds pointer to the string

; this is strlen() function inlined right here:
; set RAX to 0xFFFFFFFFFFFFFFFF (-1)
         or     rax, -1
$LL14@str_trim:
         inc    rax
         cmp    BYTE PTR [rcx+rax], 0
         jne    SHORT $LL14@str_trim
; is string length zero? exit then
         test   eax, eax
$LN18@str_trim:
         je     SHORT $LN15@str_trim
; RAX holds string length
; here is probably disassembler (or assembler printing routine) error,
; LEA RDX... should be here instead of LEA EDX...
         lea    edx, DWORD PTR [rax-1]
; idle instruction: EAX will be reset at the next instructions execution'
         mov    eax, edx
; load character at address s[str_len-1]
         movzx  eax, BYTE PTR [rdx+rcx]
; save also pointer to the last character to R8
         lea    r8, QWORD PTR [rdx+rcx]
         cmp    al, 13 ; is it '\r'?
         je     SHORT $LN2@str_trim
         cmp    al, 10 ; is it '\n'?
         jne    SHORT $LN15@str_trim
$LN2@str_trim:
; store 0 to that place
         mov    BYTE PTR [r8], 0
         mov    eax, edx
; check character for 0, but conditional jump is above...
         test   edx, edx
         jmp    SHORT $LN18@str_trim
$LN15@str_trim:
; return "s"
         mov    rax, rcx
         ret    0
str_trim ENDP

第一个特征就是MSVC编译器对字符串长度函数strlen()进行了内联（inline）式的展开和嵌入处理。编译器认为，内联处理后的执行效率会比常规的函数调用（call）的效率更高 。有关内联函数可以参考本书的第43章。

内嵌处理之后，strlen()函数的第一个指令是：OR RAX,0xffffffffffffffff。我们不清楚为何MSVC采用OR（或）指令，而没有采用MOV RAX, 0xffffffffffffffff指令直接赋值。当然，这两条指令执行的是相同操作：将所有位设置为1。因此这个数值就是赋值为−1。可以参考本书的第30章。

你也可能会问，为什么strlen()函数会用到−1这个数。当然是出于优化的目的。这里我们列出了MSVC的编译代码。

指令清单47.2　x64下的MSVC 2013的内嵌函数strlen()

; RCX = pointer to the input string
; RAX = current string length
         or     rax, -1
label:
         inc    rax
         cmp    BYTE PTR [rcx+rax], 0
         jne    SHORT label
; RAX = string length

如果把这个变量的初始值设置为0，那么是否可以把代码压缩得更短一些呢？我们可以来试试。

指令清单47.3　我们的strlen()字符串长度函数版本

; RCX = pointer to the input string
; RAX = current string length
         xor    rax, rax
label:
         cmp    byte ptr [rcx+rax], 0
         jz     exit
         inc    rax
         jmp    label
exit:
; RAX = string length

我们没能成功。因为我们不得不加入了一个额外的指令：JMP跳转指令。

在使用“－1”作为初始值之后，MSVC 2013随即在加载字符的指令之前分配了一个INC指令。如果第一个字符是0（终止符），那么RAX就直接为0，返回的字符串长度还会是0。

函数中的其余部分还是很好懂的。当然在程序的最后有另外一个技巧。除去那些内联之后的strlen()函数的展开代码，整个函数就只有3个条件指令。其实，从道理上讲这里应该有4个转移指令：第4个应当位于函数的结尾部分，用于检查当前字符是不是0。然后这段代码使用了一个跳转到“$LN18@str_ trim”标签的无条件转移指令，而这个标签后面的第一个指令就是条件转移指令JE。编译器用这种指令组用来判断输入的字符串是不是空字符串（当前字符是不是终止符），而且直接就是在strlen()执行结束后。所以这里使用JE指令有两个目的。这也许杀鸡用宰牛刀，但是无论如何，MSVC就是这样做的。

要想提示程序性能，就应当尽量脱离条件转移指令进行程序作业。有关详情请参阅本书第33章。

47.2　x64下采用编译器GCC 4.9.1进行非优化操作

str_trim:
         push   rbp
         mov    rbp, rsp
         sub    rsp, 32
         mov    QWORD PTR [rbp-24], rdi
; for() first part begins here
         mov    rax, QWORD PTR [rbp-24]
         mov    rdi, rax
         call   strlen
         mov    QWORD PTR [rbp-8], rax      ; str_len
; for() first part ends here
         jmp    .L2
; for() body begins here
.L5:
         cmp    BYTE PTR [rbp-9], 13        ; c=='\r'?
         je     .L3
         cmp    BYTE PTR [rbp-9], 10        ; c=='\n'?
         jne    .L4
.L3:
         mov    rax, QWORD PTR [rbp-8]      ; str_len
         lea    rdx, [rax-1]                ; EDX=str_len-1
         mov    rax, QWORD PTR [rbp-24]     ; s
         add    rax, rdx                    ; RAX=s+str_len-1
         mov    BYTE PTR [rax], 0           ; s[str_len-1]=0
; for() body ends here
; for() third part begins here
         sub    QWORD PTR [rbp-8], 1        ; str_len--
; for() third part ends here
.L2:
; for() second part begins here
         cmp    QWORD PTR [rbp-8], 0        ; str_len==0?
         je     .L4                         ; exit then
; check second clause, and load "c"
         mov    rax, QWORD PTR [rbp-8]      ; RAX=str_len
         lea    rdx, [rax-1]                ; RDX=str_len-1
         mov    rax, QWORD PTR [rbp-24]     ; RAX=s
         add    rax, rdx                    ; RAX=s+str_len-1
         movzx  eax, BYTE PTR [rax]         ; AL=s[str_len-1]
         mov    BYTE PTR [rbp-9], al        ; store loaded char into "c"
         cmp    BYTE PTR [rbp-9], 0         ; is it zero?
         jne    .L5                         ; yes? exit then
; for() second part ends here
.L4:
; return "s"
         mov    rax, QWORD PTR [rbp-24]
         leave
         ret

笔者在程序中增加了注释。执行完长度计算函数strlen()后，控制权将传递给标号为L2的语句。接着注意检查两个条件表达式。如果第一判断条件表达式为真，也就是说如果长度为0（str_len的值为0），那么计算机将不再检测第二个条件判断表达式。这种特性又称为“逻辑短路”。

概括地说，这个函数的执行流程如下：

运行for()语句的第一部分，也就是调用strlen()函数的循环初始化指令。

跳转到标号L2；检测循环条件是否成立。

跳转到标号L5，进入循环体；

再执行for()语句，如果条件不成立，则直接退出。

执行for()语句的第三部分，将变量str_len递减。

再次跳转到标号L2，检测循环条件是否成立、进入循环……周而复始，直到循环条件不成立。

跳转到L4标号，准备退出。

制备返回值，即变量s。

47.3　x64下的GCC 4.9.1优化

str_trim:
         push   rbx
         mov    rbx, rdi
; RBX will always be "s"
         call   strlen
; check for str_len==0 and exit if its so'
         test   rax, rax
         je     .L9
         lea    rdx, [rax-1]
; RDX will always contain str_len-1 value, not str_len
; so RDX is more like buffer index variable
         lea    rsi, [rbx+rdx]       ; RSI=s+str_len-1
         movzx  ecx, BYTE PTR [rsi]  ; load character
         test   cl, cl
         je     .L9                  ; exit if its zero'
         cmp    cl, 10
         je     .L4
         cmp    cl, 13               ; exit if its not' '\n' and not '\r'
         jne    .L9
.L4:
; this is weird instruction. we need RSI=s-1 here.
; its possible to get it by' MOV RSI, EBX / DEC RSI
; but this is two instructions instead of one
         sub    rsi, rax
; RSI = s+str_len-1-str_len = s-1
; main loop begin
.L12:
         test   rdx, rdx
; store zero at address s-1+str_len-1+1 = s-1+str_len = s+str_len-1
         mov    BYTE PTR [rsi+1+rdx], 0
; check for str_len-1==0. exit if so.
         je     .L9
         sub    rdx, 1               ; equivalent to str_len--
; load next character at address s+str_len-1
         movzx  ecx, BYTE PTR [rbx+rdx]
         test   cl, cl               ; is it zero? exit then
         je     .L9
         cmp    cl, 10               ; is it '\n'?
         je     .L12
         cmp    cl, 13               ; is it '\r'?
         je     .L12
.L9:
; return "s"
         mov    rax, rbx
         pop    rbx
         ret

GCC的实现方式更为复杂。在循环体执行前的代码只执行一次，而且它还会检查结束符是不是回车和换行CR/LF。这难道不是多此一举吗？

一般来说，实现主循环体的流程是这样的：

① 循环开始，检查CR/LF结束符，进行判断。

② 保存零字符。

但是，GCC编译器会将这两步逆序执行。因此，第一步肯定不会是保存零字符，而是进行下述判断：

① 看看第一个字符是不是CR/LF,如果不是的话，就会退出。

② 循环开始，保存零字符。

③ 根据检查字符是不是CR/LF来决定程序的执行。

这样处理之后，主循环体就小了很多，更适用于目前的CPU了。这种代码的中间变量不是str_len，而是str_len-1。或许是因为后者更适用于用作缓冲区型数据的索引标号（数组下标）。很明显，GCC注意到了，str_len-1使用了两次。因此最好的办法是分配一个变量，其值总是比目前的字符串的长度小1，然后再将其递减（按照变量str_len的递减方式递减）。

47.4　ARM64：非优化的GCC（Linaro）4.9

它将生成近乎“一一对应”的汇编指令。具体代码如下所示。

指令清单47.4　非优化的GCC（Linaro[1]）4.9

str_trim:
         stp     x29, x30, [sp, -48]!
         add     x29, sp, 0
         str     x0, [x29,24] ; copy input argument into local stack
         ldr     x0, [x29,24] ; s
         bl      strlen
         str     x0, [x29,40] ; str_len variable in local stack
         b       .L2
; main loop begin
.L5:
         ldrb    w0, [x29,39]
; W0=c
         cmp     w0, 13         ; is it '\r'?
         beq     .L3
         ldrb    w0, [x29,39]
; W0=c
         cmp     w0, 10         ; is it '\n'?
         bne     .L4            ; goto exit if it is not
.L3:
         ldr     x0, [x29,40]
; X0=str_len
         sub     x0, x0, #1
; X0=str_len-1
         ldr     x1, [x29,24]
; X1=s
         add     x0, x1, x0
; X0=s+str_len-1
         strb    wzr, [x0]    ; write byte at s+str_len-1
; decrement str_len:
         ldr     x0, [x29,40]
; X0=str_len
         sub     x0, x0, #1
; X0=str_len-1
         str     x0, [x29,40]
; save X0 (or str_len-1) to local stack
.L2:
         ldr     x0, [x29,40]
; str_len==0?
         cmp     x0, xzr
; goto exit then
         beq     .L4
         ldr     x0, [x29,40]
; X0=str_len
         sub     x0, x0, #1
; X0=str_len-1
         ldr     x1, [x29,24]
; X1=s
         add     x0, x1, x0
; X0=s+str_len-1
; load byte at address s+str_len-1 to W0
         ldrb    w0, [x0]
         strb    w0, [x29,39] ; store loaded byte to "c"
         ldrb    w0, [x29,39] ; reload it
; is it zero byte?
         cmp     w0, wzr
; goto exit, if its zero or to L5 if its not''
         bne     .L5
.L4:
; return s
         ldr     x0, [x29,24]
         ldp     x29, x30, [sp], 48
         ret

47.5　ARM64:优化GCC（Linaro）4.9

编译器采用了更为高级的优化技术。在程序开始时，首先调入第一个字符，并和十进制数10进行比对（也就是换行LF的数值）。此后，后续字符相继被调入主循环。这种处理方法和本书第47章第3节中的例子类似。

指令清单47.5　GCC（Linaro）4.9的优化

str_trim:
         stp     x29, x30, [sp, -32]!
         add     x29, sp, 0
         str     x19, [sp,16]
         mov     x19, x0
; X19 will always hold value of "s"
         bl      strlen
; X0=str_len
         cbz     x0, .L9          ; goto L9 (exit) if str_len==0
         sub     x1, x0, #1
; X1=X0-1=str_len-1
         add     x3, x19, x1
; X3=X19+X1=s+str_len-1
         ldrb    w2, [x19,x1]    ; load byte at address X19+X1=s+str_len-1
; W2=loaded character
         cbz     w2, .L9         ; is it zero? jump to exit then
         cmp     w2, 10          ; is it '\n'?
         bne     .L15
.L12:
; main loop body. loaded character is always 10 or 13 at this moment!
         sub     x2, x1, x0
; X2=X1-X0=str_len-1-str_len=-1
         add     x2, x3, x2
; X2=X3+X2=s+str_len-1+(-1)=s+str_len-2
         strb    wzr, [x2,1]     ; store zero byte at address s+str_len-2+1=s+str_len-1
         cbz     x1, .L9         ; str_len-1==0? goto exit, if so
         sub     x1, x1, #1      ; str_len--
         ldrb    w2, [x19,x1]    ; load next character at address X19+X1=s+str_len-1
         cmp     w2, 10          ; is it '\n'?
         cbz     w2, .L9         ; jump to exit, if its zero'
         beq     .L12            ; jump to begin loop, if its' '\n'
.L15:
         cmp     w2, 13          ; is it '\r'?
         beq     .L12            ; yes, jump to the loop body begin
.L9:
; return "s"
         mov     x0, x19
         ldr     x19, [sp,16]
         ldp     x29, x30, [sp], 32
         ret

47.6　ARM: Keil 6/2013优化（ARM模式）

这里我们会再次看到，编译器分配了ARM模式下的条件指令，使整个代码更为紧凑。

指令清单47.6　Keil 6/2013优化（ARM模式）

str_trim PROC
         PUSH    {r4,lr}
; R0=s
         MOV     r4,r0
; R4=s
         BL      strlen         ; strlen() takes "s" value from R0
; R0=str_len
         MOV     r3,#0
; R3 will always hold 0
|L0.16|
         CMP     r0,#0          ; str_len==0?
         ADDNE   r2,r4,r0       ; (if str_len!=0) R2=R4+R0=s+str_len
         LDRBNE  r1,[r2,#-1]    ; (if str_len!=0) R1=load byte at address R2-1=s+str_len-1
         CMPNE   r1,#0          ; (if str_len!=0) compare loaded byte against 0
         BEQ     |L0.56|        ; jump to exit if str_len==0 or loaded byte is 0
         CMP     r1,#0xd        ; is loaded byte '\r'?
         CMPNE   r1,#0xa        ; (if loaded byte is not '\r') is loaded byte '\r'?
         SUBEQ   r0,r0,#1       ; (if loaded byte is '\r' or '\n') R0-- or str_len--
         STRBEQ  r3,[r2,#-1]    ; (if loaded byte is '\r' or '\n') store R3 (zero) at
    address R2-1=s+str_len-1
         BEQ     |L0.16|        ; jump to loop begin if loaded byte was '\r' or '\n'
|L0.56|
; return "s"
         MOV     r0,r4
         POP     {r4,pc}
         ENDP

47.7　ARM:Keil 6/2013（Thumb模式）优化

在Thumb模式指令集的条件执行指令比ARM模式指令集的少，因此这种代码更接近x86的指令。但是在程序的22和23行处的偏移量0x20和0x1f，会令多数人感到匪夷所思。为什么Keil 6编译器会分配这些指令？老实说，很难讲。也许这就是Keil 6优化进程的诡异之处。不管这种代码多么令人费解，整个程序的功能确实忠实于我们的源代码。

指令清单47.7　Keil 6/2013（Thumb模式）优化

1     str_trim PROC
1             PUSH    {r4,lr}
2             MOVS    r4,r0
4     ; R4=s
5             BL      strlen          ; strlen() takes "s" value from R0
6     ; R0=str_len
7             MOVS    r3,#0
8     ; R3 will always hold 0
9             B       |L0.24|
10    |L0.12|
11            CMP     r1,#0xd         ; is loaded byte '\r'?
12            BEQ     |L0.20|
13            CMP     r1,#0xa         ; is loaded byte '\n'?
14            BNE     |L0.38|         ; jump to exit, if no
15    |L0.20|
16            SUBS    r0,r0,#1        ; R0-- or str_len--
17            STRB    r3,[r2,#0x1f]   ; store 0 at address R2+0x1F=s+str_len-0x20+0x1F=s+str_len-1
18    |L0.24|
19            CMP     r0,#0           ; str_len==0?
20            BEQ     |L0.38|         ; yes? jump to exit
21            ADDS    r2,r4,r0        ; R2=R4+R0=s+str_len
22            SUBS    r2,r2,#0x20     ; R2=R2-0x20=s+str_len-0x20
23            LDRB    r1,[r2,#0x1f]   ; load byte at
24        address R2+0x1F=s+str_len-0x20+0x1F=s+str_len-1 to R1
25            CMP     r1,#0           ; is loaded byte 0?
26            BNE     |L0.12|         ; jump to loop begin, if its not 0'
27     |L0.38|
28     ; return "s"
29            MOVS    r0,r4
30            POP     {r4,pc}
31            ENDP

47.8　MIPS

指令清单47.8　(IDA)GCC 4.4.5优化

str_trim:
; IDA is not aware of local variable names, we gave them manually:
saved_GP         = -0x10
saved_S0         = -8
saved_RA         = -4

                lui      $gp, (__gnu_local_gp >> 16)
                addiu    $sp, -0x20
                la       $gp, (__gnu_local_gp & 0xFFFF)
                sw       $ra, 0x20+saved_RA($sp)
                sw       $s0, 0x20+saved_S0($sp)
                sw       $gp, 0x20+saved_GP($sp)
; call strlen(). input string address is still in $a0, strlen() will take it from there:
                lw       $t9, (strlen & 0xFFFF)($gp)
                or       $at, $zero ; load delay slot, NOP
                jalr     $t9
; input string address is still in $a0, put it to $s0:
                move     $s0, $a0 ; branch delay slot
; result of strlen() (i.e, length of string) is in $v0 now
; jump to exit if $v0==0 (i.e., if length of string is 0):
                beqz     $v0, exit
                or       $at, $zero ; branch delay slot, NOP
                addiu    $a1, $v0, -1
; $a1 = $v0-1 = str_len-1
                addu     $a1, $s0, $a1
; $a1 = input string address + $a1 = s+strlen-1
; load byte at address   $a1:
                lb       $a0, 0($a1)
                or       $at, $zero ; load delay slot, NOP
; loaded byte is zero? jump to exit if its so':
                beqz     $a0, exit
                or       $at, $zero ; branch delay slot, NOP
                addiu    $v1, $v0, -2
; $v1 = str_len-2
                addu     $v1, $s0, $v1
; $v1 = $s0+$v1 = s+str_len-2
                li       $a2, 0xD
; skip loop body:
                b         loc_6C
                li        $a3, 0xA ; branch delay slot
loc_5C:
; load next byte from memory to $a0:
                lb        $a0, 0($v1)
                move      $a1, $v1
; $a1=s+str_len-2
; jump to exit if loaded byte is zero:
                beqz      $a0, exit
; decrement str_len:
                addiu     $v1, -1     ; branch delay slot
loc_6C:
; at this moment, $a0=loaded byte, $a2=0xD (CR symbol) and $a3=0xA (LF symbol)
; loaded byte is CR? jump to loc_7C then:
                beq       $a0, $a2, loc_7C
                addiu     $v0, -1     ; branch delay slot
; loaded byte is LF? jump to exit if its not LF':
                bne       $a0, $a3, exit
                or        $at, $zero  ; branch delay slot, NOP
loc_7C:
; loaded byte is CR at this moment
; jump to loc_5c (loop body begin) if str_len (in $v0) is not zero:
                bnez      $v0, loc_5C
; simultaneously, store zero at that place in memory:
                sb        $zero, 0($a1) ; branch delay slot
; "exit" label was named by me manually:
exit:
                lw        $ra, 0x20+saved_RA($sp)
                move      $v0, $s0
                lw        $s0, 0x20+saved_S0($sp)
                jr        $ra
                addiu     $sp, 0x20     ; branch delay slot

S-字头的寄存器就是保存寄存器（saved temporaries）。在过程调用过程中，保存寄存器的值需要保留（被调用方函数保存和恢复），因此$S0的值保存在局部栈里，在完成任务后恢复其初始状态。

[1]　Linaro是一家开源的基于ARM操作平台的组织，由多家业内公司联合成立。其开发了ARM开发工具、Linux内核以及Linux发行版（包括Android及Ubuntu）的主要自动建构系统。

第48章 toupper()函数

本章讨论的是将小写字符转换成大写字符的toupper()函数。这是一个比较常见的函数。

char toupper (char c)
{
         if(c>='a' && c<='z')
                  return c-'a'+'A';
         else
                  return c;
}

这里的程序代码中，我们可以看到’a’+’A’这个表达式。这种写法旨在增强程序的可读性。在程序的实际编译过程中，它会被优化为相应的计算指令。[1]

我们知道，小写的英文首字母a的ASCII码十六进制是61，也就是十进制的97；而大写的A的ASCII码的十六进制则是41，相当于十进制的65。很显然，在ASCII码表中，两者的差值是32（0x20）。

实际上，我们从图48.1所示的这个7位标准ASCII码表格就能看得更清楚了。

图48.1　EMACS中的7位ASCII码表

48.1　x64

48.1.1　两个比较操作

非优化的MSVC非常直接：为了执行转换到大写字母的目的，程序首先检查输入的符号的十进制数值是否在97～122这个范围，也就是小写字母a～z的范围内。如果输入值满足这个条件，那么就将它的数值减去十进制的32，从而得到相应的大写字母所对应的值了。非优化编译时，编译器同样会对代码进行小幅度的优化处理。

指令清单48.1　x64下的MSVC 2013非优化程序

 1  c$ = 8
 2  toupper PROC
 3          mov     BYTE PTR [rsp+8], cl
 4          movsx   eax, BYTE PTR c$[rsp]
 5          cmp     eax, 97
 6          jl      SHORT $LN2@toupper
 7          movsx   eax, BYTE PTR c$[rsp]
 8          cmp     eax, 122
 9          jg      SHORT $LN2@toupper
10          movsx   eax, BYTE PTR c$[rsp]
11          sub     eax, 32
12          jmp     SHORT $LN3@toupper
13          jmp     SHORT $LN1@toupper ; compiler artefact
14  $LN2@toupper:
15          movzx   eax, BYTE PTR c$[rsp] ; unnecessary casting
16  $LN1@toupper:
17  $LN3@toupper: ; compiler artefact
18          ret     0
19  toupper ENDP

需要注意的是：程序第3行把输入的字节装载到64位栈的那条指令。输入值显然是占据了64位的低8位，而其高位（从第8位到第63位）则保持不变。我们可以在调试器debugger里看到这高56位是随机的噪音数据（也就是随机数）。因为所有指令都是以字节为操作对象，因此这种存储方案不会引发问题。第15行的最后一个MOVZX指令从本地栈里取出一个字节，把它无符号扩展为32位数据中，因此这高56位的噪音数据都会被填充为零。

而非优化的GCC生成的代码几乎相同。

指令清单48.2　x64下的GCC 4.9非优化代码

toupper:
         push    rbp
         mov     rbp, rsp
         mov     eax, edi
         mov     BYTE PTR [rbp-4], al
         cmp     BYTE PTR [rbp-4], 96
         jle     .L2
         cmp     BYTE PTR [rbp-4], 122
         jg      .L2
         movzx   eax, BYTE PTR [rbp-4]
         sub     eax, 32
         jmp     .L3
.L2:
         movzx   eax, BYTE PTR [rbp-4]
.L3:
         pop     rbp
         ret

48.1.2　一个比较操作

优化的MSVC表现得更好，它只分配一个比较操作指令。

指令清单48.3　x64下的MSVC 2013优化程序

toupper PROC
        lea     eax, DWORD PTR [rcx-97]
        cmp     al, 25
        ja      SHORT $LN2@toupper
        movsx   eax, cl
        sub     eax, 32
        ret     0
$LN2@toupper:
        movzx   eax, cl
        ret     0
toupper ENDP

前面已经解释过了，如何用一个比较操作来代替两个（参见本书42.2.1节）。

如果用C/C++重写，相应的源代码如下：

int tmp=c-97;

if (tmp>25)
         return c;
else
         return c-32;

这里的变量tmp应当是一个有符号数。这里的转换过程采用了两个减法指令以及一个比较指令。而原来的算法是通过两个比较指令加一个减法指令来实现的。

优化的GCC算法更好，它不用跳转指令JUMP，转而采用CMOVcc指令（参见33.1节）。

指令清单48.4　x64下的GCC 4.9优化程序

1  toupper:
2          lea      edx, [rdi-97] ; 0x61
3          lea      eax, [rdi-32] ; 0x20
4          cmp      dl, 25
5          cmova    eax, edi
6          ret

第三行的指令代码首先准备了要减去的数值0x20（十六进制的20，十进制的32），这会给人一种印象，好像减法总会发生似的。而第五行的指令，在不应进行转换（也就是不执行减法）的时候，把原始的输入数据复制给EAX寄存器。可见它能在条件不成立的情况下舍弃错误的值。也就是说，编译器为了避免使用条件转移指令而预先进行了减法运算。

48.2　ARM

ARM模式下的Keil优化也只有一个比较指令。

指令清单48.5　ARM模式下的Keil 6/2013优化程序

toupper PROC
        SUB     r1,r0,#0x61
        CMP     r1,#0x19
        SUBLS   r0,r0,#0x20
        ANDLS   r0,r0,#0xff
        BX      lr
        ENDP

从这里的程序我们可以看到，如果R1的值小于或等于0x19（十六进制的19），那么就会执行SUBLS指令和ANDLS指令。这两条指令实际执行的是转换大写字符的操作。

而Thumb模式下的Keil优化也可以通过单条比较指令进行大写转换。

指令清单48.6　Thumb模式下的Keil 6/2013的优化指令

toupper PROC
        MOVS    r1,r0
        SUBS    r1,r1,#0x61
        CMP     r1,#0x19
        BHI     |L0.14|
        SUBS    r0,r0,#0x20
        LSLS    r0,r0,#24
        LSRS    r0,r0,#24
|L0.14|
        BX      lr
        ENDP

最后的两条指令（LSLS和LSRS）的整体作用其实就是“AND reg,0xFF”。用C/C++表达式书写的话，它就可以表示为“(i<<24)>>24”，也就是先左移动24位，然后右移24位。显然，Thumbe模式下的Keil认为，它们这里的2个双字节的指令比“AND reg,0xFF”的运行效率更高（后者要首先将0xFF载入寄存器，然后做And与操作）。

48.2.1　ARM64下的GCC

指令清单48.7　ARM64下的非优化GCC 4.9

toupper:
        sub     sp, sp, #16
        strb    w0, [sp,15]
        ldrb    w0, [sp,15]
        cmp     w0, 96
        bls     .L2
        ldrb    w0, [sp,15]
        cmp     w0, 122
        bhi     .L2
        ldrb    w0, [sp,15]
        sub     w0, w0, #32
        uxtb    w0, w0
        b       .L3
.L2:
        ldrb    w0, [sp,15]
.L3:
        add     sp, sp, 16
        ret

指令清单48.8　ARM64下的优化GCC 4.9

toupper:
        uxtb    w0, w0
        sub     w1, w0, #97
        uxtb    w1, w1
        cmp     w1, 25
        bhi     .L2
        sub     w0, w0, #32
        uxtb    w0, w0
.L2:
        ret

48.3　总结

本章演示的优化技术已经属于十分常见的编译器优化技术。逆向工程分析人员会频繁遇到这种类型的汇编指令。

[1]　然而，如果小心翼翼的话，还是有些编译器即使不用优化的办法，也能输出正确的结果。

第49章 不正确的反汇编代码

在实际工程中，反编译工程师们经常会遇到一些不正确的反汇编代码。我们来看看如何处理。

49.1　x86环境下的从一开始错误的反汇编

与opcode等长的ARM以及MIPS指令集（它们每个指令的opcode无非就是2个字节或4个字节长）不同，x86构架下的指令长度不尽相同。因此，若从x86程序的中间开始解析指令，无论什么分析工具都会分析出错误的结果。

比如说：

add      [ebp-31F7Bh], cl
dec      dword ptr [ecx-3277Bh]
dec      dword ptr [ebp-2CF7Bh]
inc      dword ptr [ebx-7A76F33Ch]
fdiv     st(4), st
db 0FFh
dec      dword ptr [ecx-21F7Bh]
dec      dword ptr [ecx-22373h]
dec      dword ptr [ecx-2276Bh]
dec      dword ptr [ecx-22B63h]
dec      dword ptr [ecx-22F4Bh]
dec      dword ptr [ecx-23343h]
jmp      dword ptr [esi-74h]
xchg     eax, ebp
clc
std
db 0FFh
db 0FFh
mov      word ptr [ebp-214h], cs ; <- disassembler finally found right track here
mov      word ptr [ebp-238h], ds
mov      word ptr [ebp-23Ch], es
mov      word ptr [ebp-240h], fs
mov      word ptr [ebp-244h], gs
pushf
pop      dword ptr [ebp-210h]
mov      eax, [ebp+4]
mov      [ebp-218h], eax
lea      eax, [ebp+4]
mov      [ebp-20Ch], eax
mov      dword ptr [ebp-2D0h], 10001h
mov      eax, [eax-4]
mov      [ebp-21Ch], eax
mov      eax, [ebp+0Ch]
mov      [ebp-320h], eax
mov      eax, [ebp+10h]
mov      [ebp-31Ch], eax
mov      eax, [ebp+4]
mov      [ebp-314h], eax
call     ds:IsDebuggerPresent
mov      edi, eax
lea      eax, [ebp-328h]
push     eax
call     sub_407663
pop      ecx
test     eax, eax
jnz      short loc_402D7B

这里我们可以看到，解析出来的前几条指令毫无道理。但是后来反汇编工具逐渐走上了正轨。

49.2　随机噪音，怎么看起来像反汇编指令？

一般来说，我们可以从以下三点来判断，一段程序是由随机代码组成的还是正常的程序段：

不寻常的指令集合。一般来说，x86最常见的指令是PUSH、MOV、CALL等。如果遇到了大杂烩式的稀有指令大拼盘（浮点运算FPU指令、输入输出IN/OUT指令和很少见的一些系统指令混在一起），多数情况就是反汇编过程出问题了。

又大又像是随机数的数值、偏移量以及立即数。

转移指令的偏移量不合逻辑，经常跳转到其他指令块的中间。

指令清单49.1　x86模式下的随机数噪声

     mov    bl, 0Ch
     mov    ecx, 0D38558Dh
     mov    eax, ds:2C869A86h
     db     67h
     mov    dl, 0CCh
     insb
     movsb
     push   eax
     xor    [edx-53h], ah
     fcom   qword ptr [edi-45A0EF72h]
     pop    esp
     pop    ss
     in     eax, dx
     dec    ebx
     push   esp
     lds    esp, [esi-41h]
     retf
     rcl    dword ptr [eax], cl
     mov    cl, 9Ch
     mov    ch, 0DFh
     push   cs
     insb
     mov    esi, 0D9C65E4Dh
     imul   ebp, [ecx], 66h
     pushf
     sal    dword ptr [ebp-64h], cl
     sub    eax, 0AC433D64h
     out    8Ch, eax
     pop    ss
     sbb    [eax], ebx
     aas
     xchg   cl, [ebx+ebx*4+14B31Eh]
     jecxz  short near ptr loc_58+1
     xor    al, 0C6h
     inc    edx
     db     36h
     pusha
     stosb
     test   [ebx], ebx
     sub    al, 0D3h ; 'L'
     pop    eax
     stosb

loc_58: ; CODE XREF: seg000:0000004A
     test   [esi], eax
     inc    ebp
     das
     db     64h
     pop    ecx
     das
     hlt

     pop    edx
     out    0B0h, al
     lodsb
     push   ebx
     cdq
     out    dx, al
     sub    al, 0Ah
     sti
     outsd
     add    dword ptr [edx], 96FCBE4Bh
     and    eax, 0E537EE4Fh
     inc    esp
     stosd
     cdq
     push   ecx
     in     al, 0CBh
     mov    ds:0D114C45Ch, al
     mov    esi, 659D1985h

指令清单49.2　x86-64下的随机数噪声

     lea    esi, [rax+rdx*4+43558D29h]

loc_AF3: ; CODE XREF: seg000:0000000000000B46
     rcl    byte ptr [rsi+rax*8+29BB423Ah], 1
     lea    ecx, cs:0FFFFFFFFB2A6780Fh
     mov    al, 96h
     mov    ah, 0CEh
     push   rsp
     lods   byte ptr [esi]

     db 2Fh ; /

     pop    rsp
     db     64h
     retf   0E993h

     cmp    ah, [rax+4Ah]
     movzx  rsi, dword ptr [rbp-25h]
     push   4Ah
     movzx  rdi, dword ptr [rdi+rdx*8]

     db 9Ah

     rcr    byte ptr [rax+1Dh], cl
     lodsd
     xor    [rbp+6CF20173h], edx
     xor    [rbp+66F8B593h], edx
     push   rbx
     sbb    ch, [rbx-0Fh]
     stosd
     int    87h
     db     46h, 4Ch
     out    33h, rax
     xchg   eax, ebp
     test   ecx, ebp
     movsd
     leave
     push   rsp

     db 16h

     xchg   eax, esi
     pop    rdi

loc_B3D: ; CODE XREF: seg000:0000000000000B5F
     mov    ds:93CA685DF98A90F9h, eax
     jnz    short near ptr loc_AF3+6
     out    dx, eax
     cwde
     mov    bh, 5Dh ; ']'
     movsb
     pop    rbp

指令清单49.3　ARM模式下的随机数噪声

BLNE     0xFE16A9D8
BGE      0x1634D0C
SVCCS    0x450685
STRNVT   R5, [PC],#-0x964
LDCGE    p6, c14, [R0],#0x168
STCCSL   p9, c9, [LR],#0x14C
CMNHIP   PC, R10,LSL#22
FLDMIADNV LR!, {D4}
MCR      p5, 2, R2,c15,c6, 4
BLGE     0x1139558
BLGT     0xFF9146E4
STRNEB   R5, [R4],#0xCA2
STMNEIB  R5, {R0,R4,R6,R7,R9-SP,PC}
STMIA    R8, {R0,R2-R4,R7,R8,R10,SP,LR}^
STRB     SP, [R8],PC,ROR#18
LDCCS    p9, c13, [R6,#0x1BC]
LDRGE    R8, [R9,#0x66E]
STRNEB   R5, [R8],#-0x8C3
STCCSL   p15, c9, [R7,#-0x84]
RSBLS    LR, R2, R11,ASR LR
SVCGT    0x9B0362
SVCGT    0xA73173
STMNEDB  R11!, {R0,R1,R4-R6,R8,R10,R11,SP}
STR      R0, [R3],#-0xCE4
LDCGT    p15, c8, [R1,#0x2CC]
LDRCCB   R1, [R11],-R7,ROR#30
BLLT     0xFED9D58C
BL       0x13E60F4
LDMVSIB  R3!, {R1,R4-R7}^
USATNE   R10, #7, SP,LSL#11
LDRGEB   LR, [R1],#0xE56
STRPLT   R9, [LR],#0x567
LDRLT    R11, [R1],#-0x29B
SVCNV    0x12DB29
MVNNVS   R5, SP,LSL#25
LDCL     p8, c14, [R12,#-0x288]
STCNEL   p2, c6, [R6,#-0xBC]!
SVCNV    0x2E5A2F
BLX      0x1A8C97E
TEQGE    R3, #0x1100000
STMLSIA  R6, {R3,R6,R10,R11,SP}
BICPLS   R12, R2, #0x5800
BNE      0x7CC408
TEQGE    R2, R4,LSL#20
SUBS     R1, R11, #0x28C
BICVS    R3, R12, R7,ASR R0
LDRMI    R7, [LR],R3,LSL#21
BLMI     0x1A79234
STMVCDB  R6, {R0-R3,R6,R7,R10,R11}
EORMI    R12, R6, #0xC5
MCRRCS   p1, 0xF, R1,R3,c2

指令清单49.4　ARM的Thumb模式下的随机数噪声

     LSRS   R3, R6, #0x12
     LDRH   R1, [R7,#0x2C]
     SUBS   R0, #0x55 ; 'U'
     ADR    R1, loc_3C
     LDR    R2, [SP,#0x218]
     CMP    R4, #0x86
     SXTB   R7, R4
     LDR    R4, [R1,#0x4C]
     STR    R4, [R4,R2]
     STR    R0, [R6,#0x20]
     BGT    0xFFFFFF72
     LDRH   R7, [R2,#0x34]
     LDRSH  R0, [R2,R4]
     LDRB   R2, [R7,R2]

     DCB 0x17
     DCB 0xED

     STRB   R3, [R1,R1]
     STR    R5, [R0,#0x6C]
     LDMIA  R3, {R0-R5,R7}
     ASRS   R3, R2, #3
     LDR    R4, [SP,#0x2C4]
     SVC    0xB5
     LDR    R6, [R1,#0x40]
     LDR    R5, =0xB2C5CA32
     STMIA  R6, {R1-R4,R6}
     LDR    R1, [R3,#0x3C]
     STR    R1, [R5,#0x60]
     BCC    0xFFFFFF70
     LDR    R4, [SP,#0x1D4]
     STR    R5, [R5,#0x40]
     ORRS   R5, R7

loc_3C ; DATA XREF: ROM:00000006
     B      0xFFFFFF98

指令清单49.5　MIPS（小端）下的随机数噪声

     lw     $t9, 0xCB3($t5)
     sb     $t5, 0x3855($t0)
     sltiu  $a2, $a0, -0x657A
     ldr    $t4, -0x4D99($a2)
     daddi  $s0, $s1, 0x50A4
     lw     $s7, -0x2353($s4)
     bgtzl  $a1, 0x17C5C

     .byte 0x17
     .byte 0xED
     .byte 0x4B # K
     .byte 0x54 # T

     lwc2   $31, 0x66C5($sp)
     lwu    $s1, 0x10D3($a1)
     ldr    $t6, -0x204B($zero)
     lwc1   $f30, 0x4DBE($s2)
     daddiu $t1, $s1, 0x6BD9
     lwu    $s5, -0x2C64($v1)
     cop0   0x13D642D
     bne    $gp, $t4, 0xFFFF9EF0
     lh     $ra, 0x1819($s1)
     sdl    $fp, -0x6474($t8)
     jal    0x78C0050
     ori    $v0, $s2, 0xC634
     blez   $gp, 0xFFFEA9D4
     swl    $t8, -0x2CD4($s2)
     sltiu  $a1, $k0, 0x685
     sdc1   $f15, 0x5964($at)
     sw     $s0, -0x19A6($a1)
     sltiu  $t6, $a3, -0x66AD
     lb     $t7, -0x4F6($t3)
     sd     $fp, 0x4B02($a1)

我们必须注意的一点是：一些编写良好的解压包或者加密程序代码（也包括一些变形代码），从代码来看也很像是随机数指令序列，然而一旦运行起来则是非常正确的。

第50章 混淆技术

代码混淆技术是一种用于阻碍逆向工程分析人员解析程序代码（或功能）的指令处理技术。

50.1　字符串变换

我们在第57章可看到，在逆向工程的过程中字符串经常起到路标的作用。注意到这个问题的编程人员就会着手解决这个问题。他们会采用一些变换的手法，让他人不能直接通过IDA或者16进制编辑器直接搜索到字符串原文。

这里我们举一个简单的例子。

比方说，我们可以这样构造一个字符串：

Mov     byte ptr [ebx], 'h'
mov     byte ptr [ebx+1], 'e'
mov     byte ptr [ebx+2], 'l'
mov     byte ptr [ebx+3], 'l'
mov     byte ptr [ebx+4], 'o'
mov     byte ptr [ebx+5], ' '
mov     byte ptr [ebx+6], 'w'
mov     byte ptr [ebx+7], 'o'
mov     byte ptr [ebx+8], 'r'
mov     byte ptr [ebx+9], 'l'
mov     byte ptr [ebx+10], 'd'

当然还有更为复杂的构造方法：

mov     ebx, offset username
cmp     byte ptr [ebx], 'j'
jnz     fail
cmp     byte ptr [ebx+1], 'o'
jnz     fail
cmp     byte ptr [ebx+2], 'h'
jnz     fail
cmp     byte ptr [ebx+3], 'n'
jnz     fail
jz      it_is_john

不管是以上的哪种情况，我们用十六进制的文本编译器都不能直接搜索到字符串原文。

实际上这两种方法适用于那些无法利用数据段构造数据的情景。因为它们可以在文本段直接构造数据，所以也常见于各种PIC和shellcode。

另外，笔者还见过这样使用sprintf()函数的：

sprintf(buf, "%s%c%s%c%s", "hel",'l',"o w",'o',"rld");

代码看起来很诡异，但是作为一个简单的反编译技巧来说，也不失为一个好办法。

加密存储字符串是另一种常见的处理方法。只是这样一来，就要在每次使用前对字符串解密。相关的例子可以参看第78章第2节。

50.2　可执行代码

50.2.1　插入垃圾代码

在正常执行指令序列中插入一些虽然可被执行但是没有任何作用的指令，本身就是一种代码混淆技术。

我们可以看一个简单的例子。

指令清单50.1　源代码

add     eax, ebx
mul     ecx

指令清单50.2　采用混淆技术后的代码

xor     esi, 011223344h ; garbage
add     esi, eax        ; garbage
add     eax, ebx
mov     edx, eax        ; garbage
shl     edx, 4          ; garbage
mul     ecx
xor     esi, ecx        ; garbage

在程序代码中插入的混淆指令，调用了源程序不会使用的ESI和EDX寄存器。混淆代码利用了源程序的中间之后，大幅度地增加了反编译的难度，何乐不为呢？

50.2.2　用多个指令组合代替原来的一个指令

MOV op1,op2这条指令，可以使用组合指令代替：PUSH op2, POP op1。

JMP label指令可以用PUSH label, RET这个指令对代替。反编译工具IDA不能识别出这种label标签的调用结构。

CALL label指令则可以用以下三个指令代替：PUSH ｛call指令后面的那个label｝、PUSH label和RET指令。

PUSH op可以用以下的指令代替。SUB ESP,4或8; MOV [ESP],操作符。

50.2.3　始终执行或者从来不会执行的代码

在下面的代码中，假定此处ESI的值肯定是0，那么我们可以在fake luggage处插入任意长度和复杂度的指令，以达到混淆的目的。这种混淆技术称为不透明谓词（opaque predicate）。

mov     esi, 1
...     ; some code not touching ESI
dec     esi
...     ; some code not touching ESI
cmp     esi, 0
jz      real_code
; fake luggage
real_code:

我们还可以看看其他的例子（同样，我们假定ESI始终会是零）。

add     eax, ebx     ; real code
mul     ecx          ; real code
add     eax, esi     ; opaque predicate. XOR, AND or SHL, etc, can be here instead of ADD.

50.2.4　把指令序列搞乱

instruction 1
instruction 2
instruction 3

上面的3行正常执行的指令序列可以用如下所示的复杂结构代替：

begin:          jmp      ins1_label

ins2_label:     instruction 2
                jmp      ins3_label

ins3_label:     instruction 3
                jmp      exit:

ins1_label:     instruction 1
                jmp      ins2_label
exit:

50.2.5　使用间接指针

dummy_data1     db       100h dup (0)
message1        db       'hello world',0

dummy_data2     db       200h dup (0)
message2        db       'another message',0

func            proc
                ...
                mov      eax, offset dummy_data1 ; PE or ELF reloc here
                add      eax, 100h
                push     eax
                call     dump_string
                ...
                mov      eax, offset dummy_data2 ; PE or ELF reloc here
                add      eax, 200h
                push     eax
                call     dump_string
                ...
func            endp

这个程序执行时，我们只能在IDA编译工具中看到dummy_data1和dummy_data2的reference(调用信息)。它不能正常显示字符串正体message1和message2的调用信息。

全局变量或者函数也可以这样混淆。

50.3　虚拟机以及伪代码

编程人员可以构建其自身的PL或者ISA解释器（类似VB.NET或者Java）。这样的话，反编译者就得花很多时间来理解这些解释器指令的意义以及细节。当然，他们基本上必须开发一种专用的反汇编或者反编译工具了。

50.4　一些其他的事情

笔者对Tiny C编译器做了一些修改，然后用它编译了一个小程序（参见url: http://go.yurichev.com/ 17220

）。请分析该程序的具体功能（参见G.1.13）。

50.5　练习题

50.5.1　练习1

这是一个很短的程序，采用打了补丁的Tiny C编译器编译。看看它能做什么？

答案请参见G.1.15。

第51章 C++

51.1　类

51.1.1　一个简单的例子

从汇编层面看，C++类（class）的组织方式和结构体数据完全一致。

我们演示一个含有两个变量、两个构造函数以及一个方法的类型数据：

#include <stdio.h>

class c
{
private:
     int v1;
     int v2;
public:
     c() // default ctor
     {
          v1=667;
          v2=999;
     };

     c(int a, int b) // ctor
     {
          v1=a;
          v2=b;
     };

     void dump()
     {
          printf ("%d; %d\n", v1, v2);
     };
};

int main()
{
     class c c1;
     class c c2(5,6);

     c1.dump();
     c2.dump();

     return 0;
};

MSVC –x86

使用MSVC编译上述程序，可得到如下所示的代码。

指令清单51.1　MSVC

_c2$ = -16 ; size = 8
_c1$ = -8 ; size = 8
_main PROC
     push  ebp
     mov   ebp, esp
     sub   esp, 16
     lea   ecx, DWORD PTR _c1$[ebp]
     call  ??0c@@QAE@XZ ; c::c
     push  6
     push  5
     lea   ecx, DWORD PTR _c2$[ebp]
     call  ??0c@@QAE@HH@Z ; c::c
     lea   ecx, DWORD PTR _c1$[ebp]
     call  ?dump@c@@QAEXXZ ; c::dump
     lea   ecx, DWORD PTR _c2$[ebp]
     call  ?dump@c@@QAEXXZ ; c::dump
     xor   eax, eax
     mov   esp, ebp
     pop   ebp
     ret   0
_main ENDP

我们来看看程序是如何实现的。程序为每个对象（类的实例）分配了8个字节内存，正好能存储2个变量。

在初始化c1时，编译器调用了无参构造函数??0c@@QAE@XZ。在初始化另一个实例（即c2）时，编译器向有参构造函数??0c@@QAE@HH@Z传递了2个参数。

在传递整个类对象（C++的术语是this）的指针时，this指针通过ECX寄存器传递给被调用方函数。这种调用规范应当符合thiscall规范，详细讲解请参阅本书的51.1.1节。

MSVC通过ECX寄存器传递this指针。不过，这种调用约定并没有统一的技术规范。GCC编译器以传递第一个函数的参数的方式传递this指针，其他的编译器多数都遵循了GCC的thiscall规范。

为什么这些函数有这些很奇怪的名字（见上面）？其实这是编译器对函数名称进行的名称改编（name mangling）的结果。

C++的类可能包含同名的但是参数不同的方法（即类成员函数）。这就是所谓的多态性。当然，不同的类可以有重名却不同的方法。

名称改编（name mangling）是一种在编译过程中，用ASCII字符串将函数、变量的名称重新改编的机制。改编后的方法（类成员函数）名称就被用作该程序内部的函数名。这完全是因为编译器的Linker和加载DLL的OS装载器均不能识别C++或OOP（面向对象的编程语言）的数据结构。

函数dump()调用了两次。我们再来看看构造函数的指令代码。

指令清单51.2　MSVC

_this$ = -4       ; size = 4
??0c@@QAE@XZ PROC ; c::c, COMDAT
; _this$ = ecx
     push ebp
     mov  ebp, esp
     push ecx
     mov  DWORD PTR _this$[ebp], ecx
     mov  eax, DWORD PTR _this$[ebp]
     mov  DWORD PTR [eax], 667
     mov  ecx, DWORD PTR _this$[ebp]
     mov  DWORD PTR [ecx+4], 999
     mov  eax, DWORD PTR _this$[ebp]
     mov  esp, ebp
     pop  ebp
     ret  0
??0c@@QAE@XZ ENDP ; c::c

_this$ = -4 ; size = 4
_a$ = 8     ; size = 4
_b$ = 12    ; size = 4
??0c@@QAE@HH@Z PROC ; c::c, COMDAT
; _this$ = ecx
     push ebp
     mov  ebp, esp
     push ecx
     mov  DWORD PTR _this$[ebp], ecx
     mov  eax, DWORD PTR _this$[ebp]
     mov  ecx, DWORD PTR _a$[ebp]
     mov  DWORD PTR [eax], ecx
     mov  edx, DWORD PTR _this$[ebp]
     mov  eax, DWORD PTR _b$[ebp]
     mov  DWORD PTR [edx+4], eax
     mov  eax, DWORD PTR _this$[ebp]
     mov  esp, ebp
     pop  ebp
     ret  8
??0c@@QAE@HH@Z ENDP ; c::c

构造函数本身就是一种函数，它们使用ECX寄存器存储结构体的指针，然后将指针复制到其自己的局部变量里。当然，第二步并不是必须的。

从C++的标准（ISO13, P.12.1）可知，构造函数不必返回返回值。事实上，从指令层面来来看，构造函数的返回值是一个新建立的对象的指针，即this指针。

现在我们来看看dump()。

指令清单51.3　MSVC

_this$ = -4           ; size = 4
?dump@c@@QAEXXZ PROC  ; c::dump, COMDAT
; _this$ = ecx
     push ebp
     mov  ebp, esp
     push ecx
     mov  DWORD PTR _this$[ebp], ecx
     mov  eax, DWORD PTR _this$[ebp]
     mov  ecx, DWORD PTR [eax+4]
     push ecx
     mov  edx, DWORD PTR _this$[ebp]
     mov  eax, DWORD PTR [edx]
     push eax
     push OFFSET ??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@
     call _printf
     add  esp, 12
     mov  esp, ebp
     pop  ebp
     ret  0
?dump@c@@QAEXXZ ENDP ; c::dump

很简单，dump()函数从ECX寄存器读取一个指向数据结构（这个结构体含有2个int型数据）的指针，然后再把这两个整型数据传递给printf()函数。

如果指定优化编译参数/Ox的话，那么MSVC能够生成更短的可执行程序。

指令清单51.4　MSVC （优化编译）

??0c@@QAE@XZ PROC ; c::c, COMDAT
; _this$ = ecx
     mov  eax, ecx
     mov  DWORD PTR [eax], 667
     mov  DWORD PTR [eax+4], 999
     ret  0
??0c@@QAE@XZ ENDP ; c::c

_a$ = 8  ; size = 4
_b$ = 12 ; size = 4
??0c@@QAE@HH@Z PROC ; c::c, COMDAT
; _this$ = ecx
     mov  edx, DWORD PTR _b$[esp-4]
     mov  eax, ecx
     mov  ecx, DWORD PTR _a$[esp-4]
     mov  DWORD PTR [eax], ecx
     mov  DWORD PTR [eax+4], edx
     ret  8
??0c@@QAE@HH@Z ENDP ; c::c

?dump@c@@QAEXXZ PROC ; c::dump, COMDAT
; _this$ = ecx
     mov  eax, DWORD PTR [ecx+4]
     mov  ecx, DWORD PTR [ecx]
     push eax
     push ecx
     push OFFSET ??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@
     call _printf
     add  esp, 12
     ret  0
?dump@c@@QAEXXZ ENDP ; c::dump

优化编译生产的代码就这么短。我们需要注意的是：在调用构造函数之后，栈指针不是通过“add esp, x”指令到恢复其初始状态的。另一方面，构造函数的最后一条指令是指令ret 8而不是RET。

这是因为此处不仅遵循了thiscall调用规范（参见51.1.1节），而且还同时遵循stdcall调用规范（64.2节）。Stdcall规范约定：应当由被调用方函数（而不是由调用方函数）恢复参数栈的初始状态。构造函数（也是本例中的被调用方函数）使用“add ESP，x”的指令把本地栈释放x字节，然后把程序控制权传递给调用方函数。

读者还可以参考本书第64章，了解各调用规范的详细约定。

必须指出的是，编译器自身能决定调用构造函数和析构函数。我们则可以通过C++语言的编程基础找到程序中的相应指令。

MSVC-x86-64

在x86-64环境里的64位应用程序使用RCX、RDX、R8以及R9这4个寄存器传递函数的前4项参数，而其他的参数则通过栈传递。然而，在调用那些涉及类成员函数的时候，编译器会通过RCX寄存器传递类对象的this指针，用RDX寄存器传递函数的第一个参数，依此类推。我们可以在类成员函数c(int a,int b)中看到这一点。

指令清单51.5　x64下的MSVC 2012优化

; void dump()

?dump@c@@QEAAXXZ PROC ; c::dump
     mov   r8d, DWORD PTR [rcx+4]
     mov   edx, DWORD PTR [rcx]
     lea   rcx, OFFSET FLAT:??_C@_07NJBDCIEC@?$CFd?$DL?5?$CFd?6?$AA@ ; '%d; %d'
     jmp   printf
?dump@c@@QEAAXXZ ENDP ; c::dump

; c(int a, int b)

??0c@@QEAA@HH@Z PROC ; c::c
     mov   DWORD PTR [rcx], edx ; 1st argument: a
     mov   DWORD PTR [rcx+4], r8d ; 2nd argument: b
     mov   rax, rcx
     ret   0
??0c@@QEAA@HH@Z ENDP ; c::c

; default ctor

??0c@@QEAA@XZ PROC ; c::c
     mov   DWORD PTR [rcx], 667
     mov   DWORD PTR [rcx+4], 999
     mov   rax, rcx
     ret   0
??0c@@QEAA@XZ ENDP ; c::c

64位环境下[1]的int型数据依然是32位数据。因此，上述程序仍然使用32位寄存器传递整型数据。

类成员函数dump()还使用了JMP printf指令取代了RET指令。我们在13.1.1节中已经见过这个hack了。

GCC-x86

除了个别不同之处以外，GCC 4.4.1的编译方式和MSVC 2012的编译手段几乎一样。

指令清单51.6　GCC 4.4.1

     public main
main proc near

var_20 = dword ptr -20h
var_1C = dword ptr -1Ch
var_18 = dword ptr -18h
var_10 = dword ptr -10h
var_8  = dword ptr -8

    push ebp
    mov  ebp, esp
    and  esp, 0FFFFFFF0h
    sub  esp, 20h
    lea  eax, [esp+20h+var_8]
    mov  [esp+20h+var_20], eax
    call _ZN1cC1Ev
    mov  [esp+20h+var_18], 6
    mov  [esp+20h+var_1C], 5
    lea  eax, [esp+20h+var_10]
    mov  [esp+20h+var_20], eax
    call _ZN1cC1Eii
    lea  eax, [esp+20h+var_8]
    mov  [esp+20h+var_20], eax
    call _ZN1c4dumpEv
    lea  eax, [esp+20h+var_10]
    mov  [esp+20h+var_20], eax
    call _ZN1c4dumpEv
    mov  eax, 0
    leave
    retn
main endp

这里我们可以看到另外一种风格的名称改编方法，当然这应当是GNU[2]的专用风格。必须注意的是，类对象的指针是以函数的第一个参数的方式传递的。当然，编程人员看不到这些技术细节。

第一个构造函数是：

                public _ZN1cC1Ev ; weak
_ZN1cC1Ev       proc near                   ; CODE XREF: main+10

arg_0           = dword ptr 8

                push    ebp
                mov     ebp, esp
                mov     eax, [ebp+arg_0]
                mov     dword ptr [eax], 667
                mov     eax, [ebp+arg_0]
                mov     dword ptr [eax+4], 999
                pop     ebp
                retn
_ZN1cC1Ev       endp

它通过外部传来的第一个参数获取结构体的指针，然后在相应地址修改了2个数值。

第二个构造函数是：

                public _ZN1cC1Eii
_ZN1cC1Eii      proc near

arg_0           = dword ptr 8
arg_4           = dword ptr 0Ch
arg_8           = dword ptr 10h

                push    ebp
                mov     ebp, esp
                mov     eax, [ebp+arg_0]
                mov     edx, [ebp+arg_4]
                mov     [eax], edx
                mov     eax, [ebp+arg_0]
                mov     edx, [ebp+arg_8]
                mov     [eax+4], edx
                pop     ebp
                retn
_ZN1cC1Eii      endp

上述函数的程序逻辑与下面的C语言代码大致相当：

void ZN1cC1Eii (int *obj, int a, int b)
{
  *obj=a;
  *(obj+1)=b;
};

结果是完全可以预期的。

现在我们来看看dump()函数：

                public _ZN1c4dumpEv
_ZN1c4dumpEv    proc near

var_18          = dword ptr -18h
var_14          = dword ptr -14h
var_10          = dword ptr -10h
arg_0           = dword ptr 8

                push    ebp
                mov     ebp, esp
                sub     esp, 18h
                mov     eax, [ebp+arg_0]
                mov     edx, [eax+4]
                mov     eax, [ebp+arg_0]
                mov     eax, [eax]
                mov     [esp+18h+var_10], edx
                mov     [esp+18h+var_14], eax
                mov     [esp+18h+var_18], offset aDD ; "%d; %d\n"
                call    _printf
                leave
                retn
_ZN1c4dumpEv    endp

这个函数在其内部表征中只有一个参数。这个参数就是这个对象的this指针。

本函数可以用C语言重写如下：

void ZN1c4dumpEv (int *obj)
{
  printf ("%d; %d\n", *obj, *(obj+1));
};

综合本节的各例可知，MSVC和GCC的区别在于函数名的名称编码风格以及传递this指针的具体方式 （MSVC通过ECX传递，而GCC以函数的第一个参数的方式传递）。

GCC-x86-64

在编译64位应用程序的时候，GCC通过RDI、RSI、RDX、RCX、R8以及R9这几个寄存器传递函数的前6个参数。它通过RDI寄存器，以第一个函数参数的形式传递this指针。另外，整数型int数据依然是32位数据。它还会不时使用转移指令JMP替代RET指令。

指令清单51.7　x64下的GCC 4.4.6

; default ctor

_ZN1cC2Ev:
    mov  DWORD PTR [rdi], 667
    mov  DWORD PTR [rdi+4], 999
    ret

; c(int a, int b)

_ZN1cC2Eii:
    mov  DWORD PTR [rdi], esi
    mov  DWORD PTR [rdi+4], edx
    ret

; dump()

_ZN1c4dumpEv:
    mov  edx, DWORD PTR [rdi+4]
    mov  esi, DWORD PTR [rdi]
    xor  eax, eax
    mov  edi, OFFSET FLAT:.LC0 ; "%d; %d\n"
    jmp  printf

51.1.2　类继承

继承而来的类与前文的简单结构体相似，但是它可以对父类进行扩展。

我们先来看一个简单的例子：

#include <stdio.h>

class object
{
    public:
        int color;
        object() { };
        object (int color) { this->color=color; };
        void print_color() { printf ("color=%d\n", color); };
};

class box : public object
{
    private:
        int width, height, depth;
    public:
        box(int color, int width, int height, int depth)
        {
            this->color=color;
            this->width=width;
            this->height=height;
            this->depth=depth;
        };
        void dump()
        {
            printf ("this is box. color=%d, width=%d, height=%d, depth=%d\n", 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\n", 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()方法，它们是在启用优化编译选项/Ox和/Ob0后，由MSVC 2008产生的代码。

指令清单51.8　MSVC 2008带参数/Ob0的优化

??_C@_09GCEDOLPA@color?$DN?$CFd?6?$AA@ DB 'color=%d', 0aH, 00H ; `string'
?print_color@object@@QAEXXZ PROC ; object::print_color, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx]
    push  eax

; 'color=%d', 0aH, 00H
    push OFFSET ??_C@_09GCEDOLPA@color?$DN?$CFd?6?$AA@
    call _printf
    add  esp, 8
    ret  0
?print_color@object@@QAEXXZ ENDP ; object::print_color

指令清单51.9　MSVC 2008带参数/Ob0的优化

?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx+12]
    mov   edx, DWORD PTR [ecx+8]
    push  eax
    mov   eax, DWORD PTR [ecx+4]
    mov   ecx, DWORD PTR [ecx]
    push  edx
    push  eax
    push  ecx

; 'this is box. color=%d, width=%d, height=%d, depth=%d', 0aH, 00H ; `string'
    push OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
    call _printf
    add  esp, 20
    ret  0
?dump@box@@QAEXXZ ENDP ; box::dump

指令清单51.10　MSVC 2008带参数/Ob0的优化

?dump@sphere@@QAEXXZ PROC ; sphere::dump, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx+4]
    mov   ecx, DWORD PTR [ecx]
    push  eax
    push  ecx

; 'this is sphere. color=%d, radius=%d', 0aH, 00H
    push OFFSET ??_C@_0CF@EFEDJLDC@this?5is?5sphere?4?5color?$DN?$CFd?0?5radius@
    call _printf
    add  esp, 12
    ret  0
?dump@sphere@@QAEXXZ ENDP ; sphere::dump

因此，这里是内存的基本排列：

① 父类object对象的存储格局如下所示。

offset

description

+0x0

int color

② 继承类对象：box和sphere（分别为盒子和球体）的存储格局分别如下面两张表所示。

box

offset

description

+0x0

int color

+0x4

int width

+0x8

int height

+0xC

int depth

sphere

offset

description

+0x0

int color

+0x4

int radius

下面分析一下函数主体main()。

指令清单51.11　MSVC 2008带参数/Ob0的优化

PUBLIC _main
_TEXT SEGMENT
_s$ = -24 ; size = 8
_b$ = -16 ; size = 16
_main PROC
    sub   esp, 24
    push  30
    push  20
    push  10
    push  1
    lea   ecx, DWORD PTR _b$[esp+40]
    call  ??0box@@QAE@HHHH@Z ; box::box
    push  40
    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
    ret   0
_main ENDP

继承类必须在其基（父）类字段的后面加入自己的字段，因此基类和继承类的类成员函数可以共存。

当程序调用类成员对象object::print_color()时，指向对象box和sphere的指针是通过this指针传递的。由于在所有继承类和基类中color字段的偏移量固定为0（offset+0x0），所有类对象的类成员函数object::print_color都可以正常运行。

因此，无论是基类还是继承类调用object::print_color()，只要该方法所引用的字段的相对地址固定不变，那么该方法就可以正常运行。

假如基于box类创建一个继承类，那么编译器就会在变量depth的后面追加您所添加新的变量，以确保基类box的各字段的相对地址在其继承类中固定不变。

因此，当父类为box类的各继承类在调用各自的方法box::dump()时，它们都能检索到color、width、height以及depths字段的正确地址。因为各字段的相对地址不会发生变化。

GCC生成的指令代码与MSVC生成的代码几乎相同。唯一的区别是：GCC不会使用ECX寄存器传递this指针，它会以函数的第一个参数的传递方式传递this指针。

51.1.3　封装

封装（encapsulation）的作用是：把既定的数据和方法限定为类的私有信息，使得其他调用方只能访问类所定义的公共方法和公共数据、不能直接访问被封装起来的私有对象。

在指令层面，到底有没有划分私有对象和公开对象的界限呢？

其实完全没有。

我们来看看这一个简单的例子：

#include <stdio.h>

class box
{
    private:
        int color, width, height, depth;
    public:
        box(int color, int width, int height, int depth)
        {
            this->color=color;
            this->width=width;
            this->height=height;
            this->depth=depth;
        };
        void dump()
        {
            printf ("this is box. color=%d, width=%d, height=%d, depth=%d\n", color, width,height, depth);
        };
};

我们启用MSVC 2008的优化选项/Ox和/Ob0编译上述程序，再查看类函数box::dump()的代码。

?dump@box@@QAEXXZ PROC ; box::dump, COMDAT
; _this$ = ecx
    mov   eax, DWORD PTR [ecx+12]
    mov   edx, DWORD PTR [ecx+8]
    push  eax
    mov   eax, DWORD PTR [ecx+4]
    mov   ecx, DWORD PTR [ecx]
    push  edx
    push  eax
    push  ecx
; 'this is box. color=%d, width=%d, height=%d, depth=%d', 0aH, 00H
    push OFFSET ??_C@_0DG@NCNGAADL@this?5is?5box?4?5color?$DN?$CFd?0?5width?$DN?$CFd?0@
    call _printf
    add  esp, 20
    ret  0
?dump@box@@QAEXXZ ENDP ; box::dump

下面这个表格显示了类的变量在内存中的偏移量的分布情况。

offset

description

+0x0

int color

+0x4

int width

+0x8

int height

+0xC

int depth

所有字段都是无法被其他函数直接访问的私有变量。但是，既然我们知道了这个对象的内存存储格局，能不能写出一个修改这些变量的程序呢？

为此，我们可以构造一个名称为hack_oop_encapsulation()的函数。如果不做调整的话，直接访问既定字段的源程序大致会是：

void hack_oop_encapsulation(class box * o)
{
    o->width=1; // that code cant be compiled':
                // "error C2248: 'box::width' : cannot access private member declared in class 'box'"
};

当然，上述代码不可能被成功编译出来。然而，只要把box的数据类型强制转换为整型数组的话，我们就可以通过编译并且直接修改相应字段。

void hack_oop_encapsulation(class box * o)
{
    unsigned int *ptr_to_object=reinterpret_cast<unsigned int*>(o);
    ptr_to_object[1]=123;
};

上述函数的功能十分简单：它将输入数据视为整型数组，然后将数组的第二个元素、一个整型int值修改为123 。

?hack_oop_encapsulation@@YAXPAVbox@@@Z PROC ; hack_oop_encapsulation
    mov eax, DWORD PTR _o$[esp-4]
    mov DWORD PTR [eax+4], 123
    ret 0
?hack_oop_encapsulation@@YAXPAVbox@@@Z ENDP ; hack_oop_encapsulation

接下来，我们验证一下它的功能。

int main()
{
    box b(1, 10, 20, 30);

    b.dump();

    hack_oop_encapsulation(&b);

    b.dump();

    return 0;
};

运行的结果为：

this is box. color=1, width=10, height=20, depth=30
this is box. color=1, width=123, height=20, depth=30

我们可以看到，封装只能够在编译阶段保护类的私有对象。虽然C++编译器禁止外部代码直接访问那些被明确屏蔽的内部对象，但是通过适当的hack技术，我们确实能够突破编译器的限制策略。

51.1.4　多重继承

多重继承，指的是一个类可以同时继承多个父类的字段和方法。

我们还是写个简单的例子：

#include <stdio.h>

class box
{
    public:
        int width, height, depth;
        box() { };
        box(int width, int height, int depth)
        {
            this->width=width;
            this->height=height;
            this->depth=depth;
        };
        void dump()
        {
            printf ("this is box. width=%d, height=%d, depth=%d\n", width, height, depth);
        };
        int get_volume()
        {
            return width * height * depth;
        };
};

class solid_object
{
    public:
        int density;
        solid_object() { };
        solid_object(int density)
        {
            this->density=density;
        };
        int get_density()
        {
            return density;
        };
        void dump()
        {
            printf ("this is solid_object. density=%d\n", 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\n", width,↙
↘ height, depth, density);
        };
        int get_weight() { return get_volume() * get_density(); };
};

int main()
{
    box b(10, 20, 30);
    solid_object so(100);
    solid_box sb(10, 20, 30, 3);

    b.dump();
    so.dump();
    sb.dump();
    printf ("%d\n", sb.get_weight());

    return 0;
};

在启用其优化选项（/Ox和/Ob0）后，我们使用MSVC编译上述程序，重点观察box::dump()、solid_object::dump()以及solid_box::dump()这3个类成员函数。

指令清单51.12　带/Ob0参数的MSVC 2008优化程序

?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
    ret   0
?dump@box@@QAEXXZ ENDP ; box::dump

指令清单51.13　带/Ob0参数的MSVC 2008优化程序

?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

指令清单51.14　带/Ob0参数的MSVC 2008优化程序

?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
    ret   0
?dump@solid_box@@QAEXXZ ENDP ; solid_box::dump

上述3个类对象的内存分布如下：

① 类box。如下表所示。

offset

description

+0x0

width

+0x4

height

+0x8

depth

② 类solid_object。如下表所示。

offset

description

+0x0

density

③ 类solid_box，可以看成是以上两个类的联合体。如下表所示。

offset

description

+0x0

width

+0x4

height

+0x8

depth

+0xC

density

以上图表采用了偏移量与对应变量的方式展现3个类对象的内存存储结构。图中一共出现了4个变量，即长width、高height、宽depth以及密度density。

体积函数get_volume()的代码如下所示。

指令清单51.15　带/Ob0参数的MSVC 2008优化程序

?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()的代码如下所示。

指令清单51.16　带/Ob0参数的MSVC 2008优化程序

?get_density@solid_object@@QAEHXZ PROC ; solid_object::get_density, COMDAT
; _this$ = ecx
    mov  eax, DWORD PTR [ecx]
    ret  0
?get_density@solid_object@@QAEHXZ ENDP ; solid_object::get_density

最有意思的是solod_box::get_weight()重量函数。

指令清单51.17　带/Ob0参数的MSVC 2008优化程序

?get_weight@solid_box@@QAEHXZ PROC ; solid_box::get_weight, COMDAT
; _this$ = ecx
    push  esi
    mov   esi, ecx
    push  edi
    lea   ecx, DWORD PTR [esi+12]
    call  ?get_density@solid_object@@QAEHXZ ; solid_object::get_density
    mov   ecx, esi
    mov   edi, eax
    call  ?get_volume@box@@QAEHXZ ; box::get_volume
    imul  eax, edi
    pop   edi
    pop   esi
    ret   0
?get_weight@solid_box@@QAEHXZ ENDP ; solid_box::get_weight

函数get_weight()（计算重量）只调用了两个方法。在调用get_volume()（计算体积）时，它传递了this指针。而在调用get_density()（密度）函数时，它传递的地址是“this指针＋12个字节”。后面这个地址对应的是solid_box类的solid_object字段。

因此，solid_object::get_density()方法认为，它处理的是常规的solid_object类，而box::get_volume()则可以正常访问原有数据类型的3个变量，如同直接操作box类一样。

因此，我们可以相信：继承了其他的、多个类而生成的类对象，在内存之中就是一种联合体型的数据结构。它继承了原有父类的全部字段和方法。在这种继承类对象调用某个具体方法时，它传递的是与该方法原有基类相对地址相应的this指针。

51.1.5　虚拟方法

我们再来看看一个简单点的例子：

#include <stdio.h>

class object
{
    public:
        int color;
        object() { };
        object (int color) { this->color=color; };
        virtual void dump()
        {
            printf ("color=%d\n", color);
        };
};

class box : public object
{
    private:
        int width, height, depth;
    public:
        box(int color, int width, int height, int depth)
        {
            this->color=color;
            this->width=width;
            this->height=height;
            this->depth=depth;
        };
        void dump()
        {
            printf ("this is box. color=%d, width=%d, height=%d, depth=%d\n", 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\n", 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中的同名函数覆盖了。

在调用虚拟函数时，编译器阶段可能无法确定对象的类型情况。当类中含有虚函数时，其基类的指针就可以指向任何派生类的对象，这时就有可能不知道基类指针到底指向的是哪个对象的情况。这时就要根据实时类型信息，确定应当调用的相应函数。

在启用优化编译选项/Ox和/Ob0后，我们再用MSVC 2008编译主函数main():

_s$ = -32 ; size = 12
_b$ = -20 ; size = 20
_main PROC
    sub   esp, 32
    push  30
    push  20
    push  10
    push  1
    lea   ecx, DWORD PTR _b$[esp+48]
    call  ??0box@@QAE@HHHH@Z ; box::box
    push  40
    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
    ret   0
_main ENDP

指向dump()函数的函数指针应当位于类对象object中的某个地方。我们在哪里去找新方法的函数地址呢？它必定由构造函数定义： main()函数没用调用其他函数，因此这个指针肯定由构造函数定义。

box类实例的构造函数为：

??_R0?AVbox@@@8 DD FLAT:??_7type_info@@6B@ ; box 'RTTI Type Descriptor'
    DD     00H
    DB     '.?AVbox@@', 00H

??_R1A@?0A@EA@box@@8 DD FLAT:??_R0?AVbox@@@8 ; box::'RTTI Base Class Descriptor at (0,-1,0,64)'
    DD     01H
    DD     00H
    DD     0ffffffffH
    DD     00H
    DD     040H
    DD     FLAT:??_R3box@@8

??_R2box@@8 DD     FLAT:??_R1A@?0A@EA@box@@8 ; box::'RTTI Base Class Array'
    DD     FLAT:??_R1A@?0A@EA@object@@8

??_R3box@@8 DD     00H ; box::'RTTI Class Hierarchy Descriptor'
    DD     00H
    DD     02H
    DD     FLAT:??_R2box@@8

??_R4box@@6B@ DD 00H ; box::'RTTI Complete Object Locator'
    DD     00H
    DD     00H
    DD     FLAT:??_R0?AVbox@@@8
    DD     FLAT:??_R3box@@8

??_7box@@6B@ DD     FLAT:??_R4box@@6B@ ; box::`vftable'
    DD     FLAT:?dump@box@@UAEXXZ

_color$ = 8    ; size = 4
_width$ = 12   ; size = 4
_height$ = 16  ; size = 4
_depth$ = 20   ; size = 4
??0box@@QAE@HHHH@Z PROC ; box::box, COMDAT
; _this$ = ecx
    push  esi
    mov   esi, ecx
    call  ??0object@@QAE@XZ ; object::object
    mov   eax, DWORD PTR _color$[esp]
    mov   ecx, DWORD PTR _width$[esp]
    mov   edx, DWORD PTR _height$[esp]
    mov   DWORD PTR [esi+4], eax
    mov   eax, DWORD PTR _depth$[esp]
    mov   DWORD PTR [esi+16], eax
    mov   DWORD PTR [esi], OFFSET ??_7box@@6B@
    mov   DWORD PTR [esi+8], ecx
    mov   DWORD PTR [esi+12], edx
    mov   eax, esi
    pop   esi
    ret   16
??0box@@QAE@HHHH@Z ENDP ; box::box

它的内存布局略有不同：第一个字段是某个box::’vftable’（虚拟函数表）的指针（具体名称由MSVC编译器设置）。

在这个表中，我们看到一个指向数据表box::RTTI Complete Object Locator的链接和一个指向类成员函数box::dump()的链接。它们的正规名称分别是虚拟方法表和RTTI[3]。虚拟方法表存储着各方法的地址，RTTI表存储着类型的信息。另外，RTTI表为C++程序提供了“强制转换运算符”dynamic_cast （将基类类型的指针或引用安全地转换为派生类型的指针或引用）和“类型查询操作符”typeid。在上述指令调用类成员函数时，它所使用的类名称仍然是文本型字符串。基于代码中dump()函数的实例情况可知，在通过调用指向基类的指针（或引用）调用其虚拟函数（类实例：：虚方法）时，指针最终会指向派生类实例的同名虚拟方法——构造函数会把指针实际指向的对象实例的类型信息存储在数据结构之中。

在内存数据表里检索虚拟方法的内存地址必定要消耗额外的CPU时间。因此虚拟方法的运行速度比一般的方法要慢一些。

在GCC生成的相应代码中，RTTI表的构造稍微有些不同。

51.2　ostream输出流

我们来看一个经典的例子“hello world!”，这里我们试图采用输出流的方式重新实现它。

#include <iostream>

int main()
{
    std::cout << "Hello, world!\n";
}

几乎所有的教科书都写明，运算重载符“<<”的作用是“重载”其他类型的数据，主要用于对象之间的运算。我们来看看操作符“<<”的输出流用法。

指令清单51.18　精简版的MSVC 2012的程序代码

$SG37112 DB 'Hello, world!', 0aH, 00H

_main PROC
    push OFFSET $SG37112
    push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
    call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU? ↙
    ↘ $char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
    add   esp, 8
    xor   eax, eax
    ret   0
_main ENDP

我们把源程序稍微修改一下：

#include <iostream>

int main()
{
    std::cout << "Hello, " << "world!\n";
}

教科书都说这种运算会从左至右依次进行。实际上确实如此：

指令清单51.19　MSVC 2012下的程序实现

$SG37112 DB 'world!', 0aH, 00H
$SG37113 DB 'Hello, ', 00H

_main PROC
    push OFFSET $SG37113 ; 'Hello, '
    push OFFSET ?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; std::cout
    call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU? ↙
    ↘ $char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
    add  esp, 8

    push OFFSET $SG37112 ; 'world!'
    push eax             ; result of previous function execution
    call ??$?6U?$char_traits@D@std@@@std@@YAAAV?$basic_ostream@DU? ↙
    ↘ $char_traits@D@std@@@0@AAV10@PBD@Z ; std::operator<<<std::char_traits<char> >
    add  esp, 8

    xor  eax, eax
    ret  0
_main ENDP

如果我们用f()函数来表示“<<”的运算功能的话，那么上述程序就可以表示为：

f(f(std::cout, "Hello, "), "world!");

GCC生成的代码 和MSVC生成的代码基本一样。

51.3　引用

C++中，引用也是指针（可以参考本书第10章），但是调用其是安全的，因为在编译它们时，是很难出现失误的（参见ISO13，p.8.3.2）。比如说，引用永远指向某个类型的既定对象，而且引用不可以是空（参见Cli,p.8.6）。更进一步讲，引用不能改变指向，不可能把某个引用重新指向其他的对象（参见Cli,p.8.5）。

接下来，我们稍微改写一下第10章的第一个源程序，把f1函数的指针全部替换为引用：

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

我们可以看到编译之后的代码和那个使用指针的代码完全一样（参见第10章）。

指令清单51.20　采用MSVC 2010优化的程序代码

_x$ = 8                                              ; size = 4
_y$ = 12                                             ; size = 4
_sum$ = 16                                           ; size = 4
_product$ = 20                                       ; size = 4
?f2@@YAXHHAAH0@Z PROC                                ; f2
        mov     ecx, DWORD PTR _y$[esp-4]
        mov     eax, DWORD PTR _x$[esp-4]
        lea     edx, DWORD PTR [eax+ecx]
        imul eax, ecx
        mov ecx, DWORD PTR _product$[esp-4]
        push esi
        mov     esi, DWORD PTR _sum$[esp]
        mov     DWORD PTR [esi], edx
        mov     DWORD PTR [ecx], eax
        pop     esi
        ret     0
?f2@@YAXHHAAH0@Z ENDP                                ; f2

至于为什么这里的程序使用了一些奇怪的名字，可以参见本书的51.1.1节。

51.4　STL/标准模板库（Standard Template Library）

请注意：本书仅保证本节内容在32位系统里有效，未对x64系统做过验证。

51.4.1　std::string（字符串）

内部

多数字符串库（Yur13,p.2.2）都把std::string定义为以下几个组件：缓冲区指针、字符串缓冲区、当前字符串的长度（便于函数操作；请参阅［Yur13］的2.2.1节）以及当前缓冲区的容量。在缓冲区里，字符串通常用0作字符串结束符，以兼容常规的C语言ASCIIZ字符串格式。

C++的标准ISO13没有定义std::string 的实现方法。然而，它们通常用上述方法定义的这种数据结构。

在C++的标准数据结构中，字符串不是类对象（不像Qt那样，把QString声明为标准类）。它把字符串定义为模板型数据（基本字符串basic_string）。这是为了兼容各种类型的字符元素。现在，它至少支持char（标准字符）以及wchar_t（宽字符）。

因此，std::string是以8位字符char为基本类型的类，而std::wstring则是以16/32位宽字符wchar_t为基本类型的类。

MSVC

当字符串长度在16字符以内时，MSVC将字符串数据直接存储在缓冲区里，不再使用“指针＋缓冲区”的复杂结构。

这就意味着；在32位系统里，字符串最少会占用24（16+4+4=24）个字节；而在64位环境下，字符串至少占用32（16+8+8=32）个字节。如果字符串的长度超过16个字符的话，这16个字节空间就算白白浪费了。

指令清单51.21　MSVC的例子程序

#include <string>
#include <stdio.h>

struct std_string
{
    union
    {
        char buf[16];
        char* ptr;
    } u;
    size_t size;      // AKA 'Mysize' in MSVC
    size_t capacity;  // AKA 'Myres' in MSVC
};

void dump_std_string(std::string s)
{
    struct std_string *p=(struct std_string*)&s;
    printf ("[%s] size:%d capacity:%d\n", p->size>16 ? p->u.ptr : p->u.buf, p->size, p-> capacity);
};

int main()
{
    std::string s1="short string";
    std::string s2="string longer that 16 bytes";

    dump_std_string(s1);
    dump_std_string(s2);

    // that works without using c_str()
    printf ("%s\n", &s1);
    printf ("%s\n", s2);
};

源代码的功能应当不需要解释。

需要注意的有以下几点：

只要字符串长度没有超过16个字符，编译器就不会使用堆（heap）存储字符串的缓冲区。在实际的程序中，多数字符串确实是短字符串。很明显，微软研发人员将16字符串作为长短字符串的分割点。

虽然我们没有在主程序main()的最后调用成员函数c_str()，但是这个程序可以通过编译，也能在屏幕上显示出字符串的内容。

这就是为什么这个程序能运转起来。

第一个例子的字符串不足16个字符，因此它会被保存到字符串缓冲区。这个缓冲区实际位于std:string对象的起始地址（可视为结构体型数据）。这个区域里的数据，采取了标准的ASCIIZ的数据结构。因此printf()函数在处理指针时直接处理了相应的ASCIIZ字符串。所以，源程序可以显示第一个字符串的内容。

第二个字符串的长度大于16字节，更危险。编程人员很容易在此疏忽大意、忘记此时应当使用string对象的成员函数c_str()。之所以本例这样还能正常工作，是因为指向缓冲区的指针正好位于结构体的开始部分。某些人可能在相当长的时间里一直这么写代码，而一直不觉得会有问题；直到他们遇到超长字符串引发程序崩溃的时候，他们才会开始意识到问题的存在。

GCC

GCC在实现std::string的时候使用了MSVC里没有的变量——reference count。

另外一个有趣的事情是：指向std::string实例的指针，并不是指向结构体的起始地址，而是指向了字符串缓冲区的指针。文件libstdc++-v3/include/bits/ basic_string.h 解释了这一问题。它说，这是为了便于调试程序：

* The reason you want _M_data pointing to the character %array and
* not the _Rep is so that the debugger can see the string
* contents. (Probably we should add a non-inline member to get
* the _Rep for the debugger to use, so users can check the actual
* string length.)

之所以使用_M_data（而不采用_Rep）指向字符串数列%array，原因在于调试人员能看到字符串的内容。也许我们可以增加一个非内驻的成员_Rep以调试，但是使用者可以检查实际的字符串长度。

有兴趣的读者可以下载看看：http://gcc.gnu.org/onlinedocs/libstdc++/libstdc++-html-USERS-4.4/a010 68.html

。

考虑到有关问题之后，我们调整了上一个小节使用的源程序：

指令清单51.22　GCC例子

#include <string>
#include <stdio.h>

struct std_string
{
    size_t length;
    size_t capacity;
    size_t refcount;
};

void dump_std_string(std::string s)
{
    char *p1=*(char**)&s; // GCC type checking workaround
    struct std_string *p2=(struct std_string*)(p1-sizeof(struct std_string));
    printf ("[%s] size:%d capacity:%d\n", p1, p2->length, p2->capacity);
};

int main()
{
    std::string s1="short string";
    std::string s2="string longer that 16 bytes";

    dump_std_string(s1);
    dump_std_string(s2);

    // GCC type checking workaround:
    printf ("%s\n", *(char**)&s1);
    printf ("%s\n", *(char**)&s2);
};

因为GCC的类型检查规则更为苛刻，所以我们不得不做很多针对性的修改。即使这个程序的printf()函数同样可以在不依赖c_str()函数的情况下打印字符串内容。

一个更加复杂的例子

#include <string>
#include <stdio.h>

int main()
{
    std::string s1="Hello, ";
    std::string s2="world!\n";
    std::string s3=s1+s2;

    printf ("%s\n", s3.c_str());
}

指令清单51.23　MSVC 2012编译的程序

$SG39512 DB 'Hello, ', 00H
$SG39514 DB 'world!', 0aH, 00H
$SG39581 DB '%s', 0aH, 00H

_s2$ = -72 ; size = 24
_s3$ = -48 ; size = 24
_s1$ = -24 ; size = 24
_main PROC
    sub  esp, 72

    push 7
    push OFFSET $SG39512
    lea  ecx, DWORD PTR _s1$[esp+80]
    mov  DWORD PTR _s1$[esp+100], 15
    mov  DWORD PTR _s1$[esp+96], 0
    mov  BYTE PTR _s1$[esp+80], 0
    call ?assign@?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAEAAV12@PBDI@Z ; ↙
    ↘std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign

    push 7
    push OFFSET $SG39514
    lea  ecx, DWORD PTR _s2$[esp+80]
    mov  DWORD PTR _s2$[esp+100], 15
    mov  DWORD PTR _s2$[esp+96], 0
    mov  BYTE PTR _s2$[esp+80], 0
    call ?assign@?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAEAAV12@PBDI@Z ; ↙
    ↘ std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign

    lea  eax, DWORD PTR _s2$[esp+72]
    push eax
    lea  eax, DWORD PTR _s1$[esp+76]
    push eax
    lea  eax, DWORD PTR _s3$[esp+80]
    push eax
    call ??$?HDU?$char_traits@D@std@@V?$allocator@D@1@@std@@YA?AV?$basic_string@DU? ↙
    ↘ $char_traits@D@std@@V?$allocator@D@2@@0@ABV10@0@Z ; std::operator+<char,std::char_traits< ↙
    ↘ char>,std::allocator<char> >

    ; inlined c_str() method:
    cmp  DWORD PTR _s3$[esp+104], 16
    lea  eax, DWORD PTR _s3$[esp+84]
    cmovae eax, DWORD PTR _s3$[esp+84]

    push eax
    push OFFSET $SG39581
    call _printf
    add  esp, 20

    cmp  DWORD PTR _s3$[esp+92], 16
    jb   SHORT $LN119@main
    push DWORD PTR _s3$[esp+72]
    call ??3@YAXPAX@Z              ; operator delete
    add  esp, 4
$LN119@main:
    cmp  DWORD PTR _s2$[esp+92], 16
    mov  DWORD PTR _s3$[esp+92], 15
    mov  DWORD PTR _s3$[esp+88], 0
    mov  BYTE PTR _s3$[esp+72], 0
    jb   SHORT $LN151@main
    push DWORD PTR _s2$[esp+72]
    call ??3@YAXPAX@Z               ; operator delete
    add  esp, 4
$LN151@main:
    cmp  DWORD PTR _s1$[esp+92], 16
    mov  DWORD PTR _s2$[esp+92], 15
    mov  DWORD PTR _s2$[esp+88], 0
    mov  BYTE PTR _s2$[esp+72], 0
    jb   SHORT $LN195@main
    push DWORD PTR _s1$[esp+72]
    call ??3@YAXPAX@Z               ; operator delete
    add  esp, 4
$LN195@main:
    xor  eax, eax
    add  esp, 72
    ret  0
_main ENDP

编译器没有采用原有模式构建字符串。使用堆来存储字符串缓冲区，数据结构自然就完全不同。ASCIIZ字符串通常存储于程序的数据段，在执行程序的时候，assign方式会构造字符串s1和s2。而后程序通过运算符“＋”构造s3。

请注意此处没有调用字符串的c_str()方式。因为代码很紧凑，所以编译器把c_str()的代码内联（内嵌）到了此处：如果字符串的长度不足16个字符，那么EAX寄存器将保留缓冲区的指针；否则它将提取堆里那个存储字符串的缓冲区指针。

最后，程序调用了3个析构函数，用于释放长字符串（长度大于16个字符的字符串）占用的堆空间。如果字符串长度小于16，那么std::string对象全部存储于数据栈，会随函数结束而自动释放。

就性能而言，短字符串的处理速度更快，因为访问堆的操作要少一些。

GCC的处理方式更为简单。GCC不会把短字符串的文本缓冲区直接存储到string结构体里。

使用GCC\ 4.8.1编译上述程序，可得到：

指令清单51.24　GCC 4.8.1下的程序编译

.LC0:
    .string "Hello, "
.LC1:
    .string "world!\n"
main:
    push  ebp
    mov   ebp, esp
    push  edi
    push  esi
    push  ebx
    and   esp, -16
    sub   esp, 32
    lea   ebx, [esp+28]
    lea   edi, [esp+20]
    mov   DWORD PTR [esp+8], ebx
    lea   esi, [esp+24]
    mov   DWORD PTR [esp+4], OFFSET FLAT:.LC0
    mov   DWORD PTR [esp], edi

    call  _ZNSsC1EPKcRKSaIcE

    mov   DWORD PTR [esp+8], ebx
    mov   DWORD PTR [esp+4], OFFSET FLAT:.LC1
    mov   DWORD PTR [esp], esi

    call  _ZNSsC1EPKcRKSaIcE

    mov   DWORD PTR [esp+4], edi
    mov   DWORD PTR [esp], ebx

    call  _ZNSsC1ERKSs

    mov   DWORD PTR [esp+4], esi
    mov   DWORD PTR [esp], ebx

    call  _ZNSs6appendERKSs

    ; inlined c_str():
    mov   eax, DWORD PTR [esp+28]
    mov   DWORD PTR [esp], eax

    call  puts

    mov   eax, DWORD PTR [esp+28]
    lea   ebx, [esp+19]
    mov   DWORD PTR [esp+4], ebx
    sub   eax, 12
    mov   DWORD PTR [esp], eax
    call  _ZNSs4_Rep10_M_disposeERKSaIcE
    mov   eax, DWORD PTR [esp+24]
    mov   DWORD PTR [esp+4], ebx
    sub   eax, 12
    mov   DWORD PTR [esp], eax
    call  _ZNSs4_Rep10_M_disposeERKSaIcE
    mov   eax, DWORD PTR [esp+20]
    mov   DWORD PTR [esp+4], ebx
    sub   eax, 12
    mov   DWORD PTR [esp], eax
    call  _ZNSs4_Rep10_M_disposeERKSaIcE
    lea   esp, [ebp-12]
    xor   eax, eax
    pop   ebx
    pop   esi
    pop   edi
    pop   ebp
    ret

可见，传递给析构函数的指针不是对象的指针，而是指向string对象之前12个字节的地址的指针——那才是结构体真正的起始地址。

全局变量std::string

虽然资深的C++编程人员都不会把std::string当作全局变量使用，但实际上由STL定义的数据类型都可以用作全局变量，

确实如此，我们来看下面这段程序：

#include <stdio.h>
#include <string>

std::string s="a string";

int main()
{
    printf ("%s\n", s.c_str());
};

实际上，在主函数main()启动之前，全局变量就已经完成初始化操作了。

指令清单 51.25　MSVC 2012（这里我们可以看到这个全局变量是
如何构造的以及一个析构体是如何注册的）

??__Es@@YAXXZ PROC
    push 8
    push OFFSET $SG39512 ; 'a string'
    mov  ecx, OFFSET ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A ; s
    call ?assign@?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@QAEAAV12@PBDI@Z ; ↙
    ↘ std::basic_string<char,std::char_traits<char>,std::allocator<char> >::assign
    push OFFSET ??__Fs@@YAXXZ ; `dynamic atexit destructor for 's''
    call _atexit
    pop  ecx
    ret  0
??__Es@@YAXXZ ENDP

指令清单51.26　MSVC 2012（从这个程序我们可以看到，
主函数main()是如何使用一个全局变量的）

$SG39512 DB 'a string', 00H
$SG39519 DB '%s', 0aH, 00H

_main PROC
    cmp   DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A+20, 16
    mov   eax, OFFSET ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A ; s
    cmovae eax, DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
    push  eax
    push  OFFSET $SG39519 ; '%s'
    call  _printf
    add   esp, 8
    xor   eax, eax
    ret   0
_main ENDP

指令清单51.27　MSVC 2012在退出之前，析构函数的调用过程

??__Fs@@YAXXZ PROC
    push  ecx
    cmp   DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A+20, 16
    jb    SHORT $LN23@dynamic
    push  esi
    mov   esi, DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A
    lea   ecx, DWORD PTR $T2[esp+8]
    call  ??0?$_Wrap_alloc@V?$allocator@D@std@@@std@@QAE@XZ
    push  OFFSET ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A ; s
    lea   ecx, DWORD PTR $T2[esp+12]
    call  ??$destroy@PAD@?$_Wrap_alloc@V?$allocator@D@std@@@std@@QAEXPAPAD@Z
    lea   ecx, DWORD PTR $T1[esp+8]
    call  ??0?$_Wrap_alloc@V?$allocator@D@std@@@std@@QAE@XZ
    push  esi
    call  ??3@YAXPAX@Z ; operator delete
    add   esp, 4
    pop   esi
$LN23@dynamic:
    mov   DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A+20, 15
    mov   DWORD PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A+16, 0
    mov   BYTE PTR ?s@@3V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@A, 0
    pop   ecx
    ret   0
??__Fs@@YAXXZ ENDP

实际上，主函数main()没有调用那个创建全局变量的构造函数，这部分任务是CRT在启动main()函数之前就已经完成的操作。不止如此，全局变量的析构函数由stdlib声明的atexit()函数提供，只有在main结束之后才会被调用。

GCC的处理方法十分相似。经GCC 4.8.1编译上述源程序，可得到：

指令清单51.28　GCC 4.8.1函数

main:
    push  ebp
    mov   ebp, esp
    and   esp, -16
    sub   esp, 16
    mov   eax, DWORD PTR s
    mov   DWORD PTR [esp], eax
    call  puts
    xor   eax, eax
    leave
    ret
.LC0:
    .string "a string"
_GLOBAL__sub_I_s:
    sub   esp, 44
    lea   eax, [esp+31]
    mov   DWORD PTR [esp+8], eax
    mov   DWORD PTR [esp+4], OFFSET FLAT:.LC0
    mov   DWORD PTR [esp], OFFSET FLAT:s
    call  _ZNSsC1EPKcRKSaIcE
    mov   DWORD PTR [esp+8], OFFSET FLAT:__dso_handle
    mov   DWORD PTR [esp+4], OFFSET FLAT:s
    mov   DWORD PTR [esp], OFFSET FLAT:_ZNSsD1Ev
    call  __cxa_atexit
    add   esp, 44
    ret
.LFE645:
    .size  _GLOBAL__sub_I_s, .-_GLOBAL__sub_I_s
    .section .init_array,"aw"
    .align 4
    .long  _GLOBAL__sub_I_s
    .globl s
    .bss
    .align 4
    .type  s, @object
    .size  s, 4
s:
    .zero 4
    .hidden __dso_handle

但是GCC没有单独建立一个专用函数，而是把每个析构函数逐一传递给atexit()函数。

51.4.2　std::list

std::list是众所皆知的双向链表的容器类，它的每个数据元素可通过链表指针（链域）串接成逻辑意义上的线性表。这种数据结构的每个元素都有2个指针，一个指针指向前一个元素，另一个指针指向后一个元素。

链域的存在决定了，每个节点要比单纯的节点数据元素多占用2个words的空间，即32位系统下要多占用8个字节，而64位系统下会多占用16个字节。

C++的标准模板库只是给现有结构体扩充了“next”“previous”指针，使之形成语意上的有序序列。

我们以2个变量组成的结构体为例，演示std::list的链结构。

虽然C++标准（ISO/IEC 14882:2011 (C++ 11 standard)）没有明确这种数据结构的具体实现方法，但是MSVC编译器和GCC编译器不约而同地选择几乎一致的实现方法。我们就以下源程序为例进行说明：

#include <stdio.h>
#include <list>
#include <iostream>

struct a
{
    int x;
    int y;
};

struct List_node
{
    struct List_node* _Next;
    struct List_node* _Prev;
    int x;
    int y;
};

void dump_List_node (struct List_node *n)
{
    printf ("ptr=0x%p _Next=0x%p _Prev=0x%p x=%d y=%d\n",
        n, n->_Next, n->_Prev, n->x, n->y);
};

void dump_List_vals (struct List_node* n)
{
    struct List_node* current=n;

    for (;;)
    {
        dump_List_node (current);
        current=current->_Next;
        if (current==n) // end
            break;
    };
};

void dump_List_val (unsigned int *a)
{
#ifdef _MSC_VER
    // GCC implementation does not have "size" field
    printf ("_Myhead=0x%p, _Mysize=%d\n", a[0], a[1]);
#endif
    dump_List_vals ((struct List_node*)a[0]);
};

int main()
{
    std::list<struct a> l;

    printf ("* empty list:\n");
    dump_List_val((unsigned int*)(void*)&l);

    struct a t1;
    t1.x=1;
    t1.y=2;
    l.push_front (t1);
    t1.x=3;
    t1.y=4;
    l.push_front (t1);
    t1.x=5;
    t1.y=6;
    l.push_back (t1);

    printf ("* 3-elements list:\n");
    dump_List_val((unsigned int*)(void*)&l);

    std::list<struct a>::iterator tmp;
    printf ("node at .begin:\n");
    tmp=l.begin();
    dump_List_node ((struct List_node *)*(void**)&tmp);
    printf ("node at .end:\n");
    tmp=l.end();
    dump_List_node ((struct List_node *)*(void**)&tmp);

    printf ("* let's count from the begin:\n");
    std::list<struct a>::iterator it=l.begin();
    printf ("1st element: %d %d\n", (*it).x, (*it).y);
    it++;
    printf ("2nd element: %d %d\n", (*it).x, (*it).y);
    it++;
    printf ("3rd element: %d %d\n", (*it).x, (*it).y);
    it++;
    printf ("element at .end(): %d %d\n", (*it).x, (*it).y);

    printf ("* let's count from the end:\n");
    std::list<struct a>::iterator it2=l.end();
    printf ("element at .end(): %d %d\n", (*it2).x, (*it2).y);
    it2--;
    printf ("3rd element: %d %d\n", (*it2).x, (*it2).y);
    it2--;
    printf ("2nd element: %d %d\n", (*it2).x, (*it2).y);
    it2--;
    printf ("1st element: %d %d\n", (*it2).x, (*it2).y);

    printf ("removing last element...\n");
    l.pop_back();
    dump_List_val((unsigned int*)(void*)&l);
};

GCC

我们首先讲解GCC编译器的编译方式。

运行上述程序时，将会得到很多输出数据。我们进行分段解说：

* empty list:
ptr=0x0028fe90 _Next=0x0028fe90 _Prev=0x0028fe90 x=3 y=0

此时它还是个空链。虽然我们还未对其进行赋值操作，但是变量x和变量y已经有数据了（也就是常人所说的虚结点/dummy node）。而且，“next”“prev”指针都指向该节点自己。

在这个时候，迭代器.begin（开始）和.end（结束）值相等。

然后程序创建了3个节点，此后整个链表的内部数据将会变为：

* 3-elements list:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
ptr=0x00034988 _Next=0x00034b40 _Prev=0x000349a0 x=1 y=2
ptr=0x00034b40 _Next=0x0028fe90 _Prev=0x00034988 x=5 y=6
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6

最后一个元素的地址还是刚才的0x0028fe90。实际上在释放结构体之前它的地址不会发生变化。而且这个节点的两个字段x和y仍然还是随机的噪音数据，这时它们的值分别是5和6。碰巧的是，这些值和最后一个元素的值相同。只是这种巧合并不会一直持续下去。

此时，这3个节点存储过程和内存分布结构如下所示。

变量l总是指向第一个节点。

实际上迭代器.begin()（开始）和.end()（结束）不是变量而是函数，它们的返回值是相应节点的指针。

双向链表通常都会采用这种虚节点（英文别称是sentinel node/岗哨节点）的实现方法。这种节点具有简化链结构和提升操作效率的作用。

迭代器其实就是一个指向节点的指针，而list.begin()和list.end()分别返回首/尾节点的指针。

node at .begin:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
node at .end:
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6

事实上，最后一个节点的Next指针指向的第一个节点，而第一个元素的Prev指针指向的是最后一个元素。因此我们很容易就能想到这是一个循环链。

所以，可利用指向第一个节点的指针（即本例的实例l）顺藤摸瓜地找到序列的最后一个节点，而不必遍历整个链。利用这种结构，我们还可毫不费力的在链尾之后插入其他节点。

运算符“++”、“--”就是把迭代器的值设为［当前节点->prev］或［当前节点->next］的值。逆向迭代器（reverse iterators，即.rbegin和.rend）的作用几乎相同，只是方向相反。

迭代器的运算符“*”用于返回节点结构体的指针，即自定义的数据结构体的启始地址，换句话说是节点第一项数据（本例中是x）的指针。

在链表里插入和删除节点时，我们只需要分配（或释放）节点的存储空间，,然后再更新所有的链域指针、即可保障整个链的有效性。

在删除节点之后，相邻节点的迭代器（链域）可能依然指向被删除的无效节点，此时迭代器就会失效。指向无效结点的迭代器又叫做“迷途指针”和“悬空指针”（dangling pointer）。当然，这种指针就不能再使用了。

在GCC（以4.8.1版为例）编译std::list的实例时，它不会存储链中的节点总数。这就意味着内置函数.size()的运行速度十分缓慢，因为它必须遍历完整个数据链才能返回节点个数。因此，那些与节点总数有关的所有函数（例如O(n)）的时间开销都和链表长度成正比。

指令清单51.29　GCC4.8.1带参数-fno内置小函数的优化

main proc near
    push  ebp
    mov   ebp, esp
    push  esi
    push  ebx
    and   esp, 0FFFFFFF0h
    sub   esp, 20h
    lea   ebx, [esp+10h]
    mov   dword ptr [esp], offset s ; "* empty list:"
    mov   [esp+10h], ebx
    mov   [esp+14h], ebx
    call  puts
    mov   [esp], ebx
    call  _Z13dump_List_valPj ; dump_List_val(uint *)
    lea   esi, [esp+18h]
    mov   [esp+4], esi
    mov   [esp], ebx
    mov   dword ptr [esp+18h], 1 ; X for new element
    mov   dword ptr [esp+1Ch], 2 ; Y for new element
    call  _ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ; std::list<a,std::allocator<a>>::push_front(a ↙
    ↘ const&)
    mov   [esp+4], esi
    mov   [esp], ebx
    mov   dword ptr [esp+18h], 3 ; X for new element
    mov   dword ptr [esp+1Ch], 4 ; Y for new element
    call  _ZNSt4listI1aSaIS0_EE10push_frontERKS0_ ; std::list<a,std::allocator<a>>::push_front(a ↙
    ↘ const&)
    mov   dword ptr [esp], 10h
    mov   dword ptr [esp+18h], 5 ; X for new element
    mov   dword ptr [esp+1Ch], 6 ; Y for new element
    call  _Znwj ; operator new(uint)
    cmp   eax, 0FFFFFFF8h
    jz    short loc_80002A6
    mov   ecx, [esp+1Ch]
    mov   edx, [esp+18h]
    mov   [eax+0Ch], ecx
    mov   [eax+8], edx

loc_80002A6: ; CODE XREF: main+86
    mov   [esp+4], ebx
    mov   [esp], eax
    call  _ZNSt8__detail15_List_node_base7_M_hookEPS0_ ; std::__detail::_List_node_base::_M_hook ↙
    ↘ (std::__detail::_List_node_base*)
    mov   dword ptr [esp], offset a3ElementsList ; "* 3-elements list:"
    call  puts
    mov   [esp], ebx
    call  _Z13dump_List_valPj ; dump_List_val(uint *)
    mov   dword ptr [esp], offset aNodeAt_begin ; "node at .begin:"
    call  puts
    mov   eax, [esp+10h]
    mov   [esp], eax
    call  _Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)
    mov   dword ptr [esp], offset aNodeAt_end ; "node at .end:"
    call  puts
    mov   [esp], ebx
    call  _Z14dump_List_nodeP9List_node ; dump_List_node(List_node *)
    mov   dword ptr [esp], offset aLetSCountFromT ; "* let's count from the begin:"
    call  puts
    mov   esi, [esp+10h]
    mov   eax, [esi+0Ch]
    mov   [esp+0Ch], eax
    mov   eax, [esi+8]
    mov   dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+8], eax
    call  __printf_chk
    mov   esi, [esi] ; operator++: get ->next pointer
    mov   eax, [esi+0Ch]
    mov   [esp+0Ch], eax
    mov   eax, [esi+8]
    mov   dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+8], eax
    call  __printf_chk
    mov   esi, [esi] ; operator++: get ->next pointer
    mov   eax, [esi+0Ch]
    mov   [esp+0Ch], eax
    mov   eax, [esi+8]
    mov   dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+8], eax
    call  __printf_chk
    mov   eax, [esi] ; operator++: get ->next pointer
    mov   edx, [eax+0Ch]
    mov   [esp+0Ch], edx
    mov   eax, [eax+8]
    mov   dword ptr [esp+4], offset aElementAt_endD ; "element at .end(): %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+8], eax
    call  __printf_chk
    mov   dword ptr [esp], offset aLetSCountFro_0 ; "* let's count from the end:" ↙
    ↘ call puts
    mov   eax, [esp+1Ch]
    mov   dword ptr [esp+4], offset aElementAt_endD ; "element at .end(): %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+0Ch], eax
    mov   eax, [esp+18h]
    mov   [esp+8], eax
    call  __printf_chk
    mov   esi, [esp+14h]
    mov   eax, [esi+0Ch]
    mov   [esp+0Ch], eax
    mov   eax, [esi+8]
    mov   dword ptr [esp+4], offset a3rdElementDD ; "3rd element: %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+8], eax
    call  __printf_chk
    mov   esi, [esi+4] ; operator--: get ->prev pointer
    mov   eax, [esi+0Ch]
    mov   [esp+0Ch], eax
    mov   eax, [esi+8]
    mov   dword ptr [esp+4], offset a2ndElementDD ; "2nd element: %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+8], eax
    call  __printf_chk
    mov   eax, [esi+4] ; operator--: get ->prev pointer
    mov   edx, [eax+0Ch]
    mov   [esp+0Ch], edx
    mov   eax, [eax+8]
    mov   dword ptr [esp+4], offset a1stElementDD ; "1st element: %d %d\n"
    mov   dword ptr [esp], 1
    mov   [esp+8], eax
    call  __printf_chk
    mov   dword ptr [esp], offset aRemovingLastEl ; "removing last element..."
    call  puts
    mov   esi, [esp+14h]
    mov   [esp], esi
    call  _ZNSt8__detail15_List_node_base9_M_unhookEv   ; std::__detail::_List_node_base:: ↙
    ↘ _M_unhook(void)
    mov   [esp], esi ; void *
    call  _ZdlPv ; operator delete(void *)
    mov   [esp], ebx
    call  _Z13dump_List_valPj ; dump_List_val(uint *)
    mov   [esp], ebx
    call  _ZNSt10_List_baseI1aSaIS0_EE8_M_clearEv ; std::_List_base<a,std::allocator<a>>:: ↙
    ↘ _M_clear(void)
    lea   esp, [ebp-8]
    xor   eax, eax
    pop   ebx
    pop   esi
    pop   ebp
    retn
main endp

运行上面这个由GCC编译的程序，可以得到如下数据：

指令清单51.30　整个输出

* empty list:
ptr=0x0028fe90 _Next=0x0028fe90 _Prev=0x0028fe90 x=3 y=0
* 3-elements list:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
ptr=0x00034988 _Next=0x00034b40 _Prev=0x000349a0 x=1 y=2
ptr=0x00034b40 _Next=0x0028fe90 _Prev=0x00034988 x=5 y=6
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6
node at .begin:
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
node at .end:
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034b40 x=5 y=6
* let's count from the begin:
1st element: 3 4
2nd element: 1 2
3rd element: 5 6
element at .end(): 5 6
* let's count from the end:
element at .end(): 5 6
3rd element: 5 6
2nd element: 1 2
1st element: 3 4
removing last element...
ptr=0x000349a0 _Next=0x00034988 _Prev=0x0028fe90 x=3 y=4
ptr=0x00034988 _Next=0x0028fe90 _Prev=0x000349a0 x=1 y=2
ptr=0x0028fe90 _Next=0x000349a0 _Prev=0x00034988 x=5 y=6

MSVC

在编译std::list的时候，MSVC 2012会存储链表的长度。除此以外，它的实现方法和GCC基本一致。这就是说，内置函数.size()只需要从内存中读取一个值就可以返回函数结果，其速度相当快。不过，这也意味着每次添增/删除节点的时候都需要调整这个值。

MSVC的链存储结构也和GCC有所区别：

可见，GCC构造的虚节点在链尾，而MSVC构造的虚节点在链首。

使用MSVC 2012（启用/Fa2.asm/GS-/Ob1选项）编译上述程序，可得到：

指令清单51.31　MSVC 2012带参数/Fa2.asm/GS-/Ob1优化的程序

_l$ = -16 ; size = 8
_t1$ = -8 ; size = 8
_main     PROC
    sub   esp, 16
    push  ebx
    push  esi
    push  edi
    push  0
    push  0
    lea   ecx, DWORD PTR _l$[esp+36]
    mov   DWORD PTR _l$[esp+40], 0
    ; allocate first "garbage" element
    call  ?_Buynode0@?$_List_alloc@$0A@U?$_List_base_types@Ua@@V? ↙
    ↘ $allocator@Ua@@@std@@@std@@@std@@QAEPAU?$_List_node@Ua@@PAX@2@PAU32@0@Z ; std:: ↙
    ↘ _List_alloc<0,std::_List_base_types<a,std::allocator<a> > >::_Buynode0
    mov   edi, DWORD PTR __imp__printf
    mov   ebx, eax
    push  OFFSET $SG40685 ; '* empty list:'
    mov   DWORD PTR _l$[esp+32], ebx
    call  edi ; printf
    lea   eax, DWORD PTR _l$[esp+32]
    push  eax
    call  ?dump_List_val@@YAXPAI@Z ; dump_List_val
    mov   esi, DWORD PTR [ebx]
    add   esp, 8
    lea   eax, DWORD PTR _t1$[esp+28]
    push  eax
    push  DWORD PTR [esi+4]
    lea   ecx, DWORD PTR _l$[esp+36]
    push  esi
    mov   DWORD PTR _t1$[esp+40], 1 ; data for a new node
    mov   DWORD PTR _t1$[esp+44], 2 ; data for a new node
    ; allocate new node
    call  ??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU? ↙
    ↘ $_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy<a,std::allocator<a> >::_Buynode<a ↙
    ↘ const &>
    mov   DWORD PTR [esi+4], eax
    mov   ecx, DWORD PTR [eax+4]
    mov   DWORD PTR _t1$[esp+28], 3 ; data for a new node
    mov   DWORD PTR [ecx], eax
    mov   esi, DWORD PTR [ebx]
    lea   eax, DWORD PTR _t1$[esp+28]
    push  eax
    push  DWORD PTR [esi+4]
    lea   ecx, DWORD PTR _l$[esp+36]
    push  esi
    mov   DWORD PTR _t1$[esp+44], 4 ; data for a new node
    ; allocate new node
    call  ??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU? ↙
    ↘ $_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy<a,std::allocator<a> >::_Buynode<a  ↙
    ↘ const &>
    mov   DWORD PTR [esi+4], eax
    mov   ecx, DWORD PTR [eax+4]
    mov   DWORD PTR _t1$[esp+28], 5 ; data for a new node
    mov   DWORD PTR [ecx], eax
    lea   eax, DWORD PTR _t1$[esp+28]
    push  eax
    push  DWORD PTR [ebx+4]
    lea   ecx, DWORD PTR _l$[esp+36]
    push  ebx
    mov   DWORD PTR _t1$[esp+44], 6 ; data for a new node
    ; allocate new node
    call  ??$_Buynode@ABUa@@@?$_List_buy@Ua@@V?$allocator@Ua@@@std@@@std@@QAEPAU? ↙
    ↘ $_List_node@Ua@@PAX@1@PAU21@0ABUa@@@Z ; std::_List_buy<a,std::allocator<a> >::_Buynode<a  ↙
    ↘ const &>
    mov   DWORD PTR [ebx+4], eax
    mov   ecx, DWORD PTR [eax+4]
    push  OFFSET $SG40689 ; '* 3-elements list:'
    mov   DWORD PTR _l$[esp+36], 3
    mov   DWORD PTR [ecx], eax
    call  edi ; printf
    lea   eax, DWORD PTR _l$[esp+32]
    push  eax
    call  ?dump_List_val@@YAXPAI@Z ; dump_List_val
    push  OFFSET $SG40831 ; 'node at .begin:'
    call  edi ; printf
    push  DWORD PTR [ebx] ; get next field of node l variable points to
    call  ?dump_List_node@@YAXPAUList_node@@@Z ; dump_List_node
    push  OFFSET $SG40835 ; 'node at .end:'
    call  edi ; printf
    push  ebx ; pointer to the node $l$ variable points to!
    call  ?dump_List_node@@YAXPAUList_node@@@Z ; dump_List_node
    push  OFFSET $SG40839 ; '* let''s count from the begin:'
    call  edi ; printf
    mov   esi, DWORD PTR [ebx] ; operator++: get ->next pointer
    push  DWORD PTR [esi+12]
    push  DWORD PTR [esi+8]
    push  OFFSET $SG40846 ; '1st element: %d %d'
    call  edi ; printf
    mov   esi, DWORD PTR [esi] ; operator++: get ->next pointer
    push  DWORD PTR [esi+12]
    push  DWORD PTR [esi+8]
    push  OFFSET $SG40848 ; '2nd element: %d %d'
    call  edi ; printf
    mov   esi, DWORD PTR [esi] ; operator++: get ->next pointer
    push  DWORD PTR [esi+12]
    push  DWORD PTR [esi+8]
    push  OFFSET $SG40850 ; '3rd element: %d %d'
    call  edi ; printf
    mov   eax, DWORD PTR [esi] ; operator++: get ->next pointer
    add   esp, 64
    push  DWORD PTR [eax+12]
    push  DWORD PTR [eax+8]
    push  OFFSET $SG40852 ; 'element at .end(): %d %d'
    call  edi ; printf
    push  OFFSET $SG40853 ; '* let''s count from the end:'
    call  edi ; printf
    push  DWORD PTR [ebx+12] ; use x and y fields from the node l variable points to
    push  DWORD PTR [ebx+8]
    push  OFFSET $SG40860 ; 'element at .end(): %d %d'
    call  edi ; printf
    mov   esi, DWORD PTR [ebx+4] ; operator--: get ->prev pointer
    push  DWORD PTR [esi+12]
    push  DWORD PTR [esi+8]
    push  OFFSET $SG40862 ; '3rd element: %d %d'
    call  edi ; printf
    mov   esi, DWORD PTR [esi+4] ; operator--: get ->prev pointer
    push  DWORD PTR [esi+12]
    push  DWORD PTR [esi+8]
    push  OFFSET $SG40864 ; '2nd element: %d %d'
    call  edi ; printf
    mov   eax, DWORD PTR [esi+4] ; operator--: get ->prev pointer
    push  DWORD PTR [eax+12]
    push  DWORD PTR [eax+8]
    push  OFFSET $SG40866 ; '1st element: %d %d'
    call  edi ; printf
    add   esp, 64
    push  OFFSET $SG40867 ; 'removing last element...'
    call  edi ; printf
    mov   edx, DWORD PTR [ebx+4]
    add   esp, 4

    ; prev=next?
    ; it  is the only element, "garbage one"?
    ; if  yes, do not delete it!
    cmp   edx, ebx
    je    SHORT $LN349@main
    mov   ecx, DWORD PTR [edx+4]
    mov   eax, DWORD PTR [edx]
    mov   DWORD PTR [ecx], eax
    mov   ecx, DWORD PTR [edx]
    mov   eax, DWORD PTR [edx+4]
    push  edx
    mov   DWORD PTR [ecx+4], eax
    call  ??3@YAXPAX@Z ; operator delete
    add   esp, 4
    mov   DWORD PTR _l$[esp+32], 2
$LN349@main:
    lea   eax, DWORD PTR _l$[esp+28]
    push  eax
    call  ?dump_List_val@@YAXPAI@Z ; dump_List_val
    mov   eax, DWORD PTR [ebx]
    add   esp, 4
    mov   DWORD PTR [ebx], ebx
    mov   DWORD PTR [ebx+4], ebx
    cmp   eax, ebx
    je    SHORT $LN412@main
$LL414@main:
    mov   esi, DWORD PTR [eax]
    push  eax
    call  ??3@YAXPAX@Z ; operator delete
    add   esp, 4
    mov   eax, esi
    cmp   esi, ebx
    jne   SHORT $LL414@main
$LN412@main:
    push  ebx
    call  ??3@YAXPAX@Z ; operator delete
    add   esp, 4
    xor   eax, eax
    pop   edi
    pop   esi
    pop   ebx
    add   esp, 16
    ret   0
_main ENDP

在构造数据链的时候，MSVC通过“Buynode”函数分配了虚节点的空间；此后，这个函数同样用于分配其他节点的存储空间。相比之下，GCC则会在局部栈里存储链表的第一个节点。

指令清单51.32　整个程序的输出

* empty list:
_Myhead=0x003CC258, _Mysize=0
ptr=0x003CC258 _Next=0x003CC258 _Prev=0x003CC258 x=6226002 y=4522072
* 3-elements list:
_Myhead=0x003CC258, _Mysize=3
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0 x=6226002 y=4522072
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
ptr=0x003CC270 _Next=0x003CC2A0 _Prev=0x003CC288 x=1 y=2
ptr=0x003CC2A0 _Next=0x003CC258 _Prev=0x003CC270 x=5 y=6
node at .begin:
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
node at .end:
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC2A0 x=6226002 y=4522072
* let's count from the begin:
1st element: 3 4
2nd element: 1 2
3rd element: 5 6
element at .end(): 6226002 4522072
* let's count from the end:
element at .end(): 6226002 4522072
3rd element: 5 6
2nd element: 1 2
1st element: 3 4
removing last element...
_Myhead=0x003CC258, _Mysize=2
ptr=0x003CC258 _Next=0x003CC288 _Prev=0x003CC270 x=6226002 y=4522072
ptr=0x003CC288 _Next=0x003CC270 _Prev=0x003CC258 x=3 y=4
ptr=0x003CC270 _Next=0x003CC258 _Prev=0x003CC288 x=1 y=2

C++11 std::forward_list单向链表

std::forward_list和std::list的结构基本相同，只是它是单向链，它的迭代器（链域）只有”next”字段而没有”prev”字段。虽然这种链表的内存开销变小了，但是无法进行逆向遍历。

51.4.3　std::vector标准向量

我们将std::vector标准向量称为PODT[4]C数组的安全封装容器。其内部结构和标准字符串std::string十分相似（参见51.4.1节）。它有一个数据缓冲区的专用指针，一个指向数组尾部的专用指针，以及一个指向分配缓冲区尾部的专用指针。

这种数组采取的各个元素以彼此相邻的方式存储于内存之中，这点和常规数组没什么区别（参见第18章）。新推出的C++11标准，为其定义了内置函数.data()。std::vector的.data()的作用就和std::string中的.c_str()一样，用于返回缓冲区的地址。

这种数据结构使用堆/heap来存储数据缓冲区，而堆的空间消耗可能会比数组本身还大。

MSVC和GCC的实现机理基本相同，只是结构体的变量名称稍微有些不同。因此，所以这里使用同一个例子进行说明。下述源程序用于遍历std::vector的数据结构。

#include <stdio.h>
#include <vector>
#include <algorithm>
#include <functional>

struct vector_of_ints
{
    // MSVC names:
    int *Myfirst;
    int *Mylast;
    int *Myend;

    // GCC structure is the same, but names are: _M_start, _M_finish, _M_end_of_storage
};

void dump(struct vector_of_ints *in)
{
    printf ("_Myfirst=%p, _Mylast=%p, _Myend=%p\n", in->Myfirst, in->Mylast, in->Myend);
    size_t size=(in->Mylast-in->Myfirst);
    size_t capacity=(in->Myend-in->Myfirst);
    printf ("size=%d, capacity=%d\n", size, capacity);
    for (size_t i=0; i<size; i++)
        printf ("element %d: %d\n", i, in->Myfirst[i]);
};

int main()
{
    std::vector<int> c;
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(1);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(2);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(3);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(4);
    dump ((struct vector_of_ints*)(void*)&c);
    c.reserve (6);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(5);
    dump ((struct vector_of_ints*)(void*)&c);
    c.push_back(6);
    dump ((struct vector_of_ints*)(void*)&c);
    printf ("%d\n", c.at(5)); // with bounds checking
    printf ("%d\n", c[8]); // operator[], without bounds checking
};

下面是采用MSVC编译后的程序输出。

_Myfirst=00000000, _Mylast=00000000, _Myend=00000000
size=0, capacity=0
_Myfirst=0051CF48, _Mylast=0051CF4C, _Myend=0051CF4C
size=1, capacity=1
element 0: 1
_Myfirst=0051CF58, _Mylast=0051CF60, _Myend=0051CF60
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0051C278, _Mylast=0051C284, _Myend=0051C284
size=3, capacity=3
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0051C290, _Mylast=0051C2A0, _Myend=0051C2A0
size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0051B180, _Mylast=0051B190, _Myend=0051B198
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0051B180, _Mylast=0051B194, _Myend=0051B198
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0051B180, _Mylast=0051B198, _Myend=0051B198
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
6
6619158

从程序中我们可以看到，当主函数main()开始运行后并没有立即分配数据缓冲区。在第一次调用push_back()函数之后，它才分配了缓冲区。此后，每调用一次push_back()函数，数组的空间和size(capacity)都增大一次。不仅空间容量逐渐增长，而且缓冲区的地址也会同期改变。这是因为每次调用push_back()函数时，都会在堆里重新分配空间。所以这种操作的时间开销很大。要想提高效率，就必须预测数组的大小并使用.reserve()方式为其预留存储空间。

最后一个数值是随机的噪音。它不属于链表的某个节点，只是一个随机数。从这一点我们可以看出，std::vector（标准向量）的下标运算符“[]”不会检测索引值是否超越下标界限。要进行这种数组边界检查，可以使用内置函数.at()。虽然.at()方法的运行速度较慢，但是它能在下标越 std::out_of_range （越界）的错误提示。

我们来看它的汇编指令。使用MSVC 2012（启用 /GS- /Ob1选项）编译上述源程序，可得到：

指令清单51.33　MSVC 2012 /GS- /Ob1

$SG52650 DB '%d', 0aH, 00H
$SG52651 DB '%d', 0aH, 00H

_this$ = -4 ; size = 4
__Pos$ = 8  ; size = 4
?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z PROC ; std::vector<int,std::allocator<int> ↙
    ↘ >::at, COMDAT
; _this$ = ecx
    push  ebp
    mov   ebp, esp
    push  ecx
    mov   DWORD PTR _this$[ebp], ecx
    mov   eax, DWORD PTR _this$[ebp]
    mov   ecx, DWORD PTR _this$[ebp]
    mov   edx, DWORD PTR [eax+4]
    sub   edx, DWORD PTR [ecx]
    sar   edx, 2
    cmp   edx, DWORD PTR __Pos$[ebp]
    ja    SHORT $LN1@at
    push  OFFSET ??_C@_0BM@NMJKDPPO@invalid?5vector?$DMT?$DO?5subscript?$AA@
    call  DWORD PTR __imp_?_Xout_of_range@std@@YAXPBD@Z
$LN1@at:
    mov   eax, DWORD PTR _this$[ebp]
    mov   ecx, DWORD PTR [eax]
    mov   edx, DWORD PTR __Pos$[ebp]
    lea   eax, DWORD PTR [ecx+edx*4]
$LN3@at:
    mov   esp, ebp
    pop   ebp
    ret   4
?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z ENDP ; std::vector<int,std::allocator<int>  ↙
    ↘ >::at

_c$ = -36 ; size = 12
$T1 = -24 ; size = 4
$T2 = -20 ; size = 4
$T3 = -16 ; size = 4
$T4 = -12 ; size = 4
$T5 = -8  ; size = 4
$T6 = -4  ; size = 4
_main PROC
    push  ebp
    mov   ebp, esp
    sub   esp, 36
    mov   DWORD PTR _c$[ebp], 0 ; Myfirst
    mov   DWORD PTR _c$[ebp+4], 0 ; Mylast
    mov   DWORD PTR _c$[ebp+8], 0 ; Myend
    lea   eax, DWORD PTR _c$[ebp]
    push  eax
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    mov   DWORD PTR $T6[ebp], 1
    lea   ecx, DWORD PTR $T6[ebp]
    push  ecx
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std:: ↙
    ↘ allocator<int> >::push_back
    lea   edx, DWORD PTR _c$[ebp]
    push  edx
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    mov   DWORD PTR $T5[ebp], 2
    lea   eax, DWORD PTR $T5[ebp]
    push  eax
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std:: ↙
    ↘ allocator<int> >::push_back
    lea   ecx, DWORD PTR _c$[ebp]
    push  ecx
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    mov   DWORD PTR $T4[ebp], 3
    lea   edx, DWORD PTR $T4[ebp]
    push  edx
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std:: ↙
    ↘ allocator<int> >::push_back
    lea   eax, DWORD PTR _c$[ebp]
    push  eax
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    mov   DWORD PTR $T3[ebp], 4
    lea   ecx, DWORD PTR $T3[ebp]
    push  ecx
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std:: ↙
    ↘ allocator<int> >::push_back
    lea   edx, DWORD PTR _c$[ebp]
    push  edx
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    push  6
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?reserve@?$vector@HV?$allocator@H@std@@@std@@QAEXI@Z ; std::vector<int,std::allocator< ↙
    ↘ int> >::reserve
    lea   eax, DWORD PTR _c$[ebp]
    push  eax
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    mov   DWORD PTR $T2[ebp], 5
    lea   ecx, DWORD PTR $T2[ebp]
    push  ecx
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std:: ↙
    ↘ allocator<int> >::push_back
    lea   edx, DWORD PTR _c$[ebp]
    push  edx
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    mov   DWORD PTR $T1[ebp], 6
    lea   eax, DWORD PTR $T1[ebp]
    push  eax
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?push_back@?$vector@HV?$allocator@H@std@@@std@@QAEX$$QAH@Z ; std::vector<int,std:: ↙
    ↘ allocator<int> >::push_back
    lea   ecx, DWORD PTR _c$[ebp]
    push  ecx
    call  ?dump@@YAXPAUvector_of_ints@@@Z ; dump
    add   esp, 4
    push  5
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?at@?$vector@HV?$allocator@H@std@@@std@@QAEAAHI@Z ; std::vector<int,std::allocator<int ↙
    ↘ > >::at
    mov   edx, DWORD PTR [eax]
    push  edx
    push  OFFSET $SG52650 ; '%d'
    call  DWORD PTR __imp__printf
    add   esp, 8
    mov   eax, 8
    shl   eax, 2
    mov   ecx, DWORD PTR _c$[ebp]
    mov   edx, DWORD PTR [ecx+eax]
    push  edx
    push  OFFSET $SG52651 ; '%d'
    call  DWORD PTR __imp__printf
    add   esp, 8
    lea   ecx, DWORD PTR _c$[ebp]
    call  ?_Tidy@?$vector@HV?$allocator@H@std@@@std@@IAEXXZ  ; std::vector<int,std::allocator<int ↙
    ↘ > >::_Tidy
    xor   eax, eax
    mov   esp, ebp
    pop   ebp
    ret   0
_main ENDP

从中我们可以看到.at()方法检查下标边界以及异常处理的详细细节。最后一个输出值是printf()函数直接从内存中提取的数值。它在提取数值的时候没有做任何越界检查。

有读者可能会问了，为什么标准向量的数据结构为什么不像标准函数std::string中的那样、单独用一个字段记录size（大小）和capacity（体积）这类的信息呢？虽然笔者无法查证，但是笔者相信大概是.at()在进行边界检查时效率更好吧。

GCC生成的汇编指令也十分雷同，不过它以内联函数的形式实现.at()。

指令清单51.34　GCC 4.8.1 –fno –inline –small –functions –O1编译

main proc  near
     push  ebp
     mov   ebp, esp
     push  edi
     push  esi
     push  ebx
     and   esp, 0FFFFFFF0h
     sub   esp, 20h
     mov   dword ptr [esp+14h], 0
     mov   dword ptr [esp+18h], 0
     mov   dword ptr [esp+1Ch], 0
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   dword ptr [esp+10h], 1
     lea   eax, [esp+10h]
     mov   [esp+4], eax
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int>>::push_back( ↙
    ↘ int const&)
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   dword ptr [esp+10h], 2
     lea   eax, [esp+10h]
     mov   [esp+4], eax
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int>>::push_back( ↙
     ↘ int const&)
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   dword ptr [esp+10h], 3
     lea   eax, [esp+10h]
     mov   [esp+4], eax
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int>>::push_back( ↙
    ↘ int const&)
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   dword ptr [esp+10h], 4
     lea   eax, [esp+10h]
     mov   [esp+4], eax
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _ZNSt6vectorIiSaIiEE9push_backERKi  ; std::vector<int,std::allocator<int>>::push_back( ↙
    ↘ int  const&)
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   ebx, [esp+14h]
     mov   eax, [esp+1Ch]
     sub   eax, ebx
     cmp   eax, 17h
     ja    short loc_80001CF
     mov   edi, [esp+18h]
     sub   edi, ebx
     sar   edi, 2
     mov   dword ptr [esp], 18h
     call  _Znwj            ; operator new(uint)
     mov   esi, eax
     test  edi, edi
     jz    short loc_80001AD
     lea   eax, ds:0[edi*4]
     mov   [esp+8], eax     ; n
     mov   [esp+4], ebx     ; src
     mov   [esp], esi       ; dest
     call  memmove

loc_80001AD: ; CODE XREF: main+F8
     mov   eax, [esp+14h]
     test  eax, eax
     jz    short loc_80001BD
     mov   [esp], eax       ; void *
     call  _ZdlPv           ; operator delete(void *)

loc_80001BD: ; CODE XREF: main+117
     mov   [esp+14h], esi
     lea   eax, [esi+edi*4]
     mov   [esp+18h], eax
     add   esi, 18h
     mov   [esp+1Ch], esi

loc_80001CF: ; CODE XREF: main+DD
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   dword ptr [esp+10h], 5
     lea   eax, [esp+10h]
     mov   [esp+4], eax
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int>>::push_back( ↙
    ↘ int const&)
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   dword ptr [esp+10h], 6
     lea   eax, [esp+10h]
     mov   [esp+4], eax
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _ZNSt6vectorIiSaIiEE9push_backERKi ; std::vector<int,std::allocator<int>>::push_back( ↙
    ↘ int const&)
     lea   eax, [esp+14h]
     mov   [esp], eax
     call  _Z4dumpP14vector_of_ints ; dump(vector_of_ints *)
     mov   eax, [esp+14h]
     mov   edx, [esp+18h]
     sub   edx, eax
     cmp   edx, 17h
     ja    short loc_8000246
     mov   dword ptr [esp], offset aVector_m_range ; "vector::_M_range_check"
     call  _ZSt20__throw_out_of_rangePKc ; std::__throw_out_of_range(char const*)

loc_8000246:                            ; CODE XREF: main+19C
     mov   eax, [eax+14h]
     mov  [esp+8], eax
     mov   dword ptr [esp+4], offset aD ; "%d\n"
     mov   dword ptr [esp], 1
     call  __printf_chk
     mov   eax, [esp+14h]
     mov   eax, [eax+20h]
     mov   [esp+8], eax
     mov   dword ptr [esp+4], offset aD ; "%d\n"
     mov   dword ptr [esp], 1
     call  __printf_chk
     mov   eax, [esp+14h]
     test  eax, eax
     jz    short loc_80002AC
     mov   [esp], eax        ; void *
     call  _ZdlPv            ; operator delete(void *)
     jmp   short loc_80002AC

     mov   ebx, eax
     mov   edx, [esp+14h]
     test  edx, edx
     jz    short loc_80002A4
     mov   [esp], edx        ; void *
     call  _ZdlPv            ; operator delete(void *)

loc_80002A4: ; CODE XREF: main+1FE
     mov   [esp], ebx
     call  _Unwind_Resume

loc_80002AC: ; CODE XREF: main+1EA
             ; main+1F4
     mov   eax, 0
     lea   esp, [ebp-0Ch]
     pop   ebx
     pop   esi
     pop   edi
     pop   ebp

locret_80002B8: ; DATA XREF: .eh_frame:08000510
                ; .eh_frame:080005BC
     retn
main endp

编译器也对.reserve()函数进行了内联处理。如果缓冲区不大，程序会调用new()方法分配缓冲区的存储空间；使用memmove()方法来复制缓冲区中的内容；最后通过delete()方法来释放缓冲区空间。

如果采用GCC编译的话，编译程序的输出为：

_Myfirst=0x(nil), _Mylast=0x(nil), _Myend=0x(nil)
size=0, capacity=0
_Myfirst=0x8257008, _Mylast=0x825700c, _Myend=0x825700c
size=1, capacity=1
element 0: 1
_Myfirst=0x8257018, _Mylast=0x8257020, _Myend=0x8257020
size=2, capacity=2
element 0: 1
element 1: 2
_Myfirst=0x8257028, _Mylast=0x8257034, _Myend=0x8257038
size=3, capacity=4
element 0: 1
element 1: 2
element 2: 3
_Myfirst=0x8257028, _Mylast=0x8257038, _Myend=0x8257038
size=4, capacity=4
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x8257040, _Mylast=0x8257050, _Myend=0x8257058
size=4, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
_Myfirst=0x8257040, _Mylast=0x8257054, _Myend=0x8257058
size=5, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
_Myfirst=0x8257040, _Mylast=0x8257058, _Myend=0x8257058
size=6, capacity=6
element 0: 1
element 1: 2
element 2: 3
element 3: 4
element 4: 5
element 5: 6
6
0

从程序我们可以看到，GCC的缓冲区buffer的增长方式与MSVC不同。具体来说，通过一个简单的例子实践可以看到：由MSVC编译的程序每次会把缓冲区增加50%；而由GCC编译的程序则会将缓冲区扩张100%——也就是翻倍。

51.4.4　std::map()和std::set()

“二叉树”是另外一种常见的基础数据结构。二叉树是每个节点最多有两个子树的有序树，每个节点都有自己的关键字（key）-值（value）。

二叉树通常会用于构造联合数组（associative arrays）。联合数组又称为词典（dictionary）或 hash 表，它由一系列的“关键字-值”构成。

二叉制树最少具备三个重要的特性：

所有的关键字都以有序方式存储。

任何类型的关键字都能构成二叉树。二叉树的算法与关键字的数据类型无关。只要能够定义一个比较二叉树关键字的排序函数，就可以构造一个二叉树。

与链表和数组相比，在二叉树中搜索特定关键字的速度比较快。

现在开始举例说明：我们将存储数列0, 1, 2, 3, 5, 6, 9, 10, 11, 12, 20, 99, 100, 101, 107, 1001, 1010，并形成下述二叉树结构：

从下面的图中，我们很容易发现这样一个规律：所有左边节点的值都比其右边节点的值小，而所有右边节点的值都比左边的节点的值大。

在遵守以上规则的前提下，我们做起查询算法来就相对简单了。我们只需要关注当前节点的数值以及我们要查询的值，将这两者进行比较，当前者比后者小时，那么我们只需要向右搜索对比查找；反之，当前者比后者大时，我们就会向左搜索对比查找。一直到找到节点的值与要搜索的值相等为止。这就是为什么刚才说只需要一个对比函数即可进行节点数或者字符串对比。

所有的键都有一个唯一的值，不能重复。

我们可以计算需要花费的步数，公式大约为：log₂n（n是二进制树中键的个数），这个公式计算的是我们要找到一个平衡树的搜索步数。这就意味着在一个1000个键的二进制树中，通过大约10步我们就能找到需要的值；而10000个键的二进制树中则需要13步。看起来还是很不错的。树总是需要像这样平衡一下，也就是说，键必须在所有水平上平衡分布。插入和移除一些节点可能都需要保持树的平衡状态。

有几个可用的流行平衡算法，包括AVL树和红黑树。后面的算法采用的办法是将每个节点标注为red（红）或者black（黑），以简化结点的平衡过程。

不管是GCC还是MSVC的模板函数std::map()和std::set()都使用了红黑树（red-black）的算法。

在std::set()中只含有键，而std::map()中除了键（std::set）还有相应的每个节点的数值。

MSVC

#include <map>
#include <set>
#include <string>
#include <iostream>

// structure is not packed!
struct tree_node
{
    struct tree_node *Left;
    struct tree_node *Parent;
    struct tree_node *Right;
    char Color; // 0 - Red, 1 - Black
    char Isnil;
    //std::pair Myval;
    unsigned int first; // called Myval in std::set
    const char *second; // not present in std::set
};

struct tree_struct
{
    struct tree_node *Myhead;
    size_t Mysize;
};

void dump_tree_node (struct tree_node *n, bool is_set, bool traverse)
{
    printf ("ptr=0x%p Left=0x%p Parent=0x%p Right=0x%p Color=%d Isnil=%d\n",
            n, n->Left, n->Parent, n->Right, n->Color, n->Isnil);
    if (n->Isnil==0)
    {
        if (is_set)
            printf ("first=%d\n", n->first);
        else
            printf ("first=%d second=[%s]\n", n->first, n->second);
    }

    if (traverse)
    {
        if (n->Isnil==1)
            dump_tree_node (n->Parent, is_set, true);
        else
        {
            if (n->Left->Isnil==0)
                dump_tree_node (n->Left, is_set, true);
            if (n->Right->Isnil==0)
                dump_tree_node (n->Right, is_set, true);
        };
    };
};
const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";

void dump_as_tree (int tabs, struct tree_node *n, bool is_set)
{
    if (is_set)
        printf ("%d\n", n->first);
    else
        printf ("%d [%s]\n", n->first, n->second);
    if (n->Left->Isnil==0)
    {
        printf ("%.*sL-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->Left, is_set);
    };
    if (n->Right->Isnil==0)
    {
        printf ("%.*sR-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->Right, is_set);
    };
};

void dump_map_and_set(struct tree_struct *m, bool is_set)
{
    printf ("ptr=0x%p, Myhead=0x%p, Mysize=%d\n", m, m->Myhead, m->Mysize);
    dump_tree_node (m->Myhead, is_set, true);
    printf ("As a tree:\n");
    printf ("root----");
    dump_as_tree (1, m->Myhead->Parent, is_set);
};

int main()
{
    // map

    std::map<int, const char*> m;

    m[10]="ten";
    m[20]="twenty";
    m[3]="three";
    m[101]="one hundred one";
    m[100]="one hundred";
    m[12]="twelve";
    m[107]="one hundred seven";
    m[0]="zero";
    m[1]="one";
    m[6]="six";
    m[99]="ninety-nine";
    m[5]="five";
    m[11]="eleven";
    m[1001]="one thousand one";
    m[1010]="one thousand ten";
    m[2]="two";
    m[9]="nine";
    printf ("dumping m as map:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&m, false);

    std::map<int, const char*>::iterator it1=m.begin();
    printf ("m.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false);
    it1=m.end();
    printf ("m.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false);

    // set

    std::set<int> s;
    s.insert(123);
    s.insert(456);
    s.insert(11);
    s.insert(12);
    s.insert(100);
    s.insert(1001);
    printf ("dumping s as set:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s, true);
    std::set<int>::iterator it2=s.begin();
    printf ("s.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false);
    it2=s.end();
    printf ("s.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false);
};

指令清单51.35　MSVC 2012

dumping m as map:
ptr=0x0020FE04, Myhead=0x005BB3A0, Mysize=17
ptr=0x005BB3A0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB580 Color=1 Isnil=1
ptr=0x005BB3C0 Left=0x005BB4C0 Parent=0x005BB3A0 Right=0x005BB440 Color=1 Isnil=0
first=10 second=[ten]
ptr=0x005BB4C0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB520 Color=1 Isnil=0
first=1 second=[one]
ptr=0x005BB4A0 Left=0x005BB3A0 Parent=0x005BB4C0 Right=0x005BB3A0 Color=1 Isnil=0
first=0 second=[zero]
ptr=0x005BB520 Left=0x005BB400 Parent=0x005BB4C0 Right=0x005BB4E0 Color=0 Isnil=0
first=5 second=[five]
ptr=0x005BB400 Left=0x005BB5A0 Parent=0x005BB520 Right=0x005BB3A0 Color=1 Isnil=0
first=3 second=[three]
ptr=0x005BB5A0 Left=0x005BB3A0 Parent=0x005BB400 Right=0x005BB3A0 Color=0 Isnil=0
first=2 second=[two]
ptr=0x005BB4E0 Left=0x005BB3A0 Parent=0x005BB520 Right=0x005BB5C0 Color=1 Isnil=0
first=6 second=[six]
ptr=0x005BB5C0 Left=0x005BB3A0 Parent=0x005BB4E0 Right=0x005BB3A0 Color=0 Isnil=0
first=9 second=[nine]
ptr=0x005BB440 Left=0x005BB3E0 Parent=0x005BB3C0 Right=0x005BB480 Color=1 Isnil=0
first=100 second=[one hundred]
ptr=0x005BB3E0 Left=0x005BB460 Parent=0x005BB440 Right=0x005BB500 Color=0 Isnil=0
first=20 second=[twenty]
ptr=0x005BB460 Left=0x005BB540 Parent=0x005BB3E0 Right=0x005BB3A0 Color=1 Isnil=0
first=12 second=[twelve]
ptr=0x005BB540 Left=0x005BB3A0 Parent=0x005BB460 Right=0x005BB3A0 Color=0 Isnil=0
first=11 second=[eleven]
ptr=0x005BB500 Left=0x005BB3A0 Parent=0x005BB3E0 Right=0x005BB3A0 Color=1 Isnil=0
first=99 second=[ninety-nine]
ptr=0x005BB480 Left=0x005BB420 Parent=0x005BB440 Right=0x005BB560 Color=0 Isnil=0
first=107 second=[one hundred seven]
ptr=0x005BB420 Left=0x005BB3A0 Parent=0x005BB480 Right=0x005BB3A0 Color=1 Isnil=0
first=101 second=[one hundred one]
ptr=0x005BB560 Left=0x005BB3A0 Parent=0x005BB480 Right=0x005BB580 Color=1 Isnil=0
first=1001 second=[one thousand one]
ptr=0x005BB580 Left=0x005BB3A0 Parent=0x005BB560 Right=0x005BB3A0 Color=0 Isnil=0
first=1010 second=[one thousand ten]
As a tree:
root----10 [ten]
        L-------1 [one]
                L-------0 [zero]
                R-------5 [five]
                        L-------3 [three]
                                L-------2 [two]
                        R-------6 [six]
                                R-------9 [nine]
        R-------100 [one hundred]
                L-------20 [twenty]
                        L-------12 [twelve]
                                L-------11 [eleven]
                        R-------99 [ninety-nine]
                R-------107 [one hundred seven]
                        L-------101 [one hundred one]
                        R-------1001 [one thousand one]
                                R-------1010 [one thousand ten]
m.begin():
ptr=0x005BB4A0 Left=0x005BB3A0 Parent=0x005BB4C0 Right=0x005BB3A0 Color=1 Isnil=0
first=0 second=[zero]
m.end():
ptr=0x005BB3A0 Left=0x005BB4A0 Parent=0x005BB3C0 Right=0x005BB580 Color=1 Isnil=1

dumping s as set:
ptr=0x0020FDFC, Myhead=0x005BB5E0, Mysize=6
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB6A0 Color=1 Isnil=1
ptr=0x005BB600 Left=0x005BB660 Parent=0x005BB5E0 Right=0x005BB620 Color=1 Isnil=0
first=123
ptr=0x005BB660 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB680 Color=1 Isnil=0
first=12
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 Isnil=0
first=11
ptr=0x005BB680 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 Isnil=0
first=100
ptr=0x005BB620 Left=0x005BB5E0 Parent=0x005BB600 Right=0x005BB6A0 Color=1 Isnil=0
first=456
ptr=0x005BB6A0 Left=0x005BB5E0 Parent=0x005BB620 Right=0x005BB5E0 Color=0 Isnil=0
first=1001
As a tree:
root----123
        L-------12
                L-------11
                R-------100
        R-------456
                R-------1001
s.begin():
ptr=0x005BB640 Left=0x005BB5E0 Parent=0x005BB660 Right=0x005BB5E0 Color=0 Isnil=0
first=11
s.end():
ptr=0x005BB5E0 Left=0x005BB640 Parent=0x005BB600 Right=0x005BB6A0 Color=1 Isnil=1

因为结构设计得并不紧密，所以2个char型值各占用了4字节空间。

对于std::map结构来说，first和second的数据数据组合可被视为std::pair的一个数据元素。而std::set的每个节点则只有1个值。

51.4.2节介绍过，MSVC编译器在构造std::list的时候会保链表的长度信息。我们可以在程序看到它也存储了这种数据类型的具体尺寸。

这两种数据结构和std::list的相同之处是，迭代器都是指向节点的指针。.begin()迭代函数指向最小的键。实际上，整个数据结构里都没有最小键的指针（和链表的情况一样）每当程序调用这个迭代器的时候，它就遍历所有键、查找出最小值。单目递增运算符operator --和单目递减运算符operator++可将指针指向树里的前一个或后一个节点。MIT出版社出版的《Introduction to Algorithms, Third Edition》（Thomas H. Cormen等人著）介绍了这两个运算符的具体算法。

迭代器.end()指向了一个隐蔽的虚节点。其Isnil为1，即是说它没有对应的关键字(key)或值（value）、不是真正意义上的数据结点，仅是一个存储控制信息的容器。这个虚节点的父节点是真正的根节点的指针，也就是整个信息树的顶点。

GCC

#include <stdio.h>
#include <map>
#include <set>
#include <string>
#include <iostream>

struct map_pair
{
    int key;
    const char *value;
};

struct tree_node
{
    int M_color; // 0 - Red, 1 - Black
    struct tree_node *M_parent;
    struct tree_node *M_left;
    struct tree_node *M_right;
};

struct tree_struct
{
    int M_key_compare;
    struct tree_node M_header;
    size_t M_node_count;
};

void dump_tree_node (struct tree_node *n, bool is_set, bool traverse,  bool dump_keys_and_values)
{
    printf ("ptr=0x%p M_left=0x%p M_parent=0x%p M_right=0x%p M_color=%d\n",
            n, n->M_left, n->M_parent, n->M_right, n->M_color);

    void *point_after_struct=((char*)n)+sizeof(struct tree_node);

    if (dump_keys_and_values)
    {
        if (is_set)
            printf ("key=%d\n", *(int*)point_after_struct);
        else
        {
            struct map_pair *p=(struct map_pair *)point_after_struct;
            printf ("key=%d value=[%s]\n", p->key, p->value);
        };
    };

    if (traverse==false)
        return;

    if (n->M_left)
        dump_tree_node (n->M_left, is_set, traverse, dump_keys_and_values);
    if (n->M_right)
        dump_tree_node (n->M_right, is_set, traverse, dump_keys_and_values);
};

const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";

void dump_as_tree (int tabs, struct tree_node *n, bool is_set)
{
    void *point_after_struct=((char*)n)+sizeof(struct tree_node);

    if (is_set)
        printf ("%d\n", *(int*)point_after_struct);
    else
    {
        struct map_pair *p=(struct map_pair *)point_after_struct;
        printf ("%d [%s]\n", p->key, p->value);
    }

    if (n->M_left)
    {
        printf ("%.*sL-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_left, is_set);
    };
    if (n->M_right)
    {
        printf ("%.*sR-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_right, is_set);
    };
};

void dump_map_and_set(struct tree_struct *m, bool is_set)
{
    printf ("ptr=0x%p, M_key_compare=0x%x, M_header=0x%p, M_node_count=%d\n",
        m, m->M_key_compare, &m->M_header, m->M_node_count);
    dump_tree_node (m->M_header.M_parent, is_set, true, true);
    printf ("As a tree:\n");
    printf ("root----");
    dump_as_tree (1, m->M_header.M_parent, is_set);
};

int main()
{
    // map

    std::map<int, const char*> m;

    m[10]="ten";
    m[20]="twenty";
    m[3]="three";
    m[101]="one hundred one";
    m[100]="one hundred";
    m[12]="twelve";
    m[107]="one hundred seven";
    m[0]="zero";
    m[1]="one";
    m[6]="six";
    m[99]="ninety-nine";
    m[5]="five";
    m[11]="eleven";
    m[1001]="one thousand one";
    m[1010]="one thousand ten";
    m[2]="two";
    m[9]="nine";

    printf ("dumping m as map:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&m, false);

    std::map<int, const char*>::iterator it1=m.begin();
    printf ("m.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false, true);
    it1=m.end();
    printf ("m.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it1, false, false, false);

    // set

    std::set<int> s;
    s.insert(123);
    s.insert(456);
    s.insert(11);
    s.insert(12);
    s.insert(100);
    s.insert(1001);
    printf ("dumping s as set:\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s, true);
    std::set<int>::iterator it2=s.begin();
    printf ("s.begin():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, true);
    it2=s.end();
    printf ("s.end():\n");
    dump_tree_node ((struct tree_node *)*(void**)&it2, true, false, false);
};

指令清单51.36　GCC 4.8.1

dumping m as map:
ptr=0x0028FE3C, M_key_compare=0x402b70, M_header=0x0028FE40, M_node_count=17
ptr=0x007A4988 M_left=0x007A4C00 M_parent=0x0028FE40 M_right=0x007A4B80 M_color=1
key=10 value=[ten]
ptr=0x007A4C00 M_left=0x007A4BE0 M_parent=0x007A4988 M_right=0x007A4C60 M_color=1
key=1 value=[one]
ptr=0x007A4BE0 M_left=0x00000000 M_parent=0x007A4C00 M_right=0x00000000 M_color=1
key=0 value=[zero]
ptr=0x007A4C60 M_left=0x007A4B40 M_parent=0x007A4C00 M_right=0x007A4C20 M_color=0
key=5 value=[five]
ptr=0x007A4B40 M_left=0x007A4CE0 M_parent=0x007A4C60 M_right=0x00000000 M_color=1
key=3 value=[three]
ptr=0x007A4CE0 M_left=0x00000000 M_parent=0x007A4B40 M_right=0x00000000 M_color=0
key=2 value=[two]
ptr=0x007A4C20 M_left=0x00000000 M_parent=0x007A4C60 M_right=0x007A4D00 M_color=1
key=6 value=[six]
ptr=0x007A4D00 M_left=0x00000000 M_parent=0x007A4C20 M_right=0x00000000 M_color=0
key=9 value=[nine]
ptr=0x007A4B80 M_left=0x007A49A8 M_parent=0x007A4988 M_right=0x007A4BC0 M_color=1
key=100 value=[one hundred]
ptr=0x007A49A8 M_left=0x007A4BA0 M_parent=0x007A4B80 M_right=0x007A4C40 M_color=0
key=20 value=[twenty]
ptr=0x007A4BA0 M_left=0x007A4C80 M_parent=0x007A49A8 M_right=0x00000000 M_color=1
key=12 value=[twelve]
ptr=0x007A4C80 M_left=0x00000000 M_parent=0x007A4BA0 M_right=0x00000000 M_color=0
key=11 value=[eleven]
ptr=0x007A4C40 M_left=0x00000000 M_parent=0x007A49A8 M_right=0x00000000 M_color=1
key=99 value=[ninety-nine]
ptr=0x007A4BC0 M_left=0x007A4B60 M_parent=0x007A4B80 M_right=0x007A4CA0 M_color=0
key=107 value=[one hundred seven]
ptr=0x007A4B60 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x00000000 M_color=1
key=101 value=[one hundred one]
ptr=0x007A4CA0 M_left=0x00000000 M_parent=0x007A4BC0 M_right=0x007A4CC0 M_color=1
key=1001 value=[one thousand one]
ptr=0x007A4CC0 M_left=0x00000000 M_parent=0x007A4CA0 M_right=0x00000000 M_color=0
key=1010 value=[one thousand ten]
As a tree:
root----10 [ten]
        L-------1 [one]
                L-------0 [zero]
                R-------5 [five]
                        L-------3 [three]
                                L-------2 [two]
                        R-------6 [six]
                                R-------9 [nine]
        R-------100 [one hundred]
                L-------20 [twenty]
                        L-------12 [twelve]
                                L-------11 [eleven]
                        R-------99 [ninety-nine]
                R-------107 [one hundred seven]
                        L-------101 [one hundred one]
                        R-------1001 [one thousand one]
                                R-------1010 [one thousand ten]
m.begin():
ptr=0x007A4BE0 M_left=0x00000000 M_parent=0x007A4C00 M_right=0x00000000 M_color=1
key=0 value=[zero]
m.end():
ptr=0x0028FE40 M_left=0x007A4BE0 M_parent=0x007A4988 M_right=0x007A4CC0 M_color=0

dumping s as set:
ptr=0x0028FE20, M_key_compare=0x8, M_header=0x0028FE24, M_node_count=6
ptr=0x007A1E80 M_left=0x01D5D890 M_parent=0x0028FE24 M_right=0x01D5D850 M_color=1
key=123
ptr=0x01D5D890 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8B0 M_color=1
key=12
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 M_color=0
key=11
ptr=0x01D5D8B0 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 M_color=0
key=100
ptr=0x01D5D850 M_left=0x00000000 M_parent=0x007A1E80 M_right=0x01D5D8D0 M_color=1
key=456
ptr=0x01D5D8D0 M_left=0x00000000 M_parent=0x01D5D850 M_right=0x00000000 M_color=0
key=1001
As a tree:
root----123
        L-------12
                L-------11
                R-------100
        R-------456
                R-------1001
s.begin():
ptr=0x01D5D870 M_left=0x00000000 M_parent=0x01D5D890 M_right=0x00000000 M_color=0
key=11
s.end():
ptr=0x0028FE24 M_left=0x01D5D870 M_parent=0x007A1E80 M_right=0x01D5D8D0 M_color=0

GCC的实现方法与MSVC十分相似，具体内容可参见：http://gcc.gnu.org/onlinedocs/libstdc++/ libstdc++-html-USERS-4.1/stl__tree_8h-source.html

。与MSVC相比，GCC创建的数据结构并没有Isnil字段，所以其内存存储结构更为紧凑。另外，迭代器.end()同样指向了一个没有任何关键字或值的虚节点。

平衡树的动态调整技术（GCC）

在平衡树里添加一个结点，可能会导致树的失衡。所以在插入结点时，需要对树进行调整，以保持树的平衡。下面的程序将演示GCC的调整技术。

指令清单51.37　GCC程序

#include <stdio.h>
#include <map>
#include <set>
#include <string>
#include <iostream>

struct map_pair
{
    int key;
    const char *value;
};

struct tree_node
{
    int M_color; // 0 - Red, 1 - Black
    struct tree_node *M_parent;
    struct tree_node *M_left;
    struct tree_node *M_right;
};

struct tree_struct
{
    int M_key_compare;
    struct tree_node M_header;
    size_t M_node_count;
};

const char* ALOT_OF_TABS="\t\t\t\t\t\t\t\t\t\t\t";

void dump_as_tree (int tabs, struct tree_node *n)
{
    void *point_after_struct=((char*)n)+sizeof(struct tree_node);

    printf ("%d\n", *(int*)point_after_struct);

    if (n->M_left)
    {
        printf ("%.*sL-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_left);
    };
    if (n->M_right)
    {
        printf ("%.*sR-------", tabs, ALOT_OF_TABS);
        dump_as_tree (tabs+1, n->M_right);
    };
};

void dump_map_and_set(struct tree_struct *m)
{
    printf ("root----");
    dump_as_tree (1, m->M_header.M_parent);
};

int main()
{
    std::set<int> s;
    s.insert(123);
    s.insert(456);
    printf ("123, 456 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    s.insert(11);
    s.insert(12);
    printf ("\n");
    printf ("11, 12 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    s.insert(100);
    s.insert(1001);
    printf ("\n");
    printf ("100, 1001 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    s.insert(667);
    s.insert(1);
    s.insert(4);
    s.insert(7);
    printf ("\n");
    printf ("667, 1, 4, 7 are inserted\n");
    dump_map_and_set ((struct tree_struct *)(void*)&s);
    printf ("\n");
};

指令清单51.38　GCC 4.8.1程序

123, 456 are inserted
root----123
        R-------456

11, 12 are inserted
root----123
        L-------11
                R-------12
        R-------456

100, 1001 are inserted
root----123
        L-------12
                L-------11
                R-------100
        R-------456
                R-------1001

667, 1, 4, 7 are inserted
root----12
        L-------4
                L-------1
                R-------11
                        L-------7
        R-------123
                L-------100
                R-------667
                        L-------456
                        R-------1001

[1]　很明显，这是为了使64位系统向下兼容32位的C/C++应用程序。

[2]　这里有一个比较好的文档[Agncr Fog, Calling Convertions(2015)]，它描述了各种编译器的不同的命名混淆规则。

[3]　RTTI是Run-time type information的缩写，意思是实时类型信息。

[4]　PODT是Plain Old Data Type，纯文本的老的数据类型。

第52章 数组与负数索引

数组的负数索引值完全不阻碍寻址。例如，array[-1]实际上表示数组array起始地址之前的存储空间！

这种技术的用途相当有限。笔者认为除了本章范例的这种场景之外，应该没有什么领域用得上这项技术了。众所周知，在表示数组的第一个元素时，C/C++使用的数组下标是0，而部分其他编程语言（FORTRAN等）使用的数组下标可能是1。在移植代码的时候，可能会忽视这种问题。此时借助负数索引值就可以用查找C/C++数组中的第一个元素。

#include <stdio.h>

int main()
{
        int random_value=0x11223344;
        unsigned char array[10];
        int i;
        unsigned char *fakearray=&array[-1];

        for (i=0; i<10; i++)
                array[i]=i;

        printf ("first element %d\n", fakearray[1]);
        printf ("second element %d\n", fakearray[2]);
        printf ("last element %d\n", fakearray[10]);

        printf ("array[-1]=%02X, array[-2]=%02X, array[-3]=%02X, array[-4]=%02X\n",
                array[-1],
                array[-2],
                array[-3],
                array[-4]);
};

指令清单52.1　非优化的MSVC 2010下的程序

 1  $SG2751 DB      'first element %d', 0aH, 00H
 2  $SG2752 DB      'second element %d', 0aH, 00H
 3  $SG2753 DB      'last element %d', 0aH, 00H
 4  $SG2754 DB      'array[-1]=%02X, array[-2]=%02X, array[-3]=%02X, array[-4'
 5          DB      ']=%02X', 0aH, 00H
 6
 7  _fakearray$ = -24      ; size = 4
 8  _random_value$ = -20   ; size = 4
 9  _array$ = -16          ; size = 10
10  _i$ = -4               ; size = 4
11  _main  PROC
12         push      ebp
13         mov       ebp, esp
14         sub       esp, 24
15         mov       DWORD PTR _random_value$[ebp], 287454020 ; 11223344H
16         ; set fakearray[] one byte earlier before array[]
17         lea       eax, DWORD PTR _array$[ebp]
18         add       eax, -1 ; eax=eax-1
19         mov       DWORD PTR _fakearray$[ebp], eax
20         mov       DWORD PTR _i$[ebp], 0
21         jmp       SHORT $LN3@main
22         ; fill array[] with 0..9
23  $LN2@main:
24         mov       ecx, DWORD PTR _i$[ebp]
25         add       ecx, 1
26         mov       DWORD PTR _i$[ebp], ecx
27  $LN3@main:
28         cmp       DWORD PTR _i$[ebp], 10
29         jge       SHORT $LN1@main
30         mov       edx, DWORD PTR _i$[ebp]
31         mov       al, BYTE PTR _i$[ebp]
32         mov       BYTE PTR _array$[ebp+edx], al
33         jmp       SHORT $LN2@main
34  $LN1@main:
35         mov       ecx, DWORD PTR _fakearray$[ebp]
36         ; ecx=address of fakearray[0], ecx+1 is fakearray[1] or array[0]
37         movzx     edx, BYTE PTR [ecx+1]
38         push      edx
39         push      OFFSET $SG2751 ; 'first element %d'
40         call      _printf
41         add       esp, 8
42         mov       eax, DWORD PTR _fakearray$[ebp]
43         ; eax=address of fakearray[0], eax+2 is fakearray[2] or array[1]
44         movzx     ecx, BYTE PTR [eax+2]
45         push      ecx
46         push      OFFSET $SG2752 ; 'second element %d'
47         call      _printf
48         add       esp, 8
49         mov       edx, DWORD PTR _fakearray$[ebp]
50         ; edx=address of fakearray[0], edx+10 is fakearray[10] or array[9]
51         movzx     eax, BYTE PTR [edx+10]
52         push      eax
53         push      OFFSET $SG2753 ; 'last element %d'
54         call      _printf
55         add       esp, 8
56         ; subtract 4, 3, 2 and 1 from pointer to array[0] in order to find values before array[]
57         lea       ecx, DWORD PTR _array$[ebp]
58         movzx     edx, BYTE PTR [ecx-4]
59         push      edx
60         lea       eax, DWORD PTR _array$[ebp]
61         movzx     ecx, BYTE PTR [eax-3]
62         push      ecx
63         lea       edx, DWORD PTR _array$[ebp]
64         movzx     eax, BYTE PTR [edx-2]
65         push      eax
66         lea       ecx, DWORD PTR _array$[ebp]
67         movzx     edx, BYTE PTR [ecx-1]
68         push      edx
69         push      OFFSET $SG2754 ; 'array[-1]=%02X, array[-2]=%02X, array[-3]=%02X, array[-4]=%02 X'
70         call      _printf
71         add       esp, 20
72         xor       eax, eax
73         mov       esp, ebp
74         pop       ebp
75         ret       0
76 _main   ENDP

数组array[]有10个字节型数据元素，它们的值依次是0到9。我们还构造数组fakearray[]和相应的指针，使fakearrary的数组指针地址比array[]数组的地址提前一个字节，确保fakearray[1]的地址和array[0]对齐。但是我们还是很好奇，在array[0]之前的负值索引元素的地址到底是什么。为此，我们在这里在array[]数组的地址之存储了一个双字常数0x11223344。只要不进行优化编译，编译器就会按照变量声明的顺序来分配其存储空间。因此，我们可以在编译后的可执行文件里验证这个存储关系：双字常数random_value正好排列于array[]数组之前。

运行这个刚编译出来的可执行文件，可以看到程序运行的结果是：

first element 0
second element 1
last element 9
array[-1]=11, array[-2]=22, array[-3]=33, array[-4]=44

请注意x86平台的小端字节序现象。

在调试工具OllyDbg的栈窗口里，我们可以看到栈内数据的排布如下：

指令清单52.2　非优化的MSVC2010编译结果

CPU Stack
Address    Value
001DFBCC   /001DFBD3 ; fakearray pointer
001DFBD0   |11223344 ; random_value
001DFBD4   |03020100 ; 4 bytes of array[]
001DFBD8   |07060504 ; 4 bytes of array[]
001DFBDC   |00CB0908 ; random garbage + 2 last bytes of array[]
001DFBE0   |0000000A ; last i value after loop was finished
001DFBE4   |001DFC2C ; saved EBP value
001DFBE8   \00CB129D ; Return Address

现在，栈内地址与变量值的对应关系是：

数组fakearray[]的地址是0x001dfbd3，它比数组array[]的地址0x001dfbd4落后了一个字节 （栈是逆向增长的存储结构）。

虽然这确实是某种意义上的hack，但是它确实不很靠谱（编译结果可能和预期相差甚远）。因此，本书不建议在生产环境下使用这种代码。即使如此，本例仍然不失为典型的演示程序。

第53章 16位的Windows程序

虽然16位的Windows程序已经近乎绝迹，但是有关复古程序以及研究加密狗的研究，往往会涉及这部分知识。

微软于1993年8月发布了最后一个16位的Windows系统，即Windows 3.11（同年发行的中文操作系统Windows 3.2也是16位操作系统）。在此之后问世的16/32位混合系统Windows 96/98/ME系统，以及32位的Windows NT系统都可以运行16位应用程序。后来推出的64位Windows NT系列操作系统不再支持16位应用程序。

16位应用程序的代码结构和MSDOS的程序十分相似。这种类型的可执行文件采用了一种名为“New Executable (NE)”的可执行程序格式。

本章的所有程序均由OpenWatcom 1.9编译。编译时的选项开关如下：

wcl.exe -i=C:/WATCOM/h/win/ -s -os -bt=windows -bcl=windows example.c

53.1　例子#1

#include <windows.h>

int PASCAL WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine,
                    int nCmdShow )
{
        MessageBeep(MB_ICONEXCLAMATION);
        return 0;
};

WinMain        proc near
               push    bp
               mov     bp, sp
               mov     ax, 30h ; '0'       ; MB_ICONEXCLAMATION constant
               push    ax
               call    MESSAGEBEEP
               xor     ax, ax              ; return 0
               pop     bp
               retn    0Ah
WinMain        endp

这个程序不难分析。

53.2　例子#2

#include <windows.h>

int PASCAL WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine,
                    int nCmdShow )
{
        MessageBox (NULL, "hello, world", "caption", MB_YESNOCANCEL);
        return 0;
};

WinMain        proc near
               push    bp
               mov     bp, sp
               xor     ax, ax              ; NULL
               push    ax
               push    ds
               mov     ax, offset aHelloWorld ; 0x18. "hello, world"
               push    ax
               push    ds
               mov     ax, offset aCaption ; 0x10. "caption"
               push    ax
               mov     ax, 3               ; MB_YESNOCANCEL
               push    ax
               call    MESSAGEBOX
               xor     ax, ax              ; return 0
               pop     bp
               retn    0Ah
WinMain        endp

dseg02:0010 aCaption db 'caption',0
dseg02:0018 aHelloWorld db 'hello, world',0

基于Pascal语言的调用约定要求：参数从左至右入栈（与cdecl相反）。调用方函数依次传递NULL、"hello, world"、"caption"和MB_YESNOCANCEL。这个规范还要求被调用函数恢复栈指针，所以RETN指令有一个0Ah参数，即被调用函数在退出的时候要释放10字节的栈空间。这种调用规范和stdcall（参阅64.2节）十分相似，只是参数传递的顺序是从左到右的“自然语言”顺序，

16位应用程序的指针是一对数据：函数首先传递的是数据段的地址，然后再传递段内的指针地址。本例子只用到了一个数据段，所以DS寄存器的值一直是可执行文件数据段的地址。

53.3　例子#3

#include <windows.h>

int PASCAL WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine,
                    int nCmdShow )
{
    int result=MessageBox (NULL, "hello, world", "caption", MB_YESNOCANCEL);

    if (result==IDCANCEL)
        MessageBox (NULL, "you pressed cancel", "caption", MB_OK);
    else if (result==IDYES)
        MessageBox (NULL, "you pressed yes", "caption", MB_OK);
    else if (result==IDNO)
        MessageBox (NULL, "you pressed no", "caption", MB_OK);

    return 0;
};

WinMain        proc near
               push    bp
               mov     bp, sp
               xor     ax, ax           ; NULL
               push    ax
               push    ds
               mov     ax, offset aHelloWorld ; "hello, world"
               push    ax
               push    ds
               mov     ax, offset aCaption ; "caption"
               push    ax
               mov     ax, 3            ; MB_YESNOCANCEL
               push    ax
               call    MESSAGEBOX
               cmp     ax, 2            ; IDCANCEL
               jnz     short loc_2F
               xor     ax, ax
               push    ax
               push    ds
               mov     ax, offset aYouPressedCanc ; "you pressed cancel"
               jmp     short loc_49
loc_2F:
               cmp     ax, 6            ; IDYES
               jnz     short loc_3D
               xor     ax, ax
               push    ax
               push    ds
               mov     ax, offset aYouPressedYes ; "you pressed yes"
               jmp     short loc_49
loc_3D:
               cmp     ax, 7            ; IDNO
               jnz     short loc_57
               xor     ax, ax
               push    ax
               push    ds
               mov     ax, offset aYouPressedNo ; "you pressed no"
loc_49:
               push    ax
               push    ds
               mov     ax, offset aCaption ; "caption"
               push    ax
               xor     ax, ax
               push    ax
               call    MESSAGEBOX
loc_57:
               xor     ax, ax
               pop     bp
               retn    0Ah
WinMain        endp

这段代码是基于前面那个例子进行了一些改动。

53.4　例子#4

#include <windows.h>

int PASCAL func1 (int a, int b, int c)
{
        return a*b+c;
};

long PASCAL func2 (long a, long b, long c)
{
        return a*b+c;
};

long PASCAL func3 (long a, long b, long c, int d)
{
        return a*b+c-d;
};

int PASCAL WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine,
                    int nCmdShow )
{
        func1 (123, 456, 789);
        func2 (600000, 700000, 800000);
        func3 (600000, 700000, 800000, 123);
        return 0;
};

func1          proc near

c              = word ptr  4
b              = word ptr  6
a              = word ptr  8

               push    bp
               mov     bp, sp
               mov     ax, [bp+a]
               imul    [bp+b]
               add     ax, [bp+c]
               pop     bp
               retn    6
func1          endp

func2          proc near

arg_0          = word ptr  4
arg_2          = word ptr  6
arg_4          = word ptr  8
arg_6          = word ptr  0Ah
arg_8          = word ptr  0Ch
arg_A          = word ptr  0Eh

               push    bp
               mov     bp, sp
               mov     ax, [bp+arg_8]
               mov     dx, [bp+arg_A]
               mov     bx, [bp+arg_4]
               mov     cx, [bp+arg_6]
               call    sub_B2 ; long 32-bit multiplication
               add     ax, [bp+arg_0]
               adc     dx, [bp+arg_2]
               pop     bp
               retn    12
func2          endp

func3          proc near

arg_0          = word ptr  4
arg_2          = word ptr  6
arg_4          = word ptr  8
arg_6          = word ptr  0Ah
arg_8          = word ptr  0Ch
arg_A          = word ptr  0Eh
arg_C          = word ptr  10h

               push    bp
               mov     bp, sp
               mov     ax, [bp+arg_A]
               mov     dx, [bp+arg_C]
               mov     bx, [bp+arg_6]
               mov     cx, [bp+arg_8]
               call    sub_B2 ; long 32-bit multiplication
               mov     cx, [bp+arg_2]
               add     cx, ax
               mov     bx, [bp+arg_4]
               adc     bx, dx           ; BX=high part, CX=low part
               mov     ax, [bp+arg_0]
               cwd                      ; AX=low part d, DX=high part d
               sub     cx, ax
               mov     ax, cx
               sbb     bx, dx
               mov     dx, bx
               pop     bp
               retn    14
func3          endp
WinMain        proc near
               push    bp
               mov     bp, sp
               mov     ax, 123
               push    ax
               mov     ax, 456
               push    ax
               mov     ax, 789
               push    ax
               call    func1
               mov     ax, 9          ; high part of 600000
               push    ax
               mov     ax, 27C0h      ; low part of 600000
               push    ax
               mov     ax, 0Ah        ; high part of 700000
               push    ax
               mov     ax, 0AE60h     ; low part of 700000
               push    ax
               mov     ax, 0Ch        ; high part of 800000
               push    ax
               mov     ax, 3500h      ; low part of 800000
               push    ax
               call    func2
               mov     ax, 9          ; high part of 600000
               push    ax
               mov     ax, 27C0h      ; low part of 600000
               push    ax
               mov     ax, 0Ah        ; high part of 700000
               push    ax
               mov     ax, 0AE60h     ; low part of 700000
               push    ax
               mov     ax, 0Ch        ; high part of 800000
               push    ax
               mov     ax, 3500h      ; low part of 800000
               push    ax
               mov     ax, 7Bh        ; 123
               push    ax
               call    func3
               xor     ax, ax         ; return 0
               pop     bp
               retn    0Ah
WinMain        endp

当16位系统（MSDOS和Win16）传递long型32位“长”数据时（这种平台上的int型数据是16位数据），它会将32位数据拆成2个16位数据、成对传递。这种方法和第24章介绍的“32位系统处理64位数据”的方法十分相似。

此处的sub_B2是编译器开发人员编写的（仿真）库函数，用于长数据的乘法运算；即它可实现2个32位数据的乘法运算。程序中的其他库函数，请参见本书的附录D和附录E。

ADD/ADC指令分别对高低16位数据进行加法运算：ADD指令可设置/清除CF标识位，而ADC指令会代入这个标识位的值。同理，SUB/SBB指令对可实现32位数据的减法运算：SUB可设置/清除CF标识位，SBB会在计算过程中代入借位标识位的值。

在返回函数值的时候，32位的返回值通过DX：AX寄存器对回传。

另外，当主函数WinMain()函数向其他函数传递32位常量时，它也把常量拆分为1对16位数据了。

如果要把int型常数123当作long型32位参数，那么编译器就会使用CWD指令把AX里的16位数据符号扩展为32位的数据、再连同DX寄存器里的高16位数据一同传递。

53.5　例子#5

#include <windows.h>

int PASCAL string_compare (char *s1, char *s2)
{
        while (1)
        {
                if (*s1!=*s2)
                        return 0;
                if (*s1==0 || *s2==0)
                        return 1; // end of string
                s1++;
                s2++;
        };

};

int PASCAL string_compare_far (char far *s1, char far *s2)
{
        while (1)
        {
                if (*s1!=*s2)
                        return 0;
                if (*s1==0 || *s2==0)
                        return 1; // end of string
                s1++;
                s2++;
        };

};

void PASCAL remove_digits (char *s)
{
        while (*s)
        {
                if (*s>='0' && *s<='9')
                        *s='-';
                s++;
        };
};

char str[]="hello 1234 world";

int PASCAL WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine,
                    int nCmdShow )
{
        string_compare ("asd", "def");
        string_compare_far ("asd", "def");
        remove_digits (str);
        MessageBox (NULL, str, "caption", MB_YESNOCANCEL);
        return 0;
};

string_compare proc near

arg_0 = word ptr  4
arg_2 = word ptr  6

      push     bp
      mov      bp, sp
      push     si
      mov      si, [bp+arg_0]
      mov      bx, [bp+arg_2]

loc_12: ; CODE XREF: string_compare+21j
      mov      al, [bx]
      cmp      al, [si]
      jz       short loc_1C
      xor      ax, ax
      jmp      short loc_2B

loc_1C: ; CODE XREF: string_compare+Ej
      test     al, al
      jz       short loc_22
      jnz      short loc_27

loc_22: ; CODE XREF: string_compare+16j
      mov      ax, 1
      jmp      short loc_2B

loc_27: ; CODE XREF: string_compare+18j
      inc      bx
      inc      si
      jmp      short loc_12

loc_2B: ; CODE XREF: string_compare+12j
        ; string_compare+1Dj
      pop      si
      pop      bp
      retn     4
string_compare endp

string_compare_far proc near ; CODE XREF: WinMain+18p

arg_0 = word ptr  4
arg_2 = word ptr  6
arg_4 = word ptr  8
arg_6 = word ptr  0Ah

      push     bp
      mov      bp, sp
      push     si
      mov      si, [bp+arg_0]
      mov      bx, [bp+arg_4]

loc_3A: ; CODE XREF: string_compare_far+35j
      mov      es, [bp+arg_6]
      mov      al, es:[bx]
      mov      es, [bp+arg_2]
      cmp      al, es:[si]
      jz       short loc_4C
      xor      ax, ax
      jmp      short loc_67

loc_4C: ; CODE XREF: string_compare_far+16j
      mov      es, [bp+arg_6]
      cmp      byte ptr es:[bx], 0
      jz       short loc_5E
      mov      es, [bp+arg_2]
      cmp      byte ptr es:[si], 0
      jnz      short loc_63

loc_5E: ; CODE XREF: string_compare_far+23j
      mov      ax, 1
      jmp      short loc_67

loc_63: ; CODE XREF: string_compare_far+2Cj
      inc      bx
      inc      si
      jmp      short loc_3A

loc_67: ; CODE XREF: string_compare_far+1Aj
        ; string_compare_far+31j
      pop      si
      pop      bp
      retn     8
string_compare_far endp

remove_digits proc near ; CODE XREF: WinMain+1Fp

arg_0 = word ptr 4

      push     bp
      mov      bp, sp
      mov      bx, [bp+arg_0]

loc_72: ; CODE XREF: remove_digits+18j
      mov      al, [bx]
      test     al, al
      jz       short loc_86
      cmp      al, 30h ; '0'
      jb       short loc_83
      cmp      al, 39h ; '9'
      ja       short loc_83
      mov      byte ptr [bx], 2Dh ; '-'

loc_83: ; CODE XREF: remove_digits+Ej
        ; remove_digits+12j
      inc      bx
      jmp      short loc_72

loc_86: ; CODE XREF: remove_digits+Aj
      pop      bp
      retn     2
remove_digits    endp

WinMain proc near ; CODE XREF: start+EDp
      push     bp
      mov      bp, sp
      mov      ax, offset aAsd ; "asd"
      push     ax
      mov      ax, offset aDef ; "def"
      push     ax
      call     string_compare
      push     ds
      mov      ax, offset aAsd ; "asd"
      push     ax
      push     ds
      mov      ax, offset aDef ; "def"
      push     ax
      call     string_compare_far
      mov      ax, offset aHello1234World ; "hello 1234 world"
      push     ax
      call     remove_digits
      xor      ax, ax
      push     ax
      push     ds
      mov      ax, offset aHello1234World ; "hello 1234 world"
      push     ax
      push     ds
      mov      ax, offset aCaption ; "caption"
      push     ax
      mov      ax, 3 ; MB_YESNOCANCEL
      push     ax
      call     MESSAGEBOX
      xor      ax, ax
      pop      bp
      retn     0Ah
WinMain endp

这个程序使用了“near”和“far”两种不同类型的指针。每种指针都对应着16位 8086CPU的一种特定的指针寻址模式。这方面详细内容，请参见本书第94章的详细介绍。

“near”指针的寻址空间是当前数据段（DS）内的所有地址。字符串比较函数string_compare() 读取2个指针，把DS寄存器的值当作寻址所需的基（段）地址、对这两个指针进行寻址。所以此处的“mov al,[bx]”指令等效于“mov al, ds:[bx]”指令，只不过原指令没有明确标出它使用的DS寄存器而已。

“far”指针的寻址空间不限于当前数据段，它可以是其他DS段的内存地址。由于需要指定基（段）地址，所以2个16位数据才能表示1个far型指针。本例的 string_compare_far()函数从2对16位数据里提取2个内存地址。函数把指针的基地址存入段寄存器ES，然后在使用Far指针寻址时通过基地址寻址（mov al, es:[bx]）。本章的例2表明，16位程序的MessageBox()函数（属于系统函数）使用的也是far指针。确实，当Windows内核访问文本字符串指针时，它不了解字符串指针的基地址是什么，所以在调用内核函数的时候需要指明指针的段地址。

near指针的寻址范围是64k，这恰好是1个数据段的长度。对于小型程序来说，这种指针可能就够用了、不必在每次寻址的时候都要传递指针的段地址。大型的程序通常会占用多个64k的数据段，所以就要在每次寻址的时候指明指针的数据段（段地址）。

代码段也有寻址意义上的差别。一个64k内存段就可以盛下所有指令的小型程序，可以只用CALL NEAR指令调用其他函数；其被调用方函数可以只用RETN指令返回调用方函数。但是，大型程序会占用数个代码段，它就需要使用CALL FAR指令、用1对16位数据作跳转的目的地址；而去其被调用方函数就必须通过RETF指令返回调用方函数。

这就是编译器“内存模型（memory model）”选项的实际意义。

面向MS-DOS和Win16的编译器，为各种内存模型准备了相应的不同库。这些库文件在代码指针和数据指针的寻址模式存在相应的区别。

53.6　例子#6

#include <windows.h>
#include <time.h>
#include <stdio.h>

char strbuf[256];

int PASCAL WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine,
                    int nCmdShow )
{
        struct tm *t;
        time_t unix_time;

        unix_time=time(NULL);

        t=localtime (&unix_time);

        sprintf (strbuf, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year+1900,  t->tm_mon, t-> tm_mday,
                t->tm_hour, t->tm_min, t->tm_sec);

        MessageBox (NULL, strbuf, "caption", MB_OK);
        return 0;
};

WinMain         proc near

var_4           = word ptr -4
var_2           = word ptr –2

                push    bp
                mov     bp, sp
                push    ax
                push    ax
                xor     ax, ax
                call    time_
                mov     [bp+var_4], ax      ; low part of UNIX time
                mov     [bp+var_2], dx      ; high part of UNIX time
                lea     ax, [bp+var_4]      ; take a pointer of high part
                call    localtime_
                mov     bx, ax ; t
                push    word ptr [bx]       ; second
                push    word ptr [bx+2]     ; minute
                push    word ptr [bx+4]     ; hour
                push    word ptr [bx+6]     ; day
                push    word ptr [bx+8]     ; month
                mov     ax, [bx+0Ah]        ; year
                add     ax, 1900
                push    ax
                mov     ax, offset a04d02d02d02d02 ; "%04d-%02d-%02d %02d:%02d:%02d"
                push    ax
                mov     ax, offset strbuf
                push    ax
                call    sprintf_
                add     sp, 10h
                xor     ax, ax              ; NULL
                push    ax
                push    ds
                mov     ax, offset strbuf
                push    ax
                push    ds
                mov     ax, offset aCaption ; "caption"
                push    ax
                xor     ax, ax              ; MB_OK
                push    ax
                call    MESSAGEBOX
                xor     ax, ax
                mov     sp, bp
                pop     bp
                retn    0Ah
WinMain         endp

“unix_time”是个32位数据。它首先被Time()函数存储在寄存器对DX:AX里，而后被主函数复制到了2个本地的16位变量。接着这个指针（地址对）又被传递给localtime()函数。Localtime()函数把这个指针指向的数据解析为标准库定义的tm结构体，返回值是这种结构体的指针。另外，这也意味着如果不使用完其返回的数值，就不应重复调用这个函数。

在调用time()函数和localtime()函数的时候，编译器使用的是Watcom调用约定：前4个参数分别通过AX、DX、BX和CX寄存器传递，其余参数通过数据栈传递。遵循这种调用约定的库函数，其函数名称（汇编层面）的尾部也有下划线标识。

不过，sprintf()函数遵循的调用约定既不是PASCAL也不是Watcom，所以编译器使用常规的cdecl规范传递参数（请参考64.1节）。

53.6.1　全局变量

我们对刚才的例子略作改动，使用全局变量再次实现它的功能：

#include <windows.h>
#include <time.h>
#include <stdio.h>

char strbuf[256];
struct tm *t;
time_t unix_time;

int PASCAL WinMain( HINSTANCE hInstance,
                    HINSTANCE hPrevInstance,
                    LPSTR lpCmdLine,
                    int nCmdShow )
{

        unix_time=time(NULL);

        t=localtime (&unix_time);

        sprintf (strbuf, "%04d-%02d-%02d %02d:%02d:%02d", t->tm_year+1900,  t->tm_mon, t-> tm_mday,
                t->tm_hour, t->tm_min, t->tm_sec);

        MessageBox (NULL, strbuf, "caption", MB_OK);
        return 0;
};

unix_time_low   dw 0
unix_time_high  dw 0
t               dw 0

WinMain         proc near
                push    bp
                mov     bp, sp
                xor     ax, ax
                call    time_
                mov     unix_time_low, ax
                mov     unix_time_high, dx
                mov     ax, offset unix_time_low
                call    localtime_
                mov     bx, ax
                mov     t, ax               ; will not be used in future...
                push    word ptr [bx]       ; seconds
                push    word ptr [bx+2]     ; minutes
                push    word ptr [bx+4]     ; hour
                push    word ptr [bx+6]     ; day
                push    word ptr [bx+8]     ; month
                mov     ax, [bx+0Ah]        ; year
                add     ax, 1900
                push    ax
                mov     ax, offset a04d02d02d02d02 ; "%04d-%02d-%02d %02d:%02d:%02d"
                push    ax
                mov     ax, offset strbuf
                push    ax
                call    sprintf_
                add     sp, 10h
                xor     ax, ax              ; NULL
                push    ax
                push    ds
                mov     ax, offset strbuf
                push    ax
                push    ds
                mov     ax, offset aCaption ; "caption"
                push    ax
                xor     ax, ax              ; MB_OK
                push    ax
                call    MESSAGEBOX
                xor     ax, ax              ; return 0
                pop     bp
                retn    0Ah
WinMain         endp

虽然编译器保留了汇编宏t的赋值指令，但是这个值实际上没有被后续代码调用。因为编译器无法判断其他模块（文件）是否会访问这个值，所有保留了有关的赋值语句。

第四部分
Java

第54章 Java

54.1　简介

Java程序的反编译工具已经十分成熟了。一般来讲，它们都是JVM（基于栈机制的Java虚拟机）的字节码（bytecode，指令流里的指令只有一个字节，故而得名。不过java指令中的操作数属于变长信息）分析工具。著名的JAD（http://varaneckas.com/jad/

）就是一款颇具代表性的JAVA反编译工具。

相对于x86平台更底层指令的反编译技术来说，面向JVM的bytecode更容易反编译。这主要是因为：

① 字节码含有更为丰富的数据类型信息。

② JVM内存模型更严格，因此字节码分析起来更为有章可循。

③ Java编译器不做任何优化工作（而JVM JIT在运行时会做优化工作），因此在反编译字节码之后，我们基本可以直接理解Java类文件里的原始指令。

什么时候JVM bytecode反编译有用呢？

① 无需重新编译反汇编的结果，而能给类文件做应急补丁。

② 分析混淆代码。

③ 需要编写自己的代码混淆器。

④ 创建面向JVM的、类似编译程序的代码生成工具（类似Scala,Clohure等等）。

让我们从简单的代码开始演示。除非特别指明，否则我们这里用到的都是JDK 1.7的自带工具。

反编译类文件的命令是：javap –c –verbose。

笔者采用的例子摘自于参考书目Jav13。

54.2　返回一个值

或许Java的最简函数是直接返回数值、不做其他操作的函数。当然，功能再少一点的、什么操作都没有的“闲置”函数，肯定不存在。函数必须具有某种行为，因此统称为“方法”。在Java的概念中，“类/class”是一切对象的模版，所有方法必定不能脱离“类”而单独存在。但是为了简化起见，本文还是把“方法”称为“函数”。

public class ret
{
        public static int main(String[] args)
        {
                return 0;
        }
}

我们采用命令javac 来编译它，命令行是：

javac ret.java

编译完后，我们可以用JDK自带的反汇编器－Javap来分析字节码，此时采用的命令应当是：

javap -c -verbose ret.class

反编译完成后，我们得到的代码如下所示。

指令清单54.1　JDK 1.7（摘要）

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是用得最多的常量。因此他们为PUSH 0的指令单独设计了单字节的指令码，即iconst_0。此外还有iconst_1（将1入栈），iconst_2（将2入栈）……，一直到iconst_5这样的单字节字节码。而且确实有iconst_m1（将−1入栈）这类的将负数推送入栈的单字节指令。

JVM常常采用栈的方式来传递参数并从函数中返回值。因此语句iconst_0将数字0压入栈，而指令ireturn则是从栈顶返回整型数（ireturn中的字母i的意思就是“返回值为integer/整数”）。这里注意我们用TOS来代表栈顶，它是英文“Top Of Stack”的首字母缩写。

我们来重新编写一下这个例子，将返回值修改为整数1234：

public class ret
{
        public static int main(String[] args)
        {
                return 1234;
        }
}

这样的话，我们得到的结果是如下所示的代码。

指令清单54.2　JDK1.7（摘要）

public static int main(java.lang.String[]);
  flags: ACC_PUBLIC, ACC_STATIC
  Code:
    stack=1, locals=1, args_size=1
       0: sipush           1234
       3: ireturn

指令sipush的功能是将操作数（这里是整数1234）入栈（si是short integer短型整数的缩写）。Short（短型）的意思就是针对16位的数值进行操作，而这里的整数1234正好就是一个16位的数值。

如果操作数比整型数据更大，那么字节码会是什么情况呢？让我们来看看实例：

public class ret
{
        public static int main(String[] args)
        {
                return 12345678;
        }
}

指令清单54.3　常量池

...
   #2 = Integer            12345678
...

  public static int main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc             #2                 // int 12345678
         2: ireturn

JVM的opcode无法直接封装32位数据，这是开发环境的局限决定的。像本例这样的32位常数将会存储到“常量池”里。常量池是一个由数组组成的表，类型为cp_info constant_pool[]，用来存储程序中使用的各种常量，包括Class/String/Integer等各种基本Java数据类型，详情参见The Java Virtual Machine Specification 4.4节。

并非只有JVM如此处理常量。像MIPS、ARM以及其他的RISC型的CPU都不能在32位的opcode中封装32位常量，因此包括MIPS和ARM在内的RISC类型的CPU都得分步骤构建这些数值，或者将其保存在数据段中。有关范例可以参考本书的28.3节或者29.1节。

在MIPS的概念中也有传统意义上的常量池，不过它的名字则叫做“数据缓冲池（文字池）/literal pool”。这种文字池与可执行程序中的“.lit4/.lit8”数据段相对应。数据段.lit4用于保存32位的单精度浮点常数，而.lit8则用于保存64位的双精度浮点常数。

我们来试试其他类型的数据。

布尔型Boolean:

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
         1: ireturn

当返回值为Ture时，JVM bytecode层面的返回值就是整数1。像C/C++一样，Java程序同样会把布尔型数值保存在32位的栈中。虽然说“逻辑真”和“整数1”的数值完全相同，但是我们不可能把布尔值当作整数值使用、也不可能把整数值当作布尔值使用。既定的类文件事先声明了数值的数据类型，而且这些数据类型会在程序运行时被实时检查。

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

还有字符型：

public class ret
{
        public static char main(String[] args)
        {
                return 'A';
        }
}

  public static char main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: bipush          65
         2: ireturn

指令bipush的意思是push byte（保存字节）。Java环境中的char型数据是16位的UTF-16字符，同短整数型数据一样同属于16位short型短数据。但是大写字母A的ASCII码是十进制数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

也许读者有些疑问：既然这些数据在运行的时候都是当作32位整型数据处理的，那么为什么还要不厌其烦地把它们声明为16位的数据类型呢？另外，字符型char数据与short短整数型的数值也是相同的，为什么还要刻意地把它声明为字符型char数据呢？

答案也很简单，是为了增加数据类型的控制以及增加源代码的可读性。Char字符型的限定符虽然在数值上与short短型整数相同，但是只要一看到char字符型的限定，我们立刻会联想到它是一个UTF16的字符集，而不会把它当作其他类型的方式去理解。在遇到被限定符short修饰的数据类型时，我们自然而然地就会把它理解为16位数据。同理，应当使用boolean声明的数据就不要把它声明为C语言风格的int型数据。

我们还可以通过限定符long声明JAVA的64位的整数型数据：

public class ret3
{
        public static long main(String[] args)
        {
                return 1234567890123456789L;
        }
}

指令清单54.4　常量池

...
   #2 = Long 1234567890123456789l
...

  public static long main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc2_w         #2         // long 1234567890123456789l
         3: lreturn

上述64位常量同样位于程序的常量池部分。它被ldc2_w指令提取之后，再由lreturn（long return）指令回传给调用方函数。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.456d
        3: dreturn

这里的指令dreturn代表return double，意思是返回双精度常数。

最后，我们举一个单精度浮点数的例子。单精度浮点常数的后面有一个限定符f，而双精度数的限定符则是字母d。字母f是float的缩写，而d则是double的缩写。

public class ret
{
        public static float main(String[] args)
        {
                return 123.456f;
        }
}

指令清单54.6　常量池

...
   #2 = Float            123.456f
...

  public static float main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: ldc             #2                    // float 123.456f
         2: freturn

同为从常量池提取32位数据的指令，“提取整数”和“提取浮点数”的指令都是ldc。而指令freturn代表的是return float，声明了返回值为单精度浮点数。

最后，我们来看看如果什么数也不返回时情况会是怎么样的，也就是return指令后不带任何参数。

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　简单的计算函数

我们继续来看看简单的计算函数：

public class calc
{
        public static int half(int a)
        {
                return a/2;
        }
}

这里我们看到的是一个除以2的简单计算函数，用到的指令是iconst_2。我们来分析一下这几条指令：

  public static int half(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0
         1: iconst_2
         2: idiv
         3: ireturn

首先，iload_0指令是提取外来的第0个函数参数，再把它压入栈中，而iconst_2指令则是将数值2压入栈中。这两个指令执行完后，函数栈的存储内容将如下所示：

      +---+
TOS ->| 2 |
      +---+
      | a |
      +---+

TOS是“Top Of Stack”的缩写，即栈顶。

idiv指令则是从栈顶取出这两个值并进行除非运算，然后把返回的结果保存在栈顶。

      +--------+
TOS ->| result |
      +--------+

ireturn指令则会提取栈顶的数据、把它作为返回值回传给调用方函数。

下面我们来看看双精度的除法的运算指令：

public class calc
{
        public static double half_double(double a)
        {
                return a/2.0;
        }
}

指令清单54.7　常量池

...
   #2 = Double               2.0d
...

  public static double half_double(double);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=2, args_size=1
         0: dload_0
         1: ldc2_w         #2                // double 2.0d
         4: ddiv
         5: dreturn

双精度浮点数的运算指令和单精度浮点数的指令十分相似。在提取常量时，它使用的指令时ldc2_w。此外，所有的三条运算指令（dload_0、ddiv以及dreturn）都带有前缀d，这个限定符表明操作数属于双精度浮点数double。

下面我们来看看含有两个参数的函数情况：

public class calc
{
        public static int sum(int a, int b)
        {
                return a+b;
        }
}

  public static int sum(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: iadd
         3: ireturn

指令iload_0用于提取第一个函数参数a，而iload_1则用于导入第二个函数参数b。在执行完这两条指令之后，栈内数据如下图所示：

      +---+
TOS ->| b |
      +---+
      | a |
      +---+

而指令iadd的含义则是将两个参数中的数值相加，并将结果保存在栈顶TOS中。

      +--------+
TOS ->| result |
      +--------+

如果我们将以上函数的两个参数的数据类型更换成long类型的话：

        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

第二条lload指令会提取外来的第二个参数。指令后缀直接从0递增到2，是因为long型数据是64位的数据，它正好占有两个32位数据的存储位置（即后文介绍的“参数槽”）。

下面我们来看看一个更加复杂的例子：

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中。

      +---------+
TOS ->| product |
      +---------+

指令iload_2将第三个参数压入栈中参加运算：

      +---------+
TOS ->|    c    |
      +---------+
      | product |
      +---------+

现在就能采用指令iadd进行加法求和运算了。

54.4　JVM的内存模型

前面提到过，在x86和其他底层运行平台上，栈通常用于传递参数的参数、存储局部变量。而我们这里要提到的JVM略有不同。

JVM的内存模型可分为：

局部变量数组（Local Variable Array，LVA）。它用来存储外来的函数参数和局部变量。iload_0一类指令的作用是从LVA中提取数值，而istore则可以将数值保存在LVA里。函数会从第0个参数槽（不涉及this指针的函数）或第1个参数槽（涉及传递this指针的函数）依次读取各项外来参数，然后分配局部变量的存储空间。
每个参数槽（args slot）都是32位存储单元，因此，数据类型为long（长）或者double（双）的参数数据都会占用两个参数槽。

操作数栈即俗称的（java）“栈”，用于存储计算操作数，或者向被调用方函数传递参数。Java程序不能直接运行于x86那样的底层硬件环境，因此它必须通过明确的入栈、出栈指令才能访问自己的栈，不能像汇编指令那样直接对栈寻址。

堆。堆主要用于存储对象和数组。

以上3种内存模型相互独立、互相隔离。

54.5　简单的函数调用

Math.random()函数可以产生从0.0～1.0之间的任意（伪）随机数。因此，如欲生成0.0～0.5之间的随机数，就要对上述结果进行除法运算：

public class HalfRandom
{
        public static double f()
        {
                return Math.random()/2;
        }
}

指令清单54.8　常量池

...
   #2 = Methodref         #18.#19         // java/lang/Math.random:()D
   #3 = Double            2.0d
...
  #12 = Utf8              ()D
...
  #18 = Class             #22             // java/lang/Math
  #19 = NameAndType       #23:#12         // random:()D
  #22 = Utf8              java/lang/Math
  #23 = Utf8              random

  public static double f();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: invokestatic  #2              // Method java/lang/Math.random:()D
         3: ldc2_w        #3              // double 2.0d
         6: ddiv
         7: dreturn

指令invokestatic调用函数Math.random()，并将结果保存在栈顶TOS。这个值随后被除以2，最终成为函数返回值。但是这些函数的名称是如何编码的？编译器使用了Methodref的表达方法把外部函数的信息编排在常量池中。常量池里的相应数据声明了与被调用函数有关的类（Class）以及方法（NameAndType）名称。Methodref表达式的第一个字段（Fieldref里的第一个值）是Class的索引号-＃18，这个索引号（实际上是指针）对应着一个Class名称（依次查询#18、#22号常量，可得到java/lang/Math）。Methodref表达式的第二个字段（Fieldref里的第二个值）是方法名称的索引号＃19，这个索引号对应着方法名称NameAndType（依次查询#19、#23号常量，可得到方法名称random）。方法名称由2个索引号组成，第一个索引号对应着外部函数名称，而第二个索引号对应着函数返回值的数据类型“()D”——双精度浮点数。

综合上述信息可知：

① JVM能检查数据类型的正确性。

② JAVA的反编译器能从已经编译好的类文件中恢复出其原来的数据类型。

最后，我们来看一个经典的字符串显示例子：Hello, world!

public class HelloWorld
{
        public static void main(String[] args)
        {
                System.out.println("Hello, World");
        }
}

指令清单54.9　常量池

...
   #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

偏移量为3的ldc指令从常量池中提取字符串Hello,World的指针，然后将其压入栈中。在Java中，这种二级指针称为reference（引用），但是它的本质仍然还是指针或者地址。[1]

熟悉的指令invokevirtual从常量池中提取println函数的信息，然后调用该函数。我们已经知道，标准库定义了许多版本的println()函数，每个版本都处理的数据类型都各不相同。本例调用的println()函数，肯定是专门处理string型数据的那个版本。

第一条指令getstatic的功能是什么呢？这个指令从对象System.out中提取引用指针的有关字段，再把它压入栈。这个引用指针的作用与println方法的this指针相似。因此，从内部来讲，println函数的输入参数实际上是两个指针：①this指针，也就是指向对象的指针；②字符串“Hello,World”的地址。

因此这并不矛盾：只有在System.out初始化为实例的时候，才能调用println()方法。

为了方便分析人员阅读， javap把有关信息全部追加到了字节码的注释里了。

54.6　调用函数beep()（蜂鸣器）

这是一个最简单的调用（调用了无参数的两个函数），其功能就是发出蜂鸣声beep：

    public static void main(String[] args)
    {
            java.awt.Toolkit.getDefaultToolkit().beep();
    };

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: invokestatic   #2                  // Method java/awt/Toolkit.getDefaultToolkit:()↙
    ↘ Ljava/awt/Toolkit;
         3: invokevirtual  #3                  // Method java/awt/Toolkit.beep:()V
         6: return

第一条指令是偏移量为0的invokestatic指令，它调用了java.awt.Toolkit.getDefaultToolkit()函数。后者的返回值是Toolkit Class类实例的引用指针。偏移量为3的invokevirtual指令调用这个类的beep()函数。

54.7　线性同余随机数产生器（PRNG）

我们再来看看PRNG（Pseudo Random Numbers Generator）随机数产生器，其实我们已经在本书的第20章介绍过它的C语言代码了。

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;
        }
}

程序在启动之初就初始化了数个成员变量（Class Fields）。这是如何进行的呢？这就得借助javap查看该类的构造函数：

  static {};
    flags: ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: ldc            #5                  // int 1664525
         2: putstatic      #3                  // Field RNG_a:I
         5: ldc            #6                  // int 1013904223
         7: putstatic      #4                  // Field RNG_c:I
        10: return

上述指令展示了变量的初始化过程。变量RNG_a和RNG_c分别占据参数槽的第三和第四存储单元。而putstatic函数将有关常数存放在相应地址。

函数my_rand()将输入值保存在变量rand_state中：

  public void my_srand(int);
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=2, args_size=2
         0: iload_1
         1: putstatic      #2                  // Field rand_state:I
         4: return

Iload_1指令提取输入变量，然后将其压入栈中。但是为什么此处是iload_1指令而不是读取第0个参数的iload_0指令？这是由于该函数调用了类的成员变量，因此要用第0个参数传递this指针。依此类推，函数的第二个参数槽用于传递成员变量、同时是隐性参数rand_state，此后的putstatic指令把栈顶的数值肤之道第二个参数槽、完成指定任务。

现在我们来看看my_rand()函数：

  public int my_rand();
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic      #2                  // Field rand_state:I
         3: getstatic      #3                  // Field RNG_a:I
         6: imul
         7: putstatic      #2                  // Field rand_state:I
        10: getstatic      #2                  // Field rand_state:I
        13: getstatic      #4                  // Field RNG_c:I
        16: iadd
        17: putstatic      #2                  // Field rand_state:I
        20: getstatic      #2                  // Field rand_state:I
        23: sipush         32767
        26: iand
        27: ireturn

这段代码分别提取类实例的各成员变量、进行各种运算，再使用putstatic指令更新rand_state的值。在偏移量为20处，rand_state的值会重新调入（此前的putstatic指令把它从栈里抛了出去）。虽然表面看来这个程序的效率很低，但是JVM肯定能够进行充分的优化、足以弥补字节码的效率缺陷。

54.8　条件转移

我们来看一个简单的例子：

public class abs
{
        public static int abs(int a)
        {
                if (a<0)
                        return -a;
                return a;
        }
}

  public static int abs(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: ifge            7
         4: iload_0
         5: ineg
         6: ireturn
         7: iload_0
         8: ireturn

如果栈顶/TOS的值大于或等于零，那么ifge将会跳转到偏移量为7的指令。特别需要注意的是，ifxx指令还会从栈顶抛弃一个值，否则它就不能进行比较运算。

后面的ineg指令是对整数求负的运算指令。

我们再看一个例子：

        public static int min (int a, int b)
        {
                if (a>b)
                        return b;
                return a;
        }

这个函数的字节码如下所示：

  public static int min(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: iload_0
         1: iload_1
         2: if_icmple       7
         5: iload_1
         6: ireturn
         7: iload_0
         8: ireturn

if_icmple指令从栈中提取（pop）两个数值并将之进行比较。如果第二个操作数小于或等于第一个操作数，那么它将跳转到偏移量为7的指令，否则继续执行下一条指令。

若对上述程序进行调整，通过较大值函数max()进行比较：

        public static int max (int a, int b)
        {
                if (a>b)
                        return a;
                return b;
        }

那么字节码则会变为：

  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

其实与刚才取较小值的程序基本相同，但是最后两个iload指令（在偏移量为5和7的位置上）位置对换了一下。

下面再看一个更复杂一些的例子：

public class cond
{
        public static void f(int i)
        {
                if (i<100)
                        System.out.print("<100");
                if (i==100)
                        System.out.print("==100");
                if (i>100)
                        System.out.print(">100");
                if (i==0)
                        System.out.print("==0");
        }
}

  public static void f(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0
         1: bipush         100
         3: if_icmpge      14
         6: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
         9: ldc            #3          // String <100
        11: invokevirtual  #4          // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        14: iload_0
        15: bipush         100
        17: if_icmpne      28
        20: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        23: ldc            #5          // String ==100
        25: invokevirtual  #4          // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        28: iload_0
        29: bipush         100
        31: if_icmple      42
        34: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        37: ldc            #6          // String >100
        39: invokevirtual  #4          // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        42: iload_0
        43: ifne           54
        46: getstatic      #2          // Field java/lang/System.out:Ljava/io/PrintStream;
        49: ldc #7 // String ==0
        51: invokevirtual  #4         // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        54: return

这段代码用以实现两个功能。首先，它可以判断输入的数与100的大小关系，如果是小于100的话，显示“<100”的字样；如果是等于100，则显示“=100”的字样；如果是大于100的话，则显示“>100”的字样。另外一个功能是一个特例，就是如果输入的数为0的话，则显示“==0”字样。

我们这里还是用到了前面提到的指令ifXX。还记得其功能吧？

指令if_icmpge从栈中提取（pop）出两个值，然后对它们进行比较。如果第二个数大于第一个数的话，那么就跳转到偏移量为14的位置；其实指令if_icmpne和指令if_icmple的运行机理基本相同，只是其转移的条件不相同而已。

在偏移量为43的位置，我们还可以看到一个指令ifne。我们认为这是一个措辞不当的助记符，如果把它的助记符换位ifnz似乎更加贴切一些（意思是当栈顶的值不是0时跳转），而实际的执行过程也是这样的——当输入的值不是0时，程序会跳转到偏移量为54的地方。而如果输入值为0，程序的执行流则不会发生跳转、继续执行偏移量为46的指令、显示“==0”这个字符串。

必须注意的是：JVM没有无符号数的数据类型。因此我们只会遇到比较有符号数的比较指令。

54.9　传递参数

我们来将前面讲到的两个取较大值和较小值的函数混合在一起使用，也就是函数min()和函数max()。

public class minmax
{
        public static int min (int a, int b)
        {
                if (a>b)
                        return b;
                return a;
        }

        public static int max (int a, int b)
        {
                if (a>b)
                        return a;
                return b;
        }

        public static void main(String[] args)
        {
                int a=123, b=456;
                int max_value=max(a, b);
                int min_value=min(a, b);
                System.out.println(min_value);
                System.out.println(max_value);
        }
}

下面是主函数main()的代码：

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: bipush        123
         2: istore_1
         3: sipush        456
         6: istore_2
         7: iload_1
         8: iload_2
         9: invokestatic  #2             // Method max:(II)I
        12: istore_3
        13: iload_1
        14: iload_2
        15: invokestatic  #3             // Method min:(II)I
        18: istore 4
        20: getstatic     #4             // Field java/lang/System.out:Ljava/io/PrintStream;
        23: iload 4
        25: invokevirtual #5             // Method java/io/PrintStream.println:(I)V
        28: getstatic     #4             // Field java/lang/System.out:Ljava/io/PrintStream;
        31: iload_3
        32: invokevirtual #5             // Method java/io/PrintStream.println:(I)V
        35: return

调用方函数通过栈向被调用方函数传递参数，而被调用方函数通过TOS()/栈顶向调用方函数传递返回值。

54.10　位操作

JVM的位操作指令和其他指令集的工作原理基本相同。

        public static int set (int a, int b)
        {
                return a | 1<<b;
        }

        public static int clear (int a, int b)
        {
                return a & (~(1<<b));
        }

  public static int set(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=2
         0: iload_0
         1: iconst_1
         2: iload_1
         3: ishl
         4: ior
         5: ireturn

  public static int clear(int, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=2
         0: iload_0
         1: iconst_1
         2: iload_1
         3: ishl
         4: iconst_m1
         5: ixor
         6: iand
         7: ireturn

指令iconst_m1将−1这个数调入栈中，其实这个值就是0Xffffffff。将任意数与−1进行异或XOR运算，其实就是对原操作数的所有位逐位取反（这一点可以参见本书的附录A.6.2）。

而当我们将所有的数据类型扩展为64位的long类型时：

        public static long lset (long a, int b)
        {
                return a | 1<<b;
        }

        public static long lclear (long a, int b)
        {
                return a & (~(1<<b));
        }

  public static long lset(long, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=2
         0: lload_0
         1: iconst_1
         2: iload_2
         3: ishl
         4: i2l
         5: lor
         6: lreturn

  public static long lclear(long, int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=2
         0: lload_0
         1: iconst_1
         2: iload_2
         3: ishl
         4: iconst_m1
         5: ixor
         6: i2l
         7: land
         8: lreturn

除了操作指令都具有一个表示操作数是64位值的“L”前缀之外，这个程序的字节码和上一个程序几乎相同。此外，第二个函数的参数还有一个整型数据。假如需要把int型的32位数据扩展为64位long型数据，那么编译器就会分配i2l完成这项任务。

54.11　循环

public class Loop
{
        public static void main(String[] args)
        {
                for (int i = 1; i <= 10; i++)
                {
                        System.out.println(i);
                }
        }
}

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: iconst_1
         1: istore_1
         2: iload_1
         3: bipush         10
         5: if_icmpgt      21
         8: getstatic      #2           // Field java/lang/System.out:Ljava/io/PrintStream;
        11: iload_1
        12: invokevirtual  #3           // Method java/io/PrintStream.println:(I)V
        15: iinc           1, 1
        18: goto           2
        21: return

这里举一个例子，显示从1～10一共10个整型数。采用循环指令的方式。

指令iconst_1将数值1调入栈顶TOS，而istore_1指令则将局部变量阵列LVA中的这项数值存储在第一个参数槽里。为什么把它存储在1号参数槽而不是第0个参数槽呢？这是因为主函数main()有一个参数是String数组，这个字符串的引用指针会占用第0号参数槽。

因此，局部变量i必须存放于第1个参数槽。

在偏移量为3和5的地方的指令，分别将变量i与循环控制变量的上限（这里是10）比较。如果此时的变量i比10大，那么指令流就会转向偏移量为21的地方，直接退出函数；否则，程序就会调用println()函数显示当前的数值。显示完后，在偏移量为11的地方，局部变量i会重新装入新的值，继续为显示方法做准备。另外，在调用println方法的时候，我们提供的参数时integer型参数。注释中的“(I)V”分别表示数据类型为integer，函数类型为void。

当显示函数println结束时，i的数值在偏移量为15的地方递增，也就是加1。这条指令有两个操作数。第一个操作数，第一个操作数表示实际运算数存储于第一号参数槽，第二个操作数表示递增的增量是1。

goto指令的功能就是跳转/GOTO。它跳转到循环体中偏移量为2的地方。

让我们来看一个稍微复杂一些的例子：斐波那契数列，简称为Fibonacci，其实前面已经提到过了。但是这里我们来看看如何用程序来实现它。

public class Fibonacci
{
        public static void main(String[] args)
        {
                int limit = 20, f = 0, g = 1;

                for (int i = 1; i <= limit; i++)
                {
                        f = f + g;
                        g = f - g;
                        System.out.println(f);
                }
        }
}

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=5, args_size=1
         0: bipush         20
         2: istore_1
         3: iconst_0
         4: istore_2
         5: iconst_1
         6: istore_3
         7: iconst_1
         8: istore         4
        10: iload          4
        12: iload_1
        13: if_icmpgt      37
        16: iload_2
        17: iload_3
        18: iadd
        19: istore_2
        20: iload_2
        21: iload_3
        22: isub
        23: istore_3
        24: getstatic      #2           // Field java/lang/System.out:Ljava/io/PrintStream;
        27: iload_2
        28: invokevirtual  #3           // Method java/io/PrintStream.println:(I)V
        31: iinc           4, 1
        34: goto           10
        37: return

我们来看看本地存储数组（Local Varible Array，LVA）与各参数槽的存储关系：

① 0号参数槽存储的是主函数main()的唯一参数；

② 1号参数槽存储的是循环控制变量limit，其值固定为20；

③ 2号参数槽存储的是变量f。

④ 3号参数槽存储的是变量g。

⑤ 4号参数槽存储的是变量i。

可见，Java编译器会按照源代码声明变量的顺序，在LVA中依次分配各变量的存储空间。

当直接向第0、1、2、3号参数槽存储数据时，可使用专用的istore_n指令。然而当直接向4及更高编号的参数槽存储数据时，就没有这样便利的专用操作指令了，需要使用带有参数的istore指令。如偏移量为8的指令所示，后一种istore指令将操作数当作参数槽的编号进行存储操作。其实iload指令也是如此。本文就不再解释偏移量为10的iload指令了。

但是，像循环迭代上限limit这样的常量也占用了参数槽，难道它还经常更新数值吗？JVM JIT编译器能够充分优化这类事务，我们不必专们进行人工干预。

54.12　switch()语句

下列范例证明，switch()语句是由tableswitch指令实现的。

        public static void f(int a)
        {
                switch (a)
                {
                case 0: System.out.println("zero"); break;
                case 1: System.out.println("one\n"); break;
                case 2: System.out.println("two\n"); break;
                case 3: System.out.println("three\n"); break;
                case 4: System.out.println("four\n"); break;
                default: System.out.println("something unknown\n"); break;
                };
        }

上述程序的字节码与源程序几乎是逐一对应：

  public static void f(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: iload_0
         1: tableswitch    { // 0 to 4
                       0: 36
                       1: 47
                       2: 58
                       3: 69
                       4: 80
                 default: 91
            }
        36: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        39: ldc           #3         // String zero
        41: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        44: goto          99
        47: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        50: ldc           #5         // String one\n
        52: invokevirtual #4        // Method java/io/PrintStream.println:(Ljava/lang /String;)V
        55: goto          99
        58: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        61: ldc           #6         // String two\n
        63: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        66: goto          99
        69: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        72: ldc           #7         // String three\n
        74: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        77: goto          99
        80: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        83: ldc           #8         // String four\n
        85: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        88: goto          99
        91: getstatic     #2         // Field java/lang/System.out:Ljava/io/PrintStream;
        94: ldc           #9         // String something unknown\n
        96: invokevirtual #4         // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        99: return

如果输入值是0，则显示为zero；如果输入值是1，则显示one；如果输入值是2，则显示two；如果输入值是3，则显示three；如果输入值是4，则显示four；如果不是以上的5种情况，则显示字符串something unknown。

54.13　数组

54.13.1　简单的例子

我们首先创建一个含有10个元素的整数型数组，然后逐次填入0～9：

程序如下：

        public static void main(String[] args)
        {
                int a[]=new int[10];
                for (int i=0; i<10; i++)
                        a[i]=i;
                dump (a);
        }

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: bipush       10
         2: newarray     int
         4: astore_1
         5: iconst_0
         6: istore_2
         7: iload_2
         8: bipush       10
        10: if_icmpge    23
        13: aload_1
        14: iload_2
        15: iload_2
        16: iastore
        17: iinc          2, 1
        20: goto          7
        23: aload_1
        24: invokestatic  #4               // Method dump:([I]V
        27: return

指令newarray创建一个可容纳10个整型（int）元素的数组。这个数组的大小是由bipush设定的，它会被保存在栈顶TOS；而数组的类型则是由newarray指令的操作数定义。我们看到newarrary的操作数是int，因此它会创建整型数组。执行完指令newarray后，系统会给新建的数组分配一个引用指针（reference），并且把这个引用指针存储到栈顶TOS。其后的astore_1指令吧引用指针存储到LVA的第一个参数槽。主函数main()的第二部分是一个循环语句。这个循环执行的指令将变量 i 依次保存到相应的数值单元中。指令aload_1获取数组的引用指针，并且把它保存在栈中。而指令iastore的功能则把栈里的整型数据保存在数组中，与此同时它会通过栈顶TOS获取数组的引用指针。而主函数main()的第三部分的功能是执行函数dump()。在偏移量为23的地方，我们可以看到aload_1指令。它负责制备dump()的唯一参数。

下面我们来继续看看函数dump()的功能：

        public static void dump(int a[])
        {
                for (int i=0; i<a.length; i++)
                        System.out.println(a[i]);
        }

该函数执行的功能是循环显示目标数组中的值。

程序如下所示。

  public static void dump(int[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: aload_0
         4: arraylength
         5: if_icmpge      23
         8: getstatic      #2           // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: iload_1
        13: iaload
        14: invokevirtual  #3           // Method java/io/PrintStream.println:(I)V
        17: iinc           1, 1
        20: goto           2
        23: return

函数会从第0号参数槽获取数组的引用指针/reference。而源代码中的a.length表达式被编译器转换成了arraylength（数组长度）指令：它通过引用指针获取数组的信息，并把数组长度保存在栈顶TOS中。在偏移量为13的指令iaload则负责加载既定的数组元素。在数组类型确定的情况下，对某个数组元素寻址需要知道数组的首地址和即定元素的索引编号。前者由偏移量为11的指令aload_0完成；后者则通过偏移量为12的指令iload_1实现。

很多人会想当然的认为，在那些带有字母前缀a的指令里，a大概是数组array的缩写。其实这种猜测并不确切。此类指令是操作数据对象引用指针的指令。数组和字符串只是对象型数据的一种特例罢了。

54.13.2　数组元素求和

我们来看看另外一个例子，其功能是将一个输入的数组的各项数值相加求和。

public class ArraySum
{
        public static int f (int[] a)
        {
                int sum=0;
                for (int i=0; i<a.length; i++)
                        sum=sum+a[i];
                return sum;
        }
}

  public static int f(int[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: iconst_0
         1: istore_1
         2: iconst_0
         3: istore_2
         4: iload_2
         5: aload_0
         6: arraylength
         7: if_icmpge      22
        10: iload_1
        11: aload_0
        12: iload_2
        13: iaload
        14: iadd
        15: istore_1
        16: iinc           2, 1
        19: goto           4
        22: iload_1
        23: ireturn

在这个成员函数的存储空间里，外来数组的引用指针存储于LVA的0号存储槽里，而局部变量sum则存储在LVA的1号存储槽里。

54.13.3　输入变量为数组的主函数main()

下面展示的是一个单参数的main()函数。这个外来参数是一个字符串。

public class UseArgument
{
        public static void main(String[] args)
        {
                System.out.print("Hi, ");
                System.out.print(args[1]);
                System.out.println(". How are you?");
        }
}

第0个参数是程序名（就像在C/C++等中的一样），因此程序员指定的第一个参数存放于第一个参数槽。

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc            #3        // String Hi,
         5: invokevirtual  #4        // Method java/io/PrintStream.print:(Ljava/lang/String;)V
         8: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: iconst_1
        13: aaload
        14: invokevirtual  #4        // Method java/io/PrintStream.print:(Ljava/lang/String;)V
        17: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
        20: ldc            #5        // String . How are you?
        22: invokevirtual  #6        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        25: return

偏移量为11的指令aload_0加载了LVA（局部变量数组）的第0个存储单元。而函数main()的唯一一个指定参数则通过偏移量为12和13的iconst_1和aaload指令的作用是获取数组的第一个元素的引用指针（也就是索引号为0的元素首地址）。偏移量为14的指令通过TOS向被调用方函数传递字符串的引用指针，这个引用指针就是此后println方法的输入变量。

54.13.4　预设初始值的数组

class Month
{
        public static String[] months =
        {
                "January",
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December"
        };

        public String get_month (int i)
        {
                return months[i];
        };
}

我们在这里列举一个有预设值的数组，它的元素是字符串型的，其值是从一月到十二月的英文单词，分别是January、February、March、April、May、June、July、August、September、October、November和December。

函数get_month的功能比较简单，它是输入一个整型的数，从而能在数组的对应位置输出相应月份的字符串。

  public java.lang.String get_month(int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: getstatic      #2            // Field months:[Ljava/lang/String;
         3: iload_1
         4: aaload
         5: areturn

aaload指令从栈里POP出数组的引用指针和元素的索引编号，并将指定元素推送入栈。在Java的概念里，字符串是对象型数据。在操作对象型数据（确切的说是引用指针）时应当使用带有a前缀的指令。同理，后面的areturn指令从侧面印证了返回指是字符串对象的引用指针。

另外一个问题是：数组months[]是如何被初始化的呢？也就是说，这个数组的初始值1到12月份的英文字符串是如何被相应地赋值到数组的相关位置的？

  static {};
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: bipush         12
         2: anewarray      #3            // class java/lang/String
         5: dup
         6: iconst_0
         7: ldc            #4            // String January
         9: aastore
        10: dup
        11: iconst_1
        12: ldc            #5            // String February
        14: aastore
        15: dup
        16: iconst_2
        17: ldc            #6            // String March
        19: aastore
        20: dup
        21: iconst_3
        22: ldc            #7            // String April
        24: aastore
        25: dup
        26: iconst_4
        27: ldc            #8            // String May
        29: aastore
        30: dup
        31: iconst_5
        32: ldc            #9            // String June
        34: aastore
        35: dup
        36: bipush         6
        38: ldc            #10           // String July
        40: aastore
        41: dup
        42: bipush         7
        44: ldc            #11           // String August
        46: aastore
        47: dup
        48: bipush         8
        50: ldc            #12           // String September
        52: aastore
        53: dup
        54: bipush         9
        56: ldc            #13           // String October
        58: aastore
        59: dup
        60: bipush         10
        62: ldc            #14           // String November
        64: aastore
        65: dup
        66: bipush         11
        68: ldc            #15           // String December
        70: aastore
        71: putstatic      #2            // Field months:[Ljava/lang/String;
        74: return

指令anewarray负责创建一个指定大小的数组，并将对象的引用指针推送入栈（字母a表示返回值为引用指针）。anewarry指令的操作数声明了目标数组的数据类型，在上面的指令里这个操作数是java/lang/String。在此之前的bipush 12则设置了数组的大小（这个值会被anewarrary指令pop出栈），而这个大小正好是一年的月份总数。后面出现的dup指令是在栈计算机领域非常著名的栈顶复制指令（在Forth等基于堆栈的编程语言里都有这条指令）。它将复制数组的引用指针。这是因为aastore指令会从栈顶pop出引用指针，而后面的aastore指令还需要再次读取该引用指针。显而易见的是，Java编译器认为在存储数组元素时分配dup指令比分配getstatic指令更为稳妥，否则它也不会一口气派发了12个dup指令。

aastore指令从TOS里依次提取（即POP）元素值、数组下标和数组的引用指针，并将指定值存储到指定的数组元素里。

最后的putstatic指令将栈顶的数据出栈并把它存储到常量解析池的#2号位置。因此它把新建数组的引用指针保存到了整个实例的第二个字段，也就是给months字段赋值。

54.13.5　可变参数函数

可变参数函数利用了数组的数据结构。

        public static void f(int... values)
        {
                for (int i=0; i<values.length; i++)
                        System.out.println(values[i]);
        }

        public static void main(String[] args)
        {
                f (1,2,3,4,5);
        }
  public static void f(int...);
    flags: ACC_PUBLIC, ACC_STATIC, ACC_VARARGS
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_0
         1: istore_1
         2: iload_1
         3: aload_0
         4: arraylength
         5: if_icmpge      23
         8: getstatic      #2            // Field java/lang/System.out:Ljava/io/PrintStream;
        11: aload_0
        12: iload_1
        13: iaload
        14: invokevirtual  #3            // Method java/io/PrintStream.println:(I)V
        17: iinc           1, 1
        20: goto           2
        23: return

在f()函数中，偏移量为3的aload_0指令提取了整数数组的指针。此后的指令依次提取数组大小等信息。

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=1, args_size=1
         0: iconst_5
         1: newarray       int
         3: dup
         4: iconst_0
         5: iconst_1
         6: iastore
         7: dup
         8: iconst_1
         9: iconst_2
        10: iastore
        11: dup
        12: iconst_2
        13: iconst_3
        14: iastore
        15: dup
        16: iconst_3
        17: iconst_4
        18: iastore
        19: dup
        20: iconst_4
        21: iconst_5
        22: iastore
        23: invokestatic   #4            // Method f:([I]V
        26: return

main()函数通过newarray指令构造了一个数组，接着填充这个数组，随后调用了f()函数。

虽然newarray属于某种构造函数，但是在main()结束之后整个数组没有被析构函数释放。实际上Java没有析构函数。JVM具有自动的垃圾回收机制。

另外，当函数main()退出后，数组对象的值其实是未消失的。其实在JAVA环境中就没有清除这个指令，原因是JAVA的内存机制会自动清理不用内存的功能，当然是在其认为必要时进行。

系统自带的format()方法又是如何处理可变参数的呢？它把输入参数分为了字符串对象和数组型对象两大部分：

        public PrintStream format(String format, Object... args)

参考链接：http://docs.oracle.com/javase/tutorial/java/data/numberformat.html

。

我们再来看看下面的例子：

        public static void main(String[] args)
        {
                int i=123;
                double d=123.456;
                System.out.format("int: %d double: %f.%n", i, d);
        }
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=7, locals=4, args_size=1
         0: bipush         123
         2: istore_1
         3: ldc2_w         #2         // double 123.456d
         6: dstore_2
         7: getstatic      #4         // Field java/lang/System.out:Ljava/io/PrintStream;
        10: ldc            #5         // String int: %d double: %f.%n
        12: iconst_2
        13: anewarray      #6         // class java/lang/Object
        16: dup
        17: iconst_0
        18: iload_1
        19: invokestatic   #7         // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
        22: aastore
        23: dup
        24: iconst_1
        25: dload_2
        26: invokestatic   #8         // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
        29: aastore
        30: invokevirtual  #9         // Method java/io/PrintStream.format:(Ljava/lang/↙
    ↘ String;[Ljava/lang/Object;]Ljava/io/PrintStream;
        33: pop
        34: return

可见，int型数据和double型数据首先经由各自的valueOf方法处理、返回相应的数据值。format()方法的输入值应当为Object型实例。而Integer和Double类是超类Object的子类，所以这种实例可以作为format()函数参数里的数组元素。另外一方面，所有数组都是均质的，也就是说它不能保存不同类型的数据元素，因此int和double类的数据类型不可能是超类数组以外任何类型数组的数据元素。

偏移量为13的指令构造了一个Object型的数组实例，而偏移量为22、29的指令，分别把整型Integer对象，和双精度Double型对象添加到超类对象的数组里。

整个程序的倒数第二行指令pop、即清除了栈顶TOS中的元素数值，因此在执行最后一个指令return时，该方法的数据栈已经被彻底释放（又称作平衡/balanced）。

54.13.6　二维数组

在Java中，二维数组其实就是一个存储着另一维度数组引用指针的一维数组。

        public static void main(String[] args)
        {
                int[][] a = new int[5][10];
                a[1][2]=3;
        }

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_5
         1: bipush          10
         3: multianewarray  #2, 2            // class "[[I"
         7: astore_1
         8: aload_1
         9: iconst_1
        10: aaload
        11: iconst_2
        12: iconst_3
        13: iastore
        14: return

为了展示效果，我们在这里创建一个大小为10×5的整型二维数组，采用的指令是new int[5][10]。

Java采用multinewarray指令构造多维数组。本例先通过iconst_5和bipush指令将各纬度的长度值推送入栈，再使用multinewarray指令声明数据类型（常量解析池#2）和数组维度(2)。

偏移量为9、10的iconst_1和aaload指令用于加载第1行的引用指针。偏移量为11的iconst_2指令则声明了指定列。偏移量为12的指令明确该元素的取值。偏移量为13的iastore指令最终完成元素赋值。

二维数组的读取操作又是如何实现的呢？

        public static int get12 (int[][] in)
        {
                return in[1][2];
        }

  public static int get12(int[][]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_1
         2: aaload
         3: iconst_2
         4: iaload
         5: ireturn

从这个程序我们可以看到：偏移量为2的aaload指令读入了指定行的引用指针，而偏移量为3的iconst_2指令声明了列编号。最终iaload指令读取了指定元素的数值。

54.13.7　三维数组

三维数组可视为存储了二维数组引用指针的一维数组。

        public static void main(String[] args)
        {
                int[][][] a = new int[5][10][15];

                a[1][2][3]=4;

                get_elem(a);
        }
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: iconst_5
         1: bipush          10
         3: bipush          15
         5: multianewarray  #2, 3            // class "[[[I"
         9: astore_1
        10: aload_1
        11: iconst_1
        12: aaload
        13: iconst_2
        14: aaload
        15: iconst_3
        16: iconst_4
        17: iastore
        18: aload_1
        19: invokestatic    #3            // Method get_elem:([[[I]I
        22: pop
        23: return

我们这里举的例子中，三个维度的数值分别是5、10以及15。

它需要使用两次aaload指令才能找到最后一维数组的引用指针。

        public static int get_elem (int[][][] a)
        {
                return a[1][2][3];
        }
  public static int get_elem(int[][][]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_1
         2: aaload
         3: iconst_2
         4: aaload
         5: iconst_3
         6: iaload
         7: ireturn

54.13.8　小结

在Java中，是否可能发生缓冲区溢出的情况？不可能。Java数组的数据实例存储了数组长度的明确信息，数组操作会作边界检查。一旦发生上标/下标溢出问题，运行环境就会进行异常处理。

Java和C/C++的多维数组在底层结构上存在显著的区别，因此JAVA不太适合用进行大规模科学计算。

54.14　字符串

54.14.1　第一个例子

Java的字符串和数组都是同等对象，因此它们的构造过程没有什么区别。

        public static void main(String[] args)
        {
               System.out.println("What is your name?");
               String input = System.console().readLine();
               System.out.println("Hello, "+input);
        }
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc            #3        // String What is your name?
         5: invokevirtual  #4        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: invokestatic   #5        // Method java/lang/System.console:()Ljava/io/Console;
        11: invokevirtual  #6        // Method java/io/Console.readLine:()Ljava/lang/String;
        14: astore_1
        15: getstatic      #2        // Field java/lang/System.out:Ljava/io/PrintStream;
        18: new            #7        // class java/lang/StringBuilder
        21: dup
        22: invokespecial  #8        // Method java/lang/StringBuilder."<init>":()V
        25: ldc            #9        // String Hello,
        27: invokevirtual  #10       // Method java/lang/StringBuilder.append:(Ljava/↙
↘ lang/String;)Ljava/lang/StringBuilder;
        30: aload_1
        31: invokevirtual  #10       // Method java/lang/StringBuilder.append:(Ljava/↙
↘ lang/String;)Ljava/lang/StringBuilder;
        34: invokevirtual  #11       // Method java/lang/StringBuilder.toString:()↙
↘ Ljava/lang/String;
        37: invokevirtual  #4        // Method java/io/PrintStream.println:(Ljava/lang↙
↘ /String;)V
        40: return

我们这里举的例子是交互式的，具体功能是能根据用户输入的用户名，显示一句问候，作为回显结果。

偏移量为11的指令调用了readLine函数。其返回值，即用户输入的字符串的引用指针，最后通过栈顶TOS返回。偏移量为14的指令将字符串的引用指针存储在LVA的第一个存储单元中。偏移量为30的指令再次加载了用户输入字符串的引用指针，在StringBuilder类的实例中与字符串（Hello, ）连接为新的字符串。最后偏移量为37的invokevirtual指令调用了println方法，显示最终的字符串。

54.14.2　第二个例子

另外一个例子是：

public class strings
{
        public static char test (String a)
        {
                return a.charAt(3);
        };

        public static String concat (String a, String b)
        {
                return a+b;
        }
}
  public static char test(java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: iconst_3
         2: invokevirtual #2            // Method java/lang/String.charAt:(I)C
         5: ireturn

编译器会利用StringBuilder类来连接字符串：

  public static java.lang.String concat(java.lang.String, java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=2
         0: new            #3            // class java/lang/StringBuilder
         3: dup
         4: invokespecial  #4            // Method java/lang/StringBuilder."<init>":()V
         7: aload_0
         8: invokevirtual  #5            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        11: aload_1
        12: invokevirtual  #5            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        15: invokevirtual  #6            // Method java/lang/StringBuilder.toString:() ↙
    ↘ Ljava/lang/String;
        18: areturn

我们再看一个将字符串和整型数连接在一起的例子：

        public static void main(String[] args)
        {
                String s="Hello!";
                int n=123;
                System.out.println("s=" + s + " n=" + n);
        }

这里同样调用了StringBuilder类的append方法连接字符串，再通过println函数显示最终的字符串。

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=3, args_size=1
         0: ldc            #2            // String Hello!
         2: astore_1
         3: bipush         123
         5: istore_2
         6: getstatic      #3            // Field java/lang/System.out:Ljava/io/PrintStream;
         9: new            #4            // class java/lang/StringBuilder
        12: dup
        13: invokespecial  #5            // Method java/lang/StringBuilder."<init>":()V
        16: ldc            #6            // String s=
        18: invokevirtual  #7            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        21: aload_1
        22: invokevirtual  #7            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        25: ldc            #8            // String n=
        27: invokevirtual  #7            // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        30: iload_2
        31: invokevirtual  #9            // Method java/lang/StringBuilder.append:(I)Ljava ↙
    ↘ /lang/StringBuilder;
        34: invokevirtual  #10            // Method java/lang/StringBuilder.toString:() ↙
    ↘ Ljava/lang/String;
        37: invokevirtual  #11            // Method java/io/PrintStream.println:(Ljava/lang ↙
    ↘ /String;)V
        40: return

54.15　异常处理

让我们再来回顾一下54.13.4节中已经讲到的例子，那是一个关于月份显示的程序实例。显然如果输入数组的数值小于0或者大于11的话，都会触发异常处理函数。

指令清单54.10　IncorrectMonthException.java（不正确的月份显示例外）

public class IncorrectMonthException extends Exception
{
        private int index;

        public IncorrectMonthException(int index)
        {
                this.index = index;
        }
        public int getIndex()
        {
                return index;
        }
}

指令清单54.11　Month2.java（月份2）

class Month2
{
        public static String[] months =
        {
                "January",
                "February",
                "March",
                "April",
                "May",
                "June",
                "July",
                "August",
                "September",
                "October",
                "November",
                "December"
        };

        public static String get_month (int i) throws IncorrectMonthException
        {
                if (i<0 || i>11)
                        throw new IncorrectMonthException(i);
                return months[i];
        };

        public static void main (String[] args)
        {
                try
                {
                        System.out.println(get_month(100));
                }
                catch(IncorrectMonthException e)
                {
                        System.out.println("incorrect month index: "+ e.getIndex());
                        e.printStackTrace();
                }
        };
}

本质上讲，IncorrectMonthException.class只具备一个对象构造函数和一个访问器。

这个类由Exception继承而来，因此它首先调用Exception类的构造函数，接着声明了自己唯一的输入值字段。

  public IncorrectMonthException(int);
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=2, args_size=2
         0: aload_0
         1: invokespecial  #1            // Method java/lang/Exception."<init>":()V
         4: aload_0
         5: iload_1
         6: putfield       #2            // Field index:I
         9: return

而getIndex()就是一个访问器/accessor。它通过aload_0指令从LVA的第0个存储槽获取IncorrectMonthException的this指针，再通过getfield指令从对象实例里提取整数值。

  public int getIndex();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: getfield         #2            // Field index:I
         4: ireturn

现在让我们来看看Month2.class中的get_month()。

指令清单54.12　Month2.class

  public static java.lang.String get_month(int) throws IncorrectMonthException;
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=1, args_size=1
         0: iload_0
         1: iflt           10
         4: iload_0
         5: bipush         11
         7: if_icmple      19
        10: new            #2            // class IncorrectMonthException
        13: dup
        14: iload_0
        15: invokespecial  #3            // Method IncorrectMonthException."<init>":(I)V
        18: athrow
        19: getstatic      #4            // Field months:[Ljava/lang/String;
        22: iload_0
        23: aaload
        24: areturn

我们来分析分析这个程序：

在偏移为1的iflt指令在栈顶值小于1的情况下触发跳转。“iflt”是英文if less than的缩写。

当index参数是无效值时，程序会跳转到偏移量为10的new指令，创建一个新的对象。而该对象的类型就是指令的操作数（常量解析池#2）IncorrectMonthException。接着偏移量为15的指令调用构造函数，并通过栈顶TOS传递局部变量index。当执行到偏移量为18的指令处时，异常处理实例已经构造完毕，athrow指令将从栈顶提取由上一条指令传递的异常处理方法的引用指针，并通知JVM系统该方法为当前类实例的异常处理函数。

此处的athrow指令并不返回控制流。此后的偏移量为19的指令开始是另外一个基本的模块，它与异常处理过程没有关系，可视为从偏移量为7的指令开始的领悟一个逻辑分支。

例外的句柄是如何工作的？我们来看看类Month2.class中的函数main()。

指令清单54.13　Month2.class

  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=2, args_size=1
         0: getstatic      #5        // Field java/lang/System.out:Ljava/io/PrintStream;
         3: bipush 100
         5: invokestatic   #6        // Method get_month:(I)Ljava/lang/String;
         8: invokevirtual  #7        // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        11: goto 47
        14: astore_1
        15: getstatic      #5        // Field java/lang/System.out:Ljava/io/PrintStream;
        18: new            #8        // class java/lang/StringBuilder
        21: dup
        22: invokespecial  #9        // Method java/lang/StringBuilder."<init>":()V
        25: ldc            #10       // String incorrect month index:
        27: invokevirtual  #11       // Method java/lang/StringBuilder.append:(Ljava/ ↙
    ↘ lang/String;)Ljava/lang/StringBuilder;
        30: aload_1
        31: invokevirtual  #12       // Method IncorrectMonthException.getIndex:()I
        34: invokevirtual  #13       // Method java/lang/StringBuilder.append:(I)Ljava ↙
    ↘ /lang/StringBuilder;
        37: invokevirtual  #14       // Method java/lang/StringBuilder.toString:() ↙
    ↘ Ljava/lang/String;
        40: invokevirtual  #7        // Method java/io/PrintStream.println:(Ljava/lang ↙
    ↘ /String;)V
        43: aload_1
        44: invokevirtual  #15       // Method IncorrectMonthException.printStackTrace ↙
:()V
        47: return
      Exception table:
        from      to target type
            0    11    14   Class IncorrectMonthException

自偏移量为14的指令开始的内容就是异常表Exception table。在程序从偏移量0运行到偏移量11（含）期间，发生的全部异常状况都会交给IncorrectMonthException处理。当输入值为无效值时，程序流向导至偏移量为14的指令。实际上，主程序在偏移量为11的地方就已经结束。正常情况下，程序不会执行到偏移量为14的指令，而且也没有任何条件转移指令或者无条件转移指令会跳转到该处。只有当程序遇到例外情况时，程序才运行到偏移量大于11的指令。异常处理的第一条位于偏移量14。此处的astore_1会把外部传入的、异常处理实例的引用指针存储到LVA的第一个存储槽。在此之后，偏移量为31的指令将会通过这个引用指针调用异常处理实例的getIndex()方法。此时偏移量为30的指令把这个引用指针已经提取出来了。异常表的其他指令都是字符串处理指令：getIndex()方法返回局部变量index的整数值，这个值由toString()方法转换为字符串，再与字符串“incorrect month index:”连接，最终通过println()和printStackTrace()方法显示出来。在调用了printStackTrace()之后，整个异常处理过程宣告完毕，程序恢复正常状态。虽然本例偏移量位47的指令是结束main()函数的return指令，但是此处可以是其他的、在正常状态下需要执行的任何指令。

接下来，我们来看看IDA显示异常处理方法的具体方式。

指令清单54.14　笔者计算机里某个class文件的异常处理方法

      .catch java/io/FileNotFoundException from met001_335 to met001_360\
  using met001_360
      .catch java/io/FileNotFoundException from met001_185 to met001_214\
  using met001_214
      .catch java/io/FileNotFoundException from met001_181 to met001_192\
  using met001_195
      .catch java/io/FileNotFoundException from met001_155 to met001_176\
  using met001_176
      .catch java/io/FileNotFoundException from met001_83 to met001_129 using \
met001_129
      .catch java/io/FileNotFoundException from met001_42 to met001_66 using \
met001_69
      .catch java/io/FileNotFoundException from met001_begin to met001_37\
  using met001_37

54.16　类

一个简单的类如下所示。

指令清单54.15　test.java

public class test
{
        public static int a;
        private static int b;

        public test()
        {
            a=0;
            b=0;
        }
        public static void set_a (int input)
        {
                a=input;
        }
        public static int get_a ()
        {
                return a;
        }
        public static void set_b (int input)
        {
                b=input;
        }
        public static int get_b ()
        {
                return b;
        }
}

构造函数把两个变量设置为0：

  public test();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial  #1            // Method java/lang/Object."<init>":()V
         4: iconst_0
         5: putstatic      #2            // Field a:I
         8: iconst_0
         9: putstatic      #3            // Field b:I
        12: return

设置a：

  public static void set_a(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: putstatic      #2            // Field a:I
         4: return

获取a：

  public static int get_a();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic      #2            // Field a:I
         3: ireturn

设置b：

  public static void set_b(int);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: putstatic        #3            // Field b:I
         4: return

获取b：

  public static int get_b();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic        #3            // Field b:I
         3: ireturn

在底层指令层面上，类中那些具有public和private属性的成员对象没有实质区别。但是.class文件级别，外部指令无法直接访问其他类里的private属性成员。

接下来，我们演示创建对象和调用方法。

指令清单54.16　ex1.java程序

public class ex1
{
        public static void main(String[] args)
        {
                test obj=new test();
                obj.set_a (1234);
                System.out.println(obj.a);
        }
}
  public static void main(java.lang.String[]);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new            #2            // class test
         3: dup
         4: invokespecial  #3            // Method test."<init>":()V
         7: astore_1
         8: aload_1
         9: pop
        10: sipush 1234
        13: invokestatic   #4            // Method test.set_a:(I)V
        16: getstatic      #5            // Field java/lang/System.out:Ljava/io/PrintStream;
        19: aload_1
        20: pop
        21: getstatic      #6            // Field test.a:I
        24: invokevirtual  #7            // Method java/io/PrintStream.println:(I)V
        27: return

new指令可以创建新的对象，但是它并没有调用构造函数（偏移量为4的指令调用了构造函数）。偏移量为13的指令调用了set_a()方法。偏移量为21的getstatic指令访问了类的一个字段。

54.17　简单的补丁

54.17.1　第一个例子

本节通过一个简单的程序演示补丁的实现方法：

public class nag
{
        public static void nag_screen()
        {
                System.out.println("This program is not registered");
        };
        public static void main(String[] args)
        {
                System.out.println("Greetings from the mega-software");
                nag_screen();
        }
}

我们可否去掉字符串“This program is not registered”？

我们使用调试工具IDA加载类文件.class，如图54.1所示。

图54.1　IDA

如图54.2所示，我们试图把该函数的第一个字节改为177，即return的字节码。

图54.2　IDA

但是如此一来程序就崩溃了（运行环境为JRE 1.7）：

Exception in thread "main" java.lang.VerifyError: Expecting a stack map frame
Exception Details:
  Location:
    nag.nag_screen()V @1: nop
  Reason:
    Error exists in the bytecode
  Bytecode:
    0000000: b100 0212 03b6 0004 b1

        at java.lang.Class.getDeclaredMethods0(Native Method)
        at java.lang.Class.privateGetDeclaredMethods(Class.java:2615)
        at java.lang.Class.getMethod0(Class.java:2856)
        at java.lang.Class.getMethod(Class.java:1668)
        at sun.launcher.LauncherHelper.getMainMethod(LauncherHelper.java:494)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:486)

也许，JVM还存在某种与栈有关的检查机制。

好吧，我们采用一个其他的方法来“打补丁”：直接覆盖nag()的调用指令，如图54.3所示。

图54.3　IDA

0就是NOP的字节码。

经过运行检验，这次的“打补丁”是成功的。

54.17.2　第二个例子

下面我们再看看另外一个简单的例子：

public class password
{
        public static void main(String[] args)
        {
                System.out.println("Please enter the password");
                String input = System.console().readLine();
                if (input.equals("secret"))
                        System.out.println("password is correct");
                else
                        System.out.println("password is not correct");
        }
}

其实现的基本思路是按照提示输入密码字符串，当输入的密码字符串为“secret”时，显示字符串密码正确（password is correct）；否则显示字符串密码不正确（password is not correct）。

将该程序调入到调试工具IDA中，如图54.4所示。

图54.4　IDA

关键之处是比较字符串的ifeq指令。这个指令其实是英文if equal（如果相等）的缩写。实际上这个助记符不太贴切，它要是ifz（也就是如果TOS是零）就更加确切了。也就是说，如果栈顶TOS的值是零，它就进行跳转。在这个例子中，只有当输入的密码有误才会触发跳转（布尔“假”/False的对应值是0）。我们的第一个想法就是调整这个指令。在ifeq的字节码里，有两个字节专门封装转移目标地址的偏移量。要想把它强行改为“无条件不转移”，必须把第三个字节的改为3（ifeq占用3个字节，PC的偏移量加3就是执行下一条指令）：

我们把相应指令改为如图54.5所示的样子 。

图54.5　IDA

结果，修改后的程序无法在JRE 1.7环境下正确执行。

Exception in thread "main" java.lang.VerifyError: Expecting a stackmap frame at branch target 24
Exception Details:
  Location:
    password.main([Ljava/lang/String;]V @21: ifeq
  Reason:
    Expected stackmap frame at this location.
  Bytecode:
    0000000: b200 0212 03b6 0004 b800 05b6 0006 4c2b
    0000010: 1207 b600 0899 0003 b200 0212 09b6 0004
    0000020: a700 0bb2 0002 120a b600 04b1
  Stackmap Table:
    append_frame(@35,Object[#20])
    same_frame(@43)

        at java.lang.Class.getDeclaredMethods0(Native Method)
        at java.lang.Class.privateGetDeclaredMethods(Class.java:2615)
        at java.lang.Class.getMethod0(Class.java:2856)
        at java.lang.Class.getMethod(Class.java:1668)
        at sun.launcher.LauncherHelper.getMainMethod(LauncherHelper.java:494)
        at sun.launcher.LauncherHelper.checkAndLoadMain(LauncherHelper.java:486)

但是在JRE 1.6版本下，如此修改的程序的确可以正常运行。

笔者也尝试了“将这个ifeq指令的字节码直接更换成3个空指令NOP”的做法。即使是这样，修改后的程序仍然不能正常运行。大概是JRE 1.7的栈映射核查更为全面吧！

接下来，我们更换一种方法：把ifeq之前的、调用input.equals方法的全部指令全都替换为NOP，把它改为如图54.6所示的样子。

图54.6　IDA

如此修改之后，在执行到ifeq指令的时候栈顶的值永远是1，因此不会满足ifeq的跳转条件。

试验说明，这种方法果然有效。

54.18　总结

和C/C++语言相比，Java语言少了些什么数据类型？

① 结构：采用类。

② 联合：采用类继承。

③ 无符号数据类型：这也直接导致了在JAVA下，实现密码算法比较困难。

④ 函数指针。

[1]　关于常规指针和引用指针的详细区别可以查阅本书的51.3节。

第五部分
在代码中发现重要而有趣的内容

目前的软件往往都很庞大，可以说，极简主义不是现代软件的突出特性。但是这并不是说当下的编程者书写的程序代码行多了，而是因为很多的库都普遍与可执行文件静态地链接在一起了。如果所有的外部库都转移至外部动态链接库DLL的话，那么可能情况就会有所不同（对于C++而言，另外的一个理由是静态模板库STL以及其他的模板库文件）。

因此，确定函数的来源很重要，其或者是来自标准库或者通用库（类似Boost和libpng），或者是与我们要找的代码相关。

为了找到我们要的代码而重新编写代码是有些荒唐。

对于一个反编译的工程师来说，一个主要的工作是快速地发现他要找的代码。

反编译工具IDA能让我们在字符串、字节串以及常数中搜索字符串。该工具甚至能将代码输出到lst或则asm文件中，进而可以采用grep和awk等命令处理字符串。

当想要知道某些代码是做什么用的时候，它可能采用一些开源的库（类似上面提到的libpng），因此当你看到一些变量、常数或者字符串很熟悉时，可以用Google搜索一下。如果发现一个开源工程在使用时，就可以拿来做比较。这样的话，也能解决一部分问题。

例如，如果一个程序采用了XML文件，第一步就需要确定采用了XML的哪个库来做处理过程，因为通常都是采用的标准库或者通用库，而不是开发者自己来自行开发。

再举一个例子，在SAP 6.0的软件中，笔者曾经试图了解网络数据包是如何压缩并解压缩的。因为它是一个很大的软件，一个包括调试信息在内的PDB文件使用起来比较方便。通过它能发现一个函数CsDecomprLZC被调用，而这个函数就是对网络数据包进行解压缩的。因此通过查询Google就能发现这个函数是用在了MaxDB中，而这就是一个SAP的开源项目。

查询的命令是：http://www.google.com/search?q=CsDecomprLZC

。

令人吃惊的是，MaxDB和SAP 6.0 软件在网络数据的压缩和解压缩方面采用了相同的代码。

第55章 编译器产生的文件特征

55.1　Microsoft Visual C++

MSVC的版本和其DLL的对应关系如下所示。

市场发行的版本

内部版本

命令行版本

能导入的DLL的版本

发行日期

6

6.0

12.00

msvcrt.dll, msvcp60.dll

June 1998

.NET (2002)

7.0

13.00

msvcr70.dll, msvcp70.dll

February 13, 2002

.NET 2003

7.1

13.10

msvcr71.dll, msvcp71.dll

April 24, 2003

2005

8.0

14.00

msvcr80.dll, msvcp80.dll

November 7, 2005

2008

9.0

15.00

msvcr90.dll, msvcp90.dll

November 19, 2007

2010

10.0

16.00

msvcr100.dll, msvcp100.dll

April 12, 2010

2012

11.0

17.00

msvcr110.dll, msvcp110.dll

September 12, 2012

2013

12.0

18.00

msvcr120.dll, msvcp120.dll

October 17, 2013

msvcp*.dll含有C++相关函数，因此导入这些DLL文件的可执行程序很可能是C++程序。

55.1.1　命名规则

名字通常都是以“?”开始的。

有关MSVC命名规则的详细介绍，请参考本书51.1.1节。

55.2　GCC编译器

GCC不仅可以编译*NIX平台的应用程序，在Cygwin和MinGW环境下它同样可以编译面向Win32平台的应用程序。

55.2.1　命名规则

命名通常以符号“_Z”开始。

有关GCC命名规则的详细介绍，请参考本书51.1.1节。

55.2.2　Cygwin

GCC在Cygwin环境下编译的应用程序，通常会导入cygwin1.dll文件。

55.2.3　MinGW

GCC在Cygwin环境下编译的应用程序，可能会导入msvcrt.dll文件。

55.3　Intel FORTRAN

由Intel Fortran编译的应用程序，可能会导入以下3个文件：

① Libifcoremd.dll。

② Libifportmd.dll。

③ Libiomp5.dll（支持OpenMP）。

库文件libifcoremd.dll定义了很多以字符串“for_”开头的函数。它就是FORTRAN的代表性前缀。

55.4　Watcom以及OpenWatcom

55.4.1　命名规则

由Watcom编译出来的程序，其符号名称通常以“W”字母开头。

以W?method$_class$n__v为例：大写字母W开头代表它是Watcom编译出来的程序，方法的名称为method，类的名称为class，没有参数且无返回值的void方法会命名为：

W?method$_class$n__v

55.5　Borland编译器

下面所示的是Borland Delphi编译器以及C++ Builder的命名规范：

@TApplication@IdleAction$qv
@TApplication@ProcessMDIAccels$qp6tagMSG
@TModule@$bctr$qpcpvt1
@TModule@$bdtr$qv
@TModule@ValidWindow$qp14TWindowsObject
@TrueColorTo8BitN$qpviiiiiit1iiiiii
@TrueColorTo16BitN$qpviiiiiit1iiiiii
@DIB24BitTo8BitBitmap$qpviiiiiit1iiiii
@TrueBitmap@$bctr$qpcl
@TrueBitmap@$bctr$qpvl
@TrueBitmap@$bctr$qiilll

由Borland编译出来的程序，其符号名称必定以字符“@”开头，后面几个字母分别代表：类名称、方法名称以及方法的参数类型。

这些符号名称可能出现在exe文件的输入表、dll文件的输出表以及调试数据等地方。

VCL的全称是Borland Visual Component Libraries，意思是Borland的可视化组件库。它们保存在bpl文件中，而不是保存在dll文件中。比如说文件vcl50.dll和rtl60.dll。

由Borland编译出来的程序还可能会导入BORLNDMM.DLL文件。

55.5.1　Delphi编程语言

通过观察我们不难看出，几乎所有的Delphi可执行文件在代码段的起始部分都有一个“Boolean”字符串，后面还跟着其他数据类型的类型名称。

这里列出一个非常典型的Delphi程序的代码段片段，它就位于Win32程序的PE头之后。

00000400  04 10 40 00 03 07 42 6f 6f 6c 65 61 6e 01 00 00  |..@...Boolean...|
00000410  00 00 01 00 00 00 00 10 40 00 05 46 61 6c 73 65  |........@..False|
00000420  04 54 72 75 65 8d 40 00 2c 10 40 00 09 08 57 69  |.True.@.,.@...Wi|
00000430  64 65 43 68 61 72 03 00 00 00 00 ff ff 00 00 90  |deChar..........|
00000440  44 10 40 00 02 04 43 68 61 72 01 00 00 00 00 ff  |D.@...Char......|
00000450  00 00 00 90 58 10 40 00 01 08 53 6d 61 6c 6c 69  |....X.@...Smalli|
00000460  6e 74 02 00 80 ff ff ff 7f 00 00 90 70 10 40 00  |nt..........p.@.|
00000470  01 07 49 6e 74 65 67 65 72 04 00 00 00 80 ff ff  |..Integer.......|
00000480  ff 7f 8b c0 88 10 40 00 01 04 42 79 74 65 01 00  |......@...Byte..|
00000490  00 00 00 ff 00 00 00 90 9c 10 40 00 01 04 57 6f  |..........@...Wo|
000004a0  72 64 03 00 00 00 00 ff ff 00 00 90 b0 10 40 00  |rd............@.|
000004b0  01 08 43 61 72 64 69 6e 61 6c 05 00 00 00 00 ff  |..Cardinal......|
000004c0  ff ff ff 90 c8 10 40 00 10 05 49 6e 74 36 34 00  |......@...Int64.|
000004d0  00 00 00 00 00 00 80 ff ff ff ff ff ff ff 7f 90  |................|
000004e0  e4 10 40 00 04 08 45 78 74 65 6e 64 65 64 02 90  |..@...Extended..|
000004f0  f4 10 40 00 04 06 44 6f 75 62 6c 65 01 8d 40 00  |..@...Double..@.|
00000500  04 11 40 00 04 08 43 75 72 72 65 6e 63 79 04 90  |..@...Currency..|
00000510  14 11 40 00 0a 06 73 74 72 69 6e 67 20 11 40 00  |..@...string .@.|
00000520  0b 0a 57 69 64 65 53 74 72 69 6e 67 30 11 40 00  |..WideString0.@.|
00000530  0c 07 56 61 72 69 61 6e 74 8d 40 00 40 11 40 00  |..Variant.@.@.@.|
00000540  0c 0a 4f 6c 65 56 61 72 69 61 6e 74 98 11 40 00  |..OleVariant..@.|
00000550  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000560  00 00 00 00 00 00 00 00 00 00 00 00 98 11 40 00  |..............@.|
00000570  04 00 00 00 00 00 00 00 18 4d 40 00 24 4d 40 00  |.........M@.$M@.|
00000580  28 4d 40 00 2c 4d 40 00 20 4d 40 00 68 4a 40 00  |(M@.,M@. M@.hJ@.|
00000590  84 4a 40 00 c0 4a 40 00 07 54 4f 62 6a 65 63 74  |.J@..J@..TObject|
000005a0  a4 11 40 00 07 07 54 4f 62 6a 65 63 74 98 11 40  |..@...TObject..@|
000005b0  00 00 00 00 00 00 00 06 53 79 73 74 65 6d 00 00  |........System..|
000005c0  c4 11 40 00 0f 0a 49 49 6e 74 65 72 66 61 63 65  |..@...IInterface|
000005d0  00 00 00 00 01 00 00 00 00 00 00 00 00 c0 00 00  |................|
000005e0  00 00 00 00 46 06 53 79 73 74 65 6d 03 00 ff ff  |....F.System....|
000005f0  f4 11 40 00 0f 09 49 44 69 73 70 61 74 63 68 c0  |..@...IDispatch.|
00000600  11 40 00 01 00 04 02 00 00 00 00 00 c0 00 00 00  |.@..............|
00000610  00 00 00 46 06 53 79 73 74 65 6d 04 00 ff ff 90  |...F.System.....|
00000620  cc 83 44 24 04 f8 e9 51 6c 00 00 83 44 24 04 f8  |..D$...Ql...D$..|
00000630  e9 6f 6c 00 00 83 44 24 04 f8 e9 79 6c 00 00 cc  |.ol...D$...yl...|
00000640  cc 21 12 40 00 2b 12 40 00 35 12 40 00 01 00 00  |.!.@.+.@.5.@....|
00000650  00 00 00 00 00 00 00 00 00 c0 00 00 00 00 00 00  |................|
00000660  46 41 12 40 00 08 00 00 00 00 00 00 00 8d 40 00  |FA.@..........@.|
00000670  bc 12 40 00 4d 12 40 00 00 00 00 00 00 00 00 00  |..@.M.@.........|
00000680  00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00  |................|
00000690  bc 12 40 00 0c 00 00 00 4c 11 40 00 18 4d 40 00  |..@.....L.@..M@.|
000006a0  50 7e 40 00 5c 7e 40 00 2c 4d 40 00 20 4d 40 00  |P~@.\~@.,M@. M@.|
000006b0  6c 7e 40 00 84 4a 40 00 c0 4a 40 00 11 54 49 6e  |l~@..J@..J@..TIn|
000006c0  74 65 72 66 61 63 65 64 4f 62 6a 65 63 74 8b c0  |terfacedObject..|
000006d0  d4 12 40 00 07 11 54 49 6e 74 65 72 66 61 63 65  |..@...TInterface|
000006e0  64 4f 62 6a 65 63 74 bc 12 40 00 a0 11 40 00 00  |dObject..@...@..|
000006f0  00 06 53 79 73 74 65 6d 00 00 8b c0 00 13 40 00  |..System......@.|
00000700  11 0b 54 42 6f 75 6e 64 41 72 72 61 79 04 00 00  |..TBoundArray...|
00000710  00 00 00 00 00 03 00 00 00 6c 10 40 00 06 53 79  |.........l.@..Sy|
00000720  73 74 65 6d 28 13 40 00 04 09 54 44 61 74 65 54  |stem(.@...TDateT|
00000730  69 6d 65 01 ff 25 48 e0 c4 00 8b c0 ff 25 44 e0  |ime..%H......%D.|

数据段（DATA）的头四个字节通常是以下三个组合中的一个任意一个：00 00 00 00、32 13 8B C0或者FF FF FF FF。在处理被压缩或者被加密的Dephi可执行文件时，这组常数就会具有指标性意义。

55.6　其他的已知DLL文件

Vcomp*.dll。微软用来实现OpenMP的文件。

第56章 Win32环境下与外部通信

在了解函数的输入和输出的情况下，我们基本可以判断出函数的具体功能。这种分析方法能够显著地节省分析时间。

如需关注文件和注册表层面的行为，使用SysInternals的Process Monitor即可，它可以给我们提供关于以上这两者的基本信息。

如需查看网络层面的通信数据，完全可以使用Wireshark这类软件。

然而要进一步分析行为级数据，就得深入程序内部挖掘指令层面的信息。

首先就要调查该程序调用的操作系统API和标准库函数。

如果目标程序由可执行文件和多个DLL文件构成，那么由这些DLL文件所提供的、可调用的函数名称就很有指标性意义。

如果我们只关心那些调用MessageBox()、显示特定文字的指令，我们可以在程序的数据段检索文本字符，找到引用这个字符串的指令，再顺藤摸瓜地找到那些调用既定MessageBox()函数的代码。

在分析电脑游戏时，如果可以确定特定关卡里出现的敌人总数是随机数，那么我们可以在代码中查找rand()函数或者类似的随机数生成函数（例如梅森旋转算法），继而找到这些函数的调用指令，最终调整程序里使用随机数的那些指令。本书75章演示了这种分析实例。

那些电脑游戏以外的、仍然调用rand()函数的程序就更值得关注了。令人感到吃惊的是，某些著名软件采用的数据压缩算法（加密机制）都调用了rand()函数。有兴趣的读者可参阅https://yurichev.com/blog/44/

。

56.1　在Windows API中最经常使用的函数

这里列出了一些最常使用的API函数。需要特别说明的是，这些函数可能不是由程序源代码直接调用的。在程序调用库函数或者调用CRT的时候，下述函数可能会被后者间接调用。

注册表的操作可以通过库文件advspi32.dll的如下功能实现：RegEnumKeyEx、RegEnumValue、RegGetValue、RegOpenKeyEx和RegQueryValueEx。

对类似ini的文本文件可以通过库文件user32.dll的如下函数实现：GetPrivateProfileString。

对话窗的操作通过库文件user32.dll的如下函数实现：MessageBoxEx、SetDlgItemText及GetDlgItemText。

对资源的操作（可以参考本书的68.2.8节）通过库文件user32.dll的函数LoadMenu实现。

对TCP/IP网络的操作是通过库文件ws2_32.dll的如下函数实现：WSARecv和WSASend。

对文件的操作是通过库文件kernel32.dll的如下函数实现相应的操作：CreateFile、ReadFile、ReadFileEx、WriteFile及WriteFileEx等。

访问Internet是通过库文件wininet.dll的WinHttpOpen等函数来实现相关功能的。

检查一个可执行文件是否含有数字签名则是通过库文件wintrust.dll的函数WinVerifyTrust等来实现的。

如果是动态链接的话，标准的MSVC库文件msvcr*.dll是通过以下函数实现相关操作的：assert、itoa、ltoa、open、printf、read、strcmp、atol、atoi、fopen、fread、fwrite、memcmp、rand、strlen、strstr以及strchr。

56.2　tracer:解析指定模块的所有函数

在调试程序时，tracer会给目标程序设置很多INT3断点。虽然这种类型的断点只能运行一次，但是它同样可以用截获特定DLL文件的所有函数。

一个典型的使用例子为：

--one-time-INT3-bp:somedll.dll!.*

如果要在调用所有以xml 开头的函数之前设置INT 3断点，那么可以采用命令：

--one-time-INT3-bp:somedll.dll!xml.*

稍显遗憾的是，这些断点只能触发一次。

如果程序执行到函数断点且成功发生中断，tracer就会显示该函数的调用信息，只是它只能显示一次。另外一个美中不足的地方是，tracer不能查看被调用方函数获取的外来参数。

无论如何，tracer的这项功能还是非常有用的。一个DLL文件通常会定义大量的函数。当我们知道既定程序调用了某个的DLL文件、想要确切知道它调用了DLL里的哪些函数的时候，我们就特别需要这样的一款工具。

举例来讲，我们可以使用tracer给cygwin的程序uptime.exe设置断点，看它调用了哪些系统函数：

tracer -l:uptime.exe --one-time-INT3-bp:cygwin1.dll!.*

这样一来，我们就可以看到它调用了cygwin1.dll的哪些库函数（虽然只会显示一次）以及调用指令的地偏移量信息：

One-time INT3 breakpoint: cygwin1.dll!__main (called from uptime.exe!OEP+0x6d (0x40106d))
One-time INT3 breakpoint: cygwin1.dll!_geteuid32 (called from uptime.exe!OEP+0xba3 (0x401ba3))
One-time INT3 breakpoint: cygwin1.dll!_getuid32 (called from uptime.exe!OEP+0xbaa (0x401baa))
One-time INT3 breakpoint: cygwin1.dll!_getegid32 (called from uptime.exe!OEP+0xcb7 (0x401cb7))
One-time INT3 breakpoint: cygwin1.dll!_getgid32 (called from uptime.exe!OEP+0xcbe (0x401cbe))
One-time INT3 breakpoint: cygwin1.dll!sysconf (called from uptime.exe!OEP+0x735 (0x401735))
One-time INT3 breakpoint: cygwin1.dll!setlocale (called from uptime.exe!OEP+0x7b2 (0x4017b2))
One-time INT3 breakpoint: cygwin1.dll!_open64 (called from uptime.exe!OEP+0x994 (0x401994))
One-time INT3 breakpoint: cygwin1.dll!_lseek64 (called from uptime.exe!OEP+0x7ea (0x4017ea))
One-time INT3 breakpoint: cygwin1.dll!read (called from uptime.exe!OEP+0x809 (0x401809))
One-time INT3 breakpoint: cygwin1.dll!sscanf (called from uptime.exe!OEP+0x839 (0x401839))
One-time INT3 breakpoint: cygwin1.dll!uname (called from uptime.exe!OEP+0x139 (0x401139))
One-time INT3 breakpoint: cygwin1.dll!time (called from uptime.exe!OEP+0x22e (0x40122e))
One-time INT3 breakpoint: cygwin1.dll!localtime (called from uptime.exe!OEP+0x236 (0x401236))
One-time INT3 breakpoint: cygwin1.dll!sprintf (called from uptime.exe!OEP+0x25a (0x40125a))
One-time INT3 breakpoint: cygwin1.dll!setutent (called from uptime.exe!OEP+0x3b1 (0x4013b1))
One-time INT3 breakpoint: cygwin1.dll!getutent (called from uptime.exe!OEP+0x3c5 (0x4013c5))
One-time INT3 breakpoint: cygwin1.dll!endutent (called from uptime.exe!OEP+0x3e6 (0x4013e6))
One-time INT3 breakpoint: cygwin1.dll!puts (called from uptime.exe!OEP+0x4c3 (0x4014c3))

第57章 字符串

57.1　字符串

57.1.1　C/C++中的字符串

在C/C++中，常规字符串都是以0字节结尾的ASCII字符串，因此又称ASCIIZ字符串。

这是历史上硬件局限性决定的。在参考书目【Rit79】中，我们可以看到以下说明：

I/O操作的最小单位是word而不是byte。毕竟PDP-7是一种以字为单位寻址的设备。字和字节的差异性在这方面的唯一影响就是：处理字符串的程序必须要忽略字符串中的Null字符，因为在构造字符串的时候必须使用null字节将字符串凑成偶数个字节、形成word字。

在Hiew或者FAR Manager中，下述程序的字符串在可执行文件中会如图57.1所示：

int main()
{
        printf ("Hello, world!\n");
};

图57.1　Hiew

57.1.2　Borland Delphi

如指令清单57.1所示，在Pascal及Borland Delphi编译的可执行程序中，字符串之前都会有一个声明字符串长度的8位/32位数据。

指令清单57.1　Delphi

CODE:00518AC8                 dd 19h
CODE:00518ACC aLoading___Plea db 'Loading... , please wait.',0

...

CODE:00518AFC                 dd 10h
CODE:00518B00 aPreparingRun__ db 'Preparing run...',0

57.1.3　Unicode编码

多数人认为，所谓Unicode编码就是用两个字节/16位数据来编码一个字符的字符封装格式。实际上这是一种常见的术语理解错误。Unicode实际上是一个标准，它规定的只是将一个数字写成字符的方法，但是没有定义具体的编码方式。

而目前比较流行的编码方式有UTF-8和UTF-16LE。前者广泛被利用在互联网和*NIX系统中，而后者主要使用在Windows环境下。

UTF-8

UTF-8是目前使用最广泛也最成功的字符编码方法之一。所有的拉丁字符都像ASCII码一样进行编码，ASCII码表以外的字符则采用多字节来编码。因为0的作用不变，所以所有的标准 C字符串函数都能正确处理包括UTF-8编码在内的所有字符串。

下面我们通过一个对照表来看看不同语言下的UTF-8的对比显示情况，采用的工具是FAR，测试样例出自己的http://go.ywrichev.com/17304。如图57.2所示。

图57.2　FAR UTF-8

从以上的对照，我们可以清楚地看到，只有英文的字符串看起来和ASCII表中的完全一样。匈牙利语言使用一些拉丁字符以及音节分隔标记来表示。这些符号使用多个字节来编码，我们这里采用了红色的下画线表示。从这个表，我们还可以看到爱尔兰语和波兰语也采用了同样的办法。而这个字符串对比的开始处，我们采用了一个欧元符号，它是用三个字节表示的。其余的系统与拉丁文没有关系。至少在俄语、阿拉伯语、希伯来语以及北印度语中，我们会发现其中一个字节是反复出现的，这也不奇怪：一个语言系统中的字符往往是在Unicode表中的相同位置处，因此它们的代码总是以系统的数字打头。

在最开始，也就是在第一个可见字符串“How much?”之前，我们会看到还有三个字节，实际上它们是字节顺序标记（Byte order mark，BOM）。BOM声明了字符串的编码系统。

UTF-16LE

很多Windows系统下的win32函数有-A和-W后缀。前面这种函数用于处理常规字符串，而后面这种带有-w的函数则是UTF-16LE字符串的专用函数（w代表wide）。

在UTF-16字符串的拉丁符号中，我们用工具Hiew或者FAR可以看到，这些字符都被字节0间隔开了，如图57.3所示。

图57.3　Hiew

程序如下所示。

int wmain()
{
        wprintf (L"Hello, world!\n");
};

而在Windows NT系统中，我们可以经常看到的显示如图57.4所示。

图57.4　Hiew

在IDA的提示信息中，严格采用双字节对单字符编码的编码方式称为Unicode。

例如：

.data:0040E000 aHelloWorld:
.data:0040E000                  unicode 0, <Hello, world!>
.data:0040E000                  dw 0Ah, 0

而图57.5所示的则是俄语的字符串，它采用的是UTF-16LE编码方式。

图57.5　Hiew工具，编码方式UTF-16LE

我们比较容易分辨的是这些字符被星型的字符分割，而这个星型字符的ASCII值是4。实际上，西里尔字母位于Unicode表的第4映射区，因此所有的西里尔字母在UTF-16LE中的编码范围是0x400～0x4ff。详情请参考https://en.wikipedia.org/wiki/Cyrillic_(Unicode_block)

再回过头来看看我们上面列出的一个显示多语言字符串的例子。图57.6所示的是其在 UTF-16LE编码方式下的样子。

图57.6　采用工具软件FAR，编码格式为UTF-16LE

从以上图中我们可以看到，字节分割符BOM位于文件的开头，而所有的拉丁字母都用字节零来分割。一些带读音分割标志的字符（主要是匈牙利语和爱尔兰语）也采用红色的下划线标出来了。

57.1.4　Base64

Base64编码十分流行，是把二进制数据转换为文本字符串的常用标准。本质上说，这种算法用4个可显示字符封装3个二进制字节。它的字符集包括 26个拉丁字母（含大小写）、0～9共10个数字、加号“+”及反斜杠“/”，总共64个字符。

Base64编码的一个显著特征是它通常（但不一定）以1到2个等号“=”为结尾。

比如说以下两个Base64编码：

AVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuWs=
WVjbbVSVfcUMu1xvjaMgjNtueRwBbxnyJw8dpGnLW8ZW8aKG3v4Y0icuQT+qEJAp9lAOuQ==

可以肯定的是，等号“=”绝不会出现在Base64编码字符串的中间。

57.2　错误/调试信息

对于逆向分析来说，程序中的调试信息都很重要。在某种程度上，调试信息能报告程序正在运行的状态。调试信息通常会由printf()一类的函数显示出来，或者被输出到日志文件中。但是在release/发行版、而非debug/测试版的软件中，即使有关指令调用了相关调试函数、也不会有任何实质性的输出内容。如果调试信息的转储数据中含有局部变量或者全局变量的信息，那么逆向工程人员就算赚到了—我们至少知道了变量的名称。比如说，我们可以通过转储信息确定Oracle RDBMS有一个函数叫做ksdwrt()。

有实际意义的字符串通常是逆向分析的重点。IDA反编译器可以显示出字符串的调用方函数和调用指令。熟练掌握这种分析之后，您可能会找到一些有趣的东西（可以参考https://yurichev.com/ blog/32/

）。

错误信息有时也很重要。Oracle RDBMS 构造了一系列函数专门处理错误信息。有兴趣的读者可以访问https://yurichev.com/blog/43/

了解详细信息。

多数情况下，我们能够迅速判断出汇报错误的函数以及引发它们报错的具体条件。有意思的是，正因如此，一些注重版权保护的程序会刻意在程序出错的时候临时调整错误信息或错误代码。毕竟，开发人员不会希望别人马上就能摸清他的防盗版措施。

本书的78.2节就演示了一个对错误信息加密的程序。

57.3　可疑的魔数字符串

一些经常被用在后门程序中的魔数字符串看起来就很可疑。比如说，我们注意到一个关于TP-Link WR740家用路由器存在后门的报道（参见http://sekurak.pl/tp-link-httptftp-backdoor/

）。只有当他人访问下述URL时，才会触发这个后门：

http://192.168.0.1/userRpmNatDebugRpm26525557/start_art.html。

事实上，字符串userRpmNatDebugRpm26525557必定存在于固件中的某个文件。然而在这个后门东窗事发之前，Google搜索不到任何信息，当然这个后门被曝光后的情况完全相反。像这种后门类的字符串，查遍RFC资料你也找不到它。再怎么调整字节序，它也不会和科学算法沾边。它也绝不是错误信息或者调试信息。因此，尽快地定位类似这个的可疑字符串是一个好主意。

字符串通常采用Base64编码。因此把文件中的字符串全都进行解码处理，再扫一眼就知道哪个文件含有这个字符串了。

更准确来讲，这种隐藏后门的办法被称为“不公开即安全（security through obscurity）”，也就是见光死。

第58章 调用宏assert()（中文称为断言）

一般来讲，assert()宏在可执行文件中保留了源代码的文件名、行数以及执行条件。

最有价值的信息是assert()宏的执行条件。我们可以通过它们来推断出变量名或者结构体的字段名称。另外一个有用的信息是文件名，通过它我们可以推断出源代码是采用什么语言编写的。同时我们还可能通过文件名来识别出其是否采用了知名的开放源代码库。

指令清单58.1　调用assert()宏的例子

.text:107D4B29   mov    dx, [ecx+42h]
.text:107D4B2D   cmp    edx, 1
.text:107D4B30   jz     short loc_107D4B4A
.text:107D4B32   push   1ECh
.text:107D4B37   push   offset aWrite_c ; "write.c"
.text:107D4B3C   push   offset aTdTd_planarcon ; "td->td_planarconfig == PLANARCONFIG_CON"...
.text:107D4B41   call   ds:_assert

...

.text:107D52CA   mov    edx, [ebp-4]
.text:107D52CD   and    edx, 3
.text:107D52D0   test   edx, edx
.text:107D52D2   jz     short loc_107D52E9
.text:107D52D4   push   58h
.text:107D52D6   push   offset aDumpmode_c ; "dumpmode.c"
.text:107D52DB   push   offset aN30    ; "(n & 3) == 0"
.text:107D52E0   call   ds:_assert

...

.text:107D6759   mov    cx, [eax+6]
.text:107D675D   cmp    ecx, 0Ch
.text:107D6760   jle    short loc_107D677A
.text:107D6762   push   2D8h
.text:107D6767   push   offset aLzw_c    ; "lzw.c"
.text:107D676C   push   offset aSpLzw_nbitsBit ; "sp->lzw_nbits <= BITS_MAX"
.text:107D6771   call   ds:_assert

最简单的分析方法就是用google来搜索条件和文件名。搜索结果显示这与开源的库文件有关。比如说，搜索字符串“sp->lzw_nbits <= BITS_MAX”，我们就它与压缩算法LZW的开源代码相关。

第59章 常数

人类在现实生活中喜欢使用整数。编程人员也是人，他们同样喜欢用10、100、1000这样的整数。

熟悉的反编译工程师都会明白，与之对应的十六进制数的关系是：10=0xA;100=0x64;1000=0x3E8而10000=0x2710。然而，在二进制层面这些数字都不算太“整”。

从二进制层面来看，更为常用的整数则是0xAAAAAAAA（1010101010101010）和0x55555555 （0101010101010101）这类特征明显的常量。以常数0x55AA为例：引导扇区、主引导扇区MBR以及IBM兼容扩展卡的ROM（只读存储单元）等关键数据都会使用这个常量。

一些算法，特别是某些加密算法，常常使用某些特殊的常数，如果我们使用调试工具IDA就能很容易发现这一点。

以MD5算法为例，其内部变量初始值分别是：

var int h0 := 0x67452301
var int h1 := 0xEFCDAB89
var int h2 := 0x98BADCFE
var int h3 := 0x10325476

也就是说，如果某段代码连续出现了上述四个常量，那么这段代码很可能就与MD5的算法相关。

而另外的例子则是CRC16/32的算法，其预置的常数表经常如下所示。

指令清单59.1　Linux/Lib/crc16.c

/** CRC table for the CRC-16. The poly is 0x8005 (x^16 + x^15 + x^2 + 1) */
u16 const crc16_table[256] = {
        0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
        0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
        0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
        …

如需详细了解 CRC32算法，请参考本书的第37章。

59.1　魔数

很多文件在文件头使用特定的魔数来表示其文件格式，这些魔数可以是一个字节或者多个字节的组合（https://en.wikipedia.org/wiki/Magic_number_(programming)

）。

比如，我们熟知的，所有的Win32以及MS-DOS格式的可执行文件的开始处总是“MZ”这两个字符。

而在标准的MIDI文件则必须以“MThd”这4个字符开头。因此，那些需要使用MIDI文件的程序，基本上都会检测目标文件的头4个字符是不是“MThd”。如果用程序来表示，则可能是下面这个样子的：（注意：buf是内存缓冲区的起始地址）

cmp [buf], 0x6468544D ; "MThd"
jnz _error_not_a_MIDI_file

当然，数据比较函数同样可以用来验证文件头中的魔数。常用的函数有：比较内存块的memcmp()函数，或者CMPSB一类的比较指令（可以参考本书附录A.6.3）。

一旦发现某个程序开始检测其他文件的魔数标识，我们就可以确信它已经加载了某种类型的目标文件。不止如此，我们还可以解读出文件操作的缓冲区以及它使用缓冲区的方式方法等信息。

59.1.1　动态主机配置协议（Dynamic Host Configuration Protocol，DHCP）

网络协议同样使用了魔术。比如，DHCP协议的网络数据就会用到魔数0x63538263—这个魔数叫做magic cookie。所有符合DHCP协议的数据包都必须使用这个魔数。如果我们找到了这个魔数，我们就可能确定此处代码可能用于实现DHCP协议。不仅如此，能接受DHCP包的程序都必须验证这个魔数，也就是与它做比对。

比如说，我们以Windows 7操作系统（64位操作系统）中的文件dhcpcore.dll文件为例，我们可以在这个文件中搜索这个魔数。结果发现了2次，出现在两个函数中，其名称分别是DhcpExtractOptionsForValidation()和and DhcpExtractFullOptions()。

指令清单59.2　dhcpcore.dll (Windows 7 x64)

.rdata:000007FF6483CBE8 dword_7FF6483CBE8 dd 63538263h           ; DATA XREF:↙
    ↘ DhcpExtractOptionsForValidation+79
.rdata:000007FF6483CBEC dword_7FF6483CBEC dd 63538263h           ; DATA XREF:↙
    ↘ DhcpExtractFullOptions+97

以下列出在该程序文件中是如何使用这个魔数的：

指令清单59.3　dhcpcore.dll (Windows 7 x64)

.text:000007FF6480875F  mov     eax, [rsi]
.text:000007FF64808761  cmp     eax, cs:dword_7FF6483CBE8
.text:000007FF64808767  jnz     loc_7FF64817179

指令清单59.4　dhcpcore.dll (Windows 7 x64)

.text:000007FF648082C7  mov     eax, [r12]
.text:000007FF648082CB  cmp     eax, cs:dword_7FF6483CBEC
.text:000007FF648082D1  jnz     loc_7FF648173AF

59.2　寻找常数

在单个文件里搜索常数时，可以使用IDA的搜索功能。其快捷键是ALT-B或ALT-I。

在海量文件检索常量时，可以使用笔者开发的小工具—binary grep (https://github.com/yurichev/ bgrep

)。它同样可以检索非可执行文件中的特定信息。

第60章 检索关键指令

如果一个程序使用了为数不多的FPU（Float Point Unit，浮点运算单元）指令，那么我们可以采用人工排查的方式把它们逐一筛选出来。

以Microsoft的表格工具软件Excel为例。我们可能要研究它对录入公式的处理方法，例如除法操作。

首先要用IDA加载Office 2010里的excel.exe（本章以版本号为14.0.4756.1000的excel为例），然后生成完整的指令清单并将其保存为后缀名为.lst的文本文件。接下来，我们就可以用终端指令检索指令清单中的全部FDIV指令（Floationg Point Divide，浮点数除法）。严格说来，使用grep不能检索出全部的浮点数除法运算指令。如果除数是常量，那么编译器就不太可能分配FDIV指令。当然这种特例不在我们的研究范围之内。

cat EXCEL.lst | grep fdiv | grep -v dbl_ > EXCEL.fdiv

总共有144个匹配结果。

然后，我们在Excel里输入计算公式“=(1/3)”，并检查每个指令的运行结果。

在调试器（或tracer）里逐一排查除法运算指令以后，我们幸运地发现在第14个FDIV指令就是我们要找的指令：

.text:3011E919 DC 33                                   fdiv qword ptr [ebx]
PID=13944|TID=28744|(0) 0x2f64e919 (Excel.exe!BASE+0x11e919)
EAX=0x02088006 EBX=0x02088018 ECX=0x00000001 EDX=0x00000001
ESI=0x02088000 EDI=0x00544804 EBP=0x0274FA3C ESP=0x0274F9F8
EIP=0x2F64E919
FLAGS=PF IF
FPU ControlWord=IC RC=NEAR PC=64bits PM UM OM ZM DM IM
FPU StatusWord=
FPU ST(0): 1.000000

此时，第一个参数（被除数）被保存在ST(0)中，而除数则保存在[EBX]中。

FDIV后面的FSTP指令，将结果写入内存：

.text:3011E91B DD 1E                                   fstp qword ptr [esi]

在FSTP指令处设置断点，我们能看到下述运算结果：

PID=32852|TID=36488|(0) 0x2f40e91b (Excel.exe!BASE+0x11e91b)
EAX=0x00598006 EBX=0x00598018 ECX=0x00000001 EDX=0x00000001
ESI=0x00598000 EDI=0x00294804 EBP=0x026CF93C ESP=0x026CF8F8
EIP=0x2F40E91B
FLAGS=PF IF
FPU ControlWord=IC RC=NEAR PC=64bits PM UM OM ZM DM IM
FPU StatusWord=C1 P
FPU ST(0): 0.333333

为了验证我们的结果，我们做一个简单而有趣的试验：在内存中对具体的内存单元直接进行修改，以便得到“直接运算可能不能生成的效果”。

比如：

tracer -l:excel.exe bpx=excel.exe!BASE+0x11E91B,set(st0,666)
PID=36540|TID=24056|(0) 0x2f40e91b (Excel.exe!BASE+0x11e91b)
EAX=0x00680006 EBX=0x00680018 ECX=0x00000001 EDX=0x00000001
ESI=0x00680000 EDI=0x00395404 EBP=0x0290FD9C ESP=0x0290FD58
EIP=0x2F40E91B
FLAGS=PF IF
FPU ControlWord=IC RC=NEAR PC=64bits PM UM OM ZM DM IM
FPU StatusWord=C1 P
FPU ST(0): 0.333333
Set ST0 register to 666.000000

这样的话，我们在Excel的相关单元就会发现数字666。从这一点可以看出，我们找到了正确的指令位置。

图60.1　通过修改内存发现效果

如果我们调试的是64位的同版本Excel程序，我们只会找到12条FDIV指令。而我们关注的运算指令是第3个FDIV指令：

tracer.exe -l:excel.exe bpx=excel.exe!BASE+0x1B7FCC,set(st0,666)

大概是在编译64位的Excel程序时，编译器使用SSE指令替代了float和double型数据的除法运算指令。其中，SSE指令集的DIVSD指令就出现了268次。

第61章 可疑的代码模型

61.1　XOR异或指令

像XOR op,op或者XOR EAX,EAX这样的指令通常用来将某个寄存器清零。只有当XOR指令的两个操作数不同的时候，它才进行真正的“异或”运算。这种实际意义上的异或运算，在常规应用程序很少见到，反而在加密算法中比较常见，即使是业余人员编写的程序也是如此。而如果XOR的第二个操作数是一个很大的数，那么这个程序就显得特别可疑。这种情况往往意味着它会进行加密或者解密、校验和等类型的复杂计算。

需要说明的是，18.3 节介绍的编译器采用的“百灵鸟”技术同样会生成大量的XOR指令。不过这些XOR指令和加/解密等科学运算无关。

我们可以利用下述AWK脚本处理IDA生成的指令清单文件（.lst），检索其中的xor指令：

gawk -e '$2=="xor" { tmp=substr($3, 0, length($3)-1); if (tmp!=$4) if($4!="esp") if ($4!="ebp")↙
    ↘ { print $1, $2, tmp, ",", $4 } }' filename.lst

值得注意的是，这种类型的脚本也适用于适配不正确的反汇编代码（可以参考本书第49章）。

61.2　手写汇编代码

当前的编译器都不会分配循环指令LOOP和位移循环指令RCL。从另外一方面来讲，喜欢直接手写汇编语言的编程人员非常熟悉这些指令。因此，如果你看到了这些指令的话，那么这部分代码十有八九由编程人员手工编写而来。本书附录A.6都将这些指令添加了“（M）”标记。

手写的汇编程序很少会具备完整的函数开头和函数结尾。

通常来说，人工手写的程序没有固定的参数传递方法。

举一个例子：Windows 2003操作系统的内核文件ntoskrnl.exe。

MultiplyTest proc near                    ; CODE XREF: Get386Stepping
             xor     cx, cx
loc_620555:                               ; CODE XREF: MultiplyTest+E
             push    cx
             call    Multiply
             pop     cx
             jb      short locret_620563
             loop    loc_620555
             clc
locret_620563:                            ; CODE XREF: MultiplyTest+C
             retn
MultiplyTest endp

Multiply     proc near                    ; CODE XREF: MultiplyTest+5
             mov     ecx, 81h
             mov     eax, 417A000h
             mul     ecx
             cmp     edx, 2
             stc
             jnz short locret_62057F
             cmp eax, 0FE7A000h
             stc
             jnz short locret_62057F
             clc
locret_62057F:                            ; CODE XREF: Multiply+10
                                          ; Multiply+18
             retn
Multiply     endp

实际上，只要查看WRK[1]v1.2的源代码就会发现：这部分指令确实来自于手写的汇编语言源文件WRK-v1.2\base\ntos\ke\i386\cpu.asm。

[1]　WRK是Windows Research Kernel（Windows研究内核）的缩写。

第62章 魔数与程序调试

通常来说，逆向工程的主要目标理解程序处理数据的具体方法。这些数据可能来自于某个文件或者来自于网络通信，不过数据的出处无关紧要。手工跟踪一个值往往是一项非常劳神费力的事情。为了完成这个任务，一个最简单的方法是使用自己的特有魔数，虽然这个办法不是百分之百可靠。

在某种意义上讲，魔数的作用和X光的造影剂十分相似：在病人的血液里注入造影剂之后，医生就可以增强X射线的观察效果、清晰地观察病人身体的内部情况。借助造影剂的作用，医生可以在X光机下清晰地观察肾脏里血液的循环过程，从而更为准确地判断脏器是否存在结石或者肿瘤等问题。

魔数要尽量“打眼”。在观测32位数据时，我们可以将魔数设置为0x0BADF00D（BADFOOD）、或者0x11101979（某人的生日，例如“1979年11月10日”）。我们可以将这样的4字节数值写入到我们要调查的程序之中。

接着，我们可以利用tracer的代码覆盖率模式（code coverage/cc模式）跟踪程序，再利用grep或者直接搜索文本文件（跟踪结果的文本文件），我们就能够很容易地发现这些值的调用点及处理方法。

在代码覆盖率模式（cc模式）下，使用tracer跟踪程序并生成与grep兼容的输出（grepable）文件。其结果大致如下所示：

0x150bf66 (_kziaia+0x14), e=         1 [MOV EBX, [EBP+8]] [EBP+8]=0xf59c934
0x150bf69 (_kziaia+0x17), e=         1 [MOV EDX, [69AEB08h]] [69AEB08h]=0
0x150bf6f (_kziaia+0x1d), e=         1 [FS: MOV EAX, [2Ch]]
0x150bf75 (_kziaia+0x23), e=         1 [MOV ECX, [EAX+EDX*4]] [EAX+EDX*4]=0xf1ac360
0x150bf78 (_kziaia+0x26), e=         1 [MOV [EBP-4], ECX] ECX=0xf1ac360

我们同样可以在网络数据包中构造魔数。使用魔数的关键在于：要使用不会重复的且不会在程序里出现的标志性数据。

除了跟踪器tracer之外，MS-DOS模拟器（DosBox）也可以用来观测魔数。在heavydebug（重度调试）模式下，DosBox可以把执行每条指令时的寄存器状态输出到纯文本文件[1]。因此，我们同样可以利用魔数来调试DOS程序。

[1]　关于MS-DOS模拟器（DosBox）的特性可以查询以下博客：https://yurichev.com/blog/ 55/。

第63章 其他的事情

63.1　总则

逆向工程人员应当尽可能地以编程人员的角度来分析问题。要理解他们的观点、并且时常扪心自问：如果自己是编程人员的话，自己会如何设计程序。

63.2　C++

在分析C++的class时，本书51.1.5节中讲到的RTTI数据可能就是分析的重点。

63.3　部分二进制文件的特征

在十六进制的编辑器里，16位/32位/64位数据数组的特征十分明显。本节以一个很简单的MIPS程序为例。前文介绍过：每个MIPS指令都是整齐的32位（即4字节）指令。当然ARM模式或者ARM64模式的程序也有这个特点。因此，在形式上这些程序的指令都构成了某种32位数组。

图63.1所示的屏幕截图，就表现出这种数组特征。笔者添加了3条红色的垂直线，以突出这种现象：

图63.1　Hiew：一个非常典型的MIPS代码

此外，本书第86章介绍了另外一个典型的例子。

63.4　内存“快照”对比

这种内存“快照”的对比技术可以凸显出内存中变化的数据，过去就常见于8位游戏的作弊领域。

如果您在老式的8位游戏机（这些机器通常内存不大，而游戏本身占用的内存更少）上启动一个游戏，您就可能看到部分游戏数据—例如，现在装有100颗子弹。这时你就可以将所有的内存空间做一个镜像。把镜像存为文件后，您可以先开一枪，这时子弹的剩余数量会显示为99发。这个时候，你再对整个内存空间做另外一个影像。我们对这两个内存影像进行比对，肯定某个字节的数值从100降为99。

考虑到这些8位的游戏程序一般都是汇编语言程序，而且各变量都是全局变量。因此基于以上的对比结果，我们就能分析出是哪个内存地址存放着子弹的数量。有了这个关键的突破点，我们接着就可以在游戏程序的汇编代码（需要进行反汇编处理）中搜索那些指令引用了这个地址，就不难发现对子弹总数进行递减运算的那条指令。然后，我们把相关指令换成NOP，就能保证子弹总数保持100不变。

在8位游戏机上运行的游戏程序，总是会被加载在固定的内存地址。此外，同款游戏的发行版本也比较固定（一旦热销也就不会再进行什么升级了）。因此铁杆玩家们都知道改写哪些地址就能“黑掉”这些游戏了。他们通常会用BASIC语言的单字节赋值指令POKE直接向已知地址写入特定字节。甚至在一些杂志中，常常会出现一些关于8位机的POKE指令列表。

与此类似，修改游戏分数排行榜也比较容易，而且它不仅仅适用于8位游戏。首先，记录一下自己的游戏得分并将存盘文件备份出来。当排行榜的总分出现变化时，再备份一次存盘文件。接下来，我们可以用DOS系统自带的二进制文件比较工具FC直接比对两个文件（存盘文件是二进制文件）。两个文件肯定有几个字节发生了变化，直接修改这些数值就可以调整游戏得分。然而，现在的游戏开发团队通常都非常清楚这些伎俩，因此可能会开发一些程序来应对这些措施。

其他类似的例子可以在本书的第85章中找到。

63.4.1　Windows注册表

在安装一个程序以后，我们就能比对安装前后Windows的注册表。这是一个非常流行的方法，可以用来确认特定程序使用了哪些注册表键值。这也许就是“Windows注册表清理”程序颇为流行的原因。

63.4.2　瞬变比较器Blink-comparator

在介绍了文件比对和内存快照比对的方法之后，笔者不禁想起了瞬变比较器/Blink-comparator （https://en.wikipedia.org/wiki/Blink_comparator

）：过去，它曾是天文学家观测天体移动的一种相片比对设备。它能够快速地切换不同时期拍摄的照片，以便天文学家通过肉眼快速地发现两图之间的区别。

实际上，正是借助于瞬变比较器，人们才在1930年发现了冥王星。

第六部分
操作系统相关

第64章 参数的传递方法（调用规范）

64.1　cdecl [C Declaration的缩写]

这是C/C++语言最常使用的参数传递方法。

调用方函数逆序向被调用方函数传递参数：以“最后（右侧）的参数、倒数第二……至第一个参数”的顺序传递参数。被调用方函数退出后，应由调用方函数调整栈指针ESP、将栈恢复成调用其他函数之前的原始状态。

指令清单64.1　cdecl

push arg3
push arg2
push arg1
call function
add esp, 12 ; returns ESP

64.2　stdcall [Standard Call的缩写]

这种调用方式与上面提到的cdecl规范类似，只是有一点不同：被调用方函数在返回之前会执行“RET x”指令还原参数栈，而不会使用单纯的“RET”指令直接返回。这里的x的数值的计算方式是：x=参数个数*指针的大小（注意：指针的大小在x86构架中的值是4，而在x64中是8）。这样调用方函数本身就不会调整栈指针，因此调用方函数不会因此使用类似于“add esp,x”这样的指令。

指令清单64.2　stdcall

push arg3
push arg2
push arg1
call function

function:
... do something ...
ret 12

此类约定在Win32的标准库文件中十分常见。然而因为Win64系统遵循调用约定的是Win64规范，所以Win64的库文件里不会出现stdcall的标志性操作指令。

以本书8.1节中的函数为例。我们给其中的函数增加__stdcall限定符即可强制它使用stdcall调用约定：

int __stdcall f2 (int a, int b, int c)
{
        return a*b+c;
};

它的编译结果与本书8.2节类似，但是最后的指令从RET变成了RET 12。在遵循stdcall约定之后，栈指针SP不再由调用方函数更新了。

这样一来，我们就可以通过函数尾部的RETN N指令直观地推算出外来参数的个数。计算方法就是：N除以4。

指令清单64.3　MSVC 2010

_a$ = 8                                     ; size = 4
_b$ = 12                                    ; size = 4
_c$ = 16                                    ; size = 4
_f2@12  PROC
        push    ebp
        mov     ebp, esp
        mov     eax, DWORD PTR _a$[ebp]
        imul    eax, DWORD PTR _b$[ebp]
        add     eax, DWORD PTR _c$[ebp]
        pop     ebp
        ret     12                          ; 0000000cH
_f2@12  ENDP

; ...
        push    3
        push    2
        push    1
        call    _f2@12
        push    eax
        push    OFFSET $SG81369
        call    _printf
        add     esp, 8

64.2.1　带有可变参数的函数

在C/C++语言的标准函数中，printf()一类的函数可能是仅存的几个带有可变参数的函数。借助这类函数的帮助，我们能比较容易地观察出cdecl和stdcall调用规范之间的区别。我们首先假定编译器知道printf()函数的参数总数。然而，在Windows环境下，printf()属于预先编译好的库函数，直接由文件MSVCRT.DLL提供。所以，我们无法通过从它的函数代码入手获悉可变参数的处理方式；但是另一方面，我们知道它肯定会处理格式化字符串。如果printf()函数当真采取了stdcall规范、根据格式化字符串统计变参的数量并且在函数尾声恢复栈指针，那么这种局面就十分危险了：万一程序员打错了几个字母，程序就会崩溃。由此可知，对于那些带有可变参数的函数而言，cdecl规范要比stdcall规范更好一些。

64.3　fastcall

这个调用约定优先使用寄存器传递参数，无法通过寄存器传递的参数则通过栈传递给被调用方函数。因为fastcall约定在内存栈方面的访问压力比较小，所以在早期的CPU平台上遵循fastcall规范的程序会比遵循stdcall和cdecl规范的程序性能更高。但是在现在的、更为复杂的CPU平台上，fastcall规范的性能优势就不那么明显了。

这种调用约定没有统一的技术规范，不同的编译器有着各自不同的实现方法。因此在使用这种约定时，我们需要特别小心：如果用两个不同的编译器编译出来了2个相互调用的、遵循fastcall规范的DLL库，那么这种互相调用的访问操作基本都会出现故障。

不管是MSVC还是GCC都使用ECX和EDX传递第一个和第二个参数，用栈传递其余的参数。此外，应由被调用方函数调整栈指针、把参数栈恢复到调用之前的初始状态（这一点与stdcall类似）。

指令清单64.4　fastcall

push arg3
mov edx, arg2
mov ecx, arg1
call function

function:
.. do something ..
ret 4

我们还是以本书8.1节中的例子来说明，把它稍微变化一下，增加一个修饰符号：

int __fastcall f3 (int a, int b, int c)
{
        return a*b+c;
};

编译完成后，我们看到的结果如下所示。

指令清单64.5　MSVC 2010/OB0

_c$ = 8                                     ; size = 4
@f3@12 PROC
; _a$ = ecx
; _b$ = edx
        mov     eax, ecx
        imul    eax, edx
        add     eax, DWORD PTR _c$[esp-4]
        ret     4
@f3@12  ENDP

; ...

        mov     edx, 2
        push    3
        lea     ecx, DWORD PTR [edx-1]
        call    @f3@12
        push    eax
        push    OFFSET $SG81390
        call    _printf
        add     esp, 8

从以上程序我们可以看到，函数调用采用了指令RET N带一个操作数的方式返回SP堆栈指针。由此可以判断，调用方函数通过栈传递了多少个外部参数。

64.3.1　GCC regparm

从某种意义上来讲，GCC regparm由fastcall进化而来。这种规范允许编程人员通过编译选项“-mregparm”设置通过寄存器传递的参数总数（最大值为3）。换句话说，这种规范最多可通过3个寄存器，即EAX、EDX和ECX传递函数参数。

当然，如果“-mregparm”的值小于3，那么就不会全部使用这三个寄存器。

这种约定要求调用方函数在调用过程结束以后调整栈指针，将参数栈恢复到其初始状态。

有关案例请参阅本书的19.1.1节。

64.3.2　Watcom/OpenWatcom

这被称为“寄存器调用规范”。头四个参数由寄存器EAX、EDX、EBX和ECX传递，其余的所有参数都则通过堆栈传递。在使用这种调用约定时，函数必须其函数名前添加标识符“__watcom”，与其他采用不同调用规范的函数区分开来。

64.4　thiscall

这是一种方便C++类成员调用this指针而特别设定的调用规范。

MSVC使用ECX寄存器传递this指针。

而GCC则把this指针作为被调用方函数的第一个参数传递。在汇编层面，这个指针显而易见：所有的类函数都比源代码多出来一个参数。

有关详情，请参阅本书的51.1.1节。

64.5　64位下的x86

64.5.1　Windows x64

64位环境下的参数传递方法在某种程度上与fastcall函数比较类似：头四个参数由寄存器RCX、RDX、R8和R9传递，而其余的参数都通过栈来传递。调用方函数必须预留32个字节或者4个64位的存储空间，以便被调用方函数保存头四个参数。小型函数可以仅凭寄存器就获取所有参数，而大型函数就可能需要保存这些传递参数的寄存器，把它们腾挪出来供后续指令调用。

调用方函数负责调整栈指针到其初始状态。

此外，Windows x86-64系统的DLL文件也采用了这种调用规范。也就是说，虽然Win32系统API遵循的是stdcall规范，但是Win64系统遵循的是Win64规范、不再使用stdcall规范。

以下述程序为例：

#include <stdio.h>

void f1(int a, int b, int c, int d, int e, int f, int g)
{
        printf ("%d %d %d %d %d %d %d\n", a, b, c, d, e, f, g);
};

int main()
{
        f1(1,2,3,4,5,6,7);
};

指令清单64.6　MSVC 2012 /0b

$SG2937 DB      '%d %d %d %d %d %d %d', 0aH, 00H

main    PROC
        sub     rsp, 72                         ; 00000048H

        mov     DWORD PTR [rsp+48], 7
        mov     DWORD PTR [rsp+40], 6
        mov     DWORD PTR [rsp+32], 5
        mov     r9d, 4
        mov     r8d, 3
        mov     edx, 2
        mov     ecx, 1
        call    f1

        xor     eax, eax
        add     rsp, 72                         ; 00000048H
        ret     0
main    ENDP

a$ = 80
b$ = 88
c$ = 96
d$ = 104
e$ = 112
f$ = 120
g$ = 128
f1      PROC
$LN3:
        mov     DWORD PTR [rsp+32], r9d
        mov     DWORD PTR [rsp+24], r8d
        mov     DWORD PTR [rsp+16], edx
        mov     DWORD PTR [rsp+8], ecx
        sub     rsp, 72                         ; 00000048H

        mov     eax, DWORD PTR g$[rsp]
        mov     DWORD PTR [rsp+56], eax
        mov     eax, DWORD PTR f$[rsp]
        mov     DWORD PTR [rsp+48], eax
        mov     eax, DWORD PTR e$[rsp]
        mov     DWORD PTR [rsp+40], eax
        mov     eax, DWORD PTR d$[rsp]
        mov     DWORD PTR [rsp+32], eax
        mov     r9d, DWORD PTR c$[rsp]
        mov     r8d, DWORD PTR b$[rsp]
        mov     edx, DWORD PTR a$[rsp]
        lea     rcx, OFFSET FLAT:$SG2937
        call    printf

        add     rsp, 72                         ; 00000048H
        ret     0
f1       ENDP

从以上的程序，我们可以很清楚地看到7个参数的传递过程。程序通过寄存器传递前4个参数、再通过栈传递了其余3个参数。f1()函数通过序言部分的指令，把传递参数的四个寄存器的外来值存储到“暂存空间/scratch space”里——这正是暂存空间的正确用法。编译器无法实现确定缺少了这4个寄存器之后，后续代码是否还有足够的寄存器可用，所以会把这4个寄存器的数据保管起来，以方便掉配这4个寄存器。Win64调用规范约定：应由调用方函数分配暂存空间给被调用方函数使用。

指令清单64.7　优化的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节介绍了另外一个Win64调用约定的程序。有兴趣的读者可去看一下。

64位下的Windows：在C/C++下传递this指针

C/C++编译器会使用RCX寄存器传递类对象的this指针、用RDX寄存器传递函数所需的第一个参数。关于这个方面的例子，可以查看本书的51.1.1节。

64.5.2　64位下的Linux

64位Linux程序传递参数的方法和64位Windows程序的传递方法几乎相同。区别在于，64位Linux程序使用6个寄存器（RDI、RSI、RDX、RCX、R8、R9）传递前几项参数，而64位Windows则只利用4个寄存器传递参数。另外，64位Linux程序没有上面提到的“零散空间”这种概念。如果被调用方函数的寄存器数量紧张，它就可以用栈存储外来参数，把相关寄存器腾出来使用。

指令清单64.8　优化的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

在上述指令操作EAX寄存器的时候，它只把数据写到了RAX寄存器的低32位（即EAX）而没有直接操作整个64位RAX寄存器。这是因为：在操作寄存器的低32位的时候，该寄存器的高32位会被自动清零。或许，这只是把x86代码移植到x86-64平台时的偷懒做法。

64.6　单/双精度数型返回值

除了Win64规范以外的所有的调用规范都规定：当返回值为单/双精度浮点型数据时，被调用方函数应当通过FPU寄存器ST(0)传递返回值。而Win64规范规定：被调用方函数应当通过XMM0寄存器的低32位（float）或低64位寄存器（double）返回单/双精度浮点型数据。

64.7　修改参数

C/C++和其他语言的编程人员可能都曾问过这样一个问题：如果被调用方函数修改了外来参数的值，将会发生什么情况？答案十分简单：外来参数都是通过栈传递的，因此被调用方函数修改的是栈里的数据。在被调用方函数退出以后，调用方函数不会再访问自己传递给别人的参数。

#include <stdio.h>

void f(int a, int b)
{
        a=a+b;
        printf ("%d\n", a);
};

指令清单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      ENDP

由此可见，只要这些参数不是C++的引用指针/references（本书的51.3节）也不是数据指针，那么被调用方函数可以随便操作外部传来的参数。

理论上讲，在被调用方函数结束以后，调用方函数能够获取被调用方函数修改过的参数，对它们加以进一步利用。然而实际上我们只能在手写的汇编指令中遇到这种情况，C/C++语言并不支持这种访问方法。

64.8　指针型函数参数

我们可以给函数参数分配一个指针，把它调配给其他函数：

#include <stdio.h>

// located in some other file
void modify_a (int *a);

void f (int a)
{
        modify_a (&a);
        printf ("%d\n", a);
};

要不是看了下面的汇编代码，我们一时还很难理解这段程序是如何运行的。

指令清单64.10　MSVC 2010的优化

$SG2796 DB     '%d', 0aH, 00H

_a$ = 8
_f      PROC
        lea    eax, DWORD PTR _a$[esp-4] ; just get the address of value in local stack
        push   eax                       ; and pass it to modify_a()
        call   _modify_a
        mov    ecx, DWORD PTR _a$[esp]   ; reload it from the local stack
        push   ecx                       ; and pass it to printf()
        push   OFFSET $SG2796 ; '%d'
        call   _printf
        add    esp, 12
        ret    0
_f      ENDP

变量a的地址通过栈传递给了一个函数，然后这个地址又被传递给了另外一个函数。第一个函数修改了变量a的值，而后printf()函数获取到了这个修改后的变量值。

细心的读者可能会问：使用一种直接通过寄存器传递参数的调用约定，又会是什么情况呢？

即便真地使用了这种调用约定，还会有阴影空间（Shadow Space）的问题。传递的数值将会从寄存器保存到了本地栈的阴影空间里，然后以地址的形式传递给其他函数。

指令清单64.11　优化的MSVC 2012（64位）

$SG2994 DB      '%d', 0aH, 00H

a$ = 48
f       PROC
        mov     DWORD PTR [rsp+8], ecx ; save input value in Shadow Space
        sub     rsp, 40
        lea     rcx, QWORD PTR a$[rsp] ; get address of value and pass it to modify_a()
        call    modify_a
        mov     edx, DWORD PTR a$[rsp] ; reload value from Shadow Space and pass it to printf()
        lea     rcx, OFFSET FLAT:$SG2994 ; '%d'
        call    printf
        add     rsp, 40
        ret     0
f       ENDP

GCC也将输入的数值保存到本地栈。

指令清单64.12　优化的GCC 4.9.1（64位）

.LC0:
        .string "%d\n"
f:
        sub     rsp, 24
        mov     DWORD PTR [rsp+12], edi   ; store input value to the local stack
        lea     rdi, [rsp+12]             ; take an address of the value and pass it to modify_a()
        call    modify_a
        mov     edx, DWORD PTR [rsp+12]   ; reload value from the local stack and pass it to printf()
        mov     esi, OFFSET FLAT:.LC0     ; '%d'
        mov     edi, 1
        xor     eax, eax
        call    __printf_chk
        add     rsp, 24
        ret

ARM64下的GCC也已同样的方式传递参数。只是在这个平台上，这个空间被称为“寄存器（内容）保存区（Register Save Area）”。

指令清单64.13　优化的GCC 4.9.1 ARM64

f:
        stp     x29, x30, [sp, -32]!
        add     x29, sp, 0       ; setup FP
        add     x1, x29, 32      ; calculate address of variable in Register Save Area
        str     w0, [x1,-4]!     ; store input value there
        mov     x0, x1           ; pass address of variable to the modify_a()
        bl      modify_a
        ldr     w1, [x29,28]     ; load value from the variable and pass it to printf()
        adrp    x0, .LC0 ; '%d'
        add     x0, x0, :lo12:.LC0
        bl      printf           ; call printf()
        ldp     x29, x30, [sp], 32
        ret
.LC0:
        .string "%d\n"

另外，与阴影空间（Shadow Space）的有关话题还可以参阅本书的46.1.2节。

第65章 线程本地存储TLS

线程本地存储（Thread Local Storage，TLS）是一种在线程内部共享数据的数据交换区域。每个线程都可以在这个区域保存它们要在内部共享的数据。一个比较知名的例子是C语言的全局变量errno。对于errno这类的全局变量来说，如果多线程进程的某一个线程对其进行了修改，那么这个变量就会影响到其他所有的线程。这显然和实际需求相悖，因此全局变量errno必须保存在TLS中。

为解决这个矛盾，C++ 11标准新增了一个限定符thread_local。它能将指定变量和特定的线程联系起来。由它限定的变量能被初始化，并且会被保存在TLS中。

指令清单65.1　C++11

#include <iostream>
#include <thread>

thread_local int tmp=3;

int main()
{
        std::cout << tmp << std::endl;
};

接下来，我们使用MinGW GCC 4.8.1编译它，不要用MSVC 2012进行编译。

在分析可执行文件的PE头之后就会发现，变量tmp被分配到TLS专用的数据保存区域了。

65.1　线性同余发生器（改）

本书第20章展示的随机数生成函数其实有一个瑕疵：在多线程并发运行时，它是不安全的。原因在于：它有一个内部的变量，它可能会同时被不同的线程读取或者修改。

65.1.1　Win32系统

未初始化的TLS数据

我们可以给这种全局变量增加限定符__declspec(thread)，这样它就能被分配到TLS中。请注意下述代码的第9行。

 1  #include <stdint.h>
 2  #include <windows.h>
 3  #include <winnt.h>
 4
 5  // from the Numerical Recipes book:
 6  #define RNG_a 1664525
 7  #define RNG_c 1013904223
 8
 9  __declspec( thread ) uint32_t rand_state;
10
11  void my_srand (uint32_t init)
12  {
13           rand_state=init;
14  }
15
16  int my_rand ()
17  {
18           rand_state=rand_state*RNG_a;
19           rand_state=rand_state+RNG_c;
20           return rand_state & 0x7fff;
21  }
22
23  int main()
24  {
25           my_srand(0x12345678);
26           printf ("%d\n", my_rand());
27  };

用MSVC 2013编译上述程序，再用Hiew打开最后生成的可执行文件。我们可以看到这种文件的PE部分出现了全新的TLS段：

指令清单65.2　优化的MSVC 2013 x86

_TLS SEGMENT
_rand_state DD 01H DUP (?)
_TLS ENDS

_DATA SEGMENT
$SG84851 DB     '%d', 0aH, 00H
_DATA ENDS
_TEXT SEGMENT

_init$ = 8                                    ; size = 4
_my_srand PROC
; FS:0=address of TIB
        mov     eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
        mov     ecx, DWORD PTR __tls_index
        mov     ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
        mov     eax, DWORD PTR _init$[esp-4]
        mov     DWORD PTR _rand_state[ecx], eax
        ret     0
_my_srand ENDP

_my_rand PROC
; FS:0=address of TIB
        mov     eax, DWORD PTR fs:__tls_array ; displayed in IDA as FS:2Ch
; EAX=address of TLS of process
        mov     ecx, DWORD PTR __tls_index
        mov     ecx, DWORD PTR [eax+ecx*4]
; ECX=current TLS segment
        imul    eax, DWORD PTR _rand_state[ecx], 1664525
        add     eax, 1013904223                         ; 3c6ef35fH
        mov     DWORD PTR _rand_state[ecx], eax
        and     eax, 32767                              ; 00007fffH
        ret     0
_my_rand ENDP

_TEXT ENDS

参数rand_state现在是位于TLS段中。此后每个线程都会拥有各自的rand_state 。这里表示的是如何寻址：从FS:2Ch调用线程信息块（Thread Information Block，TIB）的地址。如果需要，再增加一个额外的索引，最后再计算TLS段的地址。

这种程序的线程可以通过ECX寄存器访问各自的rand_state变量，因为该变量在各个线程的地址不再相同。

FS段选择器并不陌生。它其实就是TIB的专用指针，用于提高线程数据的加载速度。

GS段选择器是Win64程序使用的额外的索引寄存器。

在下面这个程序里，TLS的地址是0x58。

指令清单65.3　优化的MSVC 2013（64位）

_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设置一个固定的初始值，以防后期忘记对它进行初始化。如下述代码第9行所示，我们对它进行初始化赋值。

 1  #include <stdint.h>
 2  #include <windows.h>
 3  #include <winnt.h>
 4
 5  // from the Numerical Recipes book:
 6  #define RNG_a 1664525
 7  #define RNG_c 1013904223
 8
 9  __declspec( thread ) uint32_t rand_state=1234;
10
11  void my_srand (uint32_t init)
12  {
13         rand_state=init;
14  }
15
16  int my_rand ()
17  {
18         rand_state=rand_state*RNG_a;
19         rand_state=rand_state+RNG_c;
20         return rand_state & 0x7fff;
21  }
22
23  int main()
24  {
25         printf ("%d\n", my_rand());
26  };

以上的代码看起来并没有什么不同，但是在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_ptr
...

我们要关注的是这里显示的数1234。每当启动新的线程时，它都会分配一个新的TLS段。此后，包括1234在内的所有数据都会被复制到新建都TLS段。

一个典型的应用场景是：

启动线程A，系统同期创建该线程专用的TLS，并将变量rand_state赋值为1234。

此后，线程A多次调用my_rand()函数，rand_state变量的值不再会是初始值1234。

另行启动线程B，系统同期创建该线程专用的TLS，变量rand_state也会被赋值为1234。也就是说，在线程A和线程B里，同名变量会有不同的值。

TLS回调

如果TLS中的变量必须填充为某些数据，并且是以某些不寻常的方式进行的话，该怎么办呢？比如说，我们有以下的这个任务：编程人员忘记调用my_srand()函数来初始化随机数发生器（PRNG），而随机数发生器只有在正确初始化之后才会生成真实意义上的随机数，而不是1234这样的固定值。在这种情况下，可以采用TLS回调。

在采用这种hack之后，本例代码的可移植性（通用性）就变差了。毕竟，本例只是一个演示性质的敲门砖而已。它只是构造一个在进程/线程启动前就被系统调用的回调函数（tls_callback()）。这个回调函数用GetTickCount()函数的返回值来初始化随机数发生器（PRNG）。

#include <stdint.h>
#include <windows.h>
#include <winnt.h>

// from the Numerical Recipes book:
#define RNG_a 1664525
#define RNG_c 1013904223

__declspec( thread ) uint32_t rand_state;

void my_srand (uint32_t init)
{
        rand_state=init;
}

void NTAPI tls_callback(PVOID a, DWORD dwReason, PVOID b)
{
        my_srand (GetTickCount());
}

#pragma data_seg(".CRT$XLB")
PIMAGE_TLS_CALLBACK p_thread_callback = tls_callback;
#pragma data_seg()

int my_rand ()
{
        rand_state=rand_state*RNG_a;
        rand_state=rand_state+RNG_c;
        return rand_state & 0x7fff;
}

int main()
{
        // rand_state is already initialized at the moment (using GetTickCount())
        printf ("%d\n", my_rand());
};

我们在IDA中查看一下，代码如下所示。

指令清单65.4　优化的MSVC 2013

.text:00401020 TlsCallback_0    proc near              ; DATA XREF: .rdata:TlsCallbacks
.text:00401020                  call     ds:GetTickCount
.text:00401026                  push     eax
.text:00401027                  call     my_srand
.text:0040102C                  pop      ecx
.text:0040102D                  retn     0Ch
.text:0040102D TlsCallback_0    endp

...

.rdata:004020C0 TlsCallbacks    dd offset TlsCallback_0 ; DATA XREF: .rdata:TlsCallbacks_ptr

...

.rdata:00402118 TlsDirectory    dd offset TlsStart
.rdata:0040211C TlsEnd_ptr      dd offset TlsEnd
.rdata:00402120 TlsIndex_ptr    dd offset TlsIndex
.rdata:00402124 TlsCallbacks_ptr dd offset TlsCallbacks
.rdata:00402128 TlsSizeOfZeroFill dd 0
.rdata:0040212C TlsCharacteristics dd 300000h

在解压过程中使用TLS回调函数，可起到混淆视听的作用。经验不足的分析人员通常会感到晕头转向，无法在分析原始入口/OEP之前就已经运行的回调函数。

65.1.2　Linux系统

我们来看看在GCC下是如何定义线程本地的全局变量的：

__thread uint32_t rand_state=1234;

当然，这不是标准的C/C++修饰符，而是GCC的专用修饰符。

GS段选择器也常用于TLS寻址，但是Linux的实现方法和Windows略有不同。

指令清单65.5　x86下的优化GCC 4.8.1

.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

更多的信息可以查看参考书目Dre13。

第66章 系统调用（syscall-s）

我们都知道，所有在操作系统中运行的进程可以分成两类：一类进程对具有硬件的全部访问权限，运行于内核空间（kernel space）；另一类进程不能直接访问硬件地址，运行于用户空间（user space）。

操作系统的内核以及常规的驱动程序都运行于内核空间。普通的应用程序通常运行于用户空间。

具体来说，Linux的内核运行于空间；而Glibc（底层API）则运行于空间。

这两种空间的隔离措施对于操作系统的安全性至关重要。若没有隔离措施，所有程序都可以干扰其他进程甚至破坏操作系统的内核。另一方面来看，即使实现了两种空间的隔离措施，一旦运行于内核空间的驱动程序发生错误、或者是操作系统的内核组件存在问题，整个内核照样会崩溃甚至发生BSOD（Black Screen of Death）的“蓝天白云”故障。

虽然x86 CPU引入了特权等级的概念，将进程权限分为ring0～ring3，但是Linux和Windows只使用了其中的2个级别控制进程权限：ring0（内核空间）和ring3（用户空间）。

由操作系统提供的系统调用（syscall）构成了ring0和ring 3之间的访问机制。可以说，系统调用就是操作系统为应用程序提供的应用编程接口API。

而在Windows NT环境下，系统调用表位于系统服务分配表SSDT（System Service Dispatch Table）。

计算机病毒以及shellcode大多都会利用系统调用。这是因为系统库函数的寻址过程十分麻烦，而直接调用系统调用却相对简单。虽然系统调用的访问过程并不麻烦，但是由于系统调用本身属于底层API，因此直接使用系统调用的程序也不好写。另外需要注意的是：系统调用的总数由操作系统和系统版本两个因素共同决定的。

66.1　Linux

Linux程序通常通过80号中断/INT 80调用系统调用。在调用系统调用时，程序应当通过EAX寄存器指定被调用函数的编号，再使用其他寄存器声明系统调用的参数。

指令清单66.1　使用两次系统调用syscall的简单例子

section .text
global _start

_start:
        mov     edx,len ; buffer len
        mov     ecx,msg ; buffer
        mov     ebx,1   ; file descriptor. 1 is for stdout
        mov     eax,4   ; syscall number. 4 is for sys_write
        int     0x80

        mov     eax,1   ; syscall number. 4 is for sys_exit
        int     0x80

section .data

msg     db  'Hello, world!',0xa
len     equ $ - msg

编译指令如下所示。

nasm -f elf32 1.s
ld 1.o

完整的Linux系统调用列表可以参考：http://go.yurichev.com/17319

。

如需截获或追踪Linux系统调用的访问过程，可使用本书71章介绍的strace程序。

66.2　Windows

Windows程序可通过0x2e号中断/int 0x2e、或x86专用指令SYSENTER访问系统调用。

这里使用的中断数是0x2e，或者采用x86下的特殊指令SYSENTER。

完整的Windows系统调用列表可以参考：http://go.yurichev.com/17320

。

进一步的阅读，可以参阅Piotr Bania编写的Windows Syscall Shellcode一书：http://go.yurichev. com/17321

。

第67章 Linux

67.1　位置无关的代码

在分析Linux共享库文件（扩展名是so）时，我们经常会遇到具有下述特征的指令代码：

指令清单67.1　x86下的libc-2.17.so

.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（Position Independent Code）”。因为进程或对象会被操作系统的链接器加载到任意内存地址，所以代码里的指令无法直接确定（hardcoded）绝对内存地址。

PIC在早期的计算机系统中非常关键，而目前在没有虚拟内存支持的嵌入式系统中就更为重要了。对于那些没有采用虚拟内存技术的嵌入式设备来说，所有进程都存放于一个连续的内存块中。PIC至今仍然用于*NIX系统的共享目标库，因为不同进程可能会链接同一个共享库。库文件只会被操作系统加载一次。当应用程序调用库时，直接把共享的地址复制过来。这种情况下，调用库函数的进程会被加载到不同地址，而库文件的加载地址却固定不变。这些因素决定，共享库的库函数不得使用绝对地址（至少对内部对象而言），否则就不能被多个进程同时调用。

我们来做一个简单的试验：

#include <stdio.h>

int global_variable=123;

int f1(int var)
{
    int rt=global_variable+var;
    printf ("returning %d\n", rt);
    return rt;
};

我们在GCC 4.7.3下编译一下，然后使用IDA打开编译后的.so文件。

编译的命令行为：

gcc -fPIC -shared -O3 -o 1.so 1.c

指令清单67.2　GCC 4.7.3

.text:00000440               public __x86_get_pc_thunk_bx
.text:00000440 __x86_get_pc_thunk_bx proc near     ; CODE XREF: _init_proc+4
.text:00000440                                     ; deregister_tm_clones+4 ...
.text:00000440               mov      ebx, [esp+0]
.text:00000443               retn
.text:00000443 __x86_get_pc_thunk_bx endp

.text:00000570               public f1
.text:00000570 f1            proc near
.text:00000570
.text:00000570 var_1C        = dword ptr -1Ch
.text:00000570 var_18        = dword ptr -18h
.text:00000570 var_14        = dword ptr -14h
.text:00000570 var_8         = dword ptr -8
.text:00000570 var_4         = dword ptr -4
.text:00000570 arg_0         = dword ptr 4
.text:00000570
.text:00000570               sub     esp, 1Ch
.text:00000573               mov     [esp+1Ch+var_8], ebx
.text:00000577               call    __x86_get_pc_thunk_bx
.text:0000057C               add     ebx, 1A84h
.text:00000582               mov     [esp+1Ch+var_4], esi
.text:00000586               mov     eax, ds:(global_variable_ptr - 2000h)[ebx]
.text:0000058C               mov     esi, [eax]
.text:0000058E               lea     eax, (aReturningD - 2000h)[ebx] ; "returning %d\n"
.text:00000594               add     esi, [esp+1Ch+arg_0]
.text:00000598               mov     [esp+1Ch+var_18], eax
.text:0000059C               mov     [esp+1Ch+var_1C], 1
.text:000005A3               mov     [esp+1Ch+var_14], esi
.text:000005A7               call    ___printf_chk
.text:000005AC               mov     eax, esi
.text:000005AE               mov     ebx, [esp+1Ch+var_8]
.text:000005B2               mov     esi, [esp+1Ch+var_4]
.text:000005B6               add     esp, 1Ch
.text:000005B9               retn
.text:000005B9 f1            endp

上述代码的关键在于：每个函数都在启动之后调整了字符串"returning %d\n" 和global_variable的指针。__x86_get_pc_thunk_bx()函数通过EBX返回一个指向自身的指针。而位于其后（偏移量0x57C处）的指令再次对ebx进行了修正。这是一种获取PC指针（EIP）的取巧办法。

常数0x1A84是函数的启始地址与“全局偏移表（global offset table）GOT”和“过程链接表（Procedure Linkage Table）PLT”之间的地址差。在可执行文件中，GOT、PLT都有各自的相应段（section）。全局变量global_variable的指针正好位于全局偏移量表GOT之后。为了便于我们理解偏移量和各表之间的关系，IDA对显示的偏移量进行了某种挑战。这部分的原始指令实际上是：

.text:00000577                 call __x86_get_pc_thunk_bx
.text:0000057C                 add ebx, 1A84h
.text:00000582                 mov [esp+1Ch+var_4], esi
.text:00000586                 mov eax, [ebx-0Ch]
.text:0000058C                 mov esi, [eax]
.text:0000058E                 lea eax, [ebx-1A30h]

EBX寄存器存储着GOT PLT的指针（相应section的启始地址）。因此在计算全局变量global_variable的指针时（该指针保存在GOT中），必须从EBX减去地址差，即常数0xC。同理，在计算“returning %d\n”的字符串指针时，必须从EBX减去0x1A30。

实际上，AMD64的指令集支持基于RIP的相对寻址就是为了简化PIC代码的操作。

然后，我们用同样版本的GCC把这段C代码编译为64位目标文件。

IDA会在显示代码的时候隐藏那些基于RIP的寻址细节。因此，我们通过objdump查看汇编代码：

0000000000000720 <f1>:
 720:   48 8b 05 b9 08 20 0   mov    rax,QWORD PTR [rip+0x2008b9] # 200fe0 <_DYNAMIC+0x1d0>
 727:   53                    push   rbx
 728:   89 fb                 mov    ebx,edi
 72a:   48 8d 35 20 00 00 00  lea    rsi,[rip+0x20]         # 751 <_fini+0x9>
 731:   bf 01 00 00 00        mov    edi,0x1
 736:   03 18                 add    ebx,DWORD PTR [rax]
 738:   31 c0                 xor    eax,eax
 73a:   89 da                 mov    edx,ebx
 73c:   e8 df fe ff ff        call   620 <__printf_chk@plt>
 741:   89 d8                 mov    eax,ebx
 743:   5b                    pop    rbx
 744:   c3                    ret

我们来看看以上程序代码中的RIP后面的两个偏移量：

① 指令0x720处。0x2008b9是该地址与全局变量global_variable之间的地址差。

② 指令0x72A处。0x20则是该地址与“returning %d”字符串指针之间的地址差。

可能读者也已经注意到了，经常进行地址重复计算会降低程序的执行效率（虽然在64位系统下可能会表现稍好）。因此，注重性能的时候，最好采用使用静态链接的静态库。

67.1.1　Windows

Windows的DLL加载机制不是PIC机制。如果Windows加载器要把DLL加载到另外一个基地址，它就会内存中对DLL进行“修补”处理（重定位技术），从而可以正确地处理所有符号地址。这就意味着多个Windows进程无法在不同进程内存块的不同地址共享一份DLL，因为每个被加载在内存里的实例只能访问自己的地址空间。

67.2　在Linux下的LD_PRELOAD

Linux程序可以加载其他动态库之前、甚至在加载系统库（例如libc.so.6）之前加载自己的动态库。

借助这项功能，我们能够编写自定义的函数“替换”系统库中的同名函数。进一步说，劫持time()、read()、write()等系统函数并非难事。

接下来，我们以系统工具uptime为例进行演示。我们都知道，该应用可以显示计算机已经工作了多少时间。借助另一款系统工具strace可知，uptime通过/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

查查维基百科，我们可以得到以下的信息：

第一项数值显示的是系统已经运行的总时长；第二项数值是计算机处于空闲状态的时间总和。这两项数据都以秒为单位。

我们编写一个声明open()、read()和close()函数的自定义动态链接库。

首先要处理的就是open()函数。它应能判断程序打开的文件是否是我们需要的文件。如果两者相符，那么open()函数就应当记录并返回文件描述符。接下来要处理的是read()函数。read()函数应能判断程序打开的是否是我们关注的文件描述符。如果两者相符，那么就用某些数据替代原有文件内容；否则就调用libc.so.6里的原有函数。最后需要处理的是close()函数，它应能正确关闭已经打开的外部文件。

本例通过dlopen()和dlsym()函数获取同名函数在libc.so.6里的函数地址。虽然本例的确是要劫持系统函数，但是也得将控制权交还给原来的“正牌”函数。

另外一方面，如果我们要劫持strcmp()函数（字符串比较函数）、以此获取每组对比的字符串，那么我们就得手写一个strcpm()函数了，而无法继续调用原有函数。

这种劫持功能的程序源代码如下：

#include <stdio.h>
#include <stdarg.h>
#include <stdlib.h>
#include <stdbool.h>
#include <unistd.h>
#include <dlfcn.h>
#include <string.h>

void *libc_handle = NULL;
int (*open_ptr)(const char *, int) = NULL;
int (*close_ptr)(int) = NULL;
ssize_t (*read_ptr)(int, void*, size_t) = NULL;

bool inited = false;

_Noreturn void die (const char * fmt, ...)
{
        va_list va;
        va_start (va, fmt);

        vprintf (fmt, va);
        exit(0);
};

static void find_original_functions ()
{
        if (inited)
                return;
        libc_handle = dlopen ("libc.so.6", RTLD_LAZY);
        if (libc_handle==NULL)
                die ("can't open libc.so.6\n");

        open_ptr = dlsym (libc_handle, "open");
        if (open_ptr==NULL)
                die ("can't find open()\n");

        close_ptr = dlsym (libc_handle, "close");
        if (close_ptr==NULL)
                die ("can't find close()\n");

        read_ptr = dlsym (libc_handle, "read");
        if (read_ptr==NULL)
                die ("can't find read()\n");

        inited = true;
}

static int opened_fd=0;

int open(const char *pathname, int flags)
{
        find_original_functions();

        int fd=(*open_ptr)(pathname, flags);
        if (strcmp(pathname, "/proc/uptime")==0)
                opened_fd=fd; // that's our file! record its file descriptor
        else
                opened_fd=0;
        return fd;
};

int close(int fd)
{
        find_original_functions();

        if (fd==opened_fd)
                opened_fd=0; // the file is not opened anymore
        return (*close_ptr)(fd);
};

ssize_t read(int fd, void *buf, size_t count)
{
        find_original_functions();

        if (opened_fd!=0 && fd==opened_fd)
        {
                // that's our file!
                return snprintf (buf, count, "%d %d", 0x7fffffff, 0x7fffffff)+1;
        };
        // not our file, go to real read() function
        return (*read_ptr)(fd, buf, count);
};

我们用通用的动态库来编译它：

gcc -fpic -shared -Wall -o fool_uptime.so fool_uptime.c –ldl

最后，我们通过LD_PRELOAD指令优先加载自定义的函数库：

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、让它指向我们自定义的动态链接库，那么所有的进程都会在启动之前加载我们自定义的动态链接库。

更多例子请参阅：

① Very simple interception of the strcmp() (Yong Huang) :

https://yurichev.com/mirrors/LD_PRELOAD/Yong%20Huang%20LD_PRELOAD.txt

② Fun with LD_PRELOAD (Kevin Pulo): https://yurichev.com/mirrors/LD_PRELOAD/lca2009.pdf

。

③ File functions interception for compression/decompression：

ftp://metalab.unc.edu/pub/Linux/libs/compression

第68章 Windows NT

68.1　CRT (Win32)

所有程序都是从main()函数开始执行的吗？事实并非如此。如果用IDA或者HIEW打开可执行文件，我们可以看到原始入口OEP（Original Entry Point）总是指向其他的一段代码。这些代码会在启动程序之前进行一些维护和准备工作。这就是所谓的启动代码/startup-code即CRT代码（C RunTime）。

在通过命令行指令启动程序的时候，main()函数通过外来数组获取启动参数及系统的环境变量。然而，实际传递给程序的不是数组而是参数字符串。CRT代码会根据空格对字符串进行切割。另外，CRT代码还会通过envp数组向main()函数传递系统的环境变量。在Win32的GUI程序里，主函数变为了WinMain()，并且拥有自己的参数传递规格：

int CALLBACK WinMain(
 _In_ HINSTANCE hInstance,
 _In_ HINSTANCE hPrevInstance,
 _In_ LPSTR lpCmdLine,
 _In_ int nCmdShow
);

上述参数同样是由CRT代码准备的。

在程序结束以后，主函数main()会返回其退出代码。这个退出代码会被传递给CRT的 ExitProcess()函数，作为后者的一个参数。

通常来说，不同的编辑器会有不同的CRT代码。

以下列出的是MSVC 2008特有的CRT代码：

  1  ___tmainCRTStartup proc near
  2
  3  var_24 = dword ptr -24h
  4  var_20 = dword ptr -20h
  5  var_1C = dword ptr -1Ch
  6  ms_exc = CPPEH_RECORD ptr -18h
  7
  8        push     14h
  9        push     offset stru_4092D0
 10        call     __SEH_prolog4
 11        mov      eax, 5A4Dh
 12        cmp      ds:400000h, ax
 13        jnz      short loc_401096
 14        mov      eax, ds:40003Ch
 15        cmp      dword ptr [eax+400000h], 4550h
 16        jnz      short loc_401096
 17        mov      ecx, 10Bh
 18        cmp      [eax+400018h], cx
 19        jnz      short loc_401096
 20        cmp      dword ptr [eax+400074h], 0Eh
 21        jbe      short loc_401096
 22        xor      ecx, ecx
 23        cmp      [eax+4000E8h], ecx
 24        setnz    cl
 25        mov      [ebp+var_1C], ecx
 26        jmp      short loc_40109A
 27
 28
 29  loc_401096: ; CODE XREF: ___tmainCRTStartup+18
 30              ; ___tmainCRTStartup+29 ...
 31        and      [ebp+var_1C], 0
 32
 33  loc_40109A: ; CODE XREF: ___tmainCRTStartup+50
 34        push     1
 35        call     __heap_init
 36        pop      ecx
 37        test     eax, eax
 38        jnz      short loc_4010AE
 39        push     1Ch
 40        call     _fast_error_exit
 41        pop      ecx
 42
 43  loc_4010AE: ; CODE XREF: ___tmainCRTStartup+60
 44        call     __mtinit
 45        test     eax, eax
 46        jnz      short loc_4010BF
 47        push     10h
 48        call     _fast_error_exit
 49        pop      ecx
 50
 51  loc_4010BF: ; CODE XREF: ___tmainCRTStartup+71
 52        call     sub_401F2B
 53        and      [ebp+ms_exc.disabled], 0
 54        call     __ioinit
 55        test     eax, eax
 56        jge      short loc_4010D9
 57        push     1Bh
 58        call     __amsg_exit
 59        pop      ecx
 60
 61  loc_4010D9: ; CODE XREF: ___tmainCRTStartup+8B
 62        call     ds:GetCommandLineA
 63        mov      dword_40B7F8, eax
 64        call     ___crtGetEnvironmentStringsA
 65        mov      dword_40AC60, eax
 66        call     __setargv
 67        test     eax, eax
 68        jge      short loc_4010FF
 69        push     8
 70        call     __amsg_exit
 71        pop      ecx
 72
 73  loc_4010FF: ; CODE XREF: ___tmainCRTStartup+B1
 74        call     __setenvp
 75        test     eax, eax
 76        jge      short loc_401110
 77        push     9
 78        call     __amsg_exit
 79        pop      ecx
 80
 81  loc_401110: ; CODE XREF: ___tmainCRTStartup+C2
 82        push     1
 83        call     __cinit
 84        pop      ecx
 85        test     eax, eax
 86        jz       short loc_401123
 87        push     eax
 88        call     __amsg_exit
 89        pop      ecx
 90
 91  loc_401123: ; CODE XREF: ___tmainCRTStartup+D6
 92        mov      eax, envp
 93        mov      dword_40AC80, eax
 94        push     eax             ; envp
 95        push     argv            ; argv
 96        push     argc            ; argc
 97        call     _main
 98        add      esp, 0Ch
 99        mov      [ebp+var_20], eax
100        cmp      [ebp+var_1C], 0
101        jnz      short $LN28
102        push     eax             ; uExitCode
103        call     $LN32
104
105  $LN28:      ; CODE XREF: ___tmainCRTStartup+105
106        call     __cexit
107        jmp      short loc_401186
108
109
110  $LN27:      ; DATA XREF: .rdata:stru_4092D0
111        mov      eax, [ebp+ms_exc.exc_ptr] ; Exception filter 0 for function 401044
112        mov      ecx, [eax]
113        mov      ecx, [ecx]
114        mov      [ebp+var_24], ecx
115        push     eax
116        push     ecx
117        call     __XcptFilter
118        pop      ecx
119        pop      ecx
120
121  $LN24:
122        retn
123
124
125  $LN14:      ; DATA XREF: .rdata:stru_4092D0
126        mov      esp, [ebp+ms_exc.old_esp] ; Exception handler 0 for function 401044
127        mov      eax, [ebp+var_24]
128        mov      [ebp+var_20], eax
129        cmp      [ebp+var_1C], 0
130        jnz      short $LN29
131        push     eax             ; int
132        call     __exit
133
134
135  $LN29:      ; CODE XREF: ___tmainCRTStartup+135
136         call    __c_exit
137
138  loc_401186: ; CODE XREF: ___tmainCRTStartup+112
139        mov      [ebp+ms_exc.disabled], 0FFFFFFFEh
140        mov      eax, [ebp+var_20]
141        call     __SEH_epilog4
142        retn

在程序的第62行、第66行和第74行我们分别可以看到的是GetCommandLineA、setargv()和setenvp()这三个函数，从这三个函数的名称可以看出它们处理的分别是argc、argv和envp这三个全局变量。

最后，第97行的主函数main()会获取这些外部参数。

CRT中的函数名称通常都可以自然解释。例如第35行和第54行的heap_init()和ioinit()这两个函数。

堆的初始化操作是由CRT代码完成的。若在没有CRT代码的情况下调用内存分配函数malloc()，就会引发异常退出，并将看到下述错误代码：

runtime error R6030
- CRT not initialized

在C++程序中，CRT代码还要在启动主函数main()之前初始化全部全局对象。我们可以参考本书的51.4.1节。

主函数main()返回的数值会传递给cexit()，或者是在$LN32，随后会调用函数doexit()。

下面一个问题是：我们有没有可能不采用CRT呢？这是有可能的，前提是您清楚地知道自己在做什么。

我们可以通过MSVC的Linker程序的/ENTRY选项设置程序的入口点。

比如下面的这个程序代码：

#include <windows.h>

int main()
{
        MessageBox (NULL, "hello, world", "caption", MB_OK);
};

选用以下的命令行来编译：

cl no_crt.c user32.lib /link /entry:main

上述指令最终生成一个大小为2560字节的可执行文件。该文件中具备标准的PE文件头，调用MessageBox的指令，其数据段声明了两个字符串，并从库文件user32.dll导入MessageBox函数。整个可执行文件没有其他的内容了。

虽然这个程序确实可以正常运行，但是这种程序无法获取WinMain()函数所需的4个参数。确切地说，程序确实可以启动起来，但是在程序启动的时候外部参数没有被准备或传递过来。

不能直接采用包括4个参数在内的主函数WinMain()的方式，而且不采用main()函数。更加精确一点来说，虽然能传递参数，但是参数不是在程序一执行时就被传递的。

另外，如果通过编译指令限定PE段向更小地址对齐（默认值是4096字节），那么.编译器将会生成尺寸更小的exe文件：

cl no_crt.c user32.lib /link /entry:main /align:16

链接器Linker将会提示：

LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run

上述指令将生成一个长度为720字节的exe可执行文件。它可以运行于x86构架的Windows 7系统，但是却不能运行于64位的Windows 7系统（执行的时候，系统会给出错误提示）。从这里我们可以看到，虽然我们可以想办法让可执行文件变得更小一些，但是同时兼容性问题也会越来越突出。

68.2　Win32 PE文件

PE（Portable Executable）格式，是微软Windows环境可移植可执行文件（如exe、dll、vxd、sys和vdm等）的标准文件格式。

与其他格式的PE文件不同的是，exe和sys文件通常只有导入表而没有导出表。

和其他的PE文件一样，DLL文件也有一个原始代码入口点OEP（就是DllMain()函数的地址）。但是DLL的这个函数通常来讲什么也不会做。

sys文件通常来说是一个系统驱动程序。说到驱动程序，Windows操作系统需要在PE文件里保存其校验和，以验证该文件的正确性（Hiew就可以验证这个校验和）。

从Vista开始，所有的Windows驱动程序必须具备数字签名，否则系统会拒绝加载它们。

每个PE文件都由一段打印“This program cannot be run in DOS mode.”的DOS程序块开始。如果在DOS或者Windows 3.1环境下运行这个程序，那么只会看到上述字符串。因为DOS及Windows 3.1 系统不能识别PE格式的文件。

68.2.1　术语

模块Module：它是指一个单独的exe或者dll文件。

进程Process：加载到内存中并正在运行的程序，通常由一个exe文件和多个dll文件组成。

进程内存Process memory：每个进程都有完全属于自己的，进程间独立的，不被干扰的内存空间。 通常是模块、堆、栈等数据构成。

虚拟地址VA（Virtual Address）：程序访问存储器所使用的逻辑地址。

基地址Base Address：进程内存中加载模块的首地址。

相对虚拟地址RVA（Relative Virtual Address）：虚拟地址VA与基地址Base Address的差就是相对虚拟地址RVA。在PE文件表中的很多地址都是相对虚拟地址RVA。

导入地址表IAT（Import Address Table）：导入符号的地址数组。PE头里的IMAGE_DIRECTORY_ ENTRY_IAT指向第一个导入地址表IAT的开始位置。值得说明的是，反编译工具IDA可能会给IAT虚构一个伪段--.idata段，即使IAT是其他地址的一部分。

导入符号名称表INT（Import Name Table）：存储着所需符号名称的数组。

68.2.2　基地址

在开发各自的DLL动态链接库文件时，多数开发团队都有意让其他人直接调用自己的动态链接库。然而，具体到“谁的DLL到底应该加载到哪个地址”这种问题，却没有一种公开的协议或标准。

因此，当同一个进程的两个DLL库具有相同的基地址时，只会有一个DLL被真正加载到基地址上。而另外一个DLL则会分配到进程内存的某段空闲空间里。在调用后者时，每个虚拟地址都会被重新校对。

通常来说，MSVC编译成的可执行程序的基地址都是0x400000，而代码段则从0x401000开始。由此可知，这种程序代码段的相对虚拟地址RVA的首地址都是0x1000。而MSVC通常把DLL的基地址设定为0x10000000。

操纵系统可能会把模块加载到不同的基地址中，还可能是因为程序自身的要求。当程序“点名”启用地址空间分布的随机化（Address Space Layout Randomization，ASLR）技术的时候，操作系统会把其各个模块加载到随机的基地址上。

ASLR是shellcode的应对策略。shellcode都会调用系统函数。

在Windows Vista之前早期系统里，系统的DLL（如kernel32.dll，user32.dll的加载地址是已知的固定地址。在同一个版本的操作系统里，系统DLL里的系统函数地址也几乎一尘不变。也就是说，shellcode可以根据版本信息直接调用系统函数。

为了避免这个问题，地址空间分布的随机化ASLR技术应运而生。它能够将程序以及程序所需模块加载到无法事先确定的随机地址。

在PE文件中，我们通过设置一个标识来实现ASLR。这个标识的名称是：IMAGE_DLL_ CHARACTERISTICS_ DYNAMIC_BASE。

68.2.3　子系统

PE文件有一个子系统字段。这个字段的值通常是下列之一：

NATIVE（系统驱动程序）。

console控制台程序。

GUI（非控制台程序，也就是最常见的图文界面程序）。

68.2.4　操作系统版本

PE文件还指定了可加载它的Windows操作系统最低版本号。如需查阅版本号码和Windows发行名称的完整列表，请参阅：https://en.wikipedia.org/wiki/Windows_NT#Releases

。

举个例子，MSVC 2005编译的.exe文件只能运行在Windows NT4（版本号为4.00）及以后的操作系统上。但是MSVC 2008编译调应用程序（版本号是5.00）不兼容NT4系统，只能运行于Windows 2000及以后的操作系统。

在MSVC 2012生成的.exe文件里，操作系统版本号的默认值是6.00。这种程序仅面向Windows Vista及后期推出的操作系统。但我们可以编译选项强制编译器生成支持Windows XP的应用程序。详情请参阅https://blogs.msdn.microsoft.com/vcblog/2012/10/08/windows-xp-targeting-with-c-in-visual-studio-2012/

。

68.2.5　段

所有的可执行文件都可分解为若干段（sections）。段是代码和数据、以及常量和其他数据的组织形式。

带有IMAGE_SCN_CNT_CODE或IMAGE_SCN_MEM_EXECUTE标识的段，封装的是可执行代码。

数据段的标识为IMAGE_SCN_CNT_INITIALIZED_DATA、IMAGE_SCN_MEM_READ或IMAGE_ SCN_MEM_WRITE标记。

未初始化的数据的空段的标识为IMAGE_SCN_CNT_UNINITIALIZED_DATA、IMAGE_SCN_ MEM_READ或IMAGE_SCN_MEM_WRITE。

常数数据段（其中的数据不可被重新赋值）的标识是IMAGE_SCN_CNT_ INITIALIZED_ DATA以及IMAGE_SCN_MEM_READ，但是不包括标识IMAGE_SCN_MEM_WRITE。如果进程试图往这个数据段写入数据，那么整个进程就会崩溃。

PE可执行文件的每个段都可以拥有一个段名称。然而，名称不是段的重要特征。通常来说，代码段的段名称是.text，数据段的段名称是.data，常数段的段名称.rdata（只读数据）。其他类型的常见段名称还有：

.idata：导入段。IDA可能会给这个段分配一个伪名称；详情请参考本书68.2.1节。

.edata：导出段。这个段十分罕见。

.pdata：这个段存储的是用于异常处理的函数表项。它包含了Windors NT For MIPS、IA64以及x64所需的全部异常处理信息。详情请参考本书的68.3.3节。

.reloc：（加载）重定向段。

.bss：未初始化的数据段（BSS）。

.tls：线程本地存储段（TLS）。

.rsrc：资源。

.CRT：在早期版本的MSVC编译出的可执行文件里，可能出现这个这个段。

经过加密或者压缩处理之后，PE文件section段的段名称通常会被替换或混淆。

此外，开发人员还可以控制MSVC编译器、设定任意段的段名称。有关详情请参阅：https://msdn. microsoft.com/en-us/library/windows/desktop/cc307397.aspx

。

部分编译器（例如MinGW）和链接器可以在生成的可执行文件中加入带有调试符号、或者其他的调试信息的独立段。然而新近版本的MSVC不再支持这项功能。为了便于专业人员分析和调试应用程序，MSVC推出了全称为“程序数据库”的PDB文件格式。

PE格式的section段的数据结构大体如下：

typedef struct _IMAGE_SECTION_HEADER {
  BYTE Name[IMAGE_SIZEOF_SHORT_NAME];
  union {
    DWORD PhysicalAddress;
    DWORD VirtualSize;
  } Misc;
  DWORD VirtualAddress;
  DWORD SizeOfRawData;
  DWORD PointerToRawData;
  DWORD PointerToRelocations;
  DWORD PointerToLinenumbers;
  WORD NumberOfRelocations;
  WORD NumberOfLinenumbers;
  DWORD Characteristics;
} IMAGE_SECTION_HEADER, *PIMAGE_SECTION_HEADER;

上述代码摘自于：https://msdn.microsoft.com/en-us/library/windows/desktop/ms680341 (v=vs.85).aspx

。

简单的说，上述PointerToRawData（指向原始数据）就是段实体的偏移量Offset，而虚拟地址VirtualAddress就是Hiew里的RVA。

68.2.6　重定向段Relocations(relocs)

至少在Hiew中，它也可以表示为FIXUP-s。

重定位段是自MS-DOS时代起一直存在于可执行文件的实体段，几乎所有的可执行文件都有这个段。

前文介绍过，程序模块可能会被加载到不同的基地址。但是如何处理全局变量等局部共享数据呢？程序必须通过地址指针才能访问这类数据，然而程序又不可能事先知道共享数据的存储地址。为解决这个问题，人们推出了“位置无关代码/PIC”（详情请参阅67.1节）的解决方案。不过PIC用起来并不方便。

于是，人们又推出了基于“重定向表”的地址修正技术。重定向表记录了该文件被加载到不同基地址时需要修正的所有指针。

如果PE文件声明了一个地址为0x410000的全局变量，那么这个变量的寻址指令大致会是：

A1 00 00 41 00        mov        eax,[000410000]

此时，模块的基地址是0x400000（编译器默认值），全局变量的相对虚拟地址RVA是0x10000。

如果这个模块被加载到首地址为0x500000的基地址上，那么这个全局变量的真实地址应当被调整为0x510000。

在上述opcode中，变量地址应当是0xA1（“MOV EAX”指令）之后的那几个字节。为了通知操作系统在重定向时正确处理该地址，PE文件的重定向表必须收录这四个字节的相对地址。

当操作系统的加载器需要把这个模块加载到不同的基地址时，它会逐一枚举重定位表中所有地址，把这些地址所指向的数据当作32位的地址指针，然后减去初始基地址（这样就能获得RVA，也就是相对虚拟地址），并加上新的基地址。

如果模块被加载于其原始的基地址，那么操作系统就不会进行重定向处理。

所有全局变量的处理过程都是如此。

重定向段的数据结构并不唯一。Windows for x86程序的重定向段一般采用IMAGE_REL_BASED_ HIGHLOW的数据结构。

另外，Hiew用暗色区域显示重定向段，如图7.12所示。

而OllyDbg会在内存中用下画线的方式标记重定向段，如图13.11所示。

68.2.7　导出段和导入段

我们都知道，任何可执行文件都必须或多或少地调用由操作系统提供的服务或者DLL动态链接库。

广义地说，由某个模块（一般来说是DLL）声明的所有函数，最终都会被其他模块（.exe文件或者其他DLL）中的某条指令调用，只是调用方式不同而已。

为此，每一个DLL动态链接库文件都有一个“导出/exports“表。导出表声明了该模块定义的函数名称和函数的地址。

同时每个exe文件和DLL文件另有一个“导入”表。这个表声明了执行该模块所需的函数名称、以及相应DLL文件的文件名。

在加载完可执行文件主体的.exe文件之后，操作系统加载器开始处理导入表：它会加载记录在案的DLL文件，接着在DLL的导出表里查找所需函数的函数地址，最后把这些地址写到.exe模块的IAT导入表。

由此可见，操作系统的加载器在进程的加载过程中要比对大量的函数名称。但是检索字符串的效率不会很高。后来人们引入了基于“排行榜”或者“命中率”（对应相关数据结构里的Hints或Ordinal字段）的序号表示办法、把函数名称编排为数字，以此摆脱字符串操作的低效率问题。

因此，DLL文件只需在导出表里标注内部函数的函数“序号”。这显著提升了DLL文件的加载速度 。

举例来说，调用MFC的程序就能够通过函数“序号”调用mfc*.dll动态链接库。而这种程序不再需要导入段的数据表（Import Name Table，INT）中使用字符串存储MFC的函数名称。

当我们在IDA中加载这样的程序后，IDA会询问mfc*.dll文件的路径以获取外部函数的函数名称。如果我们没有指定MFC文件的存储路径，那么函数名称会是mfc80_123之类的字符串。

导入段

编译器通常会给导入表及其相关内容分配一个单独的section段（例如.idata），但这不是一个强制规定。

导入段涉及大量的技术术语，因此理解起来特别困难。本节将通过一个典型的例子进行集中演示。

导入段的主体是数组IMAGE_IMPORT_DESCRIPTOR。它记录着PE文件要导入哪些库文件。

在其元素的数据结构中：

Name字段存储着库名称字符串的RVA地址；

OriginalFirstThink字段是INT表的RVA地址。逐一读取这个字段对应的INT数组的值，可获取相应IMAGE_IMPORT_BY_NAME地址（RVA）、进而得到全部函数名称。IMAGE_ IMPORT_BY_NAME表（没有收录在图68.1里）里定义了一个16位整数的“hint”字段，它正是前文所说函数“序号”。
在加载模块时，如果可以通过序号检索所需函数名，那么操作系统加载器就不必进行费时的字符串比对操作。

图68.1　在PE范畴内与导入段相关的全部数据结构

FirstThunk字段存储的是IAT表的表指针。IAT表的每个成员元素都是由操作系统加载器解析出来的函数地址（RVA）。IDA会给这些元素添加“:_imp_CreateFileA”一类的名称标注。

由加载器解析出来的外部函数地址，至少有两种调用方法：

① 代码可通过call_imp_CreateFileA形式的指令直接调用外部函数。从某种意义上讲，导入函数的函数地址存储到全局变量的存储空间了。考虑到当前模块可能会被加载于与初始值不同的基地址上，那么只要把call指令引用的外部函数目标地址直接追加到reclos重定向表里就好了。

但是要把导入函数的函数地址全部追加到重定向段relocs里，会显著增加重定向表的数据容量。进一步来说，重定向表越大、程序的加载效率就越低。

② 另一种办法就是对每个调用点进行处理。在调用外部函数的时候，只要JMP到“重定向值+外部函数RVA”就可以调用外部函数了。这种调用方式也叫做“形实转换/thunks”。调用外部函数时，程序可以直接CALL 相应的thunk地址。这种调用方式无需进行重定位运算，因为CALL指令本身就能进行相对寻址，因此不必修正RVA地址。

编译器能够分派上述两种调用方法。如果外部函数的调用频率很高，那么链接器Linker很可能会通过thunk调用外部函数。但是在默认情况下，链接器并不创建thunk。

另外，FirstThunk字段里的函数指针数组不必在PE文件的导入地址段（Input Address Table，IAT section）中。笔者曾经编写过一个令.exe文件添加外部函数信息的PE_add_import工具（https://yurichev.com/PE_add_imports.html

）。它就曾经成功的将PE文件引用的外部函数替换为另外一个DLL文件的其他函数。此时生成的指令是：

MOV EAX, [yourdll.dll!function]
JMP EAX

FirstThunk字段存储的是外部函数第一条指令的地址。换而言之，在调用youdll.dll这类自定义动态链接库的时候，加载器把程序代码的相应位置直接替换为外部函数function的函数地址。

需要注意的是代码段一般都是只读的。因此，我们的应用程序将在代码段上增加一个标志IMAGE_ SCN_MEM_WRITE。否则的话，调用期间会出现5号错误（禁止访问）。

可能有读者会问，如果程序只调用一套DLL文件，而且这些DLL文件里所有的函数名称和函数地址保持不变，那么还能否进一步提高进程的加载速度？

答案是：可能的。实现在程序的FirstThunk数组里写入外部函数的函数地址即可。另外需要注意的是IMAGE_IMPORT_DESCRIPTOR结构体里的Timestamp字段。若这个字段有值，则加载器会判断DLL文件的时间戳是否与这个值相等。如果它们相等，那么加载器不做进一步处理，加载速度可能会快些。这就是所谓的“old-style binding（古板的绑定）”。Windows的SDK中有一个名为BIND.EXE的工具可以专门进行这项绑定设置。Matt Pietrek在其发布的《An In-Depth Look into the Win32 Portable Executable File Format》建议，终端用户应当在安装程序之后尽快进行这种时间戳绑定。

PE文件的打包/加密工具也可能会压缩/加密导入表。在这种情况下，Windows的加载器无法加载全部所需的DLL。这时，应由打包程序/加密程序负责加载外部函数。后者一般通过LoadLibrary()和GetProcAddress()来完成这项任务。

在Windows的安装程序的标准动态链接库DLL中，多数文件的导入地址表（Input Address Table，IAT）都位于PE文件的头部。这大概是出于优化的考虑而刻意设计成这样的。当运行一个exe可执行文件时，exe文件并不会被一次性地全部装载进内存（否则大型安装程序的加载速度就快得太离谱了），而是在访问过程中被分部映射到内存里。其目的就是加快exe文件的加载速度。

68.2.8　资源段

位于PE文件资源段里的数据无非就是图表、图形、字符串以及对话框描述等界面信息。这些资源与主程序指令分开存储，大概是为了方便实现多语言的支持：操作系统只需要根据系统的语言设置就可以选取相应的文本或图片。

而这其实也带来了一个副作用：因为PE可执行文件比较容易编辑，不具备PE文件专业知识的人也可以借助工具（ResHack等）直接修改程序资源。相关的介绍可以参见本书68.2.11节。

68.2.9　.NET

.NET的源程序并不会被编译成机器码，而会被编译成一种特殊的字节码。严格地说，这种.exe文件由字节码构成，并不是常规意义上的x86指令代码。但是这种程序的入口点（OEP）确实是一小段x86指令：

jmp           mscoree.dll!\_CorExeMain

.NET格式的PE文件由mscoree.dll处理，它同时是.NET程序的装载器。在Windows XP操作系统之前，.net程序都是通过上述jmp指令交由mscoree.dll处理的。自Windows XP系统起，操作系统的加载器能自动识别.NET格式的文件，即使没有上述JMP指令也可以正常加载.NET程序。有关详情请参阅：

https://msdn.microsoft.com/en-us/library/xh0859k0(v=vs.110).aspx

68.2.10　TLS段

这个段里存储了TLS数据（第65章）的初始化数据（如果需要的话）。当启动一个新的线程时，TLS的数据就是通过本段的数据来初始化的。

除此之外，PE文件规范还约定了“TLS！”的初始化规范，即TLS callbacks/TLS回调函数。如果程序声明了TLS回调函数，那么TLS回调函数会先于OEP执行。这项技术广泛应用于PE文件的压缩和加密程序。

68.2.11　工具

Objdump（cygwin版），可转储所有的PE文件结构。

Hiew（可以参考本书第73章）。这是一个编辑器。

Prefile。这是一个用来处理PE文件的Python库。

ResHack。它是Resource Hacker的简称，是一个资源编辑器。

PE_add_import。这是一个小工具，利用它可以将符号加入到PE可执行文件的导入表中。

PE_patcher。一个小工具，可以用来给PE文件打补丁。

PE_search_str_refs。一个小工具，可以用来在PE可执行文件中寻找函数，这些函数可能有些字符串。

68.2.12　更进一步

Daniel Pistelli:《.NET文件格式》：https://www.codeproject.com/articles/12585/the-net-file-format。

68.3　Windows SEH

68.3.1　让我们暂时把MSVC放在一边

Windows操作系统中，结构性例外程序处理机制（Structured Exception Handling，SEH）是用来处理异常情况的响应机制。然而，它是与语言无关的，与C++或者面向对象的编程语言（Oriented Object Programming，OOP）无任何关联。本节将脱离C++以及MSVC的相关特效，单独分析SEH的特性。

每个运行的进程都有一条SEH句柄链，线程信息块（Thread Information Block，TIB）有SHE的最后一个句柄。当出现异外时（比如出现了被零除、地址访问不正确或者程序主动调用RaiseException()函数等情况），操作系统会在线程信息块TIB里寻找SEH的最后一个句柄。并且把出现异常情况时与CPU有关的所有状态（包括寄存器的值等数据）传递给那个SEH句柄。此时异常处理函数开始判断自己能否应对这种异常情况。如果答案是肯定的，那么异常处理函数就会着手接管。如果异常处理函数无法处理这种情况，它就会通知操作系统无法处理它，此后操作系统会逐一尝试异常处理链中的其他处理程序，直到找到能够应对这种情况的异常处理程序为止。

在异常处理链的结尾处有一个大家都接触过的异常处理程序：它显示一个标准的对话框，通知用户进程已经崩溃，崩溃时CPU的状态信息是什么情况，用户是否愿意把这些信息发送给微软的开发人员。

图68.2　Windows XP下的崩溃截图

图68.3　Windows XP下的崩溃细节

图68.4　Windows 7下的崩溃细节

图68.5　Windows 8.1下的崩溃截图

早些时候，这个异常处理程序叫做Dr. Watson。

另外，一些开发人员会在程序里设计自己的异常处理程序，以便收集程序的崩溃信息。这些都是通过系统函数SetUnhandledExceptionFilter()注册的异常处理函数。当操作系统遇到无法应对的异常情况时，它就会调用应用程序自己注册的异常处理函数。Oracle的RDBMS就是十分典型的一个例子：它会在程序崩溃时尽可能地转储CPU以及内存数据。

接下来，我们研究一个初级的异常处理程序。它摘自于https://www.microsoft.com/msj/0197/ Exception/Exception.aspx：

#include <windows.h>
#include <stdio.h>

DWORD new_value=1234;

EXCEPTION_DISPOSITION __cdecl except_handler(
                struct _EXCEPTION_RECORD *ExceptionRecord,
                void * EstablisherFrame,
                struct _CONTEXT *ContextRecord,
                void * DispatcherContext )
{
        unsigned i;
        printf ("%s\n", __FUNCTION__);
        printf ("ExceptionRecord->ExceptionCode=0x%p\n", ExceptionRecord->ExceptionCode);
        printf ("ExceptionRecord->ExceptionFlags=0x%p\n", ExceptionRecord->ExceptionFlags);
        printf ("ExceptionRecord->ExceptionAddress=0x%p\n", ExceptionRecord->ExceptionAddress);

        if (ExceptionRecord->ExceptionCode==0xE1223344)
        {
                printf ("That's for us\n");
                // yes, we "handled" the exception
                return ExceptionContinueExecution;
        }
        else if (ExceptionRecord->ExceptionCode==EXCEPTION_ACCESS_VIOLATION)
        {
                printf ("ContextRecord->Eax=0x%08X\n", ContextRecord->Eax);
                // will it be possible to 'fix' it?
                printf ("Trying to fix wrong pointer address\n");
                ContextRecord->Eax=(DWORD)&new_value;
                // yes, we "handled" the exception
                return ExceptionContinueExecution;
        }
        else
        {
                printf ("We do not handle this\n");
                // someone else's problem
                return ExceptionContinueSearch;
        };
}

int main()
{
        DWORD handler = (DWORD)except_handler; // take a pointer to our handler

        // install exception handler
        __asm
        {                                // make EXCEPTION_REGISTRATION record:
                push    handler          // address of handler function
                push    FS:[0]           // address of previous handler
                mov      FS:[0],ESP      // add new EXECEPTION_REGISTRATION
        }

        RaiseException (0xE1223344, 0, 0, NULL);

        // now do something very bad
        int* ptr=NULL;
        int val=0;
        val=*ptr;
        printf ("val=%d\n", val);

        // deinstall exception handler
        __asm
        {                               // remove our EXECEPTION_REGISTRATION record
                mov     eax,[ESP]       // get pointer to previous record
                mov     FS:[0], EAX     // install previous record
                add     esp, 8          // clean our EXECEPTION_REGISTRATION off stack
        }

        return 0;
}

在Win32环境下，FS：段寄存器里的数据就是线程信息块（Thread Information Block，TIB）的指针。而TIB中的第一个元素正是异常处理指针链里最后一个处理程序的地址。所谓“注册”异常处理程序就是把自定义的异常处理程序的地址链接到这个异常处理指针链里。在注册异常处理程序时，需要使用一种名为_EXCEPTION_REGISTRATION的数据结构。它其实只是一个简单的单向链表，使用栈结构存储各项节点的链数据。

指令清单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进程崩溃对话框。

系统异常（system exceptions）和用户异常（user exceptions）之间的区别是什么？系统异常的有关信息是：

由WinBase.h定义的异常状态

在ntstatus.h里的相应状态

错误编号

EXCEPTION_ACCESS_VIOLATION

STATUS_ACCESS_VIOLATION

0xC0000005

EXCEPTION_DATATYPE_MISALIGNMENT

STATUS_DATATYPE_
MISALIGNMENT0x80000002

EXCEPTION_BREAKPOINTSTATUS_
BREAKPOINT0x80000003

EXCEPTION_SINGLE_STEP

STATUS_SINGLE_STEP

0x80000004

EXCEPTION_ARRAY_BOUNDS_EXCEEDED

STATUS_ARRAY_BOUNDS_EXCEEDED

0xC000008C

EXCEPTION_FLT_DENORMAL_OPERAND

STATUS_FLOAT_DENORMAL_OPERAND

0xC000008D

EXCEPTION_FLT_DIVIDE_BY_ZERO

STATUS_FLOAT_DIVIDE_BY_ZERO

0xC000008E

EXCEPTION_FLT_INEXACT_RESULT

STATUS_FLOAT_INEXACT_RESULT

0xC000008F

EXCEPTION_FLT_INVALID_OPERATION

STATUS_FLOAT_INVALID_OPERATION

0xC0000090

EXCEPTION_FLT_OVERFLOW

STATUS_FLOAT_OVERFLOW

0xC0000091

EXCEPTION_FLT_STACK_CHECK

STATUS_FLOAT_STACK_CHECK

0xC0000092

EXCEPTION_FLT_UNDERFLOW

STATUS_FLOAT_UNDERFLOW

0xC0000093

EXCEPTION_INT_DIVIDE_BY_ZERO

STATUS_INTEGER_DIVIDE_BY_ZERO

0xC0000094

EXCEPTION_INT_OVERFLOW

STATUS_INTEGER_OVERFLOW

0xC0000095

EXCEPTION_PRIV_INSTRUCTION

STATUS_PRIVILEGED_INSTRUCTION

0xC0000096

EXCEPTION_IN_PAGE_ERROR

STATUS_IN_PAGE_ERROR

0xC0000006

EXCEPTION_ILLEGAL_INSTRUCTION

STATUS_ILLEGAL_INSTRUCTION

0xC000001D

EXCEPTION_NONCONTINUABLE_EXCEPTION

STATUS_NONCONTINUABLE_EXCEPTION

0xC0000025

EXCEPTION_STACK_OVERFLOW

STATUS_STACK_OVERFLOW

0xC00000FD

EXCEPTION_INVALID_DISPOSITION

STATUS_INVALID_DISPOSITION

0xC0000026

EXCEPTION_GUARD_PAGE

STATUS_GUARD_PAGE_VIOLATION

0x80000001

EXCEPTION_INVALID_HANDLE

STATUS_INVALID_HANDLE

0xC0000008

EXCEPTION_POSSIBLE_DEADLOCK

STATUS_POSSIBLE_DEADLOCK

0xC0000194

CONTROL_C_EXIT

STATUS_CONTROL_C_EXIT

0xC000013A

32位错误代码的具体含义，如下图所示。

S是最高的两位（第30、31位），为基本的状态代码，一共有4种组合，分别是11、10、01以及00。它们分别代码的意义是：11代表错误，10代表警告，01代表信息，00代表成功。

U是第29位（第29位），它只有0和1两种状态，代表该异常是否属于用户侧异常。

上面我们提到的一个返回值是0xE1223344。最高的4位是0xE（1110）。这几个比特位代表：①这是属于用户态异常；②这是一个错误信息。实事求是地讲，本例这个程序与这些最高位的值没有关系；而考究的异常处理程序应当能够充分利用错误代码的所有信息。

接着，程序试图读取地址为0的内存数据。这个地址实际就是NULL指针指向的地址。访问这个地址必将导致系统错误，因为根据ISO C标准这个地址不应当存放任何数据（硬性规定）。此时操作系统会优先调用程序自己注册的异常处理程序。后者会判断错误代码是否为EXCEPTION_ACCESS_ VIOLATION，从而得知该异常是否是自己可以处理的问题。

读取地址为0的指令大致如下：

指令清单68.2　MSVC2010

        ...
        xor     eax, eax
        mov     eax, DWORD PTR [eax] ; exception will occur here
        push    eax
        push    OFFSET msg
        call    _printf
        add     esp, 8
        ...

我们的程序是不是可以实时处理这个错误以使得程序能继续执行呢？答案是肯定的。我们的异常处理程序能够修正EAX寄存器的值，让操作系统继续执行下去。这就是自定义异常处理程序的功能。字符串显示函数printf显示的数值是1234，因为执行了我们的异常处理函数之后，EAX的数值不再是0了，而是全局变量new_value的值。因此执行流程就得到了恢复。

以下就是程序执行的步骤：首先内存管理器检测出由中央处理器CPU发出的错误信息，接着CPU将此进程挂起，并在Windows的内核中检索异常处理程序的句柄。然后依次调用SEH链的handler。

本例是由MSVC 2010编译的程序。当然，我们并不能保证其他的编译程序同样会使用EAX寄存器存放该指针。

它所演示的地址替换技巧非常的精妙。我经常使用这种技术演示SEH的内部构造。不过，我还不曾使用这种技巧实时修复异常错误。

为什么SEH相关的记录存储于栈，而不是其他的地方？据说，如此一来操作系统就不需要关心这类数据的释放操作，毕竟函数结束以后这些数据都会被自动释放。但是，笔者也不难100%保证这种假说的正确性。这有点像本书5.2.4节讲到的 alloca()函数。

68.3.2　让我们重新回到MSVC

据说，C++语言开发环境已经能够稳妥的处理各种异常情况，只有C语言的开发人员才需要关注代码的异常处理机制。所以微软推出了一个面向MSVC的非标准C扩展。这个扩展组件不适用于C++程序。

有关详情请参阅：https://msdn.microsoft.com/en-us/library/swezty51.aspx

__try
{
    ...
}
__except(filter code)
{
    handler code
}

除了“try-except”语句之外，MSVC还支持“try-finally”语句：

__try
{
    ...
}
__finally
{
    ...
}

前者中的“filter code”是一个用来判断是否执行“handler code（响应指令）”的表达式。如果代码太长、无法表示为一个表达式，那么就得借助于独立的过滤函数。

Windows内核就大量使用这种SEH结构。以WRK（Windows Research Kernel）的某段指令为例:

指令清单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;
}

指令清单68.4　WRK-v1.2/base/ntos/cache/cachesub.c

try {

    RtlCopyBytes( (PVOID)((PCHAR)CacheBuffer + PageOffset),
                  UserBuffer,
                  MorePages ?
                    (PAGE_SIZE - PageOffset) :
                    (ReceivedLength - PageOffset) );

} except( CcCopyReadExceptionFilter( GetExceptionInformation(),
                                                            &Status ) ) {

下面也是一组过滤代码。

指令清单68.5　WRK-v1.2/base/ntos/cache/copysup.c

LONG
CcCopyReadExceptionFilter(
    IN PEXCEPTION_POINTERS ExceptionPointer,
    IN PNTSTATUS ExceptionCode
    )

/*++

Routine Description:

    This routine serves as a exception filter and has the special job of
    extracting the "real" I/O error when Mm raises STATUS_IN_PAGE_ERROR
    beneath us.

Arguments:

    ExceptionPointer - A pointer to the exception record that contains
                       the real Io Status.

    ExceptionCode - A pointer to an NTSTATUS that is to receive the real
                    status.
Return Value:

    EXCEPTION_EXECUTE_HANDLER

--*/

{
    *ExceptionCode = ExceptionPointer->ExceptionRecord->ExceptionCode;

    if ( (*ExceptionCode == STATUS_IN_PAGE_ERROR) &&
         (ExceptionPointer->ExceptionRecord->NumberParameters >= 3) ) {

        *ExceptionCode = (NTSTATUS) ExceptionPointer->ExceptionRecord->ExceptionInformation[2];
    }

    ASSERT( !NT_SUCCESS(*ExceptionCode) );

    return EXCEPTION_EXECUTE_HANDLER;
}

从内部来讲，SEH是由操作系统支持的异常处理扩展。但是异常处理函数属于_except_handler3 （SEH3）或_except_handler4（SEH4）。而且响应代码依赖于MSVC编译器。它需要由MSVC的库文件或者msvcr*.dll的动态链接库提供支持。SEH是由MSVC提供的一种机制，这一点至关重要。其他Win32的编译器的异常响应机制可能与SEH完全不同。

SEH3

SEH3定义了一个_except_handler3的异常处理函数，而且还对_EXCEPTION_REGISTRATION表进行了扩充、添加了scope table和previous try level的指针。在此基础上，SEH4对scope table表添加了四个值，以实现缓冲溢出保护。

scope table是一个表，它的元素都由filter表达式和handler code块的指针构成。这个表可以正确处理带有嵌套关系的多级try/except语句。

本书再次强调，操作系统只关心SEH里各节点的prev字段和handle字段，从不关心任何其他数据。函数_except_handler3的作用是读取其他的字段以及scope表的数值，并且判断什么时间执行哪个异常处理函数。

函数_except_handler3的源代码并不公开。然而，Sanos操作系统（与Win32系统部分兼容），再现了这个函数。在某种程度上说，由Sanos实现的_except_handler3与Windows系统的同名函数十分相似（请参阅其源文件/src/win32/msvcrt/except.c）。除此以外，Wine平台和ReactOS系统也开发了类似的函数。

如果filter指针是空指针NULL，那么handler指针将指向“finally”所在的代码块。

在执行过程中，堆栈中的previous try level改变了，因此函数_except_handler3能从目前的嵌套中获取信息，目的是知道使用哪个scope table表。

执行期间，栈中的previous try level字段将会发生变化。_except_handler3根据这项数据获取当前嵌套级的信息，进而判断要使用scope table中的哪个表项。每一个try块都分配了一个唯一的数作为标识，scopetable表中条目（entry）间的关系则描述了try块的嵌套关系。

SEH3: 一个try/except块的例子

#include <stdio.h>
#include <windows.h>
#include <excpt.h>

int main()
{
    int* p = NULL;
    __try
    {
        printf("hello #1!\n");
        *p = 13; // causes an access violation exception;
        printf("hello #2!\n");
    }
    __except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
            EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
        printf("access violation, can't recover\n");
    }
}

指令清单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
; 3 registers to be saved:
    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 ; previous try level
    push    OFFSET FLAT:$SG74605 ; 'hello #1!'
    call    _printf
    add     esp, 4
    mov     eax, DWORD PTR _p$[ebp]
    mov     DWORD PTR [eax], 13
    push    OFFSET FLAT:$SG74606 ; 'hello #2!'
    call    _printf
    add     esp, 4
    mov     DWORD PTR __$SEHRec$[ebp+20], -1 ; previous try level
    jmp     SHORT $L74616

    ; filter code:
$L74617:
$L74627:
    mov     ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov     edx, DWORD PTR [ecx]
    mov     eax, DWORD PTR [edx]
    mov     DWORD PTR $T74621[ebp], eax
    mov     eax, DWORD PTR $T74621[ebp]
    sub     eax, -1073741819; c0000005H
    neg     eax
    sbb     eax, eax
    inc     eax
$L74619:
$L74626:
    ret     0

    ; handler code:
$L74618:
    mov     esp, DWORD PTR __$SEHRec$[ebp]
    push    OFFSET FLAT:$SG74608 ; 'access violation, can''t recover'
    call    _printf
    add     esp, 4
    mov     DWORD PTR __$SEHRec$[ebp+20], -1 ; setting previous try level back to -1
$L74616:
    xor     eax, eax
    mov     ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov     DWORD PTR fs:__except_list, ecx
    pop     edi
    pop     esi
    pop     ebx
    mov     esp, ebp
    pop     ebp
    ret     0
_main     ENDP
_TEXT     ENDS
END

由此可见，SEH在栈里形成了帧结构。而Scope table则是位于文件的CONST段，确实如此，scope table各字段的值确实不会发生变化。值得关注的是previous try level字段的变化过程。其初始值是0xFFFFFFF(-1)。当执行到try语句时，专有一条指令把它赋值为0。而当try语句的主体关闭时，它又被赋值为−1。我们也看到了filter以及handler code的地址。因此，我们能很容易地分析出函数中的try-except语句。

函数序言中的SEH初始化代码可能会被多个函数共享，有时候编译器会在函数序言直接调用SEH_prolog()函数，再在函数尾声处调用SEH_epilog()函数以回收栈空间。

下面我们在tracer跟踪器中运行这个例子：

tracer.exe -l:2.exe --dump-seh

上述指令的输出如下。

指令清单68.7　tracer.exe的输出

EXCEPTION_ACCESS_VIOLATION at 2.exe!main+0x44 (0x401054) ExceptionInformation[0]=1
EAX=0x00000000 EBX=0x7efde000 ECX=0x0040cbc8 EDX=0x0008e3c8
ESI=0x00001db1 EDI=0x00000000 EBP=0x0018feac ESP=0x0018fe80
EIP=0x00401054
FLAGS=AF IF RF
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x401204 (2.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x401070  (2.exe!main+0x60) handler=0x401088 ↙
    ↘ (2.exe!main+0x78)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x401204 (2.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1,  filter=0x401531 (2.exe!mainCRTStartup+0x18d) ↙
    ↘ handler=0x401545 (2.exe!mainCRTStartup+0x1a1)
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header:     GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
                EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll! ↙
    ↘ ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!_TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!_FinalExceptionHandler@16)

我们可以看到SEH链含有4个处理函数/handler。

前两个handler是由源代码指定注册的异常处理函数。虽然我们的源代码只定义了一个handler，但是CRT的_mainCRTStartup()函数会自动设置一个配套的handler。后者的功能不多，至少能够处理一些与FPU有关的异常情况。有关源码可以参阅MSVC安装目录里的crt/src/winxfltr.c文件。

第三个handler是由ntdll.dll提供的SEH4，第四个handler也位于ntdll.dll，跟MSVC没什么关系，它的函数名是FinalExceptionHandler。

上述信息表明，SEH链含有三种类型的处理函数：一种是与MSVC彻底无关的自定义handler（即异常处理指针链中的最后一项），另外两种处理程序是由MSVC提供的SEH3和SEH4函数。

SEH3：两个try/except模块例子

#include <stdio.h>
#include <windows.h>
#include <excpt.h>

int filter_user_exceptions (unsigned int code, struct _EXCEPTION_POINTERS *ep)
{
    printf("in filter. code=0x%08X\n", code);
    if (code == 0x112233)
    {
        printf("yes, that is our exception\n");
        return EXCEPTION_EXECUTE_HANDLER;
    }
    else
    {
        printf("not our exception\n");
        return EXCEPTION_CONTINUE_SEARCH;
    };
}
int main()
{
    int* p = NULL;
    __try
    {
        __try
        {
            printf ("hello!\n");
            RaiseException (0x112233, 0, 0, NULL);
            printf ("0x112233 raised. now let's crash\n");
            *p = 13; // causes an access violation exception;
        }
        __except(GetExceptionCode()==EXCEPTION_ACCESS_VIOLATION ?
                EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
        {
            printf("access violation, can't recover\n");
        }
    }
    __except(filter_user_exceptions(GetExceptionCode(), GetExceptionInformation()))
    {
        // the filter_user_exceptions() function answering to the question
        // "is this exception belongs to this block?"
        // if yes, do the follow:
        printf("user exception caught\n");
    }
}

这里，我们可以看到两个try块。因此scope table会有两个元素，分别存储着各try块的相应指针。“Previous try level”字段的值会伴随着进入/退出try语句块而发生相应改变。

指令清单68.8　MSVC 2003

$SG74606 DB     'in filter. code=0x%08X', 0aH, 00H
$SG74608 DB     'yes, that is our exception', 0aH, 00H
$SG74610 DB     'not our exception', 0aH, 00H
$SG74617 DB     'hello!', 0aH, 00H
$SG74619 DB     '0x112233 raised. now let''s crash', 0aH, 00H
$SG74621 DB     'access violation, can''t recover', 0aH, 00H
$SG74623 DB     'user exception caught', 0aH, 00H

_code$ = 8   ; size = 4
_ep$ = 12    ; size = 4
_filter_user_exceptions PROC NEAR
    push   ebp
    mov    ebp, esp
    mov    eax, DWORD PTR _code$[ebp]
    push   eax
    push   OFFSET FLAT:$SG74606 ; 'in filter. code=0x%08X'
    call   _printf
    add    esp, 8
    cmp    DWORD PTR _code$[ebp], 1122867; 00112233H
    jne    SHORT $L74607
    push   OFFSET FLAT:$SG74608 ; 'yes, that is our exception'
    call   _printf
    add    esp, 4
    mov    eax, 1
    jmp    SHORT $L74605
$L74607:
    push   OFFSET FLAT:$SG74610 ; 'not our exception'
    call   _printf
    add    esp, 4
    xor    eax, eax
$L74605:
    pop    ebp
    ret    0
_filter_user_exceptions ENDP

; scope table:
CONST    SEGMENT
$T74644   DD    0ffffffffH   ; previous try level for outer block
          DD    FLAT:$L74634 ; outer block filter
          DD    FLAT:$L74635 ; outer block handler
          DD    00H          ; previous try level for inner block
          DD    FLAT:$L74638 ; inner block filter
          DD    FLAT:$L74639 ; inner block handler
CONST    ENDS

$T74643 = -36     ; size = 4
$T74642 = -32     ; size = 4
_p$ = -28         ; size = 4
__$SEHRec$ = -24  ; size = 24
_main    PROC NEAR
    push   ebp
    mov    ebp, esp
    push   -1 ; previous try level
    push   OFFSET FLAT:$T74644
    push   OFFSET FLAT:__except_handler3
    mov    eax, DWORD PTR fs:__except_list
    push   eax
    mov    DWORD PTR fs:__except_list, esp
    add    esp, -20
    push   ebx
    push   esi
    push   edi
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; outer try block entered. set previous try level to 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 1 ; inner try block entered. set previous try level to 1
    push   OFFSET FLAT:$SG74617 ; 'hello!'
    call   _printf
    add    esp, 4
    push   0
    push   0
    push   0
    push   1122867 ; 00112233H
    call   DWORD PTR __imp__RaiseException@16
    push   OFFSET FLAT:$SG74619 ; '0x112233 raised. now let''s crash'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0
    jmp    SHORT $L74615

; inner block filter:
$L74638:
$L74650:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T74643[ebp], eax
    mov    eax, DWORD PTR $T74643[ebp]
    sub    eax, -1073741819; c0000005H
    neg    eax
    sbb    eax, eax
    inc    eax
$L74640:
$L74648:
    ret    0

; inner block handler:
$L74639:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET FLAT:$SG74621 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; inner try block exited. set previous try level back to 0

$L74615:
    mov    DWORD PTR __$SEHRec$[ebp+20], -1 ; outer try block exited, set previous try level back to -1
    jmp    SHORT $L74633

; outer block filter:
$L74634:
$L74651:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T74642[ebp], eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    push   ecx
    mov    edx, DWORD PTR $T74642[ebp]
    push   edx
    call   _filter_user_exceptions
    add    esp, 8
$L74636:
$L74649:
    ret    0

; outer block handler:
$L74635:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET FLAT:$SG74623 ; 'user exception caught'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -1 ; both try blocks exited. set previous try level back to -1
$L74633:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:__except_list, ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
_main     ENDP

只要在handler中调用printf()函数的指令那里设置一个断点，就可以观测到SEH handler的添加过程。 或许SEH的内部处理机制与众不同。而这里我们可以看到scope table 包含着两个元素：

tracer.exe -l:3.exe bpx=3.exe!printf --dump-seh

指令清单68.9　tracer.exe输出

(0) 3.exe!printf
EAX=0x0000001b EBX=0x00000000 ECX=0x0040cc58 EDX=0x0008e3c8
ESI=0x00000000 EDI=0x00000000 EBP=0x0018f840 ESP=0x0018f838
EIP=0x004011b6
FLAGS=PF ZF IF
* SEH frame at 0x18f88c prev=0x18fe9c handler=0x771db4ad (ntdll.dll!ExecuteHandler2@20+0x3a)
* SEH frame at 0x18fe9c prev=0x18ff78 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=1
scopetable entry[0]. previous try level=-1, filter=0x401120 (3.exe!main+0xb0) handler=0x40113b ↙
    ↘ (3.exe!main+0xcb)
scopetable entry[1]. previous try level=0, filter=0x4010e8 (3.exe!main+0x78) handler=0x401100 ↙
    ↘ (3.exe!main+0x90)
* SEH frame at 0x18ff78 prev=0x18ffc4 handler=0x4012e0 (3.exe!_except_handler3)
SEH3 frame. previous trylevel=0
scopetable entry[0]. previous try level=-1, filter=0x40160d (3.exe!mainCRTStartup+0x18d) ↙
    ↘ handler=0x401621 (3.exe!mainCRTStartup+0x1a1)
* SEH frame at 0x18ffc4 prev=0x18ffe4 handler=0x771f71f5 (ntdll.dll!__except_handler4)
SEH4 frame. previous trylevel=0
SEH4 header:    GSCookieOffset=0xfffffffe GSCookieXOROffset=0x0
                EHCookieOffset=0xffffffcc EHCookieXOROffset=0x0
scopetable entry[0]. previous try level=-2, filter=0x771f74d0 (ntdll.dll! ↙
    ↘ ___safe_se_handler_table+0x20) handler=0x771f90eb (ntdll.dll!_TppTerminateProcess@4+0x43)
* SEH frame at 0x18ffe4 prev=0xffffffff handler=0x77247428 (ntdll.dll!_FinalExceptionHandler@16)

SEH4

在遭受缓冲区溢出攻击（请参见本书第18章第2节）以后，地址表scope table的地址可能被重写。MSVC 2005编译器为SEH帧增加了一些缓冲区溢出保护，把SEH3升级成了SEH4。SEH4的scope table表的指针会与security cookie进行异或运算，然后才被写到相应的数据结构里。此外Scope table新增了一个双指针表头，这两个EH cookie指针都是security cookies的指针（GS cookies只有在编译时打开/GS参数才会出现）。EH Cookie的偏移量（offset）都是基于栈帧（EBP）的相对地址，其与security_cookie的异或运算结果会被保存在栈里，充当校验码。异常处理函数的加载过程会读取这个值并检查其正确性。

由于栈内的security cookie是一次性随机值，因此远程攻击者是无法事先预测这个值。

在SEH4中最外层的级别（previous try level）是-2，而不是SEH3的-1。

这里列出了两个由MSVC 2012编译的SEH4函数。

指令清单68.10　MSVC 2012：一个try块的例子

$SG85485 DB     'hello #1!', 0aH, 00H
$SG85486 DB     'hello #2!', 0aH, 00H
$SG85488 DB     'access violation, can''t recover', 0aH, 00H

; scope table:
xdata$x          SEGMENT
__sehtable$_main DD 0fffffffeH   ; GS Cookie Offset
    DD           00H             ; GS Cookie XOR Offset
    DD           0ffffffccH      ; EH Cookie Offset
    DD           00H             ; EH Cookie XOR Offset
    DD           0fffffffeH      ; previous try level
    DD           FLAT:$LN12@main ; filter
    DD           FLAT:$LN8@main  ; handler
xdata$x          ENDS

$T2 = -36       ; size = 4
_p$ = -32       ; size = 4
tv68 = -28      ; size = 4
__$SEHRec$ = -24 ; size = 24
_main     PROC
    push   ebp
    mov    ebp, esp
    push   -2
    push   OFFSET __sehtable$_main
    push   OFFSET __except_handler4
    mov    eax, DWORD PTR fs:0
    push   eax
    add    esp, -20
    push   ebx
    push   esi
    push   edi
    mov    eax, DWORD PTR ___security_cookie
    xor    DWORD PTR __$SEHRec$[ebp+16], eax ; xored pointer to scope table
    xor    eax, ebp
    push   eax                               ; ebp ^ security_cookie
    lea    eax, DWORD PTR __$SEHRec$[ebp+8] ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
    mov    DWORD PTR fs:0, eax
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; previous try level
    push   OFFSET $SG85485 ; 'hello #1!'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    push   OFFSET $SG85486 ; 'hello #2!'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
    jmp    SHORT $LN6@main

; filter:
$LN7@main:
$LN12@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T2[ebp], eax
    cmp    DWORD PTR $T2[ebp], -1073741819 ; c0000005H
    jne    SHORT $LN4@main
    mov    DWORD PTR tv68[ebp], 1
    jmp    SHORT $LN5@main
$LN4@main:
    mov    DWORD PTR tv68[ebp], 0
$LN5@main:
    mov    eax, DWORD PTR tv68[ebp]
$LN9@main:
$LN11@main:
    ret    0

; handler:
$LN8@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85488 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; previous try level
$LN6@main:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:0, ecx
    pop    ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
_main     ENDP

指令清单68.11　MSVC 2012：两个try块的例子

$SG85486 DB     'in filter. code=0x%08X', 0aH, 00H
$SG85488 DB     'yes, that is our exception', 0aH, 00H
$SG85490 DB     'not our exception', 0aH, 00H
$SG85497 DB     'hello!', 0aH, 00H
$SG85499 DB     '0x112233 raised. now let''s crash', 0aH, 00H
$SG85501 DB     'access violation, can''t recover', 0aH, 00H
$SG85503 DB     'user exception caught', 0aH, 00H

xdata$x     SEGMENT
__sehtable$_main DD 0fffffffeH             ; GS Cookie Offset
                 DD     00H                ; GS Cookie XOR Offset
                 DD     0ffffffc8H         ; EH Cookie Offset
                 DD     00H                ; EH Cookie Offset
                 DD     0fffffffeH         ; previous try level for outer block
                 DD     FLAT:$LN19@main    ; outer block filter
                 DD     FLAT:$LN9@main     ; outer block handler
                 DD     00H                ; previous try level for inner block
                 DD     FLAT:$LN18@main    ; inner block filter
                 DD     FLAT:$LN13@main    ; inner block handler
xdata$x ENDS

$T2 = -40       ; size = 4
$T3 = -36       ; size = 4
_p$ = -32       ; size = 4
tv72 = -28      ; size = 4
__$SEHRec$ = -24 ; size = 24
_main     PROC
    push   ebp
    mov    ebp, esp
    push   -2 ; initial previous try level
    push   OFFSET __sehtable$_main
    push   OFFSET __except_handler4
    mov    eax, DWORD PTR fs:0
    push   eax ; prev
    add    esp, -24
    push   ebx
    push   esi
    push   edi
    mov    eax, DWORD PTR ___security_cookie
    xor    DWORD PTR __$SEHRec$[ebp+16], eax     ; xored pointer to scope table
    xor    eax, ebp                              ; ebp ^ security_cookie
    push   eax
    lea    eax, DWORD PTR __$SEHRec$[ebp+8]      ; pointer to VC_EXCEPTION_REGISTRATION_RECORD
    mov    DWORD PTR fs:0, eax
    mov    DWORD PTR __$SEHRec$[ebp], esp
    mov    DWORD PTR _p$[ebp], 0
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; entering outer try block, setting previous try level=0
    mov    DWORD PTR __$SEHRec$[ebp+20], 1 ; entering inner try block, setting previous try level=1
    push   OFFSET $SG85497 ; 'hello!'
    call   _printf
    add    esp, 4
    push   0
    push   0
    push   0
    push   1122867 ; 00112233H
    call   DWORD PTR __imp__RaiseException@16
    push   OFFSET $SG85499 ; '0x112233 raised. now let''s crash'
    call   _printf
    add    esp, 4
    mov    eax, DWORD PTR _p$[ebp]
    mov    DWORD PTR [eax], 13
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, set previous try level back to 0
    jmp    SHORT $LN2@main

; inner block filter:
$LN12@main:
$LN18@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T3[ebp], eax
    cmp    DWORD PTR $T3[ebp], -1073741819 ; c0000005H
    jne    SHORT $LN5@main
    mov    DWORD PTR tv72[ebp], 1
    jmp    SHORT $LN6@main
$LN5@main:
    mov    DWORD PTR tv72[ebp], 0
$LN6@main:
    mov    eax, DWORD PTR tv72[ebp]
$LN14@main:
$LN16@main:
    ret    0

; inner block handler:
$LN13@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85501 ; 'access violation, can''t recover'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], 0 ; exiting inner try block, setting previous try level back to 0
$LN2@main:
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
    jmp    SHORT $LN7@main

; outer block filter:
$LN8@main:
$LN19@main:
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    mov    edx, DWORD PTR [ecx]
    mov    eax, DWORD PTR [edx]
    mov    DWORD PTR $T2[ebp], eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+4]
    push   ecx
    mov    edx, DWORD PTR $T2[ebp]
    push   edx
    call   _filter_user_exceptions
    add    esp, 8
$LN10@main:
$LN17@main:
    ret    0

; outer block handler:
$LN9@main:
    mov    esp, DWORD PTR __$SEHRec$[ebp]
    push   OFFSET $SG85503 ; 'user exception caught'
    call   _printf
    add    esp, 4
    mov    DWORD PTR __$SEHRec$[ebp+20], -2 ; exiting both blocks, setting previous try level back to -2
$LN7@main:
    xor    eax, eax
    mov    ecx, DWORD PTR __$SEHRec$[ebp+8]
    mov    DWORD PTR fs:0, ecx
    pop    ecx
    pop    edi
    pop    esi
    pop    ebx
    mov    esp, ebp
    pop    ebp
    ret    0
    _main  ENDP

_code$ = 8 ; size = 4
_ep$ = 12  ; size = 4
_filter_user_exceptions PROC
    push   ebp
    mov    ebp, esp
    mov    eax, DWORD PTR _code$[ebp]
    push   eax
    push   OFFSET $SG85486 ; 'in filter. code=0x%08X'
    call   _printf
    add    esp, 8
    cmp    DWORD PTR _code$[ebp], 1122867 ; 00112233H
    jne    SHORT $LN2@filter_use
    push   OFFSET $SG85488 ; 'yes, that is our exception'
    call   _printf
    add    esp, 4
    mov    eax, 1
    jmp    SHORT $LN3@filter_use
    jmp    SHORT $LN3@filter_use
$LN2@filter_use:
    push   OFFSET $SG85490 ; 'not our exception'
    call   _printf
    add    esp, 4
    xor    eax, eax
$LN3@filter_use:
    pop    ebp
    ret    0
_filter_user_exceptions ENDP

Cookie Offset是EBP的值（栈帧栈底）与栈内EBP⊕security_cookie之间的差值。Cookie XOR Offset是EBP⊕security_cookie与栈中数值之间的差值。如果上述各值不符合下列条件，那么整个进程将会因为栈损坏而终止运行：

security_cookie⊕(CookieXOROffset +address_of_saved_EBP) == stack[address_of_saved_EBP + CookieOffset]

如果Cookie Offset的值是−2，就表示这个cookie并不存在（GScookie一般如此）。

笔者编写的tracer程序也能进行Cookies的合法性检查，有关详情请访问https://github.com/ dennis714/tracer/blob/master/SEH.c

。

在启用“/GS-”选项之后MSVC 2005编译器就会分配SEH3的函数，但是它依然会分配SEH4的CRT代码。

68.3.3　Windows x64

和大家想象的一样，每个函数都在序言中设置SEH栈帧将会降低运行速度。此外，在程序运行过程中不断调整“previous try level”字段同样会增加时间开销。然而在x64程序里，整个情况彻底不同了：所有try块的指针、filter和handler函数的指针都单独存储于可执行文件的.pdata段。操作系统根据.pdata段获取异常处理的全部信息。

我们把上一个章节的两个程序编译为x64程序，可以得到：

指令清单68.12　MSVC 2012

$SG86276 DB     'hello #1!', 0aH, 00H
$SG86277 DB     'hello #2!', 0aH, 00H
$SG86279 DB     'access violation, can''t recover', 0aH, 00H

pdata    SEGMENT
$pdata$main DD  imagerel $LN9
        DD      imagerel $LN9+61
        DD      imagerel $unwind$main
pdata   ENDS
pdata   SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
        DD      imagerel main$filt$0+32
        DD      imagerel $unwind$main$filt$0
pdata   ENDS
xdata   SEGMENT
$unwind$main DD 020609H
        DD      030023206H
        DD      imagerel __C_specific_handler
        DD      01H
        DD      imagerel $LN9+8
        DD      imagerel $LN9+40
        DD      imagerel main$filt$0
        DD      imagerel $LN9+40
$unwind$main$filt$0 DD 020601H
        DD      050023206H
xdata   ENDS

_TEXT   SEGMENT
main    PROC
$LN9:
        push    rbx
        sub     rsp, 32
        xor     ebx, ebx
        lea     rcx, OFFSET FLAT:$SG86276 ; 'hello #1!'
        call    printf
        mov     DWORD PTR [rbx], 13
        lea     rcx, OFFSET FLAT:$SG86277 ; 'hello #2!'
        call    printf
        jmp     SHORT $LN8@main
$LN6@main:
        lea     rcx, OFFSET FLAT:$SG86279 ; 'access violation, can''t recover'
        call    printf
        npad    1 ; align next label
$LN8@main:
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret     0
main    ENDP
_TEXT   ENDS

text$x  SEGMENT
main$filt$0 PROC
        push    rbp
        sub     rsp, 32
        mov     rbp, rdx
$LN5@main$filt$:
        mov     rax, QWORD PTR [rcx]
        xor     ecx, ecx
        cmp     DWORD PTR [rax], -1073741819; c0000005H
        sete    cl
        mov     eax, ecx
$LN7@main$filt$:
        add     rsp, 32
        pop     rbp
        ret     0
        int     3
main$filt$0 ENDP
text$x ENDS

指令清单68.13　MSVC 2012

$SG86277 DB     'in filter. code=0x%08X', 0aH, 00H
$SG86279 DB     'yes, that is our exception', 0aH, 00H
$SG86281 DB     'not our exception', 0aH, 00H
$SG86288 DB     'hello!', 0aH, 00H
$SG86290 DB     '0x112233 raised. now let''s crash', 0aH, 00H
$SG86292 DB     'access violation, can''t recover', 0aH, 00H
$SG86294 DB     'user exception caught', 0aH, 00H

pdata   SEGMENT
$pdata$filter_user_exceptions DD imagerel $LN6
        DD      imagerel $LN6+73
        DD      imagerel $unwind$filter_user_exceptions
$pdata$main DD  imagerel $LN14
        DD      imagerel $LN14+95
        DD      imagerel $unwind$main
pdata   ENDS
pdata   SEGMENT
$pdata$main$filt$0 DD imagerel main$filt$0
        DD      imagerel main$filt$0+32
        DD      imagerel $unwind$main$filt$0
$pdata$main$filt$1 DD imagerel main$filt$1
        DD      imagerel main$filt$1+30
        DD      imagerel $unwind$main$filt$1
pdata   ENDS

xdata SEGMENT
$unwind$filter_user_exceptions DD 020601H
        DD      030023206H
$unwind$main DD 020609H
        DD      030023206H
        DD      imagerel __C_specific_handler
        DD      02H
        DD      imagerel $LN14+8
        DD      imagerel $LN14+59
        DD      imagerel main$filt$0
        DD      imagerel $LN14+59
        DD      imagerel $LN14+8
        DD      imagerel $LN14+74
        DD      imagerel main$filt$1
        DD      imagerel $LN14+74
$unwind$main$filt$0 DD 020601H
        DD      050023206H
$unwind$main$filt$1 DD 020601H
        DD      050023206H
Xdata   ENDS

_TEXT   SEGMENT
main    PROC
$LN14:
        push    rbx
        sub     rsp, 32
        xor     ebx, ebx
        lea     rcx, OFFSET FLAT:$SG86288 ; 'hello!'
        call    printf
        xor     r9d, r9d
        xor     r8d, r8d
        xor     edx, edx
        mov     ecx, 1122867 ; 00112233H
        call    QWORD PTR __imp_RaiseException
        lea     rcx, OFFSET FLAT:$SG86290 ; '0x112233 raised. now let''s crash'
        call    printf
        mov     DWORD PTR [rbx], 13
        jmp     SHORT $LN13@main
$LN11@main:
        lea     rcx, OFFSET FLAT:$SG86292 ; 'access violation, can''t recover'
        call    printf
        npad    1 ; align next label
$LN13@main:
        jmp     SHORT $LN9@main
$LN7@main:
        lea     rcx, OFFSET FLAT:$SG86294 ; 'user exception caught'
        call    printf
        npad    1 ; align next label
$LN9@main:
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret     0
main    ENDP

text$x SEGMENT
main$filt$0 PROC
        push    rbp
        sub     rsp, 32
        mov     rbp, rdx
$LN10@main$filt$:
        mov     rax, QWORD PTR [rcx]
        xor     ecx, ecx
        cmp     DWORD PTR [rax], -1073741819; c0000005H
        sete    cl
        mov     eax, ecx
$LN12@main$filt$:
        add     rsp, 32
        pop     rbp
        ret     0
        int     3
main$filt$0 ENDP

main$filt$1 PROC
        push    rbp
        sub     rsp, 32
        mov     rbp, rdx
$LN6@main$filt$:
        mov     rax, QWORD PTR [rcx]
        mov     rdx, rcx
        mov     ecx, DWORD PTR [rax]
        call    filter_user_exceptions
        npad    1 ; align next label
$LN8@main$filt$:
        add     rsp, 32
        pop     rbp
        ret     0
        int     3
main$filt$1 ENDP
text$x ENDS

_TEXT SEGMENT
code$ = 48
ep$ = 56
filter_user_exceptions PROC
$LN6:
        push    rbx
        sub     rsp, 32
        mov     ebx, ecx
        mov     edx, ecx
        lea     rcx, OFFSET FLAT:$SG86277 ; 'in filter. code=0x%08X'
        call    printf
        cmp     ebx, 1122867; 00112233H
        jne     SHORT $LN2@filter_use
        lea     rcx, OFFSET FLAT:$SG86279 ; 'yes, that is our exception'
        call    printf
        mov     eax, 1
        add     rsp, 32
        pop     rbx
        ret     0
$LN2@filter_use:
        lea     rcx, OFFSET FLAT:$SG86281 ; 'not our exception'
        call    printf
        xor     eax, eax
        add     rsp, 32
        pop     rbx
        ret     0
filter_user_exceptions ENDP
_TEXT ENDS

要想查看更多信息，可以参考Igor Skochinsky撰写的文章“Compiler Internals:Exceptional and RTTL（编译器内幕:例外与RTTL）”。

除了例外信息外，.pdata段还存储着几乎所有函数的起始和结束的地址。由此可见，.pdata段是自动分析的工具的重点分析对象。

68.3.4　关于SEH的更多信息

Igor Skochinsky编写的文章“Compiler Internals:Exceptional and RTTL（编译器内幕：例外与RTTL）”。

Matt Pietrek编写的文章“A Crash Course on the Depths of Win32 Structured Exception Handling（Win32结构性例外进程的崩溃的深度分析）”。

68.4　Windows NT：临界区段

在任何一个多线程的环境下，临界区段(Critical section)是保护数据一致性和操作互斥性的重要手段。临界区段保证了在同一时间内只会有一个线程访问某些数据，阻止其他进程和中断同期操作相关数据。

在Windows NT系统的数据结构中，CRITICAL_SECTION关键段的定义如下。

指令清单68.14　(Windows Research Kernel v1.2) public/sdk/inc/nturtl.h

typedef struct _RTL_CRITICAL_SECTION {
    PRTL_CRITICAL_SECTION_DEBUG DebugInfo;

    //
    // The following three fields control entering and exiting the critical
    // section for the resource
    //

    LONG LockCount;
    LONG RecursionCount;
    HANDLE OwningThread;        // from the thread's ClientId->UniqueThread
    HANDLE LockSemaphore;
    ULONG_PTR SpinCount;        // force size on 64-bit systems when packed
} RTL_CRITICAL_SECTION, *PRTL_CRITICAL_SECTION;

下面的代码描述了函数EnterCriticalSection()的工作原理。

指令清单68.15　Windows 2008/ntdll.dll/x86（开始）

_RtlEnterCriticalSection@4

var_C           = dword ptr -0Ch
var_8           = dword ptr -8
var_4           = dword ptr -4
arg_0           = dword ptr 8

                mov     edi, edi
                push    ebp
                mov     ebp, esp
                sub     esp, 0Ch
                push    esi
                push    edi
                mov     edi, [ebp+arg_0]
                lea     esi, [edi+4] ; LockCount
                mov     eax, esi
                lock btr dword ptr [eax], 0
                jnb     wait ; jump if CF=0

loc_7DE922DD:
                mov     eax, large fs:18h
                mov     ecx, [eax+24h]
                mov     [edi+0Ch], ecx
                mov     dword ptr [edi+8], 1
                pop     edi
                xor     eax, eax
                pop     esi
                mov     esp, ebp
                pop     ebp
                retn    4

... skipped

在代码段中最重要的指令是BTR（及其LOCK前缀）：BTR指令把第一个操作数的第0位复制给CF标识位，然后再把这个位清零。由LOCK前缀修饰的都是原子性操作，可以让CPU阻止其他的系统总线读取或修改相关内存地址。如果LockCount的第0位值是1，则将其充值重置并退出函数——CPU现在正处于临界区；否则，则表示其他线程正在占用临界区，CPU将等待相关操作结束。

等待期间运行的函数是WaitForSingleObject()。

下述代码描述了LeaveCriticalSection()函数的工作机理：

指令清单68.16　Windows 2008/ntdll.dll/x86（开始）

_RtlLeaveCriticalSection@4 proc near

arg_0           = dword ptr 8

                mov     edi, edi
                push    ebp
                mov     ebp, esp
                push    esi
                mov     esi, [ebp+arg_0]
                add     dword ptr [esi+8], 0FFFFFFFFh ; RecursionCount
                jnz     short loc_7DE922B2
                push    ebx
                push    edi
                lea     edi, [esi+4] ; LockCount
                mov     dword ptr [esi+0Ch], 0
                mov     ebx, 1
                mov     eax, edi
                lock xadd [eax], ebx
                inc     ebx
                cmp     ebx, 0FFFFFFFFh
                jnz     loc_7DEA8EB7

loc_7DE922B0:
                pop     edi
                pop     ebx

loc_7DE922B2:
                xor     eax, eax
                pop     esi
                pop     ebp
                retn    4

... skipped

XADD指令的功能是：先交换操作数的值，然后再进行加法运算。在本例中，它将LockCount与数字1的和存储于第一个操作数，同时将LockCount的初始值传递给EBX寄存器。但是EBX里的这个值也随即被后面的INC指令递增，最终与LockCount的值同步。因为它带有LOCK前缀，所以属于原子操作。这就意味着所有的其他CPU（不管是几核的）都不能同时访问那片内存区域。

LOCK前缀非常重要。不同的CPU或者CPU核心（core）可能会加载同一个进程的不同线程。若使用无LOCK前缀的指令操作临界区段的数据，很可能发生无法预料的情况。

第七部分
常用工具

第69章 反汇编工具

69.1　IDA

IDA PRO简称IDA（Interactive Disassembler），是一个世界顶级的交互式反汇编工具，由总部位于比利时列日市（Liège）的Hex-Rayd公司研发。

Hex-Rayd为希望了解IDA基本功能的用户提供了一个功能有限的免费版本。这个免费版是由5.0版精简而来，它的下载地址是：https://www.hex-rays.com/products/ida/support/download_ freeware.shtml

。

本书的附录F.1收录了IDA常用的快捷键。

第70章 调试工具

70.1　tracer

我很少使用debugger，往往用自己研发的tracer工具对程序进行跟踪和调试。[1]

近期以来，我完全不用debugger了。debugger就是一个在程序执行期间辨别函数的参数，或者在某个断点查看寄存器状态的工具。每次都使用debugger进行调试，未免过于烦琐。所以我就自己编写了tracer工具。tracer程序采用控制台界面，能够从命令行里直接发送命令，同样可以在函数的执行过程中进行中断，并且还能在任意地址设置中断、查看进程状态和修改数据，完成各种各样的任务。

话说回来，在学习和摸索的过程中，初学者还是应当熟悉和掌握debugger程序的使用方法，用debugger查看寄存器的状态变化[2]，观察标识位、数据，并手动修改各项数据，验证数据对程序的影响。

70.2　OllyDbg

OllyDbg是一款十分流行的用户模式（user-mode,Ring 3级）调试程序。它的官方网站是： http://www.ollydbg.de/

。

本书的附录F.2收录了OllyDbg常用的快捷键。

70.3　GDB

GDB不是一款图形化调试器，因而不太受逆向工程研究人员关注。但是它的功能更为强大，可谓独具特色。

本书的附录F.5收录了部分GDB常用指令。

[1]　tracer工具的下载地址是http://yurichev.com/tracer-en.html。

[2]　经典的SoftICE、OllyDbg和WinDbg工具能够用高亮信息提示寄存器变化。

第71章 系统调用的跟踪工具

71.1　strace/dtruss

Linux内核提供了一款非常有用的调试工具，它可以跟踪某个进程调用的系统调用（以及该进程所接收到的信号）[1]。这款工具就是strace。它属于命令行工具，在使用时，我们可以把希望跟踪的应用程序直接指定为命令参数。例如：

# strace df -h

...

access("/etc/ld.so.nohwcap", F_OK)        = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0\220\232\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1770984, ...}) = 0
mmap2(NULL, 1780508, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb75b3000

Mac OS X的dtruss工具与Linux下的strace功能相同。

Cygwin环境里同样也有strace程序。如果我没搞错的话，Cygwin里的strace只能分析那些在Cygwin环境里编译出来的.exe文件。

[1]　有关syscalls的详细介绍，请参见本书第66章。

第72章 反编译工具

Hex-Rays Decompiler是唯一一款众所皆知的、公开销售的、品质较高的C语言反编译工具。它的官方网站是：https://www.hex-rays.com/products/decompiler/

。

第73章 其他工具

Microsoft Visual Studio Express: 鼎鼎大名的Visual Studio的精简版，可用于简单的程序调试。本书的附录F.3收集了它的部分选项及功能。其官方网站是：https://www. visualstudio.com/en-US/products/visual-studio-express-vs。

Hiew2：一款优秀的16进制编辑器，可以对应用程序进行反汇编，而且支持对可执行文件的16进制代码及汇编语言代码的修改，使用起来非常方便。其官方网站是：http://www. hiew.ru/。

Binary grep：在海量文件（包括非可执行程序）中搜索常量或任意字节序列的可执行工具。其官方网站是：https://github.com/yurichev/bgrep。

第八部分
更多范例

第74章 修改任务管理器（Vista）

如果主机上安装的是四核CPU，那么Windows任务管理器应当显示出4个CPU的性能统计图表。本章将要稍微hack 任务管理器，让它显示更多的CPU运算核心。

首先需要了解的问题是：任务管理器如何知道CPU有多少个运算核心？虽然运行于win32用户空间的GetSystemInfo()函数确实可以反馈这一信息，但是任务管理器taskmgr.exe没有直接导入这个函数。它多次调用NTAPI中的NtQuerySystemInformation()函数获取的各种系统信息，也是通过后者了解CPU的具体情况。

NtQuerySystemInformation()函数有四个参数：第一个参数是查询的系统信息类型[1]；第二个参数是一个指针，这个指针用来返回系统的HandleList；第三个参数是程序员指定分配给HandleList的内存空间大小；第四个参数是NtQuerySystemInformation返回的HandleList的大小。

要获取CPU信息，就要在调用它的时候把第一个参数设置为常量SystemBasicInformation[2]。

因此，我们要查找的调用指令大体会是“NtQuerySystemInformation(0, ?, ?, ?)”。第一步当然是用IDA打开taskmgr.exe文件。在处理微软的官方程序时，IDA能够下载与之相应的PDB文件并显示全部函数名称。显而易见的是，任务管理器是用C++编写的程序，而且它使用的函数名称和类（class）名称真的是不为人知。在它使用的类名称里，我们可以看到CAdapter、CNetPage、CPerfPage、CProcInfo、CProcPage、CSvcPage、CTaskPage和CUserPage。这些类名称和任务管理器程序窗口的标签（tab）有对应关系。

在跟踪了NtQuerySystemInformation()函数的每次调用过程之后，我们可以统计出传递给函数的第一个参数。在图74.1中可以看到，部分调用过程里的第一个参数值明显不是零，所以被标记上了“Not Zero”。另外，还有一些函数调用的情况非常特殊，本章的第二部分再进行有关讲解。总之，我们要找那些“第一个参数是零”的、NtQuerySystemInformation()函数的调用语句。

图74.1　IDA：NtQuerySystemInformation()函数的xrefs

那些不公开的名称暂且放置一边。

在检索“NtQuerySystemInformation(0, ?, ?, ?)”的调用语句时，我们可以很快地在InitPerfInfo()里找到如下所示的这种语句。

指令清单74.1　taskmgr.exe (Windows Vista)

.text:10000B4B3            xor     r9d, r9d
.text:10000B4B6            lea     rdx, [rsp+0C78h+var_C58] ; buffer
.text:10000B4BB            xor     ecx, ecx
.text:10000B4BD            lea     ebp, [r9+40h]
.text:10000B4C1            mov     r8d, ebp
.text:10000B4C4            call    cs:__imp_NtQuerySystemInformation ; 0
.text:10000B4CA            xor     ebx, ebx
.text:10000B4CC            cmp     eax, ebx
.text:10000B4CE            jge     short loc_10000B4D7
.text:10000B4D0
.text:10000B4D0 loc_10000B4D0:                     ; CODE XREF: InitPerfInfo(void)+97
.text:10000B4D0                                    ; InitPerInfo(void)+AF
.text:10000B4D0            xor     al, al
.text:10000B4D2            jmp     loc_10000B5EA
.text:10000B4D7 ; ----------------------------------------------------------------------------
.text:10000B4D7
.text:10000B4D7 loc_10000B4D7:                     ; CODE XREF: InitPerfInfo(void)+36
.text:10000B4D7            mov     eax, [rsp+0C78h+var_C50]
.text:10000B4DB            mov     esi, ebx
.text:10000B4DD            mov     r12d, 3E80h
.text:10000B4E3            mov     cs:?g_PageSize@@3KA, eax ; ulong g_PageSize
.text:10000B4E9            shr     eax, 0Ah
.text:10000B4EC            lea     r13, __ImageBase
.text:10000B4F3            imul    eax, [rsp+0C78h+var_C4C]
.text:10000B4F8            cmp     [rsp+0C78h+var_C20], bpl
.text:10000B4FD            mov     cs:?g_MEMMax@@3_JA, rax ; __int64 g_MEMMax
.text:10000B504            movzx   eax, [rsp+0C78h+var_C20] ; no. of CPUs
.text:10000B509            cmova   eax, ebp
.text:10000B50C            cmp     al, bl
.text:10000B50E            mov     cs:?g_cProcessors@@3EA, al ; uchar g_cProcessors

从微软的服务器上下载相应的PDB文件之后，IDA就能够给各个变量分配正确的变量名称。我们不难从中找到全局变量g_cProcessors。

传递给NtQuerySystemInformation()函数的第二个参数（即接收缓冲区）是var_C58。var_C20和var_C58之间的地址差值是0xC58−0xC20=0x38(56)。根据MSDN的官方说明，可知返回值的数据格式如下：

typedef struct _SYSTEM_BASIC_INFORMATION {
     BYTE Reserved1[24];
     PVOID Reserved2[4];
     CCHAR NumberOfProcessors;
} SYSTEM_BASIC_INFORMATION;

因为本例是在x64系统上的演示，所以PVOID占用8个字节。两个“reserved”保留字段共占用24+4×8＝56字节。这意味着var_C20很可能就是_SYSTEM_BASIC_INFORMATION里的NumberOfProcessors字段。

下面我们来验证这一推论。把C:\Windows\System32里的taskmgr.exe复制出来，然后我们在对复制品进行修改，以防Windows的文件保护机制自动恢复原始文件。

使用Hiew打开复制出来的文件，然后找到图74.2所示的程序地址。

图74.2　Hiew：找到修改点

接下来替换MOVZX指令，通过MOV指令直接把返回结果改为64（将CPU设为64核）。由于修改后的指令比原始指令短1个字节，所有我们还需添加1个NOP指令。如图74.3所示。

图74.3　Hiew：修改程序

修改后的这个程序可以正常运行！当然，图表中的统计信息肯定是不正确的。CPU的总负载偶尔还会超过100%。如图74.4所示。

图74.4　被骗的Windows任务管理器

刚才我们把CPU运算核心的总数改为了64（更大的值会使任务管理器崩溃）。显然Windows Vista的任务管理器无法在拥有更多运算核心的计算机上运行。这也可能是微软通过静态的数据结构把有关数值限定在64以下的原因。

74.1　使用LEA指令赋值

任务管理器taskgmr.exe传递NtQuerySystemInformation()第一个参数的指令并非都是MOV指令，部分指令是LEA。

指令清单74.2　taskmgr.exe (Windows Vista)

                 xor     r9d, r9d
                 div     dword ptr [rsp+4C8h+WndClass.lpfnWndProc]
                 lea     rdx, [rsp+4C8h+VersionInformation]
                 lea     ecx, [r9+2]      ; put 2 to ECX
                 mov     r8d, 138h
                 mov     ebx, eax
; ECX=SystemPerformanceInformation
                 call    cs:__imp_NtQuerySystemInformation ; 2
                 ...

                 mov     r8d, 30h
                 lea     r9, [rsp+298h+var_268]
                 lea     rdx, [rsp+298h+var_258]
                 lea     ecx, [r8-2Dh]    ; put 3 to ECX
; ECX=SystemTimeOfDayInformation
                 call    cs:__imp_NtQuerySystemInformation ; not zero

                 ...

                 mov     rbp, [rsi+8]
                 mov     r8d, 20h
                 lea     r9, [rsp+98h+arg_0]
                 lea     rdx, [rsp+98h+var_78]
                 lea     ecx, [r8+2Fh]    ; put 0x4F to ECX
                 mov     [rsp+98h+var_60], ebx
                 mov     [rsp+98h+var_68], rbp
; ECX=SystemSuperfetchInformation
                 call    cs:__imp_NtQuerySystemInformation ; not zero

出现这种指令的具体原因不明。但是MSVC编译器还是经常如此分配指令。或许LEA指令会带来速度或性能方面的好处吧。

您还可以在指令清单64.7（64.5.1节）里看到这种情况。

[1]　后面提到的HandleList就是函数应当反馈的类型信息。

[2]　这个常量的值为零。更多信息请参考MSDN: https://msdn.microsoft.com/en-us/library/ windows/desktop/ms724509(v=vs.85).aspx。

第75章 修改彩球游戏

彩球游戏有多个衍生版本。本章采用的是1997年发布的BallTrix版。这款程序可从http://go. yurichev.com/17311

公开下载。它的图形界面如图75.1所示。

图75.1　游戏界面

本章关注它的随机生成器，以及修改这个组件的具体方法。IDA在balltrix.exe里识别出了标准函数_rand。这个函数的地址是0x00403DA0。不仅如此，IDA还判断出该函数只会被这一处调用。

.text:00402C9C sub_402C9C       proc near               ; CODE XREF: sub_402ACA+52
.text:00402C9C                                          ; sub_402ACA+64 ...
.text:00402C9C
.text:00402C9C arg_0            = dword ptr  8
.text:00402C9C
.text:00402C9C                  push    ebp
.text:00402C9D                  mov     ebp, esp
.text:00402C9F                  push    ebx
.text:00402CA0                  push    esi
.text:00402CA1                  push    edi
.text:00402CA2                  mov     eax, dword_40D430
.text:00402CA7                  imul    eax, dword_40D440
.text:00402CAE                  add     eax, dword_40D5C8
.text:00402CB4                  mov     ecx, 32000
.text:00402CB9                  cdq
.text:00402CBA                  idiv    ecx
.text:00402CBC                  mov     dword_40D440, edx
.text:00402CC2                  call    _rand
.text:00402CC7                  cdq
.text:00402CC8                  idiv    [ebp+arg_0]
.text:00402CCB                  mov     dword_40D430, edx
.text:00402CD1                  mov     eax, dword_40D430
.text:00402CD6                  jmp     $+5
.text:00402CDB                  pop     edi
.text:00402CDC                  pop     esi
.text:00402CDD                  pop     ebx
.text:00402CDE                  leave
.text:00402CDF                  retn
.text:00402CDF sub_402C9C       endp

为了便于讨论，我们把_rand函数的调用方函数叫作“random”。在程序里有三处的代码调用了random函数。

调用random函数的前两处代码是：

.text:00402B16                  mov      eax, dword_40C03C ; 10 here
.text:00402B1B                  push     eax
.text:00402B1C                  call     random
.text:00402B21                  add      esp, 4
.text:00402B24                  inc      eax
.text:00402B25                  mov      [ebp+var_C], eax
.text:00402B28                  mov      eax, dword_40C040 ; 10 here
.text:00402B2D                  push     eax
.text:00402B2E                  call     random
.text:00402B33                  add      esp, 4

调用random函数的第三处代码是：

.text:00402BBB                  mov      eax, dword_40C058 ; 5 here
.text:00402BC0                  push     eax
.text:00402BC1                  call     random
.text:00402BC6                  add      esp, 4
.text:00402BC9                  inc      eax

综合上述代码，我们可以判定该函数只有一个参数。前两处传递的参数是10，第三处传递的参数是5。在观察游戏的界面后，可知棋盘是10×10的方阵，而彩球的颜色总共有5种。这三处调用random的指令，必定是坐标和颜色的生成指令。标准的随机函数rand()函数会生成一个在0～0x7FFF之间的返回值，用起来并不方便。实际上，编程人员会编写自己的随机函数以获取特定区间之内的随机返回值。本例需要的随机数是0～(n−1)之间的整数，n就是函数所需的唯一参数。这一假设可由任意一种debugger验证。

本章将修改第三处调用指令，让它的第三次返回值永远是0。为此，我们可把PUSH/CALL/ADD这三条指令改为NOP，然后再添加XOR EAX,EAX指令，以清空EAX寄存器。

.00402BB8: 83C410         add           esp,010
.00402BBB: A158C04000     mov           eax,[00040C058]
.00402BC0: 31C0           xor           eax,eax
.00402BC2: 90             nop
.00402BC3: 90             nop
.00402BC4: 90             nop
.00402BC5: 90             nop
.00402BC6: 90             nop
.00402BC7: 90             nop
.00402BC8: 90             nop
.00402BC9: 40             inc           eax
.00402BCA: 8B4DF8         mov           ecx,[ebp][-8]
.00402BCD: 8D0C49         lea           ecx,[ecx][ecx]*2
.00402BD0: 8B15F4D54000   mov           edx,[00040D5F4]

也就是说，我们修改调用random()函数的有关指令，让它的返回值固定为0。

修改程序后，它的运行界面如图75.2所示。

图75.2　作弊成功

不得不说我们修改得很成功。我在当初修改这个游戏的时候，希望我的同事明白“没有必要执迷于你肯定会赢的游戏”。可惜我的劝阻没能成功。

另外还有一个问题：为什么random()函数的参数会是全局变量？这是因为棋盘大小是可调整的变量，不能在程序里把它写成常量。本例中的5和10只是它的默认值。

第76章 扫雷（Windows XP）

我的扫雷水平不高，所以干脆用debugger把雷都显示出来吧！

因为地雷的具体位置是随机的，所以扫雷程序里肯定会用随机函数安置地雷。这种随机函数不是某种自制的随机数生成函数，就是标准的C函数rand()。最为美妙的事情是，微软不仅公开了其产品的PDB文件，而且在PDB文件里提供了全部的函数名等符号信息。所以，当我们用IDA打开winmine.exe程序时，它会从微软下载PDB文件并且显示所有函数名称。

在IDA里可以看到，调用rand()函数的指令只有一处：

.text:01003940 ; __stdcall Rnd(x)
.text:01003940 _Rnd@4           proc near                ; CODE XREF: StartGame()+53
.text:01003940                                           ; StartGame()+61
.text:01003940
.text:01003940 arg_0            = dword ptr 4
.text:01003940
.text:01003940                  call     ds:__imp__rand
.text:01003946                  cdq
.text:01003947                  idiv     [esp+arg_0]
.text:0100394B                  mov      eax, edx
.text:0100394D                  retn     4
.text:0100394D _Rnd@4           endp

IDA把这个函数显示为rnd() 函数，那就是说扫雷游戏的开发人员给它起的名字就是rnd。这个函数非常简单：

int Rnd(int limit)
{
     return rand() % limit;
};

微软的PDB文件没有把参数命名为limit。为了便于讨论，本文给它起名为limit。可见，rnd()函数返回值是介于0～limit之间的整数。

Rand()函数的调用方函数也只有一个StartGame()函数。而且StartGame()函数应当就是安放地雷的函数：

.text:010036C7                  push     _xBoxMac
.text:010036CD                  call     _Rnd@4          ; Rnd(x)
.text:010036D2                  push     _yBoxMac
.text:010036D8                  mov      esi, eax
.text:010036DA                  inc      esi
.text:010036DB                  call     _Rnd@4          ; Rnd(x)
.text:010036E0                  inc      eax
.text:010036E1                  mov      ecx, eax
.text:010036E3                  shl      ecx, 5          ; ECX=ECX*32
.text:010036E6                  test     _rgBlk[ecx+esi], 80h
.text:010036EE                  jnz      short loc_10036C7
.text:010036F0                  shl      eax, 5          ; EAX=EAX*32
.text:010036F3                  lea      eax, _rgBlk[eax+esi]
.text:010036FA                  or       byte ptr [eax], 80h
.text:010036FD                  dec      _cBombStart
.text:01003703                  jnz      short loc_10036C7

因为扫雷游戏允许用户设置棋盘大小，所以棋盘的 X(xBoxMac)和 Y(yBoxMac)都是全局变量。Rnd()函数根据这两个参数生成随机坐标，而后0x10036FA处的OR指令设置地雷。如果这个坐标在以前已经设置过地雷了，那么0x010036E6的TEST和JNZ指令将再次生成一次坐标。

变量cBombStart不仅是设置地雷总数的全局变量，还是循环控制变量。

SHL/左移指令意味着棋盘宽度是32。

全局数组rgBlk的容量可通过数据段里rgBlk标签的地址与下一个数据的地址推算出来。数组容量应当是这两个地址之间的差值，即0x360（864）。

.data:01005340 _rgBlk           db 360h dup(?)    ; DATA XREF: MainWndProc(x,x,x,x)+574
.data:01005340                                    ; DisplayBlk(x,x)+23
.data:010056A0 _Preferences     dd  ?             ; DATA XREF: FixMenus()+2
...

数组的元素数量为：864（总容量）/32＝27。

那么，rgBlk是否就是27×32的数组呢？当我们把棋盘设置为100×100的矩阵时，它会自动回滚为24×30的棋盘。所以棋盘盘面的最大值就是这个值，而且无论棋盘有多大，程序都把棋盘数据存储在这个数组里。

接下来，我们使用OllyDbg进行观察。在OllyDbg中运行扫雷游戏，然后在内存窗口里观察rgBlk数组（地址为0x1005340）。[1]

与这个数组有关的内存数据如下：

Address   Hex dump
01005340  10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|
01005350  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005360  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005370  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005380  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005390  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053A0  10 0F 0F 0F|0F 0F 0F 0F|8F 0F 10 0F|0F 0F 0F 0F|
010053B0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053C0  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
010053D0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010053E0  10 0F 0F 0F|0F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
010053F0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005400  10 0F 0F 8F|0F 0F 8F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005410  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005420  10 8F 0F 0F|8F 0F 0F 0F|0F 0F 10 0F|0F 0F 0F 0F|
01005430  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005440  10 8F 0F 0F|0F 0F 8F 0F|0F 8F 10 0F|0F 0F 0F 0F|
01005450  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005460  10 0F 0F 0F|0F 8F 0F 0F|0F 8F 10 0F|0F 0F 0F 0F|
01005470  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
01005480  10 10 10 10|10 10 10 10|10 10 10 0F|0F 0F 0F 0F|
01005490  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054A0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054B0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|
010054C0  0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|0F 0F 0F 0F|

与其他的16进制编辑程序相似，OllyDbg也采取了每行16 字节的显示风格。因此，一个32字节的数组对应着OllyDbg窗口里的两行数据。

启动程序的时候，我们把游戏设置为了“入门级”难度，所以棋盘大小是9×9。现在我们可以在每行0×10个字节的数据里观测到这种正方形结构。

接下来在OllyDbg单击“Run”以运行扫雷程序，然后随意点击、直到触碰地雷为止。此时即可看到棋盘中的全部地雷了，如图76.1所示。

图76.1　地雷

在比较内存数据之后，我们可得出下列结论：

0x10代表边界。

0x0F代表空白地段。

0x8F代表雷区。

现在我们可对内存数据进行标注了。然后我们再用方括号标注地雷：

border:
01005340  10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F
01005350  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #1:
01005360  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
01005370  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #2:
01005380  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
01005390  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #3:
010053A0  10 0F 0F 0F 0F 0F 0F 0F[8F]0F 10 0F 0F 0F 0F 0F
010053B0  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #4:
010053C0  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
010053D0  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #5:
010053E0  10 0F 0F 0F 0F 0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
010053F0  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #6:
01005400  10 0F 0F[8F]0F 0F[8F]0F 0F 0F 10 0F 0F 0F 0F 0F
01005410  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #7:
01005420  10[8F]0F 0F[8F]0F 0F 0F 0F 0F 10 0F 0F 0F 0F 0F
01005430  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #8:
01005440  10[8F]0F 0F 0F 0F[8F]0F 0F[8F]10 0F 0F 0F 0F 0F
01005450  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
line #9:
01005460  10 0F 0F 0F 0F[8F]0F 0F 0F[8F]10 0F 0F 0F 0F 0F
01005470  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F
border:
01005480  10 10 10 10 10 10 10 10 10 10 10 0F 0F 0F 0F 0F
01005490  0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F 0F

把所有的边界数据（0x10）去除，即可得到地雷的确切位置：

 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F 0F 0F 0F 0F 0F[8F]0F
 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F 0F 0F 0F 0F 0F 0F 0F
 0F 0F[8F]0F 0F[8F]0F 0F 0F
[8F]0F 0F[8F]0F 0F 0F 0F 0F
[8F]0F 0F 0F 0F[8F]0F 0F[8F]
 0F 0F 0F 0F[8F]0F 0F 0F[8F]

上述数据的行和列与棋盘的相应信息一一对应。

在推导出数据结构之后，在OllyDbg里修改数据的尝试更为有趣。如果把所有的0x8F都替换成0x0F，那么扫雷游戏就可以如图76.2所示这样玩。

图76.2　没有地雷的扫雷游戏

我们还可以把地雷都安置在第一行里，如图76.3所示。

图76.3　用debugger设置地雷

不过，在玩游戏之前，用OllyDbg之类的debugger查看地雷分布毕竟不够方便。我们不妨写一个专用程序，专门导出棋盘上的地雷分布情况：

// Windows XP MineSweeper cheater
// written by dennis(a)yurichev.com for http://beginners.re/ book
#include <windows.h>
#include <assert.h>
#include <stdio.h>

int main (int argc, char * argv[])
{
        int i, j;
        HANDLE h;
        DWORD PID, address, rd;
        BYTE board[27][32];

        if (argc!=3)
        {
                printf ("Usage: %s <PID><address>\n", argv[0]);
                return 0;
        };

        assert (argv[1]!=NULL);
        assert (argv[2]!=NULL);

        assert (sscanf (argv[1], "%d", &PID)==1);
        assert (sscanf (argv[2], "%x", &address)==1);
        h=OpenProcess (PROCESS_VM_OPERATION | PROCESS_VM_READ | PROCESS_VM_WRITE, FALSE, PID);

        if (h==NULL)
        {
                DWORD e=GetLastError();
                printf ("OpenProcess error: %08X\n", e);
                return 0;
        };

        if (ReadProcessMemory (h, (LPVOID)address, board, sizeof(board), &rd)!=TRUE)
        {
                printf ("ReadProcessMemory() failed\n");
                return 0;
        };

        for (i=1; i<26; i++)
        {
                if (board[i][0]==0x10 && board[i][1]==0x10)
                        break; // end of board
                for (j=1; j<31; j++)
                {
                        if (board[i][j]==0x10)
                                break; // board border
                        if (board[i][j]==0x8F)
                                printf ("*");
                        else
                                printf (" ");

                };
                printf ("\n");
        };

        CloseHandle (h);
};

指定扫雷游戏的PID[2]之后，这个程序将会导出0x01005340处[3]的地雷分布图。

上述程序可以把自身绑定到PID指定的程序上，然后读取指定程序的数据。

76.1　练习题

为什么扫雷游戏里有边界字节0x10？既然程序不会显示这些数据，那么为何还要保留这些数据？去除这些边界字节会发生什么情况？

棋盘上的每个点可被赋予不同的值，以表示“被点击过”“被用户插上棋子”等信息。请找出各个值的具体涵义。

请修改本章的最后一个程序，让它以固定的格局分布地雷。

请修改本章的最后一个程序，使它在没有PDB文件、也不使用预定地址的情况下，自动导出地雷分布图。在扫雷程序运行期间，程序可以在数据段里自动地找到棋盘数据。有关信息可参见附录G.5.1。

[1]　本章以英文版Windows XP SP3中的扫雷游戏为例。如果调试的是其他版本的扫雷游戏，那么内存地址会与本例不同。

[2]　PID即Program/process ID。Windows的任务管理器能够查看程序的PID。

[3]　不同版本的程序，其地址会发生变化。

第77章 人工反编译与Z3 SMT求解法

非专业的密码算法通常都很脆弱。如果密码学专家出手，这些算法将不堪一击。不过，即使密码学专业人士的帮忙，逆向工程分析人员同样可破解密码。

我曾经遇到过一个把64位数据转换为另一种数据的单向函数[1]。那时，我们要把hash值还原为原始数据。

77.1　人工反编译

在IDA中，程序的具体指令如下：

sub_401510     proc near
               ; ECX = input
               mov     rdx, 5D7E0D1F2E0F1F84h
               mov     rax, rcx          ; input
               imul    rax, rdx
               mov     rdx, 388D76AEE8CB1500h
               mov     ecx, eax
               and     ecx, 0Fh
               ror     rax, cl
               xor     rax, rdx
               mov     rdx, 0D2E9EE7E83C4285Bh
               mov     ecx, eax
               and     ecx, 0Fh
               rol     rax, cl
               lea     r8, [rax+rdx]
               mov     rdx, 8888888888888889h
               mov     rax, r8
               mul     rdx
               shr     rdx, 5
               mov     rax, rdx
               lea     rcx, [r8+rdx*4]
               shl     rax, 6
               sub     rcx, rax
               mov     rax, r8
               rol     rax, cl
               ; EAX = output
               retn
sub_401510     endp

ECX寄存器传递函数的第一个参数，由此可判断这是GCC编译的程序。

如果您手头没有Hex-Rays一类的反编译程序，或者您根本信不过这些自动化工具，那么您可以自己进行反编译。在进行反编译时，可以把CPU寄存器当作C语言的变量，然后把汇编语言直接翻译为等效的C指令。例如，上述程序可反编译为：

uint64_t f(uint64_t input)
{
        uint64_t  rax, rbx, rcx, rdx, r8;

        ecx=input;

        rdx=0x5D7E0D1F2E0F1F84;
        rax=rcx;
        rax*=rdx;
        rdx=0x388D76AEE8CB1500;
        rax=_lrotr(rax, rax&0xF); // rotate right
        rax^=rdx;
        rdx=0xD2E9EE7E83C4285B;
        rax=_lrotl(rax, rax&0xF); // rotate left
        r8=rax+rdx;
        rdx=0x8888888888888889;
        rax=r8;
        rax*=rdx;
        rdx=rdx>>5;
        rax=rdx;
        rcx=r8+rdx*4;
        rax=rax<<6;
        rcx=rcx-rax;
        rax=r8
        rax=_lrotl (rax, rcx&0xFF); // rotate left
        return rax;
};

力图谨慎的人，可以把上述代码再次编译为可执行程序。在工作方式上，它应当与最初的程序完全一致。

然后，我们根据寄存器的使用方法，整理刚才写出的C代码。这时就需要加倍小心、高度集中注意力，任何细小的纰漏都可能让我们前功尽弃。

首先添加上注释、进行段落划分：

uint64_t f(uint64_t input)
{
        uint64_t rax, rbx, rcx, rdx, r8;

        ecx=input;

        rdx=0x5D7E0D1F2E0F1F84;
        rax=rcx;
        rax*=rdx;
        rdx=0x388D76AEE8CB1500;
        rax=_lrotr(rax, rax&0xF); // rotate right
        rax^=rdx;
        rdx=0xD2E9EE7E83C4285B;
        rax=_lrotl(rax, rax&0xF); // rotate left
        r8=rax+rdx;

        rdx=0x8888888888888889;
        rax=r8;
        rax*=rdx;
        // RDX here is a high part of multiplication result
        rdx=rdx>>5;
        // RDX here is division result!
        rax=rdx;

        rcx=r8+rdx*4;
        rax=rax<<6;
        rcx=rcx-rax;
        rax=r8
        rax=_lrotl (rax, rcx&0xFF); // rotate left
        return rax;
};

接下来整理程序尾部的数学计算指令：

uint64_t f(uint64_t input)
{
        uint64_t rax, rbx, rcx, rdx, r8;

        ecx=input;

        rdx=0x5D7E0D1F2E0F1F84;
        rax=rcx;
        rax*=rdx;
        rdx=0x388D76AEE8CB1500;
        rax=_lrotr(rax, rax&0xF); // rotate right
        rax^=rdx;
        rdx=0xD2E9EE7E83C4285B;
        rax=_lrotl(rax, rax&0xF); // rotate left
        r8=rax+rdx;

        rdx=0x8888888888888889;
        rax=r8;
        rax*=rdx;
        // RDX here is a high part of multiplication result
        rdx=rdx>>5;
        // RDX here is division result!
        rax=rdx;

        rcx=(r8+rdx*4)-(rax<<6);
        rax=r8
        rax=_lrotl (rax, rcx&0xFF); // rotate left
        return rax;
};

根据乘法因子的特点，我们应能判断出程序是通过乘法指令等效实现的除法运算。[2]因此，我们可以使用Wolfram Mathematica来计算除数。

指令清单77.1　Wolfram Mathematica

In[1]:=N[2^(64 + 5)/16^^8888888888888889]
Out[1]:=60.

那么，原始的运算指令应当是：

uint64_t f(uint64_t input)
{
        uint64_t rax, rbx, rcx, rdx, r8;

        ecx=input;

        rdx=0x5D7E0D1F2E0F1F84;
        rax=rcx;
        rax*=rdx;
        rdx=0x388D76AEE8CB1500;
        rax=_lrotr(rax, rax&0xF); // rotate right
        rax^=rdx;
        rdx=0xD2E9EE7E83C4285B;
        rax=_lrotl(rax, rax&0xF); // rotate left
        r8=rax+rdx;

        rax=rdx=r8/60;

        rcx=(r8+rax*4)-(rax*64);
        rax=r8
        rax=_lrotl (rax, rcx&0xFF); // rotate left
        return rax;
};

整理计算指令得：

uint64_t f(uint64_t input)
{
        uint64_t rax, rbx, rcx, rdx, r8;

        rax=input;
        rax*=0x5D7E0D1F2E0F1F84;
        rax=_lrotr(rax, rax&0xF); // rotate right
        rax^=0x388D76AEE8CB1500;
        rax=_lrotl(rax, rax&0xF); // rotate left
        r8=rax+0xD2E9EE7E83C4285B;

        rcx=r8-(r8/60)*60;
        rax=r8
        rax=_lrotl (rax, rcx&0xFF); // rotate left
        return rax;
};

继续简化代码可发现，程序计算的是余数、而非商：

uint64_t f(uint64_t input)
{
        uint64_t rax, rbx, rcx, rdx, r8;

        rax=input;
        rax*=0x5D7E0D1F2E0F1F84;
        rax=_lrotr(rax, rax&0xF); // rotate right
        rax^=0x388D76AEE8CB1500;
        rax=_lrotl(rax, rax&0xF); // rotate left
        r8=rax+0xD2E9EE7E83C4285B;

        return _lrotl (r8, r8 % 60); // rotate left
};

最终，我们把程序转化成华丽的C语言代码：

#include <stdio.h>
#include <stdint.h>
#include <stdbool.h>
#include <string.h>
#include <intrin.h>

#define C1 0x5D7E0D1F2E0F1F84
#define C2 0x388D76AEE8CB1500
#define C3 0xD2E9EE7E83C4285B

uint64_t hash(uint64_t v)
{
        v*=C1;
        v=_lrotr(v, v&0xF); // rotate right
        v^=C2;
        v=_lrotl(v, v&0xF); // rotate left
        v+=C3;
        v=_lrotl(v, v % 60); // rotate left
        return v;
};

int main()
{
        printf ("%llu\n", hash(...));
};

除了密码专家之外，没什么人能够根据hash值逆推原始数据。旋转位左/右移指令足以令人望而却步——它能够保证映射函数不是单满射函数，而且还保留了碰撞的可能性；说得直白一些就是“多个输入可能产生同一个输出”。

由于这个函数采用了64位因子，所以暴力破解也不太现实。

77.2　Z3 SMT求解法

在加密学知识不足的情况下，我们可以使用微软研究团队发布的Z3工具[3]尝试破解。虽然它只是个形式验证工具，但是我们将用它来作SMT求解。也就是说，我们要用Z3来求解巨型方程式。

我们使用的Python源代码如下：

 1  from z3 import *
 2
 3  C1=0x5D7E0D1F2E0F1F84
 4  C2=0x388D76AEE8CB1500
 5  C3=0xD2E9EE7E83C4285B
 6
 7  inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
 8
 9  s = Solver()
10  s.add(i1==inp*C1)
11  s.add(i2==RotateRight (i1, i1 & 0xF))
12  s.add(i3==i2 ^ C2)
13  s.add(i4==RotateLeft(i3, i3 & 0xF))
14  s.add(i5==i4 + C3)
15  s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17  s.add(outp==10816636949158156260)
18
19  print s.check()
20  m=s.model()
21  print m
22  print (" inp=0x%X" % m[inp].as_long())
23  print ("outp=0x%X" % m[outp].as_long())

程序的第7行声明了各个变量。这些都是64位变量。其中，i1～i6都是中间变量（形参），在各个指令之间传递寄存器的值。

第10～15行之间是我们添加的约束条件。这些条件之中，第17行限定的约束条件最为重要：在使用这个函数时，我们要查找输出值为10816636949158156260的输入值。

本质上说，基于SMT的求解方法可以搜索满足全部限定条件的所有输入值。

上述程序中的RotateRight、RotateLeft和URem都是Z3提供的Python API。它们都不是Python语言提供的标准指令。

然后运行上述程序：

...>python.exe 1.py
sat
[i1 = 3959740824832824396,
 i3 = 8957124831728646493,
 i5 = 10816636949158156260,
 inp = 1364123924608584563,
 outp = 10816636949158156260,
 i4 = 14065440378185297801,
 i2 = 4954926323707358301]
 inp=0x12EE577B63E80B73
outp=0x961C69FF0AEFD7E4

程序输出中的“sat”是“satisfiable（满足条件的值）”的缩写。这就是说，我们的求解方法至少可以找到一个解。程序用方括号把最终解标注了出来。屏幕输出的最后两行是用16进制显示的输入、输出值。如果把0x12EE577B63E80B73代入原函数的输入变量，那么它的输出值与我们指定的值相符。

另外需要注意的是，因为原函数不是单满射函数，所以可能存在多个符合条件的输入值。不过，网上公开的Z3 SMT求解程序只会计算出一组解。为此，我们对上面的程序稍加修改，添加了第19行，让程序“探寻其他的解”：

 1  from z3 import *
 2
 3  C1=0x5D7E0D1F2E0F1F84
 4  C2=0x388D76AEE8CB1500
 5  C3=0xD2E9EE7E83C4285B
 6
 7  inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
 8
 9  s = Solver()
10  s.add(i1==inp*C1)
11  s.add(i2==RotateRight (i1, i1 & 0xF))
12  s.add(i3==i2 ^ C2)
13  s.add(i4==RotateLeft(i3, i3 & 0xF))
14  s.add(i5==i4 + C3)
15  s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17  s.add(outp==10816636949158156260)
18
19  s.add(inp!=0x12EE577B63E80B73)
20
21  print s.check()
22  m=s.model()
23  print m
24  print (" inp=0x%X" % m[inp].as_long())
25  print ("outp=0x%X" % m[outp].as_long())

这样一来，它就可以求得另一组解：

...>python.exe 2.py
sat
[i1 = 3959740824832824396,
 i3 = 8957124831728646493,
 i5 = 10816636949158156260,
 inp = 10587495961463360371,
 outp = 10816636949158156260,
 i4 = 14065440378185297801,
 i2 = 4954926323707358301]
 inp=0x92EE577B63E80B73
 outp=0x961C69FF0AEFD7E4

人工排除已知解的方法不太先进。其实程序可以自动地修改约束条件，并且自行排除已知解，以便自动化地求得所有解。自行求得全部解的程序十分精巧：

 1  from z3 import *
 2
 3  C1=0x5D7E0D1F2E0F1F84
 4  C2=0x388D76AEE8CB1500
 5  C3=0xD2E9EE7E83C4285B
 6
 7  inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
 8
 9  s = Solver()
10  s.add(i1==inp*C1)
11  s.add(i2==RotateRight (i1, i1 & 0xF))
12  s.add(i3==i2 ^ C2)
13  s.add(i4==RotateLeft(i3, i3 & 0xF))
14  s.add(i5==i4 + C3)
15  s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17  s.add(outp==10816636949158156260)
18
19  # copypasted  from http://stackoverflow.com/questions/11867611/z3py-checking-all-solutions-for-equation
20  result=[]
21  while True:
22       if s.check() == sat:
23            m = s.model()
24            print m[inp]
25            result.append(m)
26            # Create a new constraint the blocks the current model
27            block = []
28            for d in m:
29                 # d is a declaration
30                 if d.arity() > 0:
31                      raise Z3Exception("uninterpreted functions are not supported")
32                 # create a constant from declaration
33                 c=d()
34                 if is_array(c) or c.sort().kind() == Z3_UNINTERPRETED_SORT:
35                      raise Z3Exception("arrays and uninterpreted sorts are not supported")
36                 block.append(c != m[d])
37            s.add(Or(block))
38       else:
39                 print "results total=",len(result)
40                 break

运行上述程序，可得：

1364123924608584563
1234567890
9223372038089343698
4611686019661955794
13835058056516731602
3096040143925676201
12319412180780452009
7707726162353064105
16931098199207839913
1906652839273745429
11130024876128521237
15741710894555909141
6518338857701133333
5975809943035972467
15199181979890748275
10587495961463360371
results total= 16

可见，总共有16个输入值满足条件“输出值为0x92EE577B63E80B73”。

第二个解是1234567890。在编写本文时，笔者使用的输入值正是这个数。

接下来，我们要更深入地讨论程序的算法。在这些解里面，有没有低32位与输出值的低32位相等的解？

为此，我们修改本章的第一个脚本程序，对第17行的限定条件进行修改：

 1  from z3 import *
 2
 3  C1=0x5D7E0D1F2E0F1F84
 4  C2=0x388D76AEE8CB1500
 5  C3=0xD2E9EE7E83C4285B
 6
 7  inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
 8
 9  s = Solver()
10  s.add(i1==inp*C1)
11  s.add(i2==RotateRight (i1, i1 & 0xF))
12  s.add(i3==i2 ^ C2)
13  s.add(i4==RotateLeft(i3, i3 & 0xF))
14  s.add(i5==i4 + C3)
15  s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17  s.add(outp & 0xFFFFFFFF == inp & 0xFFFFFFFF)
18
19  print s.check()
20  m=s.model()
21  print m
22  print (" inp=0x%X" % m[inp].as_long())
23  print ("outp=0x%X" % m[outp].as_long())

上述程序证明，确实存在这种解：

sat
[i1 = 14869545517796235860,
 i3 = 8388171335828825253,
 i5 = 6918262285561543945,
 inp = 1370377541658871093,
 outp = 14543180351754208565,
 i4 = 10167065714588685486,
 i2 = 5541032613289652645]
 inp=0x13048F1D12C00535
outp=0xC9D3C17A12C00535

在此基础上，我们再添加一个约束条件——验证是否存在“最后16位是0x1234”的解：

 1  from z3 import *
 2
 3  C1=0x5D7E0D1F2E0F1F84
 4  C2=0x388D76AEE8CB1500
 5  C3=0xD2E9EE7E83C4285B
 6
 7  inp, i1, i2, i3, i4, i5, i6, outp = BitVecs('inp i1 i2 i3 i4 i5 i6 outp', 64)
 8
 9  s = Solver()
10  s.add(i1==inp*C1)
11  s.add(i2==RotateRight (i1, i1 & 0xF))
12  s.add(i3==i2 ^ C2)
13  s.add(i4==RotateLeft(i3, i3 & 0xF))
14  s.add(i5==i4 + C3)
15  s.add(outp==RotateLeft (i5, URem(i5, 60)))
16
17  s.add(outp & 0xFFFFFFFF == inp & 0xFFFFFFFF)
18  s.add(outp & 0xFFFF == 0x1234)
19
20  print s.check()
21  m=s.model()
22  print m
23  print (" inp=0x%X" % m[inp].as_long())
24  print ("outp=0x%X" % m[outp].as_long())

即使有如此苛刻的约束条件，程序仍然算出了一个解：

sat
[i1 = 2834222860503985872,
 i3 = 2294680776671411152,
 i5 = 17492621421353821227,
 inp = 461881484695179828,
 outp = 419247225543463476,
 i4 = 2294680776671411152,
 i2 = 2834222860503985872]
 inp=0x668EEC35F961234
outp=0x5D177215F961234

Z3函数的破解速度非常快。这说明原始算法十分脆弱，根本算不上加密算法。业余人员自制的算法多数都是这样。

这种方法是否可以破解那些真正专业的加密算法呢？虽然像AES、RSA这样的加密算法都同样属于大规模方程组，但是其计算规模非常之大，以至于未来几年的计算机系统都不可能对其进行破解。当然，加密学专家非常清楚这件事。

总而言之，SMT/SAT求解程序（例如Z3）可以处理非专业的加密算法。

另外，我还写过一篇与Z3有关的博客。有兴趣的读者可查阅参考文献[Yur12]。

[1]　单向函数的有关概念，可参见本书第34章的介绍。

[2]　可参见本书第41章。

[3]　http://z3.codeplex.com/。

第78章 加密狗

78.1　例1：PowerPC平台的MacOS Classic程序

本例研究的是一款运行在PowerPC平台上的MacOS Classic程序。研发这款程序的公司早已经不知去向，所以买家十分害怕加密狗出什么意外。

在不插入加密狗的情况下，程序会显示“Invalid Security Device”信息。非常幸运的是，这个字符串就在程序的可执行文件里。

虽然那时我即不熟悉Mac OS Classic系统，也没怎么用过PowerPC，但是还是放手一搏。

IDA可以毫无困难地打开这个程序。它判断该文件类型为“PEF (Mac OS or Be OS executable)”。标准的Mac OS Classic的程序文件的确采用了这种文件格式。

接下来，在文件里搜索错误信息的字符串时，我找到了下述指令：

...

seg000:000C87FC 38 60 00 01              li      %r3, 1
seg000:000C8800 48 03 93 41              bl      check1
seg000:000C8804 60 00 00 00              nop
seg000:000C8808 54 60 06 3F              clrlwi. %r0, %r3, 24
seg000:000C880C 40 82 00 40              bne     OK
seg000:000C8810 80 62 9F D8              lwz     %r3, TC_aInvalidSecurityDevice

...

这些都是PowerPC平台的指令。这款CPU是20世纪90年代出产的一款典型的32位RISC CPU。它的每条指令都占用4个字节（与MIPS和ARM的指令相似），指令名称还与MIPS指令相似。

为了便于演示，我把函数名称改为check1()。BL是Brach Link指令，常用于调用子函数。上述程序的关键点是BNE指令，在程序通过了加密狗认证的情况下进行跳转，否则就会在r3寄存器里加载字符串，然后报错。

在参阅了参考文献［SK95］之后，我发现r3寄存器用于存储返回值。如果返回值是64位数据，那么r4寄存器也会用于回传返回值。

另外，CLRLWI指令[1]还是当时的盲点。后来我阅读了参考文献［IBM00］，获悉它是清除和传递数据的复合指令。本例的这个指令会清除r3寄存器的高24位，把结果存储在r0寄存器里。所以，它不仅相当于x86的MOVZX指令（可参见本书15.1.1节），而且还能设置相应标识位，向后面的BNE指令传递标识信息。

接下来，我们探索一下check 1()函数：

seg000:00101B40                 check1: # CODE XREF: seg000:00063E7Cp
seg000:00101B40                         # sub_64070+160p ...
seg000:00101B40
seg000:00101B40                 .set arg_8, 8
seg000:00101B40
seg000:00101B40 7C 08 02 A6             mflr    %r0
seg000:00101B44 90 01 00 08             stw     %r0, arg_8(%sp)
seg000:00101B48 94 21 FF C0             stwu    %sp, -0x40(%sp)
seg000:00101B4C 48 01 6B 39             bl      check2
seg000:00101B50 60 00 00 00             nop
seg000:00101B54 80 01 00 48             lwz     %r0, 0x40+arg_8(%sp)
seg000:00101B58 38 21 00 40             addi    %sp, %sp, 0x40
seg000:00101B5C 7C 08 03 A6             mtlr    %r0
seg000:00101B60 4E 80 00 20             blr
seg000:00101B60                 # End of function check1

在IDA中，我们可以清楚地观察到：虽然程序的多个指令都调用了这个函数，但是调用方函数在调用结束之后只访问了r3寄存器的值。这个函数只起到调用其他函数的功能，因此它就是形实转换函数：即使函数序言和函数尾声都十分完整，但是对r3寄存器完全没有操作。据此判断，check1()函数的返回值与check()函数一致。

BLR指令[2]似乎是函数返回语句，可作为划分函数模块的标识。但是IDA能够识别并划分函数体，因此我们可以先不管它。由于本程序采用的是RISC（精简指令集）指令，调用方函数会通过链接寄存器（Link Register）向被调用方函数传递返回地址。就这些特征来看，PowerPC的程序与ARM程序有很多共同点。

check2()函数略为复杂：

seg000:00118684            check2: # CODE XREF: check1+Cp
seg000:00118684
seg000:00118684            .set var_18, -0x18
seg000:00118684            .set var_C, -0xC
seg000:00118684            .set var_8, -8
seg000:00118684            .set var_4, -4
seg000:00118684            .set arg_8,  8
seg000:00118684
seg000:00118684 93 E1 FF FC   stw     %r31, var_4(%sp)
seg000:00118688 7C 08 02 A6   mflr    %r0
seg000:0011868C 83 E2 95 A8   lwz     %r31, off_1485E8 # dword_24B704
seg000:00118690               .using  dword_24B704, %r31
seg000:00118690 93 C1 FF F8   stw     %r30, var_8(%sp)
seg000:00118694 93 A1 FF F4   stw     %r29, var_C(%sp)
seg000:00118698 7C 7D 1B 78   mr      %r29, %r3
seg000:0011869C 90 01 00 08   stw     %r0, arg_8(%sp)
seg000:001186A0 54 60 06 3E   clrlwi  %r0, %r3, 24
seg000:001186A4 28 00 00 01   cmplwi  %r0, 1
seg000:001186A8 94 21 FF B0   stwu    %sp, -0x50(%sp)
seg000:001186AC 40 82 00 0C   bne     loc_1186B8
seg000:001186B0 38 60 00 01   li      %r3, 1
seg000:001186B4 48 00 00 6C   b       exit
seg000:001186B8
seg000:001186B8           loc_1186B8:   # CODE XREF: check2+28j
seg000:001186B8 48 00 03 D5   bl      sub_118A8C
seg000:001186BC 60 00 00 00   nop
seg000:001186C0 3B C0 00 00   li      %r30, 0
seg000:001186C4
seg000:001186C4           skip:         # CODE XREF: check2+94j
seg000:001186C4 57 C0 06 3F   clrlwi. %r0, %r30, 24
seg000:001186C8 41 82 00 18   beq     loc_1186E0
seg000:001186CC 38 61 00 38   addi    %r3, %sp, 0x50+var_18
seg000:001186D0 80 9F 00 00   lwz     %r4, dword_24B704
seg000:001186D4 48 00 C0 55   bl      .RBEFINDNEXT
seg000:001186D8 60 00 00 00   nop
seg000:001186DC 48 00 00 1C   b       loc_1186F8
seg000:001186E0
seg000:001186E0           loc_1186E0:   # CODE XREF: check2+44j
seg000:001186E0 80 BF 00 00   lwz     %r5, dword_24B704
seg000:001186E4 38 81 00 38   addi    %r4, %sp, 0x50+var_18
seg000:001186E8 38 60 08 C2   li      %r3, 0x1234
seg000:001186EC 48 00 BF 99   bl      .RBEFINDFIRST
seg000:001186F0 60 00 00 00   nop
seg000:001186F4 3B C0 00 01   li      %r30, 1
seg000:001186F8
seg000:001186F8           loc_1186F8:   # CODE XREF: check2+58j
seg000:001186F8 54 60 04 3F   clrlwi. %r0, %r3, 16
seg000:001186FC 41 82 00 0C   beq     must_jump
seg000:00118700 38 60 00 00   li      %r3, 0             # error
seg000:00118704 48 00 00 1C   b       exit
seg000:00118708
seg000:00118708           must_jump:    # CODE XREF: check2+78j
seg000:00118708 7F A3 EB 78   mr      %r3, %r29
seg000:0011870C 48 00 00 31   bl      check3
seg000:00118710 60 00 00 00   nop
seg000:00118714 54 60 06 3F   clrlwi. %r0,%r3, 24
seg000:00118718 41 82 FF AC   beq     skip
seg000:0011871C 38 60 00 01   li      %r3, 1
seg000:00118720
seg000:00118720           exit:         # CODE XREF: check2+30j
seg000:00118720                         # check2+80j
seg000:00118720 80 01 00 58   lwz     %r0, 0x50+arg_8(%sp)
seg000:00118724 38 21 00 50   addi    %sp, %sp, 0x50
seg000:00118728 83 E1 FF FC   lwz     %r31, var_4(%sp)
seg000:0011872C 7C 08 03 A6   mtlr    %r0
seg000:00118730 83 C1 FF F8   lwz     %r30, var_8(%sp)
seg000:00118734 83 A1 FF F4   lwz     %r29, var_C(%sp)
seg000:00118738 4E 80 00 20   blr
seg000:00118738             # End of function check2

因为可执行文件保留了部分函数名称，所以分析的难度并非很高。例如，程序文件里有.RBEFINDNEXT() 和 .RBEFINDFIRST()等函数名。这可能是编译器留下的调试符号。虽然无法确定这种情况的具体原因，但是在不了解文件格式的情况下，我们可以参考与之类似的PE文件格式（可参考68.2.7节）。而这些函数最终都调用了.GetNextDeviceViaUSB()函数和.USBSendPKT()函数。从函数名称可以判断，它们都是访问USB加密狗的函数。

程序里甚至还直接调用了.GetNextEve3Device()函数。这个函数在20世纪90年代就非常著名。程序往往通过这个函数访问Mac设备上的ADB口，最终访问Sentinel Eve3 加密狗。

我们首先要把其他问题搁置一边，重点关注r3寄存器在函数返回前的赋值过程。在分析前面的指令时，我们已经知道，如果r3寄存器的值为零，那么程序将转向错误提示信息的信息窗口；所以，我们关注的是对r3寄存器进行非零赋值的指令。

上述程序中，有两条“li %r3,｛非零值｝”指令，有一条“li %r3, 0”指令。LI是Load Immediate的缩写，可见li指令的作用是“令寄存器加载立即数”。第一条指令的地址是0x001186B0。坦白地讲，如果要了解它的具体作用，还需要进一步学习PowerPC平台的汇编语言。

然而下一处简明易懂。它调用.RBEFINDFIRST()函数。如果函数验证失败，则r3的值为0，程序将跳转到exit（退出）；否则，继续调用check3()函数。如果check3()函数的验证失败，那么程序将调用.RBEFINDNEXT()函数，大概是检测下一个USB口的意思吧。

前文我们介绍过“clrlwi %r0, %r3, 16”的具体功能了。要注意的是，它清除的是16位数据。这就代表着.REBFINDFIRST()函数的返回值多半也是16位数据。

此外，B（branch）指令是无条件转移指令，BEQ的触发条件和BNE相反。这些指令就不再介绍了。

下面来分析check3()函数：

`

seg000:0011873C              check3: # CODE XREF: check2+88p
seg000:0011873C
seg000:0011873C              .set var_18, -0x18
seg000:0011873C              .set var_C, -0xC
seg000:0011873C              .set var_8, -8
seg000:0011873C              .set var_4, -4
seg000:0011873C              .set arg_8,   8
seg000:0011873C
seg000:0011873C 93 E1 FF FC    stw    %r31, var_4(%sp)
seg000:00118740 7C 08 02 A6    mflr   %r0
seg000:00118744 38 A0 00 00    li     %r5, 0
seg000:00118748 93 C1 FF F8    stw    %r30, var_8(%sp)
seg000:0011874C 83 C2 95 A8    lwz    %r30, off_1485E8 # dword_24B704
seg000:00118750                .using dword_24B704, %r30
seg000:00118750 93 A1 FF F4    stw    %r29, var_C(%sp)
seg000:00118754 3B A3 00 00    addi   %r29, %r3, 0
seg000:00118758 38 60 00 00    li     %r3, 0
seg000:0011875C 90 01 00 08    stw    %r0, arg_8(%sp)
seg000:00118760 94 21 FF B0    stwu   %sp, -0x50(%sp)
seg000:00118764 80 DE 00 00    lwz    %r6, dword_24B704
seg000:00118768 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:0011876C 48 00 C0 5D    bl     .RBEREAD
seg000:00118770 60 00 00 00    nop
seg000:00118774 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:00118778 41 82 00 0C    beq    loc_118784
seg000:0011877C 38 60 00 00    li     %r3, 0
seg000:00118780 48 00 02 F0    b      exit
seg000:00118784
seg000:00118784              loc_118784: # CODE XREF: check3+3Cj
seg000:00118784 A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:00118788 28 00 04 B2    cmplwi %r0, 0x1100
seg000:0011878C 41 82 00 0C    beq    loc_118798
seg000:00118790 38 60 00 00    li     %r3, 0
seg000:00118794 48 00 02 DC    b      exit
seg000:00118798
seg000:00118798              loc_118798: # CODE XREF: check3+50j
seg000:00118798 80 DE 00 00    lwz    %r6, dword_24B704
seg000:0011879C 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:001187A0 38 60 00 01    li     %r3, 1
seg000:001187A4 38 A0 00 00    li     %r5, 0
seg000:001187A8 48 00 C0 21    bl     .RBEREAD
seg000:001187AC 60 00 00 00    nop
seg000:001187B0 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:001187B4 41 82 00 0C    beq    loc_1187C0
seg000:001187B8 38 60 00 00    li     %r3, 0
seg000:001187BC 48 00 02 B4    b      exit
seg000:001187C0
seg000:001187C0              loc_1187C0: # CODE XREF: check3+78j
seg000:001187C0 A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:001187C4 28 00 06 4B    cmplwi %r0, 0x09AB
seg000:001187C8 41 82 00 0C    beq    loc_1187D4
seg000:001187CC 38 60 00 00    li     %r3, 0
seg000:001187D0 48 00 02 A0    b      exit
seg000:001187D4
seg000:001187D4              loc_1187D4: # CODE XREF: check3+8Cj
seg000:001187D4 4B F9 F3 D9    bl     sub_B7BAC
seg000:001187D8 60 00 00 00    nop
seg000:001187DC 54 60 06 3E    clrlwi %r0, %r3, 24
seg000:001187E0 2C 00 00 05    cmpwi  %r0, 5
seg000:001187E4 41 82 01 00    beq    loc_1188E4
seg000:001187E8 40 80 00 10    bge    loc_1187F8
seg000:001187EC 2C 00 00 04    cmpwi  %r0, 4
seg000:001187F0 40 80 00 58    bge    loc_118848
seg000:001187F4 48 00 01 8C    b      loc_118980
seg000:001187F8
seg000:001187F8              loc_1187F8: # CODE XREF: check3+ACj
seg000:001187F8 2C 00 00 0B    cmpwi  %r0, 0xB
seg000:001187FC 41 82 00 08    beq    loc_118804
seg000:00118800 48 00 01 80    b      loc_118980
seg000:00118804
seg000:00118804              loc_118804: # CODE XREF: check3+C0j
seg000:00118804 80 DE 00 00    lwz    %r6, dword_24B704
seg000:00118808 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:0011880C 38 60 00 08    li     %r3, 8
seg000:00118810 38 A0 00 00    li     %r5, 0
seg000:00118814 48 00 BF B5    bl     .RBEREAD
seg000:00118818 60 00 00 00    nop
seg000:0011881C 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:00118820 41 82 00 0C    beq    loc_11882C
seg000:00118824 38 60 00 00    li     %r3, 0
seg000:00118828 48 00 02 48    b      exit
seg000:0011882C
seg000:0011882C              loc_11882C: # CODE XREF: check3+E4j
seg000:0011882C A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:00118830 28 00 11 30    cmplwi %r0, 0xFEA0
seg000:00118834 41 82 00 0C    beq    loc_118840
seg000:00118838 38 60 00 00    li     %r3, 0
seg000:0011883C 48 00 02 34    b      exit
seg000:00118840
seg000:00118840              loc_118840: # CODE XREF: check3+F8j
seg000:00118840 38 60 00 01    li     %r3, 1
seg000:00118844 48 00 02 2C    b      exit
seg000:00118848
seg000:00118848              loc_118848: # CODE XREF: check3+B4j
seg000:00118848 80 DE 00 00    lwz    %r6, dword_24B704
seg000:0011884C 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:00118850 38 60 00 0A    li     %r3, 0xA
seg000:00118854 38 A0 00 00    li     %r5, 0
seg000:00118858 48 00 BF 71    bl     .RBEREAD
seg000:0011885C 60 00 00 00    nop
seg000:00118860 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:00118864 41 82 00 0C    beq    loc_118870
seg000:00118868 38 60 00 00    li     %r3, 0
seg000:0011886C 48 00 02 04    b      exit
seg000:00118870
seg000:00118870              loc_118870: # CODE XREF: check3+128j
seg000:00118870 A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:00118874 28 00 03 F3    cmplwi %r0, 0xA6E1
seg000:00118878 41 82 00 0C    beq    loc_118884
seg000:0011887C 38 60 00 00    li     %r3, 0
seg000:00118880 48 00 01 F0    b      exit
seg000:00118884
seg000:00118884                loc_118884: # CODE XREF: check3+13Cj
seg000:00118884 57 BF 06 3E    clrlwi  %r31, %r29, 24
seg000:00118888 28 1F 00 02    cmplwi  %r31, 2
seg000:0011888C 40 82 00 0C    bne    loc_118898
seg000:00118890 38 60 00 01    li     %r3, 1
seg000:00118894 48 00 01 DC    b      exit
seg000:00118898
seg000:00118898              loc_118898: # CODE XREF: check3+150j
seg000:00118898 80 DE 00 00    lwz    %r6, dword_24B704
seg000:0011889C 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:001188A0 38 60 00 0B    li     %r3, 0xB
seg000:001188A4 38 A0 00 00    li     %r5, 0
seg000:001188A8 48 00 BF 21    bl     .RBEREAD
seg000:001188AC 60 00 00 00    nop
seg000:001188B0 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:001188B4 41 82 00 0C    beq    loc_1188C0
seg000:001188B8 38 60 00 00    li     %r3, 0
seg000:001188BC 48 00 01 B4    b      exit
seg000:001188C0
seg000:001188C0              loc_1188C0: # CODE XREF: check3+178j
seg000:001188C0 A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:001188C4 28 00 23 1C    cmplwi %r0, 0x1C20
seg000:001188C8 41 82 00 0C    beq    loc_1188D4
seg000:001188CC 38 60 00 00    li     %r3, 0
seg000:001188D0 48 00 01 A0    b      exit
seg000:001188D4
seg000:001188D4              loc_1188D4: # CODE XREF: check3+18Cj
seg000:001188D4 28 1F 00 03    cmplwi %r31, 3
seg000:001188D8 40 82 01 94    bne    error
seg000:001188DC 38 60 00 01    li     %r3, 1
seg000:001188E0 48 00 01 90    b      exit
seg000:001188E4
seg000:001188E4              loc_1188E4: # CODE XREF: check3+A8j
seg000:001188E4 80 DE 00 00    lwz    %r6, dword_24B704
seg000:001188E8 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:001188EC 38 60 00 0C    li     %r3, 0xC
seg000:001188F0 38 A0 00 00    li     %r5, 0
seg000:001188F4 48 00 BE D5    bl     .RBEREAD
seg000:001188F8 60 00 00 00    nop
seg000:001188FC 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:00118900 41 82 00 0C    beq    loc_11890C
seg000:00118904 38 60 00 00    li     %r3, 0
seg000:00118908 48 00 01 68    b      exit
seg000:0011890C
seg000:0011890C              loc_11890C: # CODE XREF: check3+1C4j
seg000:0011890C A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:00118910 28 00 1F 40    cmplwi %r0, 0x40FF
seg000:00118914 41 82 00 0C    beq    loc_118920
seg000:00118918 38 60 00 00    li     %r3, 0
seg000:0011891C 48 00 01 54    b      exit
seg000:00118920
seg000:00118920              loc_118920: # CODE XREF: check3+1D8j
seg000:00118920 57 BF 06 3E    clrlwi %r31, %r29, 24
seg000:00118924 28 1F 00 02    cmplwi %r31, 2
seg000:00118928 40 82 00 0C    bne    loc_118934
seg000:0011892C 38 60 00 01    li     %r3, 1
seg000:00118930 48 00 01 40    b      exit
seg000:00118934
seg000:00118934              loc_118934: # CODE XREF: check3+1ECj
seg000:00118934 80 DE 00 00    lwz    %r6, dword_24B704
seg000:00118938 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:0011893C 38 60 00 0D    li     %r3, 0xD
seg000:00118940 38 A0 00 00    li     %r5, 0
seg000:00118944 48 00 BE 85    bl     .RBEREAD
seg000:00118948 60 00 00 00    nop
seg000:0011894C 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:00118950 41 82 00 0C    beq    loc_11895C
seg000:00118954 38 60 00 00    li     %r3, 0
seg000:00118958 48 00 01 18    b      exit
seg000:0011895C
seg000:0011895C              loc_11895C: # CODE XREF: check3+214j
seg000:0011895C A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:00118960 28 00 07 CF    cmplwi %r0, 0xFC7
seg000:00118964 41 82 00 0C    beq    loc_118970
seg000:00118968 38 60 00 00    li     %r3, 0
seg000:0011896C 48 00 01 04    b      exit
seg000:00118970
seg000:00118970              loc_118970: # CODE XREF: check3+228j
seg000:00118970 28 1F 00 03    cmplwi %r31, 3
seg000:00118974 40 82 00 F8    bne    error
seg000:00118978 38 60 00 01    li     %r3, 1
seg000:0011897C 48 00 00 F4    b      exit
seg000:00118980
seg000:00118980              loc_118980: # CODE XREF: check3+B8j
seg000:00118980                          # check3+C4j
seg000:00118980 80 DE 00 00    lwz    %r6, dword_24B704
seg000:00118984 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:00118988 3B E0 00 00    li     %r31, 0
seg000:0011898C 38 60 00 04    li     %r3, 4
seg000:00118990 38 A0 00 00    li     %r5, 0
seg000:00118994 48 00 BE 35    bl     .RBEREAD
seg000:00118998 60 00 00 00    nop
seg000:0011899C 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:001189A0 41 82 00 0C    beq    loc_1189AC
seg000:001189A4 38 60 00 00    li     %r3, 0
seg000:001189A8 48 00 00 C8    b      exit
seg000:001189AC
seg000:001189AC              loc_1189AC: # CODE XREF: check3+264j
seg000:001189AC A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:001189B0 28 00 1D 6A    cmplwi %r0, 0xAED0
seg000:001189B4 40 82 00 0C    bne    loc_1189C0
seg000:001189B8 3B E0 00 01    li     %r31, 1
seg000:001189BC 48 00 00 14    b      loc_1189D0
seg000:001189C0
seg000:001189C0              loc_1189C0: # CODE XREF: check3+278j
seg000:001189C0 28 00 18 28    cmplwi %r0, 0x2818
seg000:001189C4 41 82 00 0C    beq    loc_1189D0
seg000:001189C8 38 60 00 00    li     %r3, 0
seg000:001189CC 48 00 00 A4    b      exit
seg000:001189D0
seg000:001189D0              loc_1189D0: # CODE XREF: check3+280j
seg000:001189D0                          # check3+288j
seg000:001189D0 57 A0 06 3E    clrlwi %r0, %r29, 24
seg000:001189D4 28 00 00 02    cmplwi %r0, 2
seg000:001189D8 40 82 00 20    bne    loc_1189F8
seg000:001189DC 57 E0 06 3F    clrlwi.%r0, %r31, 24
seg000:001189E0 41 82 00 10    beq    good2
seg000:001189E4 48 00 4C 69    bl     sub_11D64C
seg000:001189E8 60 00 00 00    nop
seg000:001189EC 48 00 00 84    b      exit
seg000:001189F0
seg000:001189F0              good2:      # CODE XREF: check3+2A4j
seg000:001189F0 38 60 00 01    li     %r3, 1
seg000:001189F4 48 00 00 7C    b      exit
seg000:001189F8
seg000:001189F8              loc_1189F8: # CODE XREF: check3+29Cj
seg000:001189F8 80 DE 00 00    lwz    %r6, dword_24B704
seg000:001189FC 38 81 00 38    addi   %r4, %sp, 0x50+var_18
seg000:00118A00 38 60 00 05    li     %r3, 5
seg000:00118A04 38 A0 00 00    li     %r5, 0
seg000:00118A08 48 00 BD C1    bl     .RBEREAD
seg000:00118A0C 60 00 00 00    nop
seg000:00118A10 54 60 04 3F    clrlwi.%r0, %r3, 16
seg000:00118A14 41 82 00 0C    beq    loc_118A20
seg000:00118A18 38 60 00 00    li     %r3, 0
seg000:00118A1C 48 00 00 54    b      exit
seg000:00118A20
seg000:00118A20              loc_118A20: # CODE XREF: check3+2D8j
seg000:00118A20 A0 01 00 38    lhz    %r0, 0x50+var_18(%sp)
seg000:00118A24 28 00 11 D3    cmplwi %r0, 0xD300
seg000:00118A28 40 82 00 0C    bne    loc_118A34
seg000:00118A2C 3B E0 00 01    li     %r31, 1
seg000:00118A30 48 00 00 14    b      good1
seg000:00118A34
seg000:00118A34              loc_118A34: # CODE XREF: check3+2ECj
seg000:00118A34 28 00 1A EB    cmplwi %r0, 0xEBA1
seg000:00118A38 41 82 00 0C    beq    good1
seg000:00118A3C 38 60 00 00    li     %r3, 0
seg000:00118A40 48 00 00 30    b      exit
seg000:00118A44
seg000:00118A44              good1:      # CODE XREF: check3+2F4j
seg000:00118A44                          # check3+2FCj
seg000:00118A44 57 A0 06 3E    clrlwi %r0, %r29, 24
seg000:00118A48 28 00 00 03    cmplwi %r0, 3
seg000:00118A4C 40 82 00 20    bne    error
seg000:00118A50 57 E0 06 3F    clrlwi.%r0, %r31, 24
seg000:00118A54 41 82 00 10    beq    good
seg000:00118A58 48 00 4B F5    bl     sub_11D64C
seg000:00118A5C 60 00 00 00    nop
seg000:00118A60 48 00 00 10    b     exit
seg000:00118A64
seg000:00118A64              good:       # CODE XREF: check3+318j
seg000:00118A64 38 60 00 01    li    %r3, 1
seg000:00118A68 48 00 00 08    b     exit
seg000:00118A6C
seg000:00118A6C              error:      # CODE XREF: check3+19Cj
seg000:00118A6C                          # check3+238j ...
seg000:00118A6C 38 60 00 00    li    %r3, 0
seg000:00118A70
seg000:00118A70                exit:      # CODE XREF: check3+44j
seg000:00118A70                           # check3+58j ...
seg000:00118A70 80 01 00 58    lwz   %r0, 0x50+arg_8(%sp)
seg000:00118A74 38 21 00 50    addi  %sp, %sp, 0x50
seg000:00118A78 83 E1 FF FC    lwz   %r31, var_4(%sp)
seg000:00118A7C 7C 08 03 A6    mtlr  %r0
seg000:00118A80 83 C1 FF F8    lwz   %r30, var_8(%sp)
seg000:00118A84 83 A1 FF F4    lwz   %r29, var_C(%sp)
seg000:00118A88 4E 80 00 20    blr
seg000:00118A88              # End of function check3

此函数多次调用了.RBEREAD()函数。在调用后面的这个函数之后，check3()函数又大量使用CMPLWI指令将返回值与特定的固定值进行比较。由此可见.RBEREAD()函数大体是从加密狗读取数据的函数。

另外，在调用.RBEREAD()函数之前，r3寄存器的取值不外乎0、1、8、0xA、0xB、0xC、0xD、4和5。这很可能是内存地址一类的信息。

如果使用google引擎搜索这些函数名，google的查询结果多数就是Sentinel Eve 3加密狗开发手册。

其实，在不了解其他PowerPC指令的情况下，我们照样可以分析加密狗的有关操作。毕竟认证函数就是这几个，而且“认证成功”的返回值肯定是1，“认证失败”的返回值又肯定是0。

综合上述分析，只要让check1()函数的返回值固定为1或者其他某个非零值，即可破解软件狗认证。但是因为我并不熟悉PowerPC的指令，所以我决定采取保守的修改方案：修改check2()函数中0x001186FC处和0x00118718处的转移指令。

我把0x001186FC的指令改为0x48和0，即把BEQ指令替换为无条件转移指令B。即使不参阅［IBM00］的参考手册，我们也能在程序里找到B指令的opcode。

另外，我把0x00118718处修改为一个0x60和三个0字节，将有关指令改为NOP指令。当然，这也是从原程序里找的opcode。

进行上述修改之后，在不插入加密狗的情况下，程序仍然可正常运行。

总之，借助IDA和部分汇编知识，任何人都可以小规模地修改程序。

78.2　例2: SCO OpenServer

本例研究的程序是1997年开发的面向SCO OpenServer的程序。因为年代过于久远，买家早就找不到开发商了。

这款软件的加密狗驱动程序是定制程序。这个驱动程序里有“Copyright 1989, Rainbow Technologies, Inc., Irvine, CA”和“Sentinel Integrated Driver Ver. 3.0”的字样。

在SCO OpenServer上安装驱动程序之后，硬件加密狗将会加载到文件系统的/dev目录里：

/dev/rbsl8
/dev/rbsl9
/dev/rbsl10

如果不插入加密狗，程序将会报错。而且错误信息不在可执行程序里。

好在IDA可以加载SCO OpenServer的COFF程序。

在IDA里搜索“rbsl”，然后找到了下述指令：

.text:00022AB8        public SSQC
.text:00022AB8 SSQC   proc near ; CODE XREF: SSQ+7p
.text:00022AB8
.text:00022AB8 var_44 = byte ptr -44h
.text:00022AB8 var_29 = byte ptr -29h
.text:00022AB8 arg_0  = dword ptr  8
.text:00022AB8
.text:00022AB8        push    ebp
.text:00022AB9        mov     ebp, esp
.text:00022ABB        sub     esp, 44h
.text:00022ABE        push    edi
.text:00022ABF        mov     edi, offset unk_4035D0
.text:00022AC4        push    esi
.text:00022AC5        mov     esi, [ebp+arg_0]
.text:00022AC8        push    ebx
.text:00022AC9        push    esi
.text:00022ACA        call    strlen
.text:00022ACF        add     esp, 4
.text:00022AD2        cmp     eax, 2
.text:00022AD7        jnz     loc_22BA4
.text:00022ADD        inc     esi
.text:00022ADE        mov     al, [esi-1]
.text:00022AE1        movsx   eax, al
.text:00022AE4        cmp     eax, '3'
.text:00022AE9        jz      loc_22B84
.text:00022AEF        cmp     eax, '4'
.text:00022AF4        jz       loc_22B94
.text:00022AFA        cmp     eax, '5'
.text:00022AFF        jnz     short loc_22B6B
.text:00022B01        movsx   ebx, byte ptr [esi]
.text:00022B04        sub     ebx, '0'
.text:00022B07        mov     eax, 7
.text:00022B0C        add     eax, ebx
.text:00022B0E        push    eax
.text:00022B0F        lea     eax, [ebp+var_44]
.text:00022B12        push    offset aDevSlD ; "/dev/sl%d"
.text:00022B17        push    eax
.text:00022B18        call    nl_sprintf
.text:00022B1D        push    0                ; int
.text:00022B1F        push    offset aDevRbsl8 ; char *
.text:00022B24        call    _access
.text:00022B29        add     esp, 14h
.text:00022B2C        cmp     eax, 0FFFFFFFFh
.text:00022B31        jz      short loc_22B48
.text:00022B33        lea     eax, [ebx+7]
.text:00022B36        push    eax
.text:00022B37        lea     eax, [ebp+var_44]
.text:00022B3A        push    offset aDevRbslD ; "/dev/rbsl%d"
.text:00022B3F        push    eax
.text:00022B40        call    nl_sprintf
.text:00022B45        add     esp, 0Ch
.text:00022B48
.text:00022B48 loc_22B48: ; CODE XREF: SSQC+79j
.text:00022B48        mov     edx, [edi]
.text:00022B4A        test    edx, edx
.text:00022B4C        jle     short loc_22b57
.text:00022B4E        push    edx               ; int
.text:00022B4F        call    _close
.text:00022B54        add     esp, 4
.text:00022B57
.text:00022B57 loc_22B57: ; CODE XREF: SSQC+94j
.text:00022B57        push    2                  ; int
.text:00022B59        lea     eax, [ebp+var_44]
.text:00022B5C        push    eax               ; char *
.text:00022B5D        call    _open
.text:00022B62        add     esp, 8
.text:00022B65        test    eax, eax
.text:00022B67        mov     [edi], eax
.text:00022B69        jge     short loc_22B78
.text:00022B6B
.text:00022B6B loc_22B6B: ; CODE XREF: SSQC+47j
.text:00022B6B        mov     eax, 0FFFFFFFFh
.text:00022B70        pop     ebx
.text:00022B71        pop     esi
.text:00022B72        pop     edi
.text:00022B73        mov     esp,ebp
.text:00022B75        pop     ebp
.text:00022B76        retn
.text:00022B78
.text:00022B78 loc_22B78: ; CODE XREF: SSQC+B1j
.text:00022B78        pop     ebx
.text:00022B79        pop     esi
.text:00022B7A        pop     edi
.text:00022B7B        xor     eax, eax
.text:00022B7D        mov     esp, ebp
.text:00022B7F        pop     ebp
.text:00022B80        retn
.text:00022B84
.text:00022B84 loc_22B84: ; CODE XREF: SSQC+31j
.text:00022B84        mov     al, [esi]
.text:00022B86        pop     ebx
.text:00022B87        pop     esi
.text:00022B88        pop     edi
.text:00022B89        mov     ds:byte_407224, al
.text:00022B8E        mov     esp, ebp
.text:00022B90        xor     eax, eax
.text:00022B92        pop     ebp
.text:00022B93        retn
.text:00022B94
.text:00022B94 loc_22B94: ; CODE XREF: SSQC+3Cj
.text:00022B94        mov     al, [esi]
.text:00022B96        pop     ebx
.text:00022B97        pop     esi
.text:00022B98        pop     edi
.text:00022B99        mov     ds:byte_407225, al
.text:00022B9E        mov     esp, ebp
.text:00022BA0        xor     eax, eax
.text:00022BA2        pop     ebp
.text:00022BA3        retn
.text:00022BA4
.text:00022BA4 loc_22BA4: ; CODE XREF: SSQC+1Fj
.text:00022BA4        movsx   eax, ds:byte_407225
.text:00022BAB        push    esi
.text:00022BAC        push    eax
.text:00022BAD        movsx   eax, ds:byte_407224
.text:00022BB4        push    eax
.text:00022BB5        lea     eax, [ebp+var_44]
.text:00022BB8        push    offset a46CCS   ; "46%c%c%s"
.text:00022BBD        push    eax
.text:00022BBE        call    nl_sprintf
.text:00022BC3        lea     eax, [ebp+var_44]
.text:00022BC6        push    eax
.text:00022BC7        call    strlen
.text:00022BCC        add     esp, 18h
.text:00022BCF        cmp     eax, 1Bh
.text:00022BD4        jle     short loc_22BDA
.text:00022BD6        mov     [ebp+var_29], 0
.text:00022BDA
.text:00022BDA loc_22BDA: ; CODE XREF: SSQC+11Cj
.text:00022BDA        lea     eax, [ebp+var_44]
.text:00022BDD        push    eax
.text:00022BDE        call    strlen
.text:00022BE3        push    eax             ; unsigned int
.text:00022BE4        lea     eax, [ebp+var_44]
.text:00022BE7        push    eax             ; void *
.text:00022BE8        mov     eax, [edi]
.text:00022BEA        push    eax             ; int
.text:00022BEB        call    _write
.text:00022BF0        add     esp, 10h
.text:00022BF3        pop     ebx
.text:00022BF4        pop     esi
.text:00022BF5        pop     edi
.text:00022BF6        mov     esp, ebp
.text:00022BF8        pop     ebp
.text:00022BF9        retn
.text:00022BFA        db 0Eh dup(90h)
.text:00022BFA  SSQC  endp

果然，该程序要和驱动程序通信。

而且，只有下面这个形实转换函数调用了SSQC()函数：

.text:0000DBE8        public SSQ
.text:0000DBE8 SSQ    proc near  ; CODE XREF: sys_info+A9p
.text:0000DBE8                   ; sys_info+CBp...
.text:0000DBE8
.text:0000DBE8 arg_0  = dword ptr  8
.text:0000DBE8
.text:0000DBE8        push    ebp
.text:0000DBE9        mov     ebp, esp
.text:0000DBEB        mov     edx, [ebp+arg_0]
.text:0000DBEE        push    edx
.text:0000DBEF        call    SSQC
.text:0000DBF4        add     esp, 4
.text:0000DBF7        mov     esp, ebp
.text:0000DBF9        pop     ebp
.text:0000DBFA        retn
.text:0000DBFB SSQ    endp

调用SSQ()函数的指令至少有两处。

其中一处是：

.data:0040169C _51_52_53       dd offset aPressAnyKeyT_0 ; DATA XREF: init_sys+392r
.data:0040169C                                          ; sys_info+A1r
.data:0040169C                                          ; "PRESS ANY KEY TO CONTINUE: "
.data:004016A0                 dd offset a51            ; "51"
.data:004016A4                 dd offset a52            ; "52"
.data:004016A8                 dd offset a53            ; "53"

...

.data:004016B8 _3C_or_3E       dd offset a3c            ; DATA XREF: sys_info:loc_D67Br
.data:004016B8                                          ; "3C"
.data:004016BC                 dd offset a3e            ; "3E"

; these names we gave to the labels:
.data:004016C0 answers1        dd 6B05h                 ; DATA XREF: sys_info+E7r
.data:004016C4                 dd 3D87h
.data:004016C8 answers2        dd 3Ch                   ; DATA XREF: sys_info+F2r
.data:004016CC                 dd 832h
.data:004016D0 _C_and_B        db 0Ch                   ; DATA XREF: sys_info+BAr
.data:004016D0                                          ; sys_info:OKr
.data:004016D1 byte_4016D1     db 0Bh                   ; DATA XREF: sys_info+FDr
.data:004016D2                 db     0

...

.text:0000D652                 xor    eax, eax
.text:0000D654                 mov    al, ds:ctl_port
.text:0000D659                 mov    ecx, _51_52_53[eax*4]
.text:0000D660                 push   ecx
.text:0000D661                 call   SSQ
.text:0000D666                 add    esp, 4
.text:0000D669                 cmp    eax, 0FFFFFFFFh
.text:0000D66E                 jz     short loc_D6D1
.text:0000D670                 xor    ebx, ebx
.text:0000D672                 mov    al, _C_and_B
.text:0000D677                 test   al, al
.text:0000D679                 jz     short loc_D6C0
.text:0000D67B
.text:0000D67B loc_D67B: ; CODE XREF: sys_info+106j
.text:0000D67B                 mov    eax, _3C_or_3E[ebx*4]
.text:0000D682                 push   eax
.text:0000D683                 call   SSQ
.text:0000D688                 push   offset a4g     ; "4G"
.text:0000D68D                 call   SSQ
.text:0000D692                 push   offset a0123456789 ; "0123456789"
.text:0000D697                 call   SSQ
.text:0000D69C                 add    esp, 0Ch
.text:0000D69F                 mov    edx, answers1[ebx*4]
.text:0000D6A6                 cmp    eax, edx
.text:0000D6A8                 jz     short OK
.text:0000D6AA                 mov    ecx, answers2[ebx*4]
.text:0000D6B1                 cmp    eax, ecx
.text:0000D6B3                 jz     short OK
.text:0000D6B5                 mov    al, byte_4016D1[ebx]
.text:0000D6BB                 inc    ebx
.text:0000D6BC                 test   al, al
.text:0000D6BE                 jnz    short loc_D67B
.text:0000D6C0
.text:0000D6C0 loc_D6C0: ; CODE XREF: sys_info+C1j
.text:0000D6C0                 inc    ds:ctl_port
.text:0000D6C6                 xor    eax, eax
.text:0000D6C8                 mov    al, ds:ctl_port
.text:0000D6CD                 cmp    eax, edi
.text:0000D6CF                 jle    short loc_D652
.text:0000D6D1
.text:0000D6D1 loc_D6D1: ; CODE XREF: sys_info+98j
.text:0000D6D1           ; sys_info+B6j
.text:0000D6D1                 mov    edx, [ebp+var_8]
.text:0000D6D4                 inc    edx
.text:0000D6D5                 mov    [ebp+var_8], edx
.text:0000D6D8                 cmp    edx, 3
.text:0000D6DB                 jle    loc_D641
.text:0000D6E1
.text:0000D6E1 loc_D6E1: ; CODE XREF: sys_info+16j
.text:0000D6E1           ; sys_info+51j ...
.text:0000D6E1                 pop    ebx
.text:0000D6E2                 pop    edi
.text:0000D6E3                 mov    esp, ebp
.text:0000D6E5                 pop    ebp
.text:0000D6E6                 retn
.text:0000D6E8 OK:        ; CODE XREF: sys_info+F0j
.text:0000D6E8            ; sys_info+FBj
.text:0000D6E8                 mov    al, _C_and_B[ebx]
.text:0000D6EE                 pop    ebx
.text:0000D6EF                 pop    edi
.text:0000D6F0                 mov    ds:ctl_model, al
.text:0000D6F5                 mov    esp, ebp
.text:0000D6F7                 pop    ebp
.text:0000D6F8                 retn
.text:0000D6F8 sys_info        endp

“3C”和“3E”听起来很熟：它是私有的、单功能加密－哈希函数，用于Rainbow公司生产的一款没有内存的Sentinel Pro加密狗。

本书的第34章详细介绍过哈希函数。

就这个程序而言，我们可以据此判断它仅检测加密狗的“有/无”信息。这款加密狗上不具备内存芯片，也就无法存储信息；换句话说这个程序不会向加密狗写数据。后面连续出现的双字符代码是指令代码，由SSQC()函数接收和处理。其他字符串的哈希值都以16位数字的形式存储在加密狗里。因为开发厂商的加密算法是私有算法，所以编写驱动程序替身、或者仿制硬件加密狗的做法都行不通。但是，“截获所有访问加密狗的操作、继而找到程序核对的哈希值”确实行得通。不怕麻烦的话，我们还可以摸索程序逻辑、基于私有的加密哈希函数再建一个软件，从而使用自制软件替代原有软件对数据文件进行加解密。

代码51/52/53用于选择LPT打印机接口。3x/4x用于选择相应的加密狗“系列”。不同类型的Sentinel Pro加密狗可以接在同一个LPT接口上，而区分加密狗的工作则由应用程序完成。

除了字符串“0123456789”以外，传递给哈希函数的值都是双字符的字符串。然后，函数返回的哈希值与一系列有效值进行比较。如果该值有效，全局变量ctl_model将被赋值为0xC或0xB。

此外，程序还定义了字符串“PRESS ANY KEY TO CONTINUE:”，这应当是通过加密狗认证之后的提示信息。不过整个程序没有调用过这个字符串，恐怕这属于源程序的bug吧。

接下来，我们要关注全局变量ctl_mode的读取指令。

其中一处是：

.text:0000D708 prep_sys proc near ; CODE XREF: init_sys+46Ap
.text:0000D708
.text:0000D708 var_14   = dword ptr -14h
.text:0000D708 var_10   = byte ptr -10h
.text:0000D708 var_8    = dword ptr -8
.text:0000D708 var_2    = word ptr -2
.text:0000D708
.text:0000D708          push   ebp
.text:0000D709          mov    eax, ds:net_env
.text:0000D70E          mov    ebp, esp
.text:0000D710          sub    esp, 1Ch
.text:0000D713          test   eax, eax
.text:0000D715          jnz    short loc_D734
.text:0000D717          mov    al, ds:ctl_model
.text:0000D71C          test   al, al
.text:0000D71E          jnz    short loc_D77E
.text:0000D720          mov    [ebp+var_8], offset aIeCvulnvvOkgT_ ; "Ie-cvulnvV\\\bOKG]T_"
.text:0000D727          mov    edx, 7
.text:0000D72C          jmp    loc_D7E7

...

.text:0000D7E7 loc_D7E7: ; CODE XREF: prep_sys+24j
.text:0000D7E7           ; prep_sys+33j
.text:0000D7E7          push   edx
.text:0000D7E8          mov    edx, [ebp+var_8]
.text:0000D7EB          push   20h
.text:0000D7ED          push   edx
.text:0000D7EE          push   16h
.text:0000D7F0          call   err_warn
.text:0000D7F5          push   offset station_sem
.text:0000D7FA          call   ClosSem
.text:0000D7FF          call   startup_err

如果ctl_mode为0，那么一条经过加密处理的错误信息将被传递到解密程序，从而出现在屏幕上。

这个错误信息的解密方法就是简单的XOR算法：

.text:0000A43C err_warn       proc near         ; CODE XREF: prep_sys+E8p
.text:0000A43C                                  ; prep_sys2+2Fp ...
.text:0000A43C
.text:0000A43C var_55         = byte ptr -55h
.text:0000A43C var_54         = byte ptr -54h
.text:0000A43C arg_0          = dword ptr  8
.text:0000A43C arg_4          = dword ptr  0Ch
.text:0000A43C arg_8          = dword ptr  10h
.text:0000A43C arg_C          = dword ptr  14h
.text:0000A43C
.text:0000A43C                push    ebp
.text:0000A43D                mov     ebp, esp
.text:0000A43F                sub     esp, 54h
.text:0000A442                push    edi
.text:0000A443                mov     ecx, [ebp+arg_8]
.text:0000A446                xor     edi, edi
.text:0000A448                test    ecx, ecx
.text:0000A44A                push    esi
.text:0000A44B                jle     short loc_A466
.text:0000A44D                mov     esi, [ebp+arg_C] ; key
.text:0000A450                mov     edx, [ebp+arg_4] ; string
.text:0000A453
.text:0000A453 loc_A453:                               ; CODE XREF: err_warn+28j
.text:0000A453                xor     eax, eax
.text:0000A455                mov     al, [edx+edi]
.text:0000A458                xor     eax, esi
.text:0000A45A                add     esi, 3
.text:0000A45D                inc     edi
.text:0000A45E                cmp     edi, ecx
.text:0000A460                mov     [ebp+edi+var_55], al
.text:0000A464                jl      short loc_A453
.text:0000A466
.text:0000A466 loc_A466:                       ; CODE XREF: err_warn+Fj
.text:0000A466                mov     [ebp+edi+var_54], 0
.text:0000A46B                mov     eax, [ebp+arg_0]
.text:0000A46E                cmp     eax, 18h
.text:0000A473                jnz     short loc_A49C
.text:0000A475                lea     eax, [ebp+var_54]
.text:0000A478                push    eax
.text:0000A479                call    status_line
.text:0000A47E                add     esp, 4
.text:0000A481
.text:0000A481 loc_A481:                      ; CODE XREF: err_warn+72j
.text:0000A481                push    50h
.text:0000A483                push    0
.text:0000A485                lea     eax, [ebp+var_54]
.text:0000A488                push    eax
.text:0000A489                call    memset
.text:0000A48E                call    pcv_refresh
.text:0000A493                add     esp, 0Ch
.text:0000A496                pop     esi
.text:0000A497                pop     edi
.text:0000A498                mov     esp, ebp
.text:0000A49A                pop     ebp
.text:0000A49B                retn
.text:0000A49C
.text:0000A49C loc_A49C:                      ; CODE XREF: err_warn+37j
.text:0000A49C                push    0
.text:0000A49E                lea     eax, [ebp+var_54]
.text:0000A4A1                mov     edx, [ebp+arg_0]
.text:0000A4A4                push    edx
.text:0000A4A5                push    eax
.text:0000A4A6                call    pcv_lputs
.text:0000A4AB                add     esp, 0Ch
.text:0000A4AE                jmp     short loc_A481
.text:0000A4AE err_warn       endp

因为程序对提示信息进行了加密处理（这是常见手段），所以我无法在可执行程序里直接找到错误的提示信息。

此外，程序里还有一个调用哈希函数SSQ()的地方。在调用过程中，该处指令向哈希函数传递了字符串“offln”。后续指令将返回值与0xFE81和0x12A9进行比对。如果两个值不匹配，程序将启用timer()函数（貌似是等待用户重新插入加密狗），然后在屏幕上显示另一个错误提示信息：

.text:0000DA55 loc_DA55:                                ; CODE XREF: sync_sys+24Cj
.text:0000DA55                  push    offset aOffln    ; "offln"
.text:0000DA5A                  call    SSQ
.text:0000DA5F                  add     esp, 4
.text:0000DA62                  mov     dl, [ebx]
.text:0000DA64                  mov     esi, eax
.text:0000DA66                  cmp     dl, 0Bh
.text:0000DA69                  jnz     short loc_DA83
.text:0000DA6B                  cmp     esi, 0FE81h
.text:0000DA71                  jz      OK
.text:0000DA77                  cmp     esi, 0FFFFF8EFh
.text:0000DA7D                  jz      OK
.text:0000DA83
.text:0000DA83 loc_DA83:                             ; CODE XREF: sync_sys+201j
.text:0000DA83                  mov     cl, [ebx]
.text:0000DA85                  cmp     cl, 0Ch
.text:0000DA88                  jnz     short loc_DA9F
.text:0000DA8A                  cmp     esi, 12A9h
.text:0000DA90                  jz      OK
.text:0000DA96                  cmp     esi, 0FFFFFFF5h
.text:0000DA99                  jz      OK
.text:0000DA9F
.text:0000DA9F loc_DA9F:                             ; CODE XREF: sync_sys+220j
.text:0000DA9F                  mov     eax, [ebp+var_18]
.text:0000DAA2                  test    eax, eax
.text:0000DAA4                  jz      short loc_DAB0
.text:0000DAA6                  push    24h
.text:0000DAA8                  call    timer
.text:0000DAAD                  add     esp, 4
.text:0000DAB0
.text:0000DAB0 loc_DAB0:                             ; CODE XREF: sync_sys+23Cj
.text:0000DAB0                  inc     edi
.text:0000DAB1                  cmp     edi, 3
.text:0000DAB4                  jle     short loc_DA55
.text:0000DAB6                  mov     eax, ds:net_env
.text:0000DABB                  test    eax, eax
.text:0000DABD                  jz      short error

...

.text:0000DAF7 error:                               ; CODE XREF: sync_sys+255j
.text:0000DAF7                                      ; sync_sys+274j ...
.text:0000DAF7                  mov     [ebp+var_8], offset encrypted_error_message2
.text:0000DAFE                  mov     [ebp+var_C], 17h ; decrypting key
.text:0000DB05                  jmp     decrypt_end_print_message

...

; this name I gave to label:
.text:0000D9B6 decrypt_end_print_message:          ; CODE XREF: sync_sys+29Dj
.text:0000D9B6                                     ; sync_sys+2ABj
.text:0000D9B6                  mov     eax, [ebp+var_18]
.text:0000D9B9                  test    eax, eax
.text:0000D9BB                  jnz     short loc_D9FB
.text:0000D9BD                  mov     edx, [ebp+var_C] ; key
.text:0000D9C0                  mov     ecx, [ebp+var_8] ; string
.text:0000D9C3                  push    edx
.text:0000D9C4                  push    20h
.text:0000D9C6                  push    ecx
.text:0000D9C7                  push    18h
.text:0000D9C9                  call    err_warn
.text:0000D9CE                  push    0Fh
.text:0000D9D0                  push    190h
.text:0000D9D5                  call    sound
.text:0000D9DA                  mov     [ebp+var_18], 1
.text:0000D9E1                  add     esp, 18h
.text:0000D9E4                  call    pcv_kbhit
.text:0000D9E9                  test    eax, eax
.text:0000D9EB                  jz      short loc_D9FB

...

; this name I gave to label:
.data:00401736 encrypted_error_message2 db 74h, 72h, 78h, 43h, 48h, 6, 5Ah, 49h, 4Ch, 2 dup(47h)
.data:00401736                  db 51h, 4Fh, 47h, 61h, 20h, 22h, 3Ch, 24h, 33h, 36h,76h
.data:00401736                  db 3Ah, 33h, 31h, 0Ch, 0, 0Bh, 1Fh, 7, 1Eh, 1Ah

可见，破解加密狗的工作十分简单：我们只需要找到相关的CMP指令，把它后面的转移指令替换为无条件转移指令即可。当然，编写自制的SCO OpenServer驱动程序也不失为一种方法。

解密错误信息

我们还能够破解源程序中的错误信息。程序里err_warn()函数所采用的解密算法非常简单。

指令清单78.1　Decryption function

.text:0000A44D                 mov     esi, [ebp+arg_C] ; key
.text:0000A450                 mov     edx, [ebp+arg_4] ; string
.text:0000A453 loc_A453:
.text:0000A453                 xor     eax, eax
.text:0000A455                 mov     al, [edx+edi] ; load encrypted byte
.text:0000A458                 xor     eax, esi      ; decrypt it
.text:0000A45A                 add     esi, 3        ; change key for the next byte
.text:0000A45D                 inc     edi
.text:0000A45E                 cmp     edi, ecx
.text:0000A460                 mov     [ebp+edi+var_55], al
.text:0000A464                 jl      short loc_A453

由此可见，程序不仅向解密函数传递了加密后的字符串，而且向它传递了加密密钥：

.text:0000DAF7 error:                            ; CODE XREF: sync_sys+255j
.text:0000DAF7                                   ; sync_sys+274j ...
.text:0000DAF7                 mov     [ebp+var_8], offset encrypted_error_message2
.text:0000DAFE                 mov     [ebp+var_C], 17h ; decrypting key
.text:0000DB05                 jmp     decrypt_end_print_message

...

; this name we gave to label manually:
.text:0000D9B6 decrypt_end_print_message:        ; CODE XREF: sync_sys+29Dj
.text:0000D9B6                                   ; sync_sys+2ABj
.text:0000D9B6                 mov     eax, [ebp+var_18]
.text:0000D9B9                 test    eax, eax
.text:0000D9BB                 jnz     short loc_D9FB
.text:0000D9BD                 mov     edx, [ebp+var_C] ; key
.text:0000D9C0                 mov     ecx, [ebp+var_8] ; string
.text:0000D9C3                 push    edx
.text:0000D9C4                 push    20h
.text:0000D9C6                 push    ecx
.text:0000D9C7                 push    18h
.text:0000D9C9                 call    err_warn

这就是简单的XOR算法：每个字节都与密钥进行XOR运算，而且每解密一个字节密钥就增加3。

为了验证这个猜想，我专门编写了一个Python脚本程序：

指令清单78.2　Python 3.x

#!/usr/bin/python
import sys

msg=[0x74, 0x72, 0x78, 0x43, 0x48, 0x6, 0x5A, 0x49, 0x4C, 0x47, 0x47,
0x51, 0x4F, 0x47, 0x61, 0x20, 0x22, 0x3C, 0x24, 0x33, 0x36, 0x76,
0x3A, 0x33, 0x31, 0x0C, 0x0, 0x0B, 0x1F, 0x7, 0x1E, 0x1A]

key=0x17
tmp=key
for i in msg:
        sys.stdout.write ("%c" % (i^tmp))
        tmp=tmp+3
sys.stdout.flush()

它解密出来的字符串正是“check security device connection”。

程序还用到了其他的加密字符串和相应密钥。然而我们不需要原始密钥就可以进行密文解密。首先，密钥实际只有1个字节。解密核心的XOR指令以字节为操作单位。其次，虽然密钥存储于ESI寄存器，但是它只用了ESI寄存器地址最低的那个字节。即使密钥大于255，但是参与演算的密钥还是不会大于1个字节的值。

最终，我们可以使用0～255之间的密钥暴力破解加密字符串。与此同时，我们要排除那些含有控制字符的解密结果。

指令清单78.3　Python 3.x

#!/usr/bin/python
import sys, curses.ascii

msgs=[
[0x74, 0x72, 0x78, 0x43, 0x48, 0x6, 0x5A, 0x49, 0x4C, 0x47, 0x47,
0x51, 0x4F, 0x47, 0x61, 0x20, 0x22, 0x3C, 0x24, 0x33, 0x36, 0x76,
0x3A, 0x33, 0x31, 0x0C, 0x0, 0x0B, 0x1F, 0x7, 0x1E, 0x1A],

[0x49, 0x65, 0x2D, 0x63, 0x76, 0x75, 0x6C, 0x6E, 0x76, 0x56, 0x5C,
8, 0x4F, 0x4B, 0x47, 0x5D, 0x54, 0x5F, 0x1D, 0x26, 0x2C, 0x33,
0x27, 0x28, 0x6F, 0x72, 0x75, 0x78, 0x7B, 0x7E, 0x41, 0x44],

[0x45, 0x61, 0x31, 0x67, 0x72, 0x79, 0x68, 0x52, 0x4A, 0x52, 0x50,
0x0C, 0x4B, 0x57, 0x43, 0x51, 0x58, 0x5B, 0x61, 0x37, 0x33, 0x2B,
0x39, 0x39, 0x3C, 0x38, 0x79, 0x3A, 0x30, 0x17, 0x0B, 0x0C],

[0x40, 0x64, 0x79, 0x75, 0x7F, 0x6F, 0x0, 0x4C, 0x40, 0x9, 0x4D, 0x5A,
0x46, 0x5D, 0x57, 0x49, 0x57, 0x3B, 0x21, 0x23, 0x6A, 0x38, 0x23,
0x36, 0x24, 0x2A, 0x7C, 0x3A, 0x1A, 0x6, 0x0D, 0x0E, 0x0A, 0x14,
0x10],

[0x72, 0x7C, 0x72, 0x79, 0x76, 0x0,
0x50, 0x43, 0x4A, 0x59, 0x5D, 0x5B, 0x41, 0x41, 0x1B, 0x5A,
0x24, 0x32, 0x2E, 0x29, 0x28, 0x70, 0x20, 0x22, 0x38, 0x28, 0x36,
0x0D, 0x0B, 0x48, 0x4B, 0x4E]]

def is_string_printable(s):
    return all(list(map(lambda x: curses.ascii.isprint(x), s)))

cnt=1
for msg in msgs:
        print ("message #%d" % cnt)
        for key in range(0,256):
                result=[]
                tmp=key
                for i in msg:
                        result.append (i^tmp)
                        tmp=tmp+3
                if is_string_printable (result):
                        print ("key=", key, "value=", "".join(list(map(chr, result))))
        cnt=cnt+1

上述程序的运行结果如下。

指令清单78.4　Results

message #1
key= 20 value= 'eb^h%|''hudw|_af{n~f%ljmSbnwlpk
key= 21 value= ajc]i"}cawtgv{^bgto}g"millcmvkqh
key= 22 value= bkd\j#rbbvsfuz!cduh|d#bhomdlujni
key= 23 value= check security device connection
key= 24 value= lifbl!pd|tqhsx#ejwjbb!'nQofbshlo
message #2
key= 7 value= No security device found
key= 8 value= An#rbbvsVuz!cduhld#ghtme?!#!'!#!
message #3
key= 7 value= Bk<waoqNUpu$'yreoa\wpmpusj,bkIjh
key= 8 value= Mj?vfnrOjqv%gxqd''_vwlstlk/clHii
key= 9 value= Lm>ugasLkvw&fgpgag^uvcrwml. 'mwhj
key= 10 value= Ol!td'tMhwx'efwfbf!tubuvnm!anvok
key= 11 value= No security device station found
key= 12 value= In#rjbvsnuz!{duhdd#r{'whho#gPtme
message #4
key= 14 value= Number of authorized users exceeded
key= 15 value= Ovlmdq!hg#'juknuhydk!vrbsp!Zy'dbefe
message #5
key= 17 value= check security device station
key= 18 value= `ijbh!td`tmhwx'efwfbf!tubuVnm!'!

虽然出现了人类语言之外的字符串，但是我们还是能够找到英语字符串。

另外，因为程序采用的解密算法是非常简单的XOR算法，所以它的加密函数也不会复杂到哪去。如果有必要的话，我们甚至可以用它的算法加密自己的字符串，然后把自制的密文放在程序里面。

78.3　例3: MS-DOS

本例研究的是一款1995年研发的MS-DOS程序，联络不上开发商了。

在从前的那个坚守DOS阵地的时代，所有的MS-DOS程序基本都在16位的8086或者80286 CPU上运行。因此大批的程序都是16位程序。这种程序的指令与本书介绍过的汇编指令大体相同，只是寄存器是16位寄存器、而且指令集略微小些罢了。

MS-DOS系统没有系统驱动程序，全部程序都可以直接访问硬件端口。故而程序中大量出现了OUT/IN指令。就当今的操作系统来说，基本上只有驱动程序才会使用这些指令，而且应用程序已经不能直接访问硬件端口了。

在当时的技术条件下，访问加密狗的MS-DOS程序必须直接访问LPT打印端口。那么我们就搜索这些端口操作指令好了：

seg030:0034               out_port proc far ; CODE XREF: sent_pro+22p
seg030:0034                                 ; sent_pro+2Ap ...
seg030:0034
seg030:0034               arg_0    =byteptr 6
seg030:0034
seg030:0034 55                     push   bp
seg030:0035 8B EC                  mov    bp, sp
seg030:0037 8B 16 7E E7            mov    dx, _out_port ; 0x378
seg030:003B 8A 46 06               mov    al, [bp+arg_0]
seg030:003E EE                     out    dx, al
seg030:003F 5D                     pop    bp
seg030:0040 CB                     retf
seg030:0040               out_port endp

上述指令的标签名称全部是笔者自行添加的。我们发现，只有下面这个函数调用了out_port()函数：

seg030:0041               sent_pro proc far ; CODE XREF: check_dongle+34p
seg030:0041
seg030:0041               var_3    = byte ptr -3
seg030:0041               var_2    = word ptr -2
seg030:0041               arg_0    = dword ptr  6
seg030:0041
seg030:0041 C8 04 00 00            enter    4, 0
seg030:0045 56                     push     si
seg030:0046 57                     push     di
seg030:0047 8B 16 82 E7            mov      dx, _in_port_1 ; 0x37A
seg030:004B EC                     in       al, dx
seg030:004C 8A D8                  mov      bl, al
seg030:004E 80 E3 FE               and      bl, 0FEh
seg030:0051 80 CB 04               or       bl, 4
seg030:0054 8A C3                  mov      al, bl
seg030:0056 88 46 FD               mov      [bp+var_3], al
seg030:0059 80 E3 1F               and      bl, 1Fh
seg030:005C 8A C3                  mov      al, bl
seg030:005E EE                     out      dx, al
seg030:005F 68 FF 00               push     0FFh
seg030:0062 0E                     push     cs
seg030:0063 E8 CE FF               call     near ptr out_port
seg030:0066 59                     pop      cx
seg030:0067 68 D3 00               push     0D3h
seg030:006A 0E                     push     cs
seg030:006B E8 C6 FF               call     near ptr out_port
seg030:006E 59                     pop      cx
seg030:006F 33 F6                  xor      si, si
seg030:0071 EB 01                  jmp      short loc_359D4
seg030:0073
seg030:0073                loc_359D3: ; CODE XREF: sent_pro+37j
seg030:0073 46                     inc      si
seg030:0074
seg030:0074                loc_359D4: ; CODE XREF: sent_pro+30j
seg030:0074 81 FE 96 00            cmp      si, 96h
seg030:0078 7C F9                  jl       short loc_359D3
seg030:007A 68 C3 00               push     0C3h
seg030:007D 0E                     push     cs
seg030:007E E8 B3 FF               call     near ptr out_port
seg030:0081 59                     pop      cx
seg030:0082 68 C7 00               push     0C7h
seg030:0085 0E                     push     cs
seg030:0086 E8 AB FF               call     near ptr out_port
seg030:0089 59                     pop      cx
seg030:008A 68 D3 00               push     0D3h
seg030:008D 0E                     push     cs
seg030:008E E8 A3 FF               call     near ptr out_port
seg030:0091 59                     pop      cx
seg030:0092 68 C3 00               push     0C3h
seg030:0095 0E                     push     cs
seg030:0096 E8 9B FF               call     near ptr out_port
seg030:0099 59                     pop      cx
seg030:009A 68 C7 00               push     0C7h
seg030:009D 0E                     push     cs
seg030:009E E8 93 FF               call     near ptr out_port
seg030:00A1 59                     pop      cx
seg030:00A2 68 D3 00               push     0D3h
seg030:00A5 0E                     push     cs
seg030:00A6 E8 8B FF               call     near ptr out_port
seg030:00A9 59                     pop      cx
seg030:00AA BF FF FF               mov      di, 0FFFFh
seg030:00AD EB 40                  jmp      short loc_35A4F
seg030:00AF
seg030:00AF                loc_35A0F: ; CODE XREF: sent_pro+BDj
seg030:00AF BE 04 00               mov      si, 4
seg030:00B2
seg030:00B2                loc_35A12: ; CODE XREF: sent_pro+ACj
seg030:00B2 D1 E7                  shl      di, 1
seg030:00B4 8B 16 80 E7            mov      dx, _in_port_2 ; 0x379
seg030:00B8 EC                     in       al, dx
seg030:00B9 A8 80                  test     al, 80h
seg030:00BB 75 03                  jnz      short loc_35A20
seg030:00BD 83 CF 01               or       di, 1
seg030:00C0
seg030:00C0                loc_35A20: ; CODE XREF: sent_pro+7Aj
seg030:00C0 F7 46 FE 08+           test     [bp+var_2], 8
seg030:00C5 74 05                  jz       short loc_35A2C
seg030:00C7 68 D7 00               push     0D7h ; '+'
seg030:00CA EB 0B                  jmp      short loc_35A37
seg030:00CC
seg030:00CC                loc_35A2C: ; CODE XREF: sent_pro+84j
seg030:00CC 68 C3 00               push     0C3h
seg030:00CF 0E                     push     cs
seg030:00D0 E8 61 FF               call     near ptr out_port
seg030:00D3 59                     pop      cx
seg030:00D4 68 C7 00               push     0C7h
seg030:00D7
seg030:00D7                loc_35A37: ; CODE XREF: sent_pro+89j
seg030:00D7 0E                     push     cs
seg030:00D8 E8 59 FF               call     near ptr out_port
seg030:00DB 59                     pop      cx
seg030:00DC 68 D3 00               push     0D3h
seg030:00DF 0E                     push     cs
seg030:00E0 E8 51 FF               call     near ptr out_port
seg030:00E3 59                     pop      cx
seg030:00E4 8B 46 FE               mov      ax, [bp+var_2]
seg030:00E7 D1 E0                  shl      ax, 1
seg030:00E9 89 46 FE               mov      [bp+var_2], ax
seg030:00EC 4E                     dec      si
seg030:00ED 75 C3                  jnz      short loc_35A12
seg030:00EF
seg030:00EF                loc_35A4F: ; CODE XREF: sent_pro+6Cj
seg030:00EF C4 5E 06               les      bx, [bp+arg_0]
seg030:00F2 FF 46 06               inc      word ptr [bp+arg_0]
seg030:00F5 26 8A 07               mov      al, es:[bx]
seg030:00F8 98                     cbw
seg030:00F9 89 46 FE               mov      [bp+var_2], ax
seg030:00FC 0B C0                  or       ax, ax
seg030:00FE 75 AF                  jnz      short loc_35A0F
seg030:0100 68 FF 00               push     0FFh
seg030:0103 0E                     push     cs
seg030:0104 E8 2D FF               call     near ptr out_port
seg030:0107 59                     pop      cx
seg030:0108 8B 16 82 E7            mov      dx, _in_port_1 ; 0x37A
seg030:010C EC                     in       al, dx
seg030:010D 8A C8                  mov      cl, al
seg030:010F 80 E1 5F               and      cl, 5Fh
seg030:0112 8A C1                  mov      al, cl
seg030:0114 EE                     out      dx, al
seg030:0115 EC                     in       al, dx
seg030:0116 8A C8                  mov      cl, al
seg030:0118 F6 C1 20               test     cl, 20h
seg030:011B 74 08                  jz       short loc_35A85
seg030:011D 8A 5E FD               mov      bl, [bp+var_3]
seg030:0120 80 E3 DF               and      bl, 0DFh
seg030:0123 EB 03                  jmp      short loc_35A88
seg030:0125
seg030:0125                loc_35A85: ; CODE XREF: sent_pro+DAj
seg030:0125 8A 5E FD               mov      bl, [bp+var_3]
seg030:0128
seg030:0128                loc_35A88: ; CODE XREF: sent_pro+E2j
seg030:0128 F6 C1 80               test     cl, 80h
seg030:012B 74 03                  jz       short loc_35A90
seg030:012D 80 E3 7F               and      bl, 7Fh
seg030:0130
seg030:0130                loc_35A90: ; CODE XREF: sent_pro+EAj
seg030:0130 8B 16 82 E7            mov      dx, _in_port_1 ; 0x37A
seg030:0134 8A C3                  mov      al, bl
seg030:0136 EE                     out      dx, al
seg030:0137 8B C7                  mov      ax, di
seg030:0139 5F                     pop      di
seg030:013A 5E                     pop      si
seg030:013B C9                     leave
seg030:013C CB                     retf
seg030:013C               sent_pro endp

可见，这个加密狗还是Sentinel Pro出品的“哈希验证”型加密狗。通过观察程序中传递的文本字符串，我们能够查到这种加密狗信息。而且它的16位返回值最终要和固定值进行比较。

输出端口地址通常是0x378，即USB问世之前、老式打印机才用的打印终端LPT接口。在设计这种接口时，恐怕没有人会想到要从打印机接收数据，因此这种接口是单向通信接口[3]。应用程序能够通过0x379端口访问打印机的状态寄存器，获取“缺纸”“确定/ack”“繁忙”之类的信号信息。也就是说，打印机的状态寄存器是主机了解打印机工作状态的唯一途径。因此，加密狗肯定通过这个寄存器获取反馈信息，逐次轮训有关比特位罢了。

源程序的_in_port_2和_in_port_1标签，分别访问了状态字寄存器（0x379）和控制寄存器（0x37A）。

看来，通过seg030:00B9的指令，程序通过“繁忙”标志为获取返回信息：每个比特位都存储于DI寄存器，由函数尾部的指令返回。

那么，发送给输出端口的这些字节都有什么涵义？笔者没有进行深究，只知道是发送给加密狗的指令。而且就这种任务来说，也不必把各个控制指令搞清楚。

检测加密狗的指令如下：

00000000 struct_0        struc ; (sizeof=0x1B)
00000000 field_0         db 25 dup(?)               ; string(C)
00000019 _A              dw?
0000001B struct_0        ends

dseg:3CBC 61 63 72 75+_Q   struct_0 <'hello', 01122h>
dseg:3CBC 6E 00 00 00+         ; DATA XREF: check_dongle+2Eo

... skipped ...

dseg:3E00 63 6F 66 66+     struct_0 <'coffee', 7EB7h>
dseg:3E1B 64 6F 67 00+     struct_0 <'dog', 0FFADh>
dseg:3E36 63 61 74 00+     struct_0 <'cat', 0FF5Fh>
dseg:3E51 70 61 70 65+     struct_0 <'paper', 0FFDFh>
dseg:3E6C 63 6F 6B 65+     struct_0 <'coke', 0F568h>
dseg:3E87 63 6C 6F 63+     struct_0 <'clock', 55EAh>
dseg:3EA2 64 69 72 00+     struct_0 <'dir', 0FFAEh>
dseg:3EBD 63 6F 70 79+     struct_0 <'copy', 0F557h>

seg030:0145               check_dongle proc far ; CODE XREF: sub_3771D+3EP
seg030:0145
seg030:0145               var_6 = dword ptr -6
seg030:0145               var_2 = word ptr -2
seg030:0145
seg030:0145 C8 06 00 00         enter    6, 0
seg030:0149 56                  push     si
seg030:014A 66 6A 00            push     large 0          ; newtime
seg030:014D 6A 00               push     0                ; cmd
seg030:014F 9A C1 18 00+        call     _biostime
seg030:0154 52                  push     dx
seg030:0155 50                  push     ax
seg030:0156 66 58               pop      eax
seg030:0158 83 C4 06            add      sp, 6
seg030:015B 66 89 46 FA         mov      [bp+var_6], eax
seg030:015F 66 3B 06 D8+        cmp      eax, _expiration
seg030:0164 7E 44               jle      short loc_35B0A
seg030:0166 6A 14               push     14h
seg030:0168 90                  nop
seg030:0169 0E                  push     cs
seg030:016A E8 52 00            call     near ptr get_rand
seg030:016D 59                  pop      cx
seg030:016E 8B F0               mov      si, ax
seg030:0170 6B C0 1B            imul     ax, 1Bh
seg030:0173 05 BC 3C            add      ax, offset _Q
seg030:0176 1E                  push     ds
seg030:0177 50                  push     ax
seg030:0178 0E                  push     cs
seg030:0179 E8 C5 FE            call     near ptr sent_pro
seg030:017C 83 C4 04            add      sp, 4
seg030:017F 89 46 FE            mov      [bp+var_2], ax
seg030:0182 8B C6               mov      ax, si
seg030:0184 6B C0 12            imul     ax, 18
seg030:0187 66 0F BF C0         movsx    eax, ax
seg030:018B 66 8B 56 FA         mov      edx, [bp+var_6]
seg030:018F 66 03 D0            add      edx, eax
seg030:0192 66 89 16 D8+        mov      _expiration, edx
seg030:0197 8B DE               mov      bx, si
seg030:0199 6B DB 1B            imul     bx, 27
seg030:019C 8B 87 D5 3C         mov      ax, _Q._A[bx]
seg030:01A0 3B 46 FE            cmp      ax, [bp+var_2]
seg030:01A3 74 05               jz       short loc_35B0A
seg030:01A5 B8 01 00            mov      ax, 1
seg030:01A8 EB 02               jmp      short loc_35B0C
seg030:01AA
seg030:01AA                 loc_35B0A: ; CODE XREF: check_dongle+1Fj
seg030:01AA                            ; check_dongle+5Ej
seg030:01AA 33 C0               xor      ax, ax
seg030:01AC
seg030:01AC                 loc_35B0C: ; CODE XREF: check_dongle+63j
seg030:01AC 5E                  pop      si
seg030:01AD C9                  leave
seg030:01AE CB                  retf
seg030:01AE                 check_dongle endp

一般来说，应用程序在执行重要功能之前都会检验加密狗。受并行打印口和加密狗硬件特性的共同制约，验证LPT加密狗的核实操作肯定非常非常慢。所以多数软件都会存储验证结果，在一段时间内免去验证的烦恼。它们多数会通过boiostime()函数获取当前时间。

另外，程序还使用了标准C函数get_rand()：

seg030:01BF               get_rand proc far ; CODE XREF: check_dongle+25p
seg030:01BF
seg030:01BF               arg_0    =word ptr 6
seg030:01BF
seg030:01BF 55                     push    bp
seg030:01C0 8B EC                  mov     bp, sp
seg030:01C2 9A 3D 21 00+           call    _rand
seg030:01C7 66 0F BF C0            movsx   eax, ax
seg030:01CB 66 0F BF 56+           movsx   edx, [bp+arg_0]
seg030:01D0 66 0F AF C2            imul    eax, edx
seg030:01D4 66 BB 00 80+           mov     ebx, 8000h
seg030:01DA 66 99                  cdq
seg030:01DC 66 F7 FB               idiv    ebx
seg030:01DF 5D                     pop     bp
seg030:01E0 CB                     retf
seg030:01E0               get_rand endp

可见，程序会把随机文本字符串发送到加密狗上，然后把加密狗的返回值与正确的哈希值进行比对。

加密狗检测主函数的调用方法如下：

seg033:087B 9A 45 01 96+     call    check_dongle
seg033:0880 0B C0            or      ax, ax
seg033:0882 74 62            jz      short OK
seg033:0884 83 3E 60 42+     cmp     word_620E0, 0
seg033:0889 75 5B            jnz     short OK
seg033:088B FF 06 60 42      inc     word_620E0
seg033:088F 1E               push    ds
seg033:0890 68 22 44         push    offset aTrupcRequiresA ; "This Software Requires a Software Lock\n"
seg033:0893 1E               push    ds
seg033:0894 68 60 E9         push    offset byte_6C7E0 ; dest
seg033:0897 9A 79 65 00+     call    _strcpy
seg033:089C 83 C4 08         add     sp, 8
seg033:089F 1E               push    ds
seg033:08A0 68 42 44         push    offset aPleaseContactA ; "Please Contact ..."
seg033:08A3 1E               push    ds
seg033:08A4 68 60 E9         push    offset byte_6C7E0 ; dest
seg033:08A7 9A CD 64 00+     call    _strcat

由此可知，破解加密狗的方法十分简单；我们只要把check_dongle()函数的返回值强制设置为0即可。例如，不妨在代码的开头处加上下列指令：

mov ax,0
retf

细心的读者可能会想起C语言的strcpy()函数仅仅需要2个参数而已，但是这个指令却传递了4个值。

seg033:088F 1E                       push    ds
seg033:0890 68 22 44                 push    offset aTrupcRequiresA ; "This Softwar ↙
    ↘ Requires a Software Lock\n"
seg033:0893 1E                       push    ds
seg033:0894 68 60 E9                 push    offset byte_6C7E0 ; dest
seg033:0897 9A 79 65 00+             call    _strcpy
seg033:089C 83 C4 08                 add     sp, 8

这是MS-DOS特有的寻址方式。如需了解详情，请参见第94章。

如您所见，在使用strcpy()和其他函数时，操作系统都是把指针分解为一对16位值、然后再进行传递的。

在程序启动时，DS寄存器的值被设置为当前程序数据段的地址。程序里的字符串信息都存储在数据段里。

在send_pro()函数里，字符串的每个字节都被加载到seg030:00EF。与此同时，LES指令从外部传递的参数里加载ES:BX对。地址seg030:00F5处的MOV指令，在内存中ES:BX对所描述的地址上读取字节。

而seg030:00F2的指令只对16位值进行递增处理，却没有处理的值。这意味着传递给函数的字符串位于两个数据段的地址之外。

[1]　CLRLWI是Clear left word immediate的缩写，用于清除指定的高/前n位，再把结果复制到目标操作符。

[2]　BLR是Branch to Link Register的缩写。

[3]　本文指的是LPT并行接口。实际上，IEEE 1284标准允许打印机回传数据。

第79章 “QR9”：魔方态加密模型

非专业的加密系统偶尔会让人感到匪夷所思。

本章将与您共同逆向分析一款已经找不到源代码的数据加密软件。

首先利用IDA的导出功能，导出这款程序的指令清单：

.text:00541000 set_bit          proc near                 ; CODE XREF: rotate1+42
.text:00541000                                            ; rotate2+42 ...
.text:00541000
.text:00541000 arg_0            = dword ptr  4
.text:00541000 arg_4            = dword ptr  8
.text:00541000 arg_8            = dword ptr  0Ch
.text:00541000 arg_C            = byte  ptr  10h
.text:00541000
.text:00541000                  mov     al, [esp+arg_C]
.text:00541004                  mov     ecx, [esp+arg_8]
.text:00541008                  push    esi
.text:00541009                  mov     esi, [esp+4+arg_0]
.text:0054100D                  test    al, al
.text:0054100F                  mov     eax, [esp+4+arg_4]
.text:00541013                  mov     dl, 1
.text:00541015                  jz      short loc_54102B
.text:00541017                  shl     dl, cl
.text:00541019                  mov     cl, cube64[eax+esi*8]
.text:00541020                  or      cl, dl
.text:00541022                  mov     cube64[eax+esi*8], cl
.text:00541029                  pop     esi
.text:0054102A                  retn
.text:0054102B
.text:0054102B loc_54102B:                                ; CODE XREF: set_bit+15
.text:0054102B                  shl     dl, cl
.text:0054102D                  mov     cl, cube64[eax+esi*8]
.text:00541034                  not     dl
.text:00541036                  and     cl, dl
.text:00541038                  mov     cube64[eax+esi*8], cl
.text:0054103F                  pop     esi
.text:00541040                  retn
.text:00541040 set_bit          endp
.text:00541040
.text:00541041                  align 10h
.text:00541050
.text:00541050 ; =============== S U B R O U T I N E =============
.text:00541050
.text:00541050
.text:00541050 get_bit          proc near                 ; CODE XREF: rotate1+16
.text:00541050                                            ; rotate2+16 ...
.text:00541050
.text:00541050 arg_0            = dword ptr  4
.text:00541050 arg_4            = dword ptr  8
.text:00541050 arg_8            = byte  ptr  0Ch
.text:00541050
.text:00541050                  mov     eax, [esp+arg_4]
.text:00541054                  mov     ecx, [esp+arg_0]
.text:00541058                  mov     al, cube64[eax+ecx*8]
.text:0054105F                  mov     cl, [esp+arg_8]
.text:00541063                  shr     al, cl
.text:00541065                  and     al, 1
.text:00541067                  retn
.text:00541067 get_bit          endp
.text:00541067
.text:00541068                  align 10h
.text:00541070
.text:00541070 ; =========== S U B R O U T I N E ============
.text:00541070
.text:00541070
.text:00541070 rotate1          proc near              ; CODE XREF: rotate_all_with_password+8E
.text:00541070
.text:00541070 internal_array_64= byte ptr -40h
.text:00541070 arg_0            = dword ptr  4
.text:00541070
.text:00541070                  sub     esp, 40h
.text:00541073                  push    ebx
.text:00541074                  push    ebp
.text:00541075                  mov     ebp, [esp+48h+arg_0]
.text:00541079                  push    esi
.text:0054107A                  push    edi
.text:0054107B                  xor     edi, edi               ; EDI is loop1 counter
.text:0054107D                  lea     ebx, [esp+50h+internal_array_64]
.text:00541081
.text:00541081 first_loop1_begin:                              ; CODE XREF: rotate1+2E
.text:00541081                  xor     esi, esi               ; ESI is loop2 counter
.text:00541083
.text:00541083 first_loop2_begin:                              ; CODE XREF: rotate1+25
.text:00541083                  push    ebp                    ; arg_0
.text:00541084                  push    esi
.text:00541085                  push    edi
.text:00541086                  call    get_bit
.text:0054108B                  add     esp, 0Ch
.text:0054108E                  mov     [ebx+esi], al    ; store to internal array
.text:00541091                  inc     esi
.text:00541092                  cmp     esi, 8
.text:00541095                  jl      short first_loop2_begin
.text:00541097                  inc     edi
.text:00541098                  add     ebx, 8
.text:0054109B                  cmp     edi, 8
.text:0054109E                  jl      short first_loop1_begin
.text:005410A0                  lea     ebx, [esp+50h+internal_array_64]
.text:005410A4                  mov     edi,7; EDI is loop1 counter, initial state is 7
.text:005410A9
.text:005410A9 second_loop1_begin:                     ; CODE XREF: rotate1+57
.text:005410A9                  xor     esi, esi       ; ESI is loop2 counter
.text:005410AB
.text:005410AB second_loop2_begin:                     ; CODE XREF: rotate1+4E
.text:005410AB                  mov     al, [ebx+esi]  ; value from internal array
.text:005410AE                  push    eax
.text:005410AF                  push    ebp            ; arg_0
.text:005410B0                  push    edi
.text:005410B1                  push    esi
.text:005410B2                  call    set_bit
.text:005410B7                  add     esp, 10h
.text:005410BA                  inc     esi            ; increment loop2 counter
.text:005410BB                  cmp     esi, 8
.text:005410BE                  jl      short second_loop2_begin
.text:005410C0                  dec     edi            ; decrement loop2 counter
.text:005410C1                  add     ebx, 8
.text:005410C4                  cmp     edi, 0FFFFFFFFh
.text:005410C7                  jg      short second_loop1_begin
.text:005410C9                  pop     edi
.text:005410CA                  pop     esi
.text:005410CB                  pop     ebp
.text:005410CC                  pop     ebx
.text:005410CD                  add     esp, 40h
.text:005410D0                  retn
.text:005410D0 rotate1          endp
.text:005410D0
.text:005410D1                  align 10h
.text:005410E0
.text:005410E0 ; ============ S U B R O U T I N E =========
.text:005410E0
.text:005410E0
.text:005410E0 rotate2          proc near                ; CODE XREF: rotate_all_with_password+7A
.text:005410E0
.text:005410E0 internal_array_64= byte ptr -40h
.text:005410E0 arg_0            = dword ptr  4
.text:005410E0
.text:005410E0                  sub     esp, 40h
.text:005410E3                  push    ebx
.text:005410E4                  push    ebp
.text:005410E5                  mov     ebp, [esp+48h+arg_0]
.text:005410E9                  push    esi
.text:005410EA                  push    edi
.text:005410EB                  xor     edi, edi         ; loop1 counter
.text:005410ED                  lea     ebx, [esp+50h+internal_array_64]
.text:005410F1
.text:005410F1 loc_5410F1:                               ; CODE XREF: rotate2+2E
.text:005410F1                  xor     esi, esi         ; loop2 counter
.text:005410F3
.text:005410F3 loc_5410F3:                               ; CODE XREF: rotate2+25
.text:005410F3                  push    esi              ; loop2
.text:005410F4                  push    edi              ; loop1
.text:005410F5                  push    ebp              ; arg_0
.text:005410F6                  call    get_bit
.text:005410FB                  add     esp, 0Ch
.text:005410FE                  mov     [ebx+esi], al    ; store to internal array
.text:00541101                  inc     esi              ; increment loop1 counter
.text:00541102                  cmp     esi, 8
.text:00541105                  jl      short loc_5410F3
.text:00541107                  inc     edi              ; increment loop2 counter
.text:00541108                  add     ebx, 8
.text:0054110B                  cmp     edi, 8
.text:0054110E                  jl      short loc_5410F1
.text:00541110                  lea     ebx, [esp+50h+internal_array_64]
.text:00541114                  mov     edi, 7  ; loop1 counter is initial state 7
.text:00541119
.text:00541119 loc_541119:                             ; CODE XREF: rotate2+57
.text:00541119                  xor     esi, esi       ; loop2 counter
.text:0054111B
.text:0054111B loc_54111B:                             ; CODE XREF: rotate2+4E
.text:0054111B                  mov     al, [ebx+esi]  ; get byte from internal array
.text:0054111E                  push    eax
.text:0054111F                  push    edi            ; loop1 counter
.text:00541120                  push    esi            ; loop2 counter
.text:00541121                  push    ebp            ; arg_0
.text:00541122                  call    set_bit
.text:00541127                  add     esp, 10h
.text:0054112A                  inc     esi            ; increment loop2 counter
.text:0054112B                  cmp     esi, 8
.text:0054112E                  jl      short loc_54111B
.text:00541130                  dec     edi            ; decrement loop2 counter
.text:00541131                  add     ebx, 8
.text:00541134                  cmp     edi, 0FFFFFFFFh
.text:00541137                  jg      short loc_541119
.text:00541139                  pop     edi
.text:0054113A                  pop     esi
.text:0054113B                  pop     ebp
.text:0054113C                  pop     ebx
.text:0054113D                  add     esp, 40h
.text:00541140                  retn
.text:00541140 rotate2          endp
.text:00541140
.text:00541141                  align 10h
.text:00541150
.text:00541150 ; ============ S U B R O U T I N E ===========
.text:00541150
.text:00541150
.text:00541150 rotate3          proc near      ; CODE XREF: rotate_all_with_password+66
.text:00541150
.text:00541150 var_40           = byte ptr -40h
.text:00541150 arg_0            = dword ptr  4
.text:00541150
.text:00541150                  sub     esp, 40h
.text:00541153                  push    ebx
.text:00541154                  push    ebp
.text:00541155                  mov     ebp, [esp+48h+arg_0]
.text:00541159                  push    esi
.text:0054115A                  push    edi
.text:0054115B                  xor     edi, edi
.text:0054115D                  lea     ebx, [esp+50h+var_40]
.text:00541161
.text:00541161 loc_541161:                                ; CODE XREF: rotate3+2E
.text:00541161                  xor     esi, esi
.text:00541163
.text:00541163 loc_541163:                                ; CODE XREF: rotate3+25
.text:00541163                  push    esi
.text:00541164                  push    ebp
.text:00541165                  push    edi
.text:00541166                  call    get_bit
.text:0054116B                  add     esp, 0Ch
.text:0054116E                  mov     [ebx+esi], al
.text:00541171                  inc     esi
.text:00541172                  cmp     esi, 8
.text:00541175                  jl      short loc_541163
.text:00541177                  inc     edi
.text:00541178                  add     ebx, 8
.text:0054117B                  cmp     edi, 8
.text:0054117E                  jl      short loc_541161
.text:00541180                  xor     ebx, ebx
.text:00541182                  lea     edi, [esp+50h+var_40]
.text:00541186
.text:00541186 loc_541186:                                ; CODE XREF: rotate3+54
.text:00541186                  mov     esi, 7
.text:0054118B
.text:0054118B loc_54118B:                                ; CODE XREF: rotate3+4E
.text:0054118B                  mov     al, [edi]
.text:0054118D                  push    eax
.text:0054118E                  push    ebx
.text:0054118F                  push    ebp
.text:00541190                  push    esi
.text:00541191                  call    set_bit
.text:00541196                  add     esp, 10h
.text:00541199                  inc     edi
.text:0054119A                  dec     esi
.text:0054119B                  cmp     esi, 0FFFFFFFFh
.text:0054119E                  jg      short loc_54118B
.text:005411A0                  inc     ebx
.text:005411A1                  cmp     ebx, 8
.text:005411A4                  jl      short loc_541186
.text:005411A6                  pop     edi
.text:005411A7                  pop     esi
.text:005411A8                  pop     ebp
.text:005411A9                  pop     ebx
.text:005411AA                  add     esp, 40h
.text:005411AD                  retn
.text:005411AD rotate3          endp
.text:005411AD
.text:005411AE                  align 10h
.text:005411B0
.text:005411B0 ; ============ S U B R O U T I N E ===========
.text:005411B0
.text:005411B0
.text:005411B0 rotate_all_with_password proc near       ; CODE XREF: crypt+1F
.text:005411B0                                          ; decrypt+36
.text:005411B0
.text:005411B0 arg_0            = dword ptr  4
.text:005411B0 arg_4            = dword ptr  8
.text:005411B0
.text:005411B0                  mov     eax, [esp+arg_0]
.text:005411B4                  push    ebp
.text:005411B5                  mov     ebp, eax
.text:005411B7                  cmp     byte ptr [eax], 0
.text:005411BA                  jz      exit
.text:005411C0                  push    ebx
.text:005411C1                  mov     ebx, [esp+8+arg_4]
.text:005411C5                  push    esi
.text:005411C6                  push    edi
.text:005411C7
.text:005411C7 loop_begin:                   ; CODE XREF: rotate_all_with_password+9F
.text:005411C7                  movsx   eax, byte ptr [ebp+0]
.text:005411CB                  push    eax    ; C
.text:005411CC                  call    _tolower
.text:005411D1                  add     esp, 4
.text:005411D4                  cmp     al, 'a'
.text:005411D6                  jl      short next_character_in_password
.text:005411D8                  cmp     al, 'z'
.text:005411DA                  jg      short next_character_in_password
.text:005411DC                  movsx   ecx, al
.text:005411DF                  sub     ecx, 'a'
.text:005411E2                  cmp     ecx, 24
.text:005411E5                  jle     short skip_subtracting
.text:005411E7                  sub     ecx, 24
.text:005411EA
.text:005411EA skip_subtracting:              ; CODE XREF: rotate_all_with_password+35
.text:005411EA                  mov     eax, 55555556h
.text:005411EF                  imul    ecx
.text:005411F1                  mov     eax, edx
.text:005411F3                  shr     eax, 1Fh
.text:005411F6                  add     edx, eax
.text:005411F8                  mov     eax, ecx
.text:005411FA                  mov     esi, edx
.text:005411FC                  mov     ecx, 3
.text:00541201                  cdq
.text:00541202                  idiv    ecx
.text:00541204                  sub     edx, 0
.text:00541207                  jz      short call_rotate1
.text:00541209                  dec     edx
.text:0054120A                  jz      short call_rotate2
.text:0054120C                  dec     edx
.text:0054120D                  jnz     short next_character_in_password
.text:0054120F                  test    ebx, ebx
.text:00541211                  jle     short next_character_in_password
.text:00541213                  mov     edi, ebx
.text:00541215
.text:00541215 call_rotate3:                 ; CODE XREF: rotate_all_with_password+6F
.text:00541215                  push    esi
.text:00541216                  call    rotate3
.text:0054121B                  add     esp, 4
.text:0054121E                  dec     edi
.text:0054121F                  jnz     short call_rotate3
.text:00541221                  jmp     short next_character_in_password
.text:00541223
.text:00541223 call_rotate2:                  ; CODE XREF: rotate_all_with_password+5A
.text:00541223                  test    ebx, ebx
.text:00541225                  jle     short next_character_in_password
.text:00541227                  mov     edi, ebx
.text:00541229
.text:00541229 loc_541229:                    ; CODE XREF: rotate_all_with_password+83
.text:00541229                  push    esi
.text:0054122A                  call    rotate2
.text:0054122F                  add     esp, 4
.text:00541232                  dec     edi
.text:00541233                  jnz     short loc_541229
.text:00541235                  jmp     short next_character_in_password
.text:00541237
.text:00541237 call_rotate1:                  ; CODE XREF: rotate_all_with_password+57
.text:00541237                  test    ebx, ebx
.text:00541239                  jle     short next_character_in_password
.text:0054123B                  mov     edi, ebx
.text:0054123D
.text:0054123D loc_54123D:                    ; CODE XREF: rotate_all_with_password+97
.text:0054123D                  push    esi
.text:0054123E                  call    rotate1
.text:00541243                  add     esp, 4
.text:00541246                  dec     edi
.text:00541247                  jnz     short loc_54123D
.text:00541249
.text:00541249 next_character_in_password:    ; CODE XREF: rotate_all_with_password+26
.text:00541249                                ; rotate_all_with_password+2A ...
.text:00541249                  mov     al, [ebp+1]
.text:0054124C                  inc     ebp
.text:0054124D                  test    al, al
.text:0054124F                  jnz     loop_begin
.text:00541255                  pop     edi
.text:00541256                  pop     esi
.text:00541257                  pop     ebx
.text:00541258
.text:00541258 exit:                         ; CODE XREF: rotate_all_with_password+A
.text:00541258                  pop     ebp
.text:00541259                  retn
.text:00541259 rotate_all_with_password endp
.text:00541259
.text:0054125A                  align 10h
.text:00541260
.text:00541260 ; ============= S U B R O U T I N E ===============
.text:00541260
.text:00541260
.text:00541260 crypt            proc near            ; CODE XREF: crypt_file+8A
.text:00541260
.text:00541260 arg_0            = dword ptr  4
.text:00541260 arg_4            = dword ptr  8
.text:00541260 arg_8            = dword ptr  0Ch
.text:00541260
.text:00541260                  push    ebx
.text:00541261                  mov     ebx, [esp+4+arg_0]
.text:00541265                  push    ebp
.text:00541266                  push    esi
.text:00541267                  push    edi
.text:00541268                  xor     ebp, ebp
.text:0054126A
.text:0054126A loc_54126A:                           ; CODE XREF: crypt+41
.text:0054126A                  mov     eax, [esp+10h+arg_8]
.text:0054126E                  mov     ecx, 10h
.text:00541273                  mov     esi, ebx
.text:00541275                  mov     edi, offset cube64
.text:0054127A                  push    1
.text:0054127C                  push    eax
.text:0054127D                  rep movsd
.text:0054127F                  call    rotate_all_with_password
.text:00541284                  mov     eax, [esp+18h+arg_4]
.text:00541288                  mov     edi, ebx
.text:0054128A                  add     ebp, 40h
.text:0054128D                  add     esp, 8
.text:00541290                  mov     ecx, 10h
.text:00541295                  mov     esi, offset cube64
.text:0054129A                  add     ebx, 40h
.text:0054129D                  cmp     ebp, eax
.text:0054129F                  rep movsd
.text:005412A1                  jl      short loc_54126A
.text:005412A3                  pop     edi
.text:005412A4                  pop     esi
.text:005412A5                  pop     ebp
.text:005412A6                  pop     ebx
.text:005412A7                  retn
.text:005412A7 crypt            endp
.text:005412A7
.text:005412A8                  align 10h
.text:005412B0
.text:005412B0 ; =========== S U B R O U T I N E ============
.text:005412B0
.text:005412B0
.text:005412B0 ; int __cdecl decrypt(int, int, void *Src)
.text:005412B0 decrypt          proc near            ; CODE XREF: decrypt_file+99
.text:005412B0
.text:005412B0 arg_0            = dword ptr  4
.text:005412B0 arg_4            = dword ptr  8
.text:005412B0 Src              = dword ptr  0Ch
.text:005412B0
.text:005412B0                  mov     eax, [esp+Src]
.text:005412B4                  push    ebx
.text:005412B5                  push    ebp
.text:005412B6                  push    esi
.text:005412B7                  push    edi
.text:005412B8                  push    eax            ; Src
.text:005412B9                  call    __strdup
.text:005412BE                  push    eax            ; Str
.text:005412BF                  mov     [esp+18h+Src], eax
.text:005412C3                  call    __strrev
.text:005412C8                  mov     ebx, [esp+18h+arg_0]
.text:005412CC                  add     esp, 8
.text:005412CF                  xor     ebp, ebp
.text:005412D1
.text:005412D1 loc_5412D1:                           ; CODE XREF: decrypt+58
.text:005412D1                  mov     ecx, 10h
.text:005412D6                  mov     esi, ebx
.text:005412D8                  mov     edi, offset cube64
.text:005412DD                  push    3
.text:005412DF                  rep movsd
.text:005412E1                  mov     ecx, [esp+14h+Src]
.text:005412E5                  push    ecx
.text:005412E6                  call    rotate_all_with_password
.text:005412EB                  mov     eax, [esp+18h+arg_4]
.text:005412EF                  mov     edi, ebx
.text:005412F1                  add     ebp, 40h
.text:005412F4                  add     esp, 8
.text:005412F7                  mov     ecx, 10h
.text:005412FC                  mov     esi, offset cube64
.text:00541301                  add     ebx, 40h
.text:00541304                  cmp     ebp, eax
.text:00541306                  rep movsd
.text:00541308                  jl      short loc_5412D1
.text:0054130A                  mov     edx, [esp+10h+Src]
.text:0054130E                  push    edx                ; Memory
.text:0054130F                  call    _free
.text:00541314                  add     esp, 4
.text:00541317                  pop     edi
.text:00541318                  pop     esi
.text:00541319                  pop     ebp
.text:0054131A                  pop     ebx
.text:0054131B                  retn
.text:0054131B decrypt          endp
.text:0054131B
.text:0054131C                  align 10h
.text:00541320
.text:00541320 ; ============= S U B R O U T I N E ============
.text:00541320
.text:00541320
.text:00541320 ; int __cdecl crypt_file(int Str, char *Filename, int password)
.text:00541320 crypt_file       proc near               ; CODE XREF: _main+42
.text:00541320
.text:00541320 Str              = dword ptr  4
.text:00541320 Filename         = dword ptr  8
.text:00541320 password         = dword ptr  0Ch
.text:00541320
.text:00541320                  mov     eax, [esp+Str]
.text:00541324                  push    ebp
.text:00541325                  push    offset Mode      ; "rb"
.text:0054132A                  push    eax              ; Filename
.text:0054132B                  call    _fopen           ; open file
.text:00541330                  mov     ebp, eax
.text:00541332                  add     esp, 8
.text:00541335                  test    ebp, ebp
.text:00541337                  jnz     short loc_541348
.text:00541339                  push    offset Format    ; "Cannot open input file!\n"
.text:0054133E                  call    _printf
.text:00541343                  add     esp, 4
.text:00541346                  pop     ebp
.text:00541347                  retn
.text:00541348
.text:00541348 loc_541348:                               ; CODE XREF: crypt_file+17
.text:00541348                  push    ebx
.text:00541349                  push    esi
.text:0054134A                  push    edi
.text:0054134B                  push    2                ; Origin
.text:0054134D                  push    0                ; Offset
.text:0054134F                  push    ebp              ; File
.text:00541350                  call    _fseek
.text:00541355                  push    ebp              ; File
.text:00541356                  call    _ftell           ; get file size
.text:0054135B                  push    0                ; Origin
.text:0054135D                  push    0                ; Offset
.text:0054135F                  push    ebp              ; File
.text:00541360                  mov     [esp+2Ch+Str], eax
.text:00541364                  call    _fseek           ; rewind to start
.text:00541369                  mov     esi, [esp+2Ch+Str]
.text:0054136D                  and     esi, 0FFFFFFC0h  ; reset all lowest 6 bits
.text:00541370                  add     esi, 40h         ; align size to 64-byte border
.text:00541373                  push    esi              ; Size
.text:00541374                  call    _malloc
.text:00541379                  mov     ecx, esi
.text:0054137B                  mov     ebx, eax   ; allocated buffer pointer -> to EBX
.text:0054137D                  mov     edx, ecx
.text:0054137F                  xor     eax, eax
.text:00541381                  mov     edi, ebx
.text:00541383                  push    ebp         ; File
.text:00541384                  shr     ecx, 2
.text:00541387                  rep stosd
.text:00541389                  mov     ecx, edx
.text:0054138B                  push    1           ; Count
.text:0054138D                  and     ecx, 3
.text:00541390                  rep stosb            ; memset (buffer, 0, aligned_size)
.text:00541392                  mov     eax, [esp+38h+Str]
.text:00541396                  push    eax               ; ElementSize
.text:00541397                  push    ebx               ; DstBuf
.text:00541398                  call    _fread            ; read file
.text:0054139D                  push    ebp               ; File
.text:0054139E                  call    _fclose
.text:005413A3                  mov     ecx, [esp+44h+password]
.text:005413A7                  push    ecx               ; password
.text:005413A8                  push    esi               ; aligned size
.text:005413A9                  push    ebx               ; buffer
.text:005413AA                  call    crypt             ; do crypt
.text:005413AF                  mov     edx, [esp+50h+Filename]
.text:005413B3                  add     esp, 40h
.text:005413B6                  push    offset aWb        ; "wb"
.text:005413BB                  push    edx               ; Filename
.text:005413BC                  call    _fopen
.text:005413C1                  mov     edi, eax
.text:005413C3                  push    edi               ; File
.text:005413C4                  push    1                 ; Count
.text:005413C6                  push    3                 ; Size
.text:005413C8                  push    offset aQr9       ; "QR9"
.text:005413CD                  call    _fwrite           ; write file signature
.text:005413D2                  push    edi               ; File
.text:005413D3                  push    1                 ; Count
.text:005413D5                  lea     eax, [esp+30h+Str]
.text:005413D9                  push    4                 ; Size
.text:005413DB                  push    eax               ; Str
.text:005413DC                  call    _fwrite           ; write original file size
.text:005413E1                  push    edi               ; File
.text:005413E2                  push    1                 ; Count
.text:005413E4                  push    esi               ; Size
.text:005413E5                  push    ebx               ; Str
.text:005413E6                  call    _fwrite           ; write encrypted file
.text:005413EB                  push    edi               ; File
.text:005413EC                  call    _fclose
.text:005413F1                  push    ebx               ; Memory
.text:005413F2                  call    _free
.text:005413F7                  add     esp, 40h
.text:005413FA                  pop     edi
.text:005413FB                  pop     esi
.text:005413FC                  pop     ebx
.text:005413FD                  pop     ebp
.text:005413FE                  retn
.text:005413FE crypt_file       endp
.text:005413FE
.text:005413FF                  align 10h
.text:00541400
.text:00541400 ; =========== S U B R O U T I N E ==============
.text:00541400
.text:00541400
.text:00541400 ; int __cdecl decrypt_file(char *Filename, int, void *Src)
.text:00541400 decrypt_file     proc near                   ; CODE XREF: _main+6E
.text:00541400
.text:00541400 Filename         = dword ptr  4
.text:00541400 arg_4            = dword ptr  8
.text:00541400 Src              = dword ptr  0Ch
.text:00541400
.text:00541400                  mov     eax, [esp+Filename]
.text:00541404                  push    ebx
.text:00541405                  push    ebp
.text:00541406                  push    esi
.text:00541407                  push    edi
.text:00541408                  push    offset aRb        ; "rb"
.text:0054140D                  push    eax               ; Filename
.text:0054140E                  call    _fopen
.text:00541413                  mov     esi, eax
.text:00541415                  add     esp, 8
.text:00541418                  test    esi, esi
.text:0054141A                  jnz     short loc_54142E
.text:0054141C                  push    offset aCannotOpenIn_0 ; "Cannot open input file!\n"
.text:00541421                  call    _printf
.text:00541426                  add     esp, 4
.text:00541429                  pop     edi
.text:0054142A                  pop     esi
.text:0054142B                  pop     ebp
.text:0054142C                  pop     ebx
.text:0054142D                  retn
.text:0054142E
.text:0054142E loc_54142E:                                ; CODE XREF: decrypt_file+1A
.text:0054142E                  push    2                 ; Origin
.text:00541430                  push    0                 ; Offset
.text:00541432                  push    esi               ; File
.text:00541433                  call    _fseek
.text:00541438                  push    esi               ; File
.text:00541439                  call    _ftell
.text:0054143E                  push    0                 ; Origin
.text:00541440                  push    0                 ; Offset
.text:00541442                  push    esi               ; File
.text:00541443                  mov     ebp, eax
.text:00541445                  call    _fseek
.text:0054144A                  push    ebp               ; Size
.text:0054144B                  call    _malloc
.text:00541450                  push    esi               ; File
.text:00541451                  mov     ebx, eax
.text:00541453                  push    1                 ; Count
.text:00541455                  push    ebp               ; ElementSize
.text:00541456                  push    ebx               ; DstBuf
.text:00541457                  call    _fread
.text:0054145C                  push    esi               ; File
.text:0054145D                  call    _fclose
.text:00541462                  add     esp, 34h
.text:00541465                  mov     ecx, 3
.text:0054146A                  mov     edi, offset aQr9_0 ; "QR9"
.text:0054146F                  mov     esi, ebx
.text:00541471                  xor     edx, edx
.text:00541473                  repe cmpsb
.text:00541475                  jz      short loc_541489
.text:00541477                  push    offset aFileIsNotCrypt ; "File is not encrypted!\n"
.text:0054147C                  call    _printf
.text:00541481                  add     esp, 4
.text:00541484                  pop     edi
.text:00541485                  pop     esi
.text:00541486                  pop     ebp
.text:00541487                  pop     ebx
.text:00541488                  retn
.text:00541489
.text:00541489 loc_541489:                               ; CODE XREF: decrypt_file+75
.text:00541489                  mov     eax, [esp+10h+Src]
.text:0054148D                  mov     edi, [ebx+3]
.text:00541490                  add     ebp, 0FFFFFFF9h
.text:00541493                  lea     esi, [ebx+7]
.text:00541496                  push    eax              ; Src
.text:00541497                  push    ebp              ; int
.text:00541498                  push    esi              ; int
.text:00541499                  call    decrypt
.text:0054149E                  mov     ecx, [esp+1Ch+arg_4]
.text:005414A2                  push    offset aWb_0     ; "wb"
.text:005414A7                  push    ecx              ; Filename
.text:005414A8                  call    _fopen
.text:005414AD                  mov     ebp, eax
.text:005414AF                  push    ebp              ; File
.text:005414B0                  push    1                ; Count
.text:005414B2                  push    edi              ; Size
.text:005414B3                  push    esi              ; Str
.text:005414B4                  call    _fwrite
.text:005414B9                  push    ebp              ; File
.text:005414BA                  call    _fclose
.text:005414BF                  push    ebx              ; Memory
.text:005414C0                  call    _free
.text:005414C5                  add     esp, 2Ch
.text:005414C8                  pop     edi
.text:005414C9                  pop     esi
.text:005414CA                  pop     ebp
.text:005414CB                  pop     ebx
.text:005414CC                  retn
.text:005414CC decrypt_file     endp

笔者在分析过程中逐步添加了各标签的名称。

我们从文件头开始分析。第一个函数读取两个文件名和一个密码：

.text:00541320 ; int __cdecl crypt_file(int Str, char *Filename, int password)
.text:00541320 crypt_file       proc near
.text:00541320
.text:00541320 Str              = dword ptr  4
.text:00541320 Filename         = dword ptr  8
.text:00541320 password         = dword ptr  0Ch
.text:00541320

如果不能成功打开明文文件，程序就会进行异常处理：

.text:00541320                  mov      eax, [esp+Str]
.text:00541324                  push     ebp
.text:00541325                  push     offset Mode    ; "rb"
.text:0054132A                  push     eax            ; Filename
.text:0054132B                  call     _fopen         ; open file
.text:00541330                  mov      ebp, eax
.text:00541332                  add      esp, 8
.text:00541335                  test     ebp, ebp
.text:00541337                  jnz      short loc_541348
.text:00541339                  push     offset Format  ; "Cannot open input file!\n"
.text:0054133E                  call     _printf
.text:00541343                  add      esp, 4
.text:00541346                  pop      ebp
.text:00541347                  pop      ebp
.text:00541348
.text:00541348 loc_541348:

通过fseek()/ftell()函数获取文件大小：

.text:00541348 push    ebx
.text:00541349 push    esi
.text:0054134A push    edi
.text:0054134B push    2             ; Origin
.text:0054134D push    0             ; Offset
.text:0054134F push    ebp           ; File

; move current file position to the end
.text:00541350 call    _fseek
.text:00541355 push    ebp           ; File
.text:00541356 call    _ftell        ; get current file position
.text:0054135B push    0             ; Origin
.text:0054135D push    0             ; Offset
.text:0054135F push    ebp           ; File
.text:00541360 mov     [esp+2Ch+Str], eax

; move current file position to the start
.text:00541364 call    _fseek

上述指令把文件大小向64字节边界对齐。程序所采用的加密算法只能处理64字节消息块。它的算法相当直白：把文件尺寸除以64，舍弃余数，然后把整除结果乘以64。下述指令的“与”运算起到整除64并清除余数的作用，然后加法运算指令把上述整除的商再加上64。这组指令将使文件大小向64字节边界对齐。

.text:00541369 mov     esi, [esp+2Ch+Str]
; reset all lowest 6 bits
.text:0054136D and     esi, 0FFFFFFC0h
; align size to 64-byte border
.text:00541370 add     esi, 40h

接下来按照上述结果分配缓冲区：

.text:00541373               push    esi                ; Size
.text:00541374               call    _malloc

调用memset()，即清除缓冲区数据：[1]

.text:00541379 mov     ecx, esi
.text:0054137B mov     ebx, eax          ; allocated buffer pointer -> to EBX
.text:0054137D mov     edx, ecx
.text:0054137F xor     eax, eax
.text:00541381 mov     edi, ebx
.text:00541383 push    ebp               ; File
.text:00541384 shr     ecx, 2
.text:00541387 rep stosd
.text:00541389 mov     ecx, edx
.text:0054138B push    1                 ; Count
.text:0054138D and     ecx, 3
.text:00541390 rep stosb                 ; memset (buffer, 0, aligned_size)

调用标准C函数fread()读取文件：

.text:00541392                  mov     eax, [esp+38h+Str]
.text:00541396                  push    eax          ; ElementSize
.text:00541397                  push    ebx          ; DstBuf
.text:00541398                  call    _fread       ; read file
.text:0054139D                  push    ebp          ; File
.text:0054139E                  call    _fclose

调用crypt()函数，并且向这个函数传递缓冲区、缓冲区尺寸及密码字符串：

.text:005413A3                  mov     ecx, [esp+44h+password]
.text:005413A7                  push    ecx          ; password
.text:005413A8                  push    esi          ; aligned size
.text:005413A9                  push    ebx          ; buffer
.text:005413AA                  call    crypt        ; do crypt

创建输出文件。虽然研发人员确实检测了能否成功打开文件，但是他们忘记了检测能否正确创建文件：

.text:005413AF                  mov     edx, [esp+50h+Filename]
.text:005413B3                  add     esp, 40h
.text:005413B6                  push    offset aWb      ; "wb"
.text:005413BB                  push    edx             ; Filename
.text:005413BC                  call    _fopen
.text:005413C1                  mov     edi, eax

新建文件的句柄（handle）存储在EDI寄存器里。然后写上签名“QR9”：

.text:005413C3                  push    edi          ; File
.text:005413C4                  push    1            ; Count
.text:005413C6                  push    3            ; Size
.text:005413C8                  push    offset aQr9  ; "QR9"
.text:005413CD                  call    _fwrite      ; write file signature

标注原始文件的实际大小（未经数据对齐处理的原始值）：

.text:005413D2                  push    edi          ; File
.text:005413D3                  push    1            ; Count
.text:005413D5                  lea     eax, [esp+30h+Str]
.text:005413D9                  push    4            ;Size
.text:005413DB                  push    eax          ; Str
.text:005413DC                  call    _fwrite      ; write original file size

写入密文的缓冲区：

.text:005413E1                  push    edi          ; File
.text:005413E2                  push    1            ; Count
.text:005413E4                  push    esi          ; Size
.text:005413E5                  push    ebx          ; Str
.text:005413E6                  call    _fwrite      ; write encrypted file

关闭文件、释放缓冲区：

.text:005413EB                  push    edi          ; File
.text:005413EC                  call    _fclose
.text:005413F1                  push    ebx          ; Memory
.text:005413F2                  call    _free
.text:005413F7                  add     esp, 40h
.text:005413FA                  pop     edi
.text:005413FB                  pop     esi
.text:005413FC                  pop     ebx
.text:005413FD                  pop     ebp
.text:005413FE                  retn
.text:005413FE crypt_file       endp

通过上面的分析，我们可整理出源代码如下：

void crypt_file(char *fin, char* fout, char *pw)
{
        FILE *f;
        int flen, flen_aligned;
        BYTE *buf;

        f=fopen(fin, "rb");

        if (f==NULL)
        {
                printf ("Cannot open input file!\n");
                return;
        };

        fseek (f, 0, SEEK_END);
        flen=ftell (f);
        fseek (f, 0, SEEK_SET);

        flen_aligned=(flen&0xFFFFFFC0)+0x40;

        buf=(BYTE*)malloc (flen_aligned);
        memset (buf, 0, flen_aligned);

        fread (buf, flen, 1, f);

        fclose (f);

        crypt (buf, flen_aligned, pw);

        f=fopen(fout, "wb");

        fwrite ("QR9", 3, 1, f);
        fwrite (&flen, 4, 1, f);
        fwrite (buf, flen_aligned, 1, f);

        fclose (f);

        free (buf);
};

解密过程几乎如出一辙：

.text:00541400 ; int __cdecl decrypt_file(char *Filename, int, void *Src)
.text:00541400 decrypt_file     proc near
.text:00541400
.text:00541400 Filename         = dword ptr  4
.text:00541400 arg_4            = dword ptr  8
.text:00541400 Src              = dword ptr  0Ch
.text:00541400
.text:00541400                  mov     eax, [esp+Filename]
.text:00541404                  push    ebx
.text:00541405                  push    ebp
.text:00541406                  push    esi
.text:00541407                  push    edi
.text:00541408                  push    offset aRb        ; "rb"
.text:0054140D                  push    eax               ; Filename
.text:0054140E                  call    _fopen
.text:00541413                  mov     esi, eax
.text:00541415                  add     esp, 8
.text:00541418                  test    esi, esi
.text:0054141A                  jnz     short loc_54142E
.text:0054141C                  push    offset aCannotOpenIn_0 ; "Cannot open input file!\n"
.text:00541421                  call    _printf
.text:00541426                  add     esp, 4
.text:00541429                  pop     edi
.text:0054142A                  pop     esi
.text:0054142B                  pop     ebp
.text:0054142C                  pop     ebx
.text:0054142D                  retn
.text:0054142E
.text:0054142E loc_54142E:
.text:0054142E                  push    2               ; Origin
.text:00541430                  push    0               ; Offset
.text:00541432                  push    esi             ; File
.text:00541433                  call    _fseek
.text:00541438                  push    esi             ; File
.text:00541439                  call    _ftell
.text:0054143E                  push    0               ; Origin
.text:00541440                  push    0               ; Offset
.text:00541442                  push    esi             ; File
.text:00541443                  mov     ebp, eax
.text:00541445                  call    _fseek
.text:0054144A                  push    ebp             ; Size
.text:0054144B                  call    _malloc
.text:00541450                  push    esi             ; File
.text:00541451                  mov     ebx, eax
.text:00541453                  push   1                ; Count
.text:00541455                  push    ebp             ; ElementSize
.text:00541456                  push    ebx             ; DstBuf
.text:00541457                  call    _fread
.text:0054145C                  push    esi             ; File
.text:0054145D                  call    _fclose

检测前三个字节的程序签名：

.text:00541462                  add     esp, 34h
.text:00541465                  mov     ecx, 3
.text:0054146A                  mov     edi, offset aQr9_0 ; "QR9"
.text:0054146F                  mov     esi, ebx
.text:00541471                  xor     edx, edx
.text:00541473                  repe cmpsb
.text:00541475                  jz      short loc_541489

如果签名有误，则进行错误提示：

.text:00541477                  push    offset aFileIsNotCrypt ; "File is not encrypted!\n"
.text:0054147C                  call    _printf
.text:00541481                  add     esp, 4
.text:00541484                  pop     edi
.text:00541485                  pop     esi
.text:00541486                  pop     ebp
.text:00541487                  pop     ebx
.text:00541488                  retn
.text:00541489
.text:00541489 loc_541489:

调用decrypt()函数：

.text:00541489                  mov     eax, [esp+10h+Src]
.text:0054148D                  mov     edi, [ebx+3]
.text:00541490                  add     ebp, 0FFFFFFF9h
.text:00541493                  lea     esi, [ebx+7]
.text:00541496                  push    eax                  ; Src
.text:00541497                  push    ebp                  ; int
.text:00541498                  push    esi                  ; int
.text:00541499                  call    decrypt
.text:0054149E                  mov     ecx, [esp+1Ch+arg_4]
.text:005414A2                  push    offset aWb_0         ; "wb"
.text:005414A7                  push    ecx                  ; Filename
.text:005414A8                  call    _fopen
.text:005414AD                  mov     ebp, eax
.text:005414AF                  push    ebp                  ; File
.text:005414B0                  push    1                    ; Count
.text:005414B2                  push    edi                  ; Size
.text:005414B3                  push    esi                  ; Str
.text:005414B4                  call    _fwrite
.text:005414B9                  push    ebp                  ; File
.text:005414BA                  call    _fclose
.text:005414BF                  push    ebx                  ; Memory
.text:005414C0                  call    _free
.text:005414C5                  add     esp, 2Ch
.text:005414C8                  pop     edi
.text:005414C9                  pop     esi
.text:005414CA                  pop     ebp
.text:005414CB                  pop     ebx
.text:005414CC                  retn
.text:005414CC decrypt_file     endp

通过上面的分析，我们可整理出decrypt_file()的源代码如下：

void decrypt_file(char *fin, char* fout, char *pw)
{
        FILE *f;
        int real_flen, flen;
        BYTE *buf;

        f=fopen(fin, "rb");

        if (f==NULL)
        {
                printf ("Cannot open input file!\n");
                return;
        };

        fseek (f, 0, SEEK_END);
        flen=ftell (f);
        fseek (f, 0, SEEK_SET);

        buf=(BYTE*)malloc (flen);

        fread (buf, flen, 1, f);

        fclose (f);

        if (memcmp (buf, "QR9", 3)!=0)
        {
                printf ("File is not encrypted!\n");
                return;
        };

        memcpy (&real_flen, buf+3, 4);

        decrypt (buf+(3+4), flen-(3+4), pw);

        f=fopen(fout, "wb");

        fwrite (buf+(3+4), real_flen, 1, f);

        fclose (f);

        free (buf);
};

接下来，我们深入研究crypt()函数：

.text:00541260 crypt            proc near
.text:00541260
.text:00541260 arg_0            = dword ptr  4
.text:00541260 arg_4            = dword ptr  8
.text:00541260 arg_8            = dword ptr  0Ch
.text:00541260
.text:00541260                  push    ebx
.text:00541261                  mov     ebx, [esp+4+arg_0]
.text:00541265                  push    ebp
.text:00541266                  push    esi
.text:00541267                  push    edi
.text:00541268                  xor     ebp, ebp
.text:0054126A
.text:0054126A loc_54126A:

这段指令从输入缓冲区中读取部分数据（即消息块），把它传递到内部数组。为了方便讨论，笔者把内部数组叫作“cube64”。信息的尺寸存储在ECX寄存器里。MOVSD代表move 32-bit dword。而16个32位dwords数据就是64个字节。

.text:0054126A                  mov     eax, [esp+10h+arg_8]
.text:0054126E                  mov     ecx, 10h
.text:00541273                  mov     esi, ebx ; EBX is pointer within input buffer
.text:00541275                  mov     edi, offset cube64
.text:0054127A                  push    1
.text:0054127C                  push    eax
.text:0054127D                  rep movsd

调用 rotate_all_with_password():

.text:0054127F                  call    rotate_all_with_password

把加密内容从cube64复制到缓冲区：

.text:00541284                  mov     eax, [esp+18h+arg_4]
.text:00541288                  mov     edi, ebx
.text:0054128A                  add     ebp, 40h
.text:0054128D                  add     esp, 8
.text:00541290                  mov     ecx, 10h
.text:00541295                  mov     esi, offset cube64
.text:0054129A                  add     ebx, 40h; add 64 to input buffer pointer
.text:0054129D                  cmp     ebp, eax; EBP contain amount of encrypted data.
.text:0054129F                  rep movsd

如果EBP不大于传入函数的文件大小，那么继续处理下一个消息块：

.text:005412A1                  jl      short loc_54126A
.text:005412A3                  pop     edi
.text:005412A4                  pop     esi
.text:005412A5                  pop     ebp
.text:005412A6                  pop     ebx
.text:005412A7                  retn
.text:005412A7 crypt            endp

通过上面的分析，我们可整理出crypt()的源代码如下：

void crypt (BYTE *buf, int sz, char *pw)
{
        int i=0;

        do
        {
                memcpy (cube, buf+i, 8*8);
                rotate_all (pw, 1);
                memcpy (buf+i, cube, 8*8);
                i+=64;
        }
        while (i<sz);
};

然后我们再分析rotate_all_with_password()函数。它有两个参数：密码字符串和数字参数。在加密crypt()函数里数字参数是1，而在同样调用rotate_all_with_password()函数的解密decrypt()函数里，这个数字参数的值为3。

.text:005411B0 rotate_all_with_password proc near
.text:005411B0
.text:005411B0 arg_0            = dword ptr 4
.text:005411B0 arg_4            = dword ptr 8
.text:005411B0
.text:005411B0                  mov     eax, [esp+arg_0]
.text:005411B4                  push    ebp
.text:005411B5                  mov     ebp, eax

检测密码中的当前字符。如果是零字节，就退出：

.text:005411B7                  cmp     byte ptr [eax], 0
.text:005411BA                  jz      exit
.text:005411C0                  push    ebx
.text:005411C1                  mov     ebx, [esp+8+arg_4]
.text:005411C5                  push    esi
.text:005411C6                  push    edi
.text:005411C7
.text:005411C7 loop_begin:

调用标准C函数tolower()：

.text:005411C7                  movsx   eax, byte ptr [ebp+0]
.text:005411CB                  push    eax                ; C
.text:005411CC                  call    _tolower
.text:005411D1                  add     esp, 4

这个函数会忽略（跳过）密码中的非拉丁字符。在进行程序测试的时候，加密工具确实忽略了非拉丁字符。

.text:005411D4                  cmp     al, 'a'
.text:005411D6                  jl      short next_character_in_password
.text:005411D8                  cmp     al, 'z'
.text:005411DA                  jg      short next_character_in_password
.text:005411DC                  movsx   ecx, al

从当前字符的ASCII值中减去97（即字母“a”）：

.text:005411DF                  sub     ecx, 'a' ; 97

经过刚才的处理，字母a的值就变成了0，b的值为1，z的值为 25:

.text:005411E2                  cmp     ecx, 24
.text:005411E5                  jle     short skip_subtracting
.text:005411E7                  sub     ecx, 24

似乎“y”和“z”是特例。在执行上述指令之后，“y”变成了0，“z”变成了1。这也就是说，26个拉丁字母最终被转换为了0～23之间（共计24个）的数字。

.text:005411EA
.text:005411EA skip_subtracting:              ;CODE XREF: rotate_all_ with_password+35

接着，它以乘法指令实现除法运算。本书第41章介绍过“除以9”的详细步骤。下面这段指令把密码字符的值除以3：

.text:005411EA                  mov     eax, 55555556h
.text:005411EF                  imul    ecx
.text:005411F1                  mov     eax, edx
.text:005411F3                  shr     eax, 1Fh
.text:005411F6                  add     edx, eax
.text:005411F8                  mov     eax, ecx
.text:005411FA                  mov     esi, edx
.text:005411FC                  mov     ecx, 3
.text:00541201                  cdq
.text:00541202                  idiv    ecx

除法运算的余数存储在EDX寄存器里：

.text:00541204 sub      edx, 0
.text:00541207 jz       short call_rotate1 ; if remainder is zero, go to rotate1
.text:00541209 dec      edx
.text:0054120A jz       short call_rotate2 ; .. if it is 1, go to rotate2
.text:0054120C dec      edx
.text:0054120D jnz      short next_character_in_password
.text:0054120F test     ebx, ebx
.text:00541211 jle      short next_character_in_password
.text:00541213 mov      edi, ebx

如果余数为2，那么就会调用rotate3()。此时，EDI寄存器存储的是rotate_all_with_password()函数的第二个参数。前文介绍过，在加密过程中这个值为1，在解密过程中这个值为3。函数进行了循环处理。在加密时，rotate1/2/3的调用次数与函数的第一个参数相等。

.text:00541215 call_rotate3:
.text:00541215                  push    esi
.text:00541216                  call    rotate3
.text:0054121B                  add     esp, 4
.text:0054121E                  dec     edi
.text:0054121F                  jnz     short call_rotate3
.text:00541221                  jmp     short next_character_in_password
.text:00541223
.text:00541223 call_rotate2:
.text:00541223                  test    ebx, ebx
.text:00541225                  jle     short next_character_in_password
.text:00541227                  mov     edi, ebx
.text:00541229
.text:00541229 loc_541229:
.text:00541229                  push    esi
.text:0054122A                  call    rotate2
.text:0054122F                  add     esp, 4
.text:00541232                  dec     edi
.text:00541233                  jnz     short loc_541229
.text:00541235                  jmp     short next_character_in_password
.text:00541237
.text:00541237 call_rotate1:
.text:00541237                  test    ebx, ebx
.text:00541239                  jle     short next_character_in_password
.text:0054123B                  mov     edi, ebx
.text:0054123D
.text:0054123D loc_54123D:
.text:0054123D                  push    esi
.text:0054123E                  call    rotate1
.text:00541243                  add     esp, 4
.text:00541246                  dec     edi
.text:00541247                  jnz     short loc_54123D
.text:00541249

从密码字符串中提取第二个字符：

.text:00541249 next_character_in_password:
.text:00541249                   mov     al, [ebp+1]

密码字符串指针递增：

.text:0054124C                  inc     ebp
.text:0054124D                  test    al, al
.text:0054124F                  jnz     loop_begin
.text:00541255                  pop     edi
.text:00541256                  pop     esi
.text:00541257                  pop     ebx
.text:00541258
.text:00541258 exit:
.text:00541258                  pop     ebp
.text:00541259                  retn
.text:00541259 rotate_all_with_password endp

通过上面的分析，我们可整理出rotate_all ()的源代码如下：

void rotate_all (char *pwd, int v)
{
        char *p=pwd;

        while (*p)
        {
                char c=*p;
                int q;

                c=tolower (c);

                if (c>='a' && c<='z')
                {
                        q=c-'a';
                        if (q>24)
                                q-=24;

                        int quotient=q/3;
                        int remainder=q % 3;

                        switch (remainder)
                        {
                        case 0: for (int i=0; i<v; i++) rotate1 (quotient); break;
                        case 1: for (int i=0; i<v; i++) rotate2 (quotient); break;
                        case 2: for (int i=0; i<v; i++) rotate3 (quotient); break;
                        };
                };

                p++;
        };
};

然后我们再分析rotate1/2/3函数。这三个函数都会调用它们之外的两个函数。根据函数功能，笔者把它们命名为set_bit()和get_bit()。

首先来分析get_bit():

.text:00541050 get_bit          proc near
.text:00541050
.text:00541050 arg_0            = dword ptr  4
.text:00541050 arg_4            = dword ptr  8
.text:00541050 arg_8            = byte ptr  0Ch
.text:00541050
.text:00541050                  mov      eax, [esp+arg_4]
.text:00541054                  mov      ecx, [esp+arg_0]
.text:00541058                  mov      al, cube64[eax+ecx*8]
.text:0054105F                  mov      cl, [esp+arg_8]
.text:00541063                  shr      al, cl
.text:00541065                  and      al, 1
.text:00541067                  retn
.text:00541067 get_bit          endp

上述指令先计算cube64的数组索引“arg_4+arg0*8”，然后将数组中的单个字节向右位移arg_8位，隔离最低位再返回数值。

接下来我们分析set_bit()函数：

.text:00541000 set_bit          proc near
.text:00541000
.text:00541000 arg_0            = dword ptr  4
.text:00541000 arg_4            = dword ptr  8
.text:00541000 arg_8            = dword ptr  0Ch
.text:00541000 arg_C            = byte ptr  10h
.text:00541000
.text:00541000                  mov      al, [esp+arg_C]
.text:00541004                  mov      ecx, [esp+arg_8]
.text:00541008                  push     esi
.text:00541009                  mov      esi, [esp+4+arg_0]
.text:0054100D                  test     al, al
.text:0054100F                  mov      eax, [esp+4+arg_4]
.text:00541013                  mov      dl, 1
.text:00541015                  jz       short loc_54102B

DL寄存器的值目前为1。函数将其左移arg_8位。例如，如果arg_8是4，那么DL寄存器的值将变为0x10，即二进制的1000b。

.text:00541017                  shl      dl, cl
.text:00541019                  mov      cl, cube64[eax+esi*8]

从数组中提取1个位，然后进行设置：

.text:00541020                  or       cl, dl

存储运算结果：

.text:00541022                  mov      cube64[eax+esi*8], cl
.text:00541029                  pop      esi
.text:0054102A                  retn
.text:0054102B
.text:0054102B loc_54102B:
.text:0054102B                  shl      dl, cl

如果arg_C的值不是0：

.text:0054102D                  mov      cl, cube64[eax+esi*8]

对DL的值求“非”。例如，如果前一步位移使DL为0x10(即二进制的1000b)，在进行非运算之后，它的值将变为0xEF（即二进制的11101111b）。

.text:00541034                  not      dl

下述指令将过滤相关位。如果DL寄存器的某个位是1，那么它会保留CL寄存器的对应位；否则将把CL寄存器的相关位设置为0。如果刚才DL的值不变，还是11101111b，那么CL的第5位（从数权最低位开始数）将变为0，其余位保持不变。

.text:00541036                  and      cl, dl

存储运算结果：

.text:00541038                  mov      cube64[eax+esi*8], cl
.text:0054103F                  pop      esi
.text:00541040                  retn
.text:00541040 set_bit          endp

Get_bit()函数的工作模式也差不多。在arg_C为0的情况下，函数将清除数组中的特定比特位，否则设置相关比特位为1。

我们已经知道数组占用64字节。无论是set_bit()函数还是get_bit()函数，它们的前两个参数都相当于2D坐标。然后数组变为8×8矩阵。

通过上面的分析，我们可整理出set_bit()和get_bit()的源代码如下：

#define IS_SET(flag, bit)       ((flag) & (bit))
#define SET_BIT(var, bit)       ((var) |= (bit))
#define REMOVE_BIT(var, bit)    ((var) &= ~(bit))

static BYTE cube[8][8];

void set_bit (int x, int y, int shift, int bit)
{
        if (bit)
                SET_BIT (cube[x][y], 1<<shift);
        else
                REMOVE_BIT (cube[x][y], 1<<shift);
};

bool get_bit (int x, int y, int shift)
{
        if ((cube[x][y]>>shift)&1==1)
                return 1;
        return 0;
};

回顾rotate1/2/3函数，可看到：

.text:00541070 rotate1          proc near
.text:00541070

函数使用局部数据栈给数组分配了64个字节：

.text:00541070 internal_array_64 = byte ptr -40h
.text:00541070 arg_0             = dword ptr  4
.text:00541070
.text:00541070                   sub      esp, 40h
.text:00541073                   push     ebx
.text:00541074                   push     ebp
.text:00541075                   mov      ebp, [esp+48h+arg_0]
.text:00541079                   push     esi
.text:0054107A                   push     edi
.text:0054107B                   xor      edi, edi   ;EDI is loop1 counter

EBX寄存器是指向内部数组的指针：

.text:0054107D                   lea      ebx, [esp+50h+internal_array_64]
.text:00541081

函数使用了2层循环：

.text:00541081 first_loop1_begin:
.text:00541081     xor      esi, esi       ; ESI is loop 2 counter
.text:00541083
.text:00541083 first_loop2_begin:
.text:00541083     push    ebp             ; arg_0
.text:00541084     push    esi             ; loop 1 counter
.text:00541085     push    edi             ; loop 2 counter
.text:00541086     call    get_bit
.text:0054108B     add     esp, 0Ch
.text:0054108E     mov     [ebx+esi], al   ; store to internal array
.text:00541091     inc     esi             ; increment loop 1 counter
.text:00541092     cmp     esi, 8
.text:00541095     jl      short first_loop2_begin
.text:00541097     inc     edi             ; increment loop 2 counter

; increment internal array pointer by 8 at each loop 1 iteration
.text:00541098     add     ebx, 8
.text:0054109B     cmp     edi, 8
.text:0054109E     jl      short first_loop1_begin

这两个循环的控制变量，不仅取值范围都在0～7之间，而且它们分别充当了get_bit()函数的第一个、第二个参数。而get_bit()函数使用的第三个参数是rotate1()函数的唯一一个参数。而后，get_bit()函数的返回值存储在内部数组里。

函数再次制备了指向内部数组的指针：

.text:005410A0     lea     ebx, [esp+50h+internal_array_64]
.text:005410A4     mov     edi, 7          ; EDI is loop1 counter, initial state is 7
.text:005410A9
.text:005410A9 second_loop1_begin:
.text:005410A9     xor     esi, esi        ; ESI is loop2 counter
.text:005410AB
.text:005410AB second_loop2_begin:
.text:005410AB     mov     al, [ebx+esi]   ; value from internal array
.text:005410AE     push    eax
.text:005410AF     push    ebp             ; arg_0
.text:005410B0     push    edi             ; loop1 counter
.text:005410B1     push    esi             ; loop2 counter
.text:005410B2     call    set_bit
.text:005410B7     add     esp, 10h
.text:005410BA     inc     esi             ; increment loop2 counter
.text:005410BB     cmp     esi, 8
.text:005410BE     jl      short second_loop2_begin
.text:005410C0     dec     edi             ; decrement loop2 counter
.text:005410C1     add     ebx, 8          ; increment pointer in internal array
.text:005410C4     cmp     edi, 0FFFFFFFFh
.text:005410C7     jg      short second_loop1_begin
.text:005410C9     pop     edi
.text:005410CA     pop     esi
.text:005410CB     pop     ebp
.text:005410CC     pop     ebx
.text:005410CD     add     esp, 40h
.text:005410D0     retn
.text:005410D0 rotate1         endp

虽然上述代码通过 set_bit()函数把内部数组里的数据复制到了全局数组里，但是数组的排列顺序完全不一样。第一层循环的循环控制变量从7逐渐变为0，呈递减的变化趋势。

通过上面的分析，我们可整理出rotate1()的源代码如下：

void rotatel (int v)
{
        bool tmp[8][8];//internal array
        int i, j;

        for (i=0; i<8; i++)
                for (j=0; j<8; j++)
                        tmp[i][j]=get_bit (i,j, v);

        for (i=0; i<8; i++)
                for (j=0; j<8; j++)
                        set_bit (j, 7-i, v, tmp[x][y]);
};

既然已经搞清了rotate1()函数，那么我们继续研究rotate2()函数：

.text:005410E0 rotate2 proc near
.text:005410E0
.text:005410E0 internal_array_64 = byte ptr -40h
.text:005410E0 arg_0 = dword ptr  4
.text:005410E0
.text:005410E0     sub     esp, 40h
.text:005410E3     push    ebx
.text:005410E4     push    ebp
.text:005410E5     mov     ebp, [esp+48h+arg_0]
.text:005410E9     push    esi
.text:005410EA     push    edi
.text:005410EB     xor     edi, edi       ; loop 1 counter
.text:005410ED     lea     ebx, [esp+50h+internal_array_64]
.text:005410F1
.text:005410F1 loc_5410F1:
.text:005410F1     xor     esi, esi       ; loop 2 counter
.text:005410F3
.text:005410F3 loc_5410F3:
.text:005410F3     push    esi            ; loop 2 counter
.text:005410F4     push    edi            ; loop 1 counter
.text:005410F5     push    ebp            ; arg_0
.text:005410F6     call    get_bit
.text:005410FB     add     esp, 0Ch
.text:005410FE     mov     [ebx+esi], al  ; store to internal array
.text:00541101     inc     esi            ; increment loop 1 counter
.text:00541102     cmp     esi, 8
.text:00541105     jl      short loc_5410F3
.text:00541107     inc     edi            ; increment loop 2 counter
.text:00541108     add     ebx, 8
.text:0054110B     cmp     edi, 8
.text:0054110E     jl      short loc_5410F1
.text:00541110     lea     ebx, [esp+50h+internal_array_64]
.text:00541114     mov     edi, 7         ; loop 1 counter is initial state 7
.text:00541119
.text:00541119 loc_541119:
.text:00541119     xor     esi, esi       ; loop 2 counter
.text:0054111B
.text:0054111B loc_54111B:
.text:0054111B     mov     al, [ebx+esi]  ; get byte from internal array
.text:0054111E     push    eax
.text:0054111F     push    edi            ; loop 1 counter
.text:00541120     push    esi            ; loop 2 counter
.text:00541121     push    ebp            ; arg_0
.text:00541122     call    set_bit
.text:00541127     add     esp, 10h
.text:0054112A     inc     esi            ; increment loop 2 counter
.text:0054112B     cmp     esi, 8
.text:0054112E     jl      short loc_54111B
.text:00541130     dec     edi            ; decrement loop 2 counter
.text:00541131     add     ebx, 8
.text:00541134     cmp     edi, 0FFFFFFFFh
.text:00541137     jg      short loc_541119
.text:00541139     pop     edi
.text:0054113A     pop     esi
.text:0054113B     pop     ebp
.text:0054113C     pop     ebx
.text:0054113D     add     esp, 40h
.text:00541140     retn
.text:00541140 rotate2 endp

在调用get_bit()和set_bit()时，rotate1/2以不同的顺序传递参数。除此之外，rotate1/2基本相同。

可得rotate1()的源代码如下：

void rotate2 (int v)
{
        bool tmp[8][8]; // internal array
        int i, j;

        for (i=0; i<8; i++)
                for (j=0; j<8; j++)
                        tmp[i][j]=get_bit (v, i, j);

        for (i=0; i<8; i++)
                for (j=0; j<8; j++)
                        set_bit (v, j, 7-i, tmp[i][j]);
};

下面我们分析rotate3()函数：

void rotate3 (int v)
{
        bool tmp[8][8];
        int i, j;

        for (i=0; i<8; i++)
                for (j=0; j<8; j++)
                        tmp[i][j]=get_bit (i, v, j);

        for (i=0; i<8; i++)
                for (j=0; j<8; j++)
                        set_bit (7-j, v, i, tmp[i][j]);
};

这个函数更容易分析。如果把cube64当作8×8×8的3D立方体、每个元素都是一个比特位，那么get_bit()函数和set_bit()函数的作用就是从相应坐标读取1个比特位。

如此来看，rotate1/2/3的作用就是以特定隔层旋转所有比特位。这三个参数分别旋转魔方的不同平面，而参数v代表的是设置隔层的位置（0～7）。

或许，这个算法的作者就是把数据当作8×8×8的魔方来处理的。

再次整理decrypt()函数的分析结果后，可推导出下列源代码：

void decrypt (BYTE *buf, int sz, char *pw)
{
        char *p=strdup (pw);
        strrev (p);
        int i=0;

        do
        {
                memcpy (cube, buf+i, 8*8);
                rotate_all (p, 3);
                memcpy (buf+i, cube, 8*8);
                i+=64;
        }
        while (i<sz);

        free (p);
};

该函数通过标准C函数strrev()[2]对密码字符串进行逆序排列，而且传递给rotate_all()函数的参数是3。除此之外，它和crypt()函数基本一致。

因此，解密过程调用了3次rotate1/2/3。

这和魔方的玩法没什么区别！如果要撤销调整魔方的操作，那么就要用相反的次序和相反方向进行等幅操作。举例来说，您刚刚顺时针转动了魔方的某层方块、后来又要撤销那次操作，那么就把那层方块顺时针转回来，要不然就再顺时针旋转3次。

如果说rotate1()函数旋转的是魔方的“正”面，那么rotate2()函数旋转的则是“上”面，而rotate3()函数则用于旋转魔方的侧面。

不妨再回顾一下rotate_all()函数的代码：

q=c-'a';
if (q>24)
        q-=24;

int quotient=q/3; // in range 0..7
int remainder=q % 3;

switch (remainder)
{
    case 0: for (int i=0; i<v; i++) rotate1 (quotient); break; // front
    case 1: for (int i=0; i<v; i++) rotate2 (quotient); break; // top
    case 2: for (int i=0; i<v; i++) rotate3 (quotient); break; // left
};

综合上述分析可知：密码中的每个字符都描述了旋转的“面”（0～2）和“层”（0～7）信息。3个面×8个层＝24（密码字节的取值范围）。为了把26个字母映射到24个数据元素，该程序把字母表的最后两个字母重新映射为0和1。

但是这种算法非常脆弱：在使用短密码的情况下，加密文件的密文篇幅和明文原文的尺寸相等。

重新分析整个程序，可整理得源代码如下：

#include <windows.h>

#include <stdio.h>
#include <assert.h>

#define IS_SET(flag, bit)        ((flag) & (bit))
#define SET_BIT(var, bit)        ((var) |= (bit))
#define REMOVE_BIT(var, bit)     ((var) &= ~(bit))

static BYTE cube[8][8];

void set_bit (int x, int y, int z, bool bit)
{
        if (bit)
                SET_BIT (cube[x][y], 1<<z);
        else
                REMOVE_BIT (cube[x][y], 1<<z);
};

bool get_bit (int x, int y, int z)
{
        if ((cube[x][y]>>z)&1==1)
                return true;
        return false;
};

void rotate_f (int row)
{
        bool tmp[8][8];
        int x, y;

        for (x=0; x<8; x++)
                for (y=0; y<8; y++)
                        tmp[x][y]=get_bit (x, y, row);

        for (x=0; x<8; x++)
                for (y=0; y<8; y++)
                        set_bit (y, 7-x, row, tmp[x][y]);
};

void rotate_t (int row)
{
        bool tmp[8][8];
        int y, z;

        for (y=0; y<8; y++)
                for (z=0; z<8; z++)
                        tmp[y][z]=get_bit (row, y, z);

        for (y=0; y<8; y++)
                for (z=0; z<8; z++)
                        set_bit (row, z, 7-y, tmp[y][z]);
};

void rotate_l (int row)
{
        bool tmp[8][8];
        int x, z;

        for (x=0; x<8; x++)
                for (z=0; z<8; z++)
                        tmp[x][z]=get_bit (x, row, z);

        for (x=0; x<8; x++)
                for (z=0; z<8; z++)
                        set_bit (7-z, row, x, tmp[x][z]);
};

void rotate_all (char *pwd, int v)
{
        char *p=pwd;

        while (*p)
        {
                char c=*p;
                int q;

                c=tolower (c);

                if (c>='a' && c<='z')
                {
                        q=c-'a';
                        if (q>24)
                                q-=24;

                        int quotient=q/3;
                        int remainder=q % 3;

                        switch (remainder)
                        {
                        case 0: for (int i=0; i<v; i++) rotate_f (quotient); break;
                        case 1: for (int i=0; i<v; i++) rotate_t (quotient); break;
                        case 2: for (int i=0; i<v; i++) rotate_l (quotient); break;
                        };
                };
                p++;
        };
};

void crypt (BYTE *buf, int sz, char *pw)
{
        int i=0;

        do
        {
                memcpy (cube, buf+i, 8*8);
                rotate_all (pw, 1);
                memcpy (buf+i, cube, 8*8);
                i+=64;
        };
        while (i<sz);
};

void decrypt (BYTE *buf, int sz, char *pw)
{
        char *p=strdup (pw);
        strrev (p);
        int i=0;

        do
        {
                memcpy (cube, buf+i, 8*8);
                rotate_all (p, 3);
                memcpy (buf+i, cube, 8*8);
                i+=64;
        }
        while (i<sz);

        free (p);
};

void crypt_file(char *fin, char* fout, char *pw)
{
        FILE *f;
        int flen, flen_aligned;
        BYTE *buf;

        f=fopen(fin, "rb");

        if (f==NULL)
        {
                printf ("Cannot open input file!\n");
                return;
        };

        fseek (f, 0, SEEK_END);
        flen=ftell (f);
        fseek (f, 0, SEEK_SET);

        flen_aligned=(flen&0xFFFFFFC0)+0x40;

        buf=(BYTE*)malloc (flen_aligned);
        memset (buf, 0, flen_aligned);

        fread (buf, flen, 1, f);

        fclose (f);

        crypt (buf, flen_aligned, pw);

        f=fopen(fout, "wb");

        fwrite ("QR9", 3, 1, f);
        fwrite (&flen, 4, 1, f);
        fwrite (buf, flen_aligned, 1, f);

        fclose (f);

        free (buf);

};

void decrypt_file(char *fin, char* fout, char *pw)
{
        FILE *f;
        int real_flen, flen;
        BYTE *buf;

        f=fopen(fin, "rb");

        if (f==NULL)
        {
                printf ("Cannot open input file!\n");
                return;
        };

        fseek (f, 0, SEEK_END);
        flen=ftell (f);
        fseek (f, 0, SEEK_SET);

        buf=(BYTE*)malloc (flen);

        fread (buf, flen, 1, f);

        fclose (f);

        if (memcmp (buf, "QR9", 3)!=0)
        {
                printf ("File is not encrypted!\n");
                return;
        };

        memcpy (&real_flen, buf+3, 4);

        decrypt (buf+(3+4), flen-(3+4), pw);

        f=fopen(fout, "wb");

        fwrite (buf+(3+4), real_flen, 1, f);

        fclose (f);

        free (buf);
};

// run: input output 0/1 password
// 0 for encrypt, 1 for decrypt

int main(int argc, char *argv[])
{
        if (argc!=5)
        {
                printf ("Incorrect parameters!\n");
                return 1;
        };

        if (strcmp (argv[3], "0")==0)
                crypt_file (argv[1], argv[2], argv[4]);
        else
                if (strcmp (argv[3], "1")==0)
                        decrypt_file (argv[1], argv[2], argv[4]);
                else
                        printf ("Wrong param %s\n", argv[3]);

        return 0;
};

[1]　实际上calloc()函数可替代malloc()和memset()两个函数。

[2]　请参考MSDN的说明：https://msdn.microsoft.com/en-us/library/9hby7w40(VS.80).aspx。

第80章 SAP

80.1　关闭客户端的网络数据包压缩功能

根据公开资料的记载，默认情况下，SAP GUI（客户端）和SAP服务端之间的通信是压缩通信，而非加密通信。[1]

决定SAP GUI（客户端）是否采用网络数据包压缩功能的环境变量是TDW_NOCOMPRESS。若把这个变量设置为1，那么就可以关闭客户端的网络数据压缩功能。不过，如果当真进行了这种设置，客户端程序就会弹出如图80.1所示的提示窗口，而且这个弹出窗口根本关不掉。

图80.1　截屏

本节的任务就是去除这个提示窗口。

在此之前，我们可以确定的事实是：

① 在SAP GUI客户端程序里，环境变量TDW_ NOCOMPRESS被设为1。

② 程序的某个文件里应当存在字符串“data compression switched off”。

借助文件管理器FAR程序[2]，我们可在SAPguilib.dll里找到这个字符串。

然后，我们用IDA程序打开文件SAPguilib.dll，在文件里搜索字符串“TDW_NOCOMPRESS”。所幸的是，我们可以找到这个字符串，而且整个文件只有一处指令调用了这个字符串。

该文件的相关指令为：[3]

.text:6440D51B                  lea     eax, [ebp+2108h+var_211C]
.text:6440D51E                  push    eax                ; int
.text:6440D51F                  push    offset aTdw_nocompress ; "TDW_NOCOMPRESS"
.text:6440D524                  mov     byte ptr [edi+15h], 0
.text:6440D528                  call    chk_env
.text:6440D52D                  pop     ecx
.text:6440D52E                  pop     ecx
.text:6440D52F                  push    offset byte_64443AF8
.text:6440D534                  lea     ecx, [ebp+2108h+var_211C]

; demangled name: int ATL::CStringT::Compare(char const *)const
.text:6440D537                  call    ds:mfc90_1603
.text:6440D53D                  test    eax, eax
.text:6440D53F                  jz      short loc_6440D55A
.text:6440D541                  lea     ecx, [ebp+2108h+var_211C]

; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:6440D544                  call    ds:mfc90_910
.text:6440D54A                  push    eax                ; Str
.text:6440D54B                  call    ds:atoi
.text:6440D551                  test    eax, eax
.text:6440D553                  setnz   al
.text:6440D556                  pop     ecx
.text:6440D557                  mov     [edi+15h], al

据此可知，chk_env()函数通过第二个参数获取环境变量字符串，然后MFC字符串处理函数会继续处理这个字符串，接下来标准C函数atoi()（从字符串转换为数字）再从这个字符串里提取数值。最终，程序会把解析出来的环境变量的变量值存储在地址edi+15h里。

接下来，我们一起分析chk_env()函数[4]：

.text:64413F20 ; int __cdecl chk_env(char *VarName, int)
.text:64413F20 chk_env          proc near
.text:64413F20
.text:64413F20 DstSize          = dword ptr -0Ch
.text:64413F20 var_8            = dword ptr -8
.text:64413F20 DstBuf           = dword ptr -4
.text:64413F20 VarName          = dword ptr  8
.text:64413F20 arg_4            = dword ptr  0Ch
.text:64413F20
.text:64413F20                  push    ebp
.text:64413F21                  mov     ebp, esp
.text:64413F23                  sub     esp, 0Ch
.text:64413F26                  mov     [ebp+DstSize], 0
.text:64413F2D                  mov     [ebp+DstBuf], 0
.text:64413F34                  push    offset unk_6444C88C
.text:64413F39                  mov     ecx, [ebp+arg_4]

; (demangled name) ATL::CStringT::operator=(char const *)
.text:64413F3C                  call    ds:mfc90_820
.text:64413F42                  mov     eax, [ebp+VarName]
.text:64413F45                  push    eax              ; VarName
.text:64413F46                  mov     ecx, [ebp+DstSize]
.text:64413F49                  push    ecx              ; DstSize
.text:64413F4A                  mov     edx, [ebp+DstBuf]
.text:64413F4D                  push    edx              ; DstBuf
.text:64413F4E                  lea     eax, [ebp+DstSize]
.text:64413F51                  push    eax              ; ReturnSize
.text:64413F52                  call    ds:getenv_s
.text:64413F58                  add     esp, 10h
.text:64413F5B                  mov     [ebp+var_8], eax
.text:64413F5E                  cmp     [ebp+var_8], 0
.text:64413F62                  jz      short loc_64413F68
.text:64413F64                  xor     eax, eax
.text:64413F66                  jmp     short loc_64413FBC
.text:64413F68
.text:64413F68 loc_64413F68:
.text:64413F68                  cmp     [ebp+DstSize], 0
.text:64413F6C                  jnz     short loc_64413F72
.text:64413F6E                  xor     eax, eax
.text:64413F70                  jmp     short loc_64413FBC
.text:64413F72
.text:64413F72 loc_64413F72:
.text:64413F72                  jmp     short loc_64413FBC
.text:64413F75                  push    ecx
.text:64413F76                  mov     ecx, [ebp+arg_4]

; demangled name: ATL::CSimpleStringT<char, 1>::Preallocate(int)
.text:64413F79                  call    ds:mfc90_2691
.text:64413F7F                  mov     [ebp+DstBuf], eax
.text:64413F82                  mov     edx, [ebp+VarName]
.text:64413F85                  push    edx              ; VarName
.text:64413F86                  mov     eax, [ebp+DstSize]
.text:64413F89                  push    eax              ; DstSize
.text:64413F8A                  mov     ecx, [ebp+DstBuf]
.text:64413F8D                  push    ecx              ; DstBuf
.text:64413F8E                  lea     edx, [ebp+DstSize]
.text:64413F91                  push    edx              ; ReturnSize
.text:64413F92                  call    ds:getenv_s
.text:64413F98                  add     esp, 10h
.text:64413F9B                  mov     [ebp+var_8], eax
.text:64413F9E                  push    0FFFFFFFFh
.text:64413FA0                  mov     ecx, [ebp+arg_4]

; demangled name: ATL::CSimpleStringT::ReleaseBuffer(int)
.text:64413FA3                  call    ds:mfc90_5835
.text:64413FA9                  cmp     [ebp+var_8], 0
.text:64413FAD                  jz      short loc_64413FB3
.text:64413FAF                  xor     eax, eax
.text:64413FB1                  jmp     short loc_64413FBC
.text:64413FB3
.text:64413FB3 loc_64413FB3:
.text:64413FB3                  mov     ecx, [ebp+arg_4]

; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:64413FB6                  call    ds:mfc90_910
.text:64413FBC
.text:64413FBC loc_64413FBC:
.text:64413FBC
.text:64413FBC                  mov     esp, ebp
.text:64413FBE                  mov     esp, ebp
.text:64413FBF                  retn
.text:64413FBF chk_env          endp

其中，getenv_s()函数[5]是微软退出的改进版getenv()函数[6]，它提升了原有函数的安全特性。

这个函数多处调用了MFC字符串处理函数。

不仅如此，它还检测了其他的环境变量。如果打开SAP GUI（客户端）程序的日志记录功能，就会在trace log里看到它检测的环境变量，如下表所示。

DPTRACE | “GUI-OPTION: Trace set to %d”
TDW_HEXDUMP | “GUI-OPTION: Hexdump enabled”
TDW_WORKDIR | “GUI-OPTION: working directory ‘%s’ ”
TDW_SPLASHSRCEENOFF | “GUI-OPTION: Splash Screen Off” / “GUI-OPTION: Splash Screen On”
TDW_REPLYTIMEOUT | “GUI-OPTION: reply timeout %d milliseconds”
TDW_PLAYBACKTIMEOUT | “GUI-OPTION: PlaybackTimeout set to %d milliseconds”
TDW_NOCOMPRESS | “GUI-OPTION: no compression read”
TDW_EXPERT | “GUI-OPTION: expert mode”
TDW_PLAYBACKPROGRESS | “GUI-OPTION: PlaybackProgress”
TDW_PLAYBACKNETTRAFFIC | “GUI-OPTION: PlaybackNetTraffic”
TDW_PLAYLOG | “GUI-OPTION: /PlayLog is YES, file %s”
TDW_PLAYTIME | “GUI-OPTION: /PlayTime set to %d milliseconds”
TDW_LOGFILE | “GUI-OPTION: TDW_LOGFILE ‘%s’ ”
TDW_WAN | “GUI-OPTION: WAN - low speed connection enabled”
TDW_FULLMENU | “GUI-OPTION: FullMenu enabled”
SAP_CP / SAP_CODEPAGE | “GUI-OPTION: SAP_CODEPAGE ‘%d’ ”
UPDOWNLOAD_CP | “GUI-OPTION: UPDOWNLOAD_CP ‘%d’ ”
SNC_PARTNERNAME | “GUI-OPTION: SNC name ‘%s’ ”
SNC_QOP | “GUI-OPTION: SNC_QOP ‘%s’ ”
SNC_LIB | “GUI-OPTION: SNC is set to: %s”
SAPGUI_INPLACE | “GUI-OPTION: environment variable SAPGUI_INPLACE is on”

函数把这些变量都存储在数组里，而且把EDI寄存器当作这个数组的指针。在调用chk_evn()函数之前，程序首先设置了EDI的值：

.text:6440EE00                lea     edi, [ebp+2884h+var_2884] ; options here like +0x15...
.text:6440EE03                lea     ecx, [esi+24h]
.text:6440EE06                call    load_command_line
.text:6440EE0B                mov     edi, eax
.text:6440EE0D                xor     ebx, ebx
.text:6440EE0F                cmp     edi, ebx
.text:6440EE11                jz      short loc_6440EE42
.text:6440EE13                push    edi
.text:6440EE14                push    offset aSapguiStoppedA ; "Sapgui stopped after ↙
↘ commandline interp"...
.text:6440EE19                push    dword_644F93E8
.text:6440EE1F                call    FEWTraceError

那么，我们关注的“data record mode switched on”字符串在这个文件里吗？整个文件里，只有CDwsGui::PrepareInfoWindow()构造函数调用了这个字符串。我们可通过日志文件的调试调用（debugging calls）信息了解各个class/method的名字。

例如，下述调试调用信息就透露了构造函数的函数名称：

.text:64405160                  push    dword ptr [esi+2854h]
.text:64405166                  push    offset aCdwsguiPrepare ; "\nCDwsGui::PrepareInfoWindow: ↙
    ↘ sapgui env"...
.text:6440516B                  push    dword ptr [esi+2848h]
.text:64405171                  call    dbg
.text:64405176                  add     esp, 0Ch

以及：

.text:6440237A                  push    eax
.text:6440237B                  push    offset aCclientStart_6 ; "CClient::Start: set shortcut ↙
    ↘ user to '\%"...
.text:64402380                  push    dword ptr [edi+4]
.text:64402383                  call    dbg
.text:64402388                  add     esp, 0Ch

这些信息的作用很大。

接下来，我们直奔那个令人恼火的弹出窗口：

.text:64404F4F CDwsGui__PrepareInfoWindow proc near
.text:64404F4F
.text:64404F4F pvParam         = byte ptr -3Ch
.text:64404F4F var_38          = dword ptr -38h
.text:64404F4F var_34          = dword ptr -34h
.text:64404F4F rc              = tagRECT ptr -2Ch
.text:64404F4F cy              = dword ptr -1Ch
.text:64404F4F h               = dword ptr -18h
.text:64404F4F var_14          = dword ptr -14h
.text:64404F4F var_10          = dword ptr -10h
.text:64404F4F var_4           = dword ptr -4
.text:64404F4F
.text:64404F4F                 push    30h
.text:64404F51                 mov     eax, offset loc_64438E00
.text:64404F56                 call    __EH_prolog3
.text:64404F5B                 mov     esi, ecx          ; ECX is pointer to object
.text:64404F5D                 xor     ebx, ebx
.text:64404F5F                 lea     ecx, [ebp+var_14]
.text:64404F62                 mov     [ebp+var_10], ebx

; demangled name: ATL::CStringT(void)
.text:64404F65                 call    ds:mfc90_316
.text:64404F6B                 mov     [ebp+var_4], ebx
.text:64404F6E                 lea     edi, [esi+2854h]
.text:64404F74                 push    offset aEnvironmentInf ; "Environment information:\n"
.text:64404F79                 mov     ecx, edi

; demangled name: ATL::CStringT::operator=(char const *)
.text:64404F7B                 call    ds:mfc90_820
.text:64404F81                 cmp     [esi+38h], ebx
.text:64404F84                 mov     ebx, ds:mfc90_2539
.text:64404F8A                 jbe     short loc_64404FA9
.text:64404F8C                 push    dword ptr [esi+34h]
.text:64404F8F                 lea     eax, [ebp+var_14]
.text:64404F92                 push    offset aWorkingDirecto ; "working directory: '\%s'\n"
.text:64404F97                 push    eax

; demangled name: ATL::CStringT::Format(char const *,...)
.text:64404F98                 call    ebx ; mfc90_2539
.text:64404F9A                 add     esp, 0Ch
.text:64404F9D                 lea     eax, [ebp+var_14]
.text:64404FA0                 push    eax
.text:64404FA1                 mov     ecx, edi

; demangled name: ATL::CStringT::operator+=(class ATL::CSimpleStringT<char, 1> const &)
.text:64404FA3                 call    ds:mfc90_941
.text:64404FA9
.text:64404FA9 loc_64404FA9:
.text:64404FA9                 mov     eax, [esi+38h]
.text:64404FAC                 test    eax, eax
.text:64404FAE                 jbe     short loc_64404FD3
.text:64404FB0                 push    eax
.text:64404FB1                 lea     eax, [ebp+var_14]
.text:64404FB4                 push    offset aTraceLevelDAct ; "trace level \%d activated\n"
.text:64404FB9                 push    eax

; demangled name: ATL::CStringT::Format(char const *,...)
.text:64404FBA                 call    ebx ; mfc90_2539
.text:64404FBC                 add     esp, 0Ch
.text:64404FBF                 lea     eax, [ebp+var_14]
.text:64404FC2                 push    eax
.text:64404FC3                 mov     ecx, edi

; demangled name: ATL::CStringT::operator+=(class ATL::CSimpleStringT<char, 1> const &)
.text:64404FC5                 call    ds:mfc90_941
.text:64404FCB                 xor     ebx, ebx
.text:64404FCD                 inc     ebx
.text:64404FCE                 mov     [ebp+var_10], ebx
.text:64404FD1                 jmp     short loc_64404FD6
.text:64404FD3
.text:64404FD3 loc_64404FD3:
.text:64404FD3                 xor     ebx, ebx
.text:64404FD5                 inc     ebx
.text:64404FD6
.text:64404FD6 loc_64404FD6:
.text:64404FD6                 cmp     [esi+38h], ebx
.text:64404FD9                 jbe     short loc_64404FF1
.text:64404FDB                 cmp     dword ptr [esi+2978h], 0
.text:64404FE2                 jz      short loc_64404FF1
.text:64404FE4                 push    offset aHexdumpInTrace ; "hexdump in trace activated\n"
.text:64404FE9                 mov     ecx, edi

; demangled name: ATL::CStringT::operator+=(char const *)
.text:64404FEB                 call    ds:mfc90_945
.text:64404FF1
.text:64404FF1 loc_64404FF1:
.text:64404FF1
.text:64404FF1                 cmp     byte ptr [esi+78h], 0
.text:64404FF5                 jz      short loc_64405007
.text:64404FF7                 push    offset aLoggingActivat ; "logging activated\n"
.text:64404FFC                 mov     ecx, edi

; demangled name: ATL::CStringT::operator+=(char const *)
.text:64404FFE                 call    ds:mfc90_945
.text:64405004                 mov     [ebp+var_10], ebx
.text:64405007
.text:64405007 loc_64405007:
.text:64405007                 cmp     byte ptr [esi+3Dh], 0
.text:6440500B                 jz      short bypass
.text:6440500D                 push    offset aDataCompressio ;"data compression switched off\n"
.text:64405012                 mov     ecx, edi

; demangled name: ATL::CStringT::operator+=(char const *)
.text:64405014                 call    ds:mfc90_945
.text:6440501A                 mov     [ebp+var_10], ebx
.text:6440501D
.text:6440501D bypass:
.text:6440501D                 mov     eax, [esi+20h]
.text:64405020                 test    eax, eax
.text:64405022                 jz      short loc_6440503A
.text:64405024                 cmp     dword ptr [eax+28h], 0
.text:64405028                 jz      short loc_6440503A
.text:6440502A                 push    offset aDataRecordMode ; "data record mode switched on\n"
.text:6440502F                 mov     ecx, edi

; demangled name: ATL::CStringT::operator+=(char const *)
.text:64405031                 call    ds:mfc90_945
.text:64405037                 mov     [ebp+var_10], ebx
.text:6440503A
.text:6440503A loc_6440503A:
.text:6440503A
.text:6440503A                 mov     ecx, edi
.text:6440503C                 cmp     [ebp+var_10], ebx
.text:6440503F                 jnz     loc_64405142
.text:64405045                 push    offset aForMaximumData ; "\nFor maximum data security ↙
    ↘ delete\nthe s"

; demangled name: ATL::CStringT::operator+=(char const *)
.text:6440504A                 call    ds:mfc90_945
.text:64405050                 xor     edi, edi
.text:64405052                 push    edi                ; fWinIni
.text:64405053                 lea     eax, [ebp+pvParam]
.text:64405056                 push    eax                ; PvParam
.text:64405057                 push    edi                ; uiParam
.text:64405058                 push    30h                ; uiAction
.text:6440505A                 call    ds:SystemParametersInfoA
.text:64405060                 mov     eax, [ebp+var_34]
.text:64405063                 cmp     eax, 1600
.text:64405068                 jle     short loc_64405072
.text:6440506A                 cdq
.text:6440506B                 sub     eax, edx
.text:6440506D                 sar     eax, 1
.text:6440506F                 mov     [ebp+var_34], eax
.text:64405072
.text:64405072 loc_64405072:
.text:64405072                 push    edi                ; hWnd
.text:64405073                 mov     [ebp+cy], 0A0h
.text:6440507A                 call    ds:GetDC
.text:64405080                 mov     [ebp+var_10], eax
.text:64405083                 mov     ebx, 12Ch
.text:64405088                 cmp     eax, edi
.text:6440508A                 jz      loc_64405113
.text:64405090                 push    11h                ; i
.text:64405092                 call    ds:GetStockObject
.text:64405098                 mov     edi, ds:SelectObject
.text:6440509E                 push    eax                ;h
.text:6440509F                 push    [ebp+var_10]       ; hdc
.text:644050A2                 call    edi ; SelectObject
.text:644050A4                 and     [ebp+rc.left], 0
.text:644050A8                 and     [ebp+rc.top], 0
.text:644050AC                 mov     [ebp+h], eax
.text:644050AF                 push    401h               ; format
.text:644050B4                 lea     eax, [ebp+rc]
.text:644050B7                 push    eax                ; lprc
.text:644050B8                 lea     ecx, [esi+2854h]
.text:644050BE                 mov     [ebp+rc.right], ebx
.text:644050C1                 mov     [ebp+rc.bottom], 0B4h

; demangled name: ATL::CSimpleStringT::GetLength(void)
.text:644050C8                 call    ds:mfc90_3178
.text:644050CE                 push    eax                ; cchText
.text:644050CF                 lea     ecx, [esi+2854h]

; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:644050D5                 call    ds:mfc90_910
.text:644050DB                 push    eax                ; lpchText
.text:644050DC                 push    [ebp+var_10]       ; hdc
.text:644050DF                 call    ds:DrawTextA
.text:644050E5                 push    4                  ; nIndex
.text:644050E7                 call    ds:GetSystemMetrics
.text:644050ED                 mov     ecx, [ebp+rc.bottom]
.text:644050F0                 sub     ecx, [ebp+rc.top]
.text:644050F3                 cmp     [ebp+h], 0
.text:644050F7                 lea     eax, [eax+ecx+28h]
.text:644050FB                 mov     [ebp+cy], eax
.text:644050FE                 jz      short loc_64405108
.text:64405100                 push    [ebp+h]           ; h
.text:64405103                 push    [ebp+var_10]      ; hdc
.text:64405106                 call    edi ; SelectObject
.text:64405108
.text:64405108 loc_64405108:
.text:64405108                 push    [ebp+var_10]      ; hDC
.text:6440510B                 push    0                 ; hWnd
.text:6440510D                 call    ds:ReleaseDC
.text:64405113
.text:64405113 loc_64405113:
.text:64405113                 mov     eax, [ebp+var_38]
.text:64405116                 push    80h               ; uFlags
.text:6440511B                 push    [ebp+cy]          ; cy
.text:6440511E                 inc     eax
.text:6440511F                 push    ebx                ; cx
.text:64405120                 push    eax                ; Y
.text:64405121                 mov     eax, [ebp+var_34]
.text:64405124                 add     eax, 0FFFFFED4h
.text:64405129                 cdq
.text:6440512A                 sub     eax, edx
.text:6440512C                 sar     eax, 1
.text:6440512E                 push    eax                 ; X
.text:6440512F                 push    0                   ; hWndInsertAfter
.text:64405131                 push    dword ptr [esi+285Ch] ; hWnd
.text:64405137                 call    ds:SetWindowPos
.text:6440513D                 xor     ebx, ebx
.text:6440513F                 inc     ebx
.text:64405140                 jmp     short loc_6440514D
.text:64405142
.text:64405142 loc_64405142:
.text:64405142                 push    offset byte_64443AF8

; demangled name: ATL::CStringT::operator=(char const *)
.text:64405147                 call    ds:mfc90_820
.text:6440514D
.text:6440514D loc_6440514D:
.text:6440514D                 cmp     dword_6450B970, ebx
.text:64405153                 jl      short loc_64405188
.text:64405155                 call    sub_6441C910
.text:6440515A                 mov     dword_644F858C, ebx
.text:64405160                 push    dword ptr [esi+2854h]
.text:64405166                 push    offset aCdwsguiPrepare ; "\nCDwsGui::PrepareInfoWindow: ↙
    ↘ sapgui env"...
.text:6440516B                 push    dword ptr [esi+2848h]
.text:64405171                 call    dbg
.text:64405176                 add     esp, 0Ch
.text:64405179                 mov     dword_644F858C, 2
.text:64405183                 call    sub_6441C920
.text:64405188
.text:64405188 loc_64405188:
.text:64405188                 or      [ebp+var_4], 0FFFFFFFFh
.text:6440518C                 lea     ecx, [ebp+var_14]

; demangled name: ATL::CStringT::~CStringT()
.text:6440518F                 call    ds:mfc90_601
.text:64405195                 call    __EH_epilog3
.text:6440519A                 retn
.text:6440519A CDwsGui__PrepareInfoWindow endp

在执行上述函数的最初几个指令时，数据对象（thiscall）的指针存储于ECX寄存器。[7]本例中，对象明显使用了CDwsGui类。它所记录的选项开关，直接决定着函数窗口的提示信息。

如果地址this+0x3D的值不是0，那么整个程序就会关闭网络数据的压缩功能：

.text:64405007 loc_64405007:
.text:64405007                  cmp    byte ptr [esi+3Dh], 0
.text:6440500B                  jz     short bypass
.text:6440500D                  push   offset aDataCompressio ; "data compression switched off\n"
.text:64405012                  mov    ecx, edi

; demangled name: ATL::CStringT::operator+=(char const *)
.text:64405014                  call   ds:mfc90_945
.text:6440501A                  mov    [ebp+var_10], ebx
.text:6440501D
.text:6440501D bypass:

更有意思的是，决定程序是否显示提示窗口的关键因素是变量var_10：

.text:6440503C                 cmp    [ebp+var_10], ebx
.text:6440503F                 jnz    exit ; bypass drawing

; add strings "For maximum data security delete" / "the setting(s) as soon as possible !":

.text:64405045                 push   offset aForMaximumData ; "\nFor maximum data security ↙
    ↘ delete\nthe s"...
.text:6440504A                 call   ds:mfc90_945 ; ATL::CStringT::operator+=(char const *)
.text:64405050                 xor    edi, edi
.text:64405052                 push   edi                 ; fWinIni
.text:64405053                 lea    eax, [ebp+pvParam]
.text:64405056                 push   eax                 ; pvParam
.text:64405057                 push   edi                 ; uiParam
.text:64405058                 push   30h                 ; uiAction
.text:6440505A                 call   ds:SystemParametersInfoA
.text:64405060                 mov    eax, [ebp+var_34]
.text:64405063                 cmp    eax, 1600
.text:64405068                 jle    short loc_64405072
.text:6440506A                 cdq
.text:6440506B                 sub    eax, edx
.text:6440506D                 sar    eax, 1
.text:6440506F                 mov    [ebp+var_34], eax
.text:64405072
.text:64405072 loc_64405072:

start drawing:

.text:64405072                 push   edi                 ; hWnd
.text:64405073                 mov    [ebp+cy], 0A0h
.text:6440507A                 call   ds:GetDC

那么，我们通过实践来验证刚才这些推测吧。

首先找到这个JNZ指令：

.text:6440503F                 jnz    exit ; bypass drawing

把它改为JMP之后，SAPGUI程序就再也不会显示恼人的提示窗口了！

下一步，我们找到load_command_line()函数（函数名称是笔者命名的名字）里偏移量为0x15的数据，以及CDwsGui::PrepareInfoWindow里的变量this+0x3D。这两个值是相等的值么？

为了验证这一猜测，笔者在程序里搜索与偏移量0x15有关的全部指令。在SAPGUI这样的小型程序里，某个规定变量一般只会被同一个文件调用；换而言之，我们不必检索其他文件。

在当前文件里，第一处赋值的指令如下：

.text:64404C19 sub_64404C19     proc near
.text:64404C19
.text:64404C19 arg_0            = dword ptr  4
.text:64404C19
.text:64404C19                  push    ebx
.text:64404C1A                  push    ebp
.text:64404C1B                  push    esi
.text:64404C1C                  push    edi
.text:64404C1D                  mov     edi, [esp+10h+arg_0]
.text:64404C21                  mov     eax, [edi]
.text:64404C23                  mov     esi, ecx ; ESI/ECX are pointers to some unknown object.
.text:64404C25                  mov     [esi], eax
.text:64404C27                  mov     eax, [edi+4]
.text:64404C2A                  mov     [esi+4], eax
.text:64404C2D                  mov     eax, [edi+8]
.text:64404C30                  mov     [esi+8], eax
.text:64404C33                  lea     eax, [edi+0Ch]
.text:64404C36                  push    eax
.text:64404C37                  lea     ecx, [esi+0Ch]

; demangled name:  ATL::CStringT::operator=(class ATL::CStringT ... &)
.text:64404C3A                  call    ds:mfc90_817
.text:64404C40                  mov     eax, [edi+10h]
.text:64404C43                  mov     [esi+10h], eax
.text:64404C46                  mov     al, [edi+14h]
.text:64404C49                  mov     [esi+14h], al
.text:64404C4C                  mov     al, [edi+15h] ; copy byte from 0x15 offset
.text:64404C4F                  mov     [esi+15h], al ; to 0x15 offset in CDwsGui object

上述函数的调用方函数是CDwsGui::CopyOptions。这些名字都是参照调试信息命名的。

但是在整理程序流程之后，我们会发现在“时间上”第一次调用上述函数的调用方函数是CDWsGui::Init()：

.text:6440B0BF loc_6440B0BF:
.text:6440B0BF                  mov     eax, [ebp+arg_0]
.text:6440B0C2                  push    [ebp+arg_4]
.text:6440B0C5                  mov     [esi+2844h], eax
.text:6440B0CB                  lea     eax, [esi+28h] ; ESI is pointer to CDwsGui object
.text:6440B0CE                  push    eax
.text:6440B0CF                  call    CDwsGui__CopyOptions

综合上述分析可知：由load_command_line()函数填充的数组，是CDwsGui class（类）的一部分。这个数组的地址是this+0x28。0x15+0x28＝0x3D。这应该就是数据被传递的终点站。

接下来，我们在程序里查找“使用偏移量0x3D的指令”。我们发现CDwsGui::SapguiRun函数（根据调试信息进行命名）就使用了这个偏移量：

.text:64409D58                cmp     [esi+3Dh], bl ; ESI is pointer to CDwsGui object
.text:64409D5B                lea     ecx, [esi+2B8h]
.text:64409D61                setz    al
.text:64409D64                push    eax           ; arg_10 of CConnectionContext:: ↙
    ↘ CreateNetwork
.text:64409D65                push    dword ptr [esi+64h]

; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:64409D68                call    ds:mfc90_910
.text:64409D68                                        ; no arguments
.text:64409D6E                push    eax
.text:64409D6F                lea     ecx, [esi+2BCh]

; demangled name: const char* ATL::CSimpleStringT::operator PCXSTR
.text:64409D75                call     ds:mfc90_910
.text:64409D75                                      ; no arguments
.text:64409D7B                push    eax
.text:64409D7C                push    esi
.text:64409D7D                lea     ecx, [esi+8]
.text:64409D80                call    CConnectionContext__CreateNetwork

而后，验证我们的推测：把“setz al”换为“xor eax, eax / nop”指令，清除环境变量TDW_NOCOMPRESS，然后再次运行SAPGUI。此后，令人不快的提示窗口果然不见了，而且Wireshark显示网络包不再压缩了！显然，这种修改可以对CConnectionContext对象的压缩标识进行直接操作。

可见，压缩标识传递到了CConnectionContext::CreateNetwork的第五个参数。不过这个构造函数还调用了其他函数：

...
.text:64403476                  push    [ebp+compression]
.text:64403479                  push    [ebp+arg_C]
.text:6440347C                  push    [ebp+arg_8]
.text:6440347F                  push    [ebp+arg_4]
.text:64403482                  push    [ebp+arg_0]
.text:64403485                  call    CNetwork__CNetwork

压缩标识接着被传递到构造函数CNetwork::CNetwork的第五个参数。根据这个参数，构造函数CNetwork在对象体CNetwork设置标识、并设置另一个与压缩传输可能有关的变量。这个构造函数的有关操作如下：

.text:64411DF1                  cmp     [ebp+compression], esi
.text:64411DF7                  jz      short set_EAX_to_0
.text:64411DF9                  mov     al, [ebx+78h]    ; another value may affect compression?
.text:64411DFC                  cmp     al, '3'
.text:64411DFE                  jz      short set_EAX_to_1
.text:64411E00                  cmp     al, '4'
.text:64411E02                  jnz     short set_EAX_to_0
.text:64411E04
.text:64411E04 set_EAX_to_1:
.text:64411E04                  xor     eax, eax
.text:64411E06                  inc     eax              ; EAX -> 1
.text:64411E07                  jmp     short loc_64411E0B
.text:64411E09
.text:64411E09 set_EAX_to_0:
.text:64411E09
.text:64411E09                  xor     eax, eax         ; EAX -> 0
.text:64411E0B
.text:64411E0B loc_64411E0B:
.text:64411E0B                  mov     [ebx+3A4h], eax ; EBX is pointer to CNetwork object

综上，我们可以确定CNetwork class存储压缩标识的相对地址是this+0x3A4。

然后我们以0x3A4为着手点，继续分析SAPguilib.dll。这个偏移量再次出现在CDwsGui::OnClientMessageWrite里。当然，笔者还是通过调试信息才能确定构造函数的准确名称：

.text:64406F76 loc_64406F76:
.text:64406F76                  mov     ecx, [ebp+7728h+var_7794]
.text:64406F79                  cmp     dword ptr [ecx+3A4h], 1
.text:64406F80                  jnz     compression_flag_is_zero
.text:64406F86                  mov     byte ptr [ebx+7], 1
.text:64406F8A                  mov     eax, [esi+18h]
.text:64406F8D                  mov     ecx, eax
.text:64406F8F                  test    eax, eax
.text:64406F91                  ja      short loc_64406FFF
.text:64406F93                  mov     ecx, [esi+14h]
.text:64406F96                  mov     eax, [esi+20h]
.text:64406F99
.text:64406F99 loc_64406F99:
.text:64406F99                  push    dword ptr [edi+2868h] ; int
.text:64406F9F                  lea     edx, [ebp+7728h+var_77A4]
.text:64406FA2                  push    edx                ; int
.text:64406FA3                  push    30000              ; int
.text:64406FA8                  lea     edx, [ebp+7728h+Dst]
.text:64406FAB                  push    edx                ; Dst
.text:64406FAC                  push    ecx                ; int
.text:64406FAD                  push    eax                ; Src
.text:64406FAE                  push    dword ptr [edi+28C0h] ; int
.text:64406FB4                  call    sub_644055C5     ; actual compression routine
.text:64406FB9                  add     esp, 1Ch
.text:64406FBC                  cmp     eax, 0FFFFFFF6h
.text:64406FBF                  jz      short loc_64407004
.text:64406FC1                  cmp     eax, 1
.text:64406FC4                  jz       loc_6440708C
.text:64406FCA                  cmp     eax, 2
.text:64406FCD                  jz      short loc_64407004
.text:64406FCF                  push    eax
.text:64406FD0                  push    offset aCompressionErr ; "compression error [rc = \%d]- program wi" ...
.text:64406FD5                  push    offset aGui_err_compre ; "GUI_ERR_COMPRESS"
.text:64406FDA                  push    dword ptr [edi+28D0h]
.text:64406FE0                  call    SapPcTxtRead

由此可见，压缩网络数据的关键函数是 sub_644055C5。这个函数分别调用了 memcpy()函数，以及函数sub_64417440（IDA显示的函数名）。而sub_64417440的指令是：

.text:6441747C                  push    offset aErrorCsrcompre ; "\nERROR: CsRCompress: invalid  handle"
.text:64417481                  call    eax ; dword_644F94C8
.text:64417483                  add     esp, 4

到此为止，我们完整地分析了压缩网络数据包的函数。参照笔者先前的分析[8]可知，这个网络包压缩函数是SAP和开源项目MaxDB的公用函数（上述两个产品都是SAP开发的）。因此，实际上我们可以找到它的源代码。

最后要分析的是：

.text:64406F79                  cmp     dword ptr [ecx+3A4h], 1
.text:64406F80                  jnz     compression_flag_is_zero

把此处的JNZ替换为无条件转移指令JMP，删除环境变量TDW_NOCOMPRESS。瞧！再用WireShark分析网络数据包时，我们就会发现SAPGUI不再压缩网络数据了。

综上，在找到环境变量与数据压缩功能的切合点之后，我们可以强行启用这个功能，也可以强制程序关闭这项功能。

80.2　SAP 6.0的密码验证函数

某天，在VMware平台上打开SAP 6.0 IDES时，笔者发现自己忘记SAP账户名了。几经周折找到了账户名之后，我尝试着用常用密码进行登录。结果可想而知，笔者最终遇到了提示信息“Password logon no longer possible-too many failed attempts”，再也无法登录。

好消息是SAP官方提供了完整的disp+work.pdb文件。这个PDB文件涵盖的信息还十分全面：函数名、结构体、类型、局部变量及参数名等等，简直是应有尽有。

为了便于挖掘信息，笔者使用TYPEINFODUMP程序[9]，把PDB文件转换为了人类可读的文本文件。

转换后的文本文件含有函数名称、函数参数、局部变量等信息：

FUNCTION ThVmcSysEvent
  Address:       10143190 Size:      675 bytes   Index:     60483  TypeIndex:     60484
  Type: int NEAR_C ThVmcSysEvent (unsigned int, unsigned char, unsigned short*)
Flags: 0
PARAMETER events
  Address: Reg335+288 Size:          4 bytes   Index:    60488  TypeIndex:   60489
  Type: unsigned int
Flags: d0
PARAMETER opcode
  Address: Reg335+296 Size:          1 bytes   Index:    60490  TypeIndex:   60491
  Type: unsigned char
Flags: d0
PARAMETER serverName
  Address: Reg335+304 Size:          8 bytes   Index:    60492  TypeIndex:   60493
  Type: unsigned short*
Flags: d0
STATIC_LOCAL_VAR func
  Address:       12274af0 Size:      8 bytes  Index:     60495  TypeIndex:   60496
  Type: wchar_t*
Flags: 80
LOCAL_VAR admhead
  Address: Reg335+304 Size:          8 bytes  Index:    60498  TypeIndex:     60499
  Type: unsigned char*
Flags: 90
LOCAL_VAR record
  Address: Reg335+64 Size:         204 bytes  Index:    60501  TypeIndex:      60502
  Type: AD_RECORD
Flags: 90
LOCAL_VAR adlen
  Address: Reg335+296 Size:        4 bytes  Index:    60508  TypeIndex:      60509
  Type: int
Flags: 90

不仅如此，它还解释了结构体的有关信息：

STRUCT DBSL_STMTID
Size: 120  Variables: 4  Functions: 0  Base classes: 0
MEMBER moduletype
  Type:  DBSL_MODULETYPE
  Offset:         0   Index:        3  TypeIndex:    38653
MEMBER module
  Type:  wchar_t module[40]
  Offset:         4   Index:        3  TypeIndex:    831
MEMBER stmtnum
  Type: long
  Offset:         84   Index:       3  TypeIndex:    440
MEMBER timestamp
  Type:  wchar_t timestamp[15]
  Offset:         88   Index:       3  TypeIndex:    6612

此外，调试呼叫（debugging calls）也可提供大量信息。

不久，笔者就注意到设置日志详细程度的全局变量ct_level。SAP官方对这个变量有详细的解释： http://help.sap.com/saphelp_nwpi71/helpdata/en/46/962416a5a613e8e10000000a155369/ content.htm

。

disp+work.exe文件保留了大量的调试信息：

cmp     cs:ct_level, 1
jl      short loc_1400375DA
call    DpLock
lea     rcx, aDpxxtool4_c ; "dpxxtool4.c"
mov     edx, 4Eh          ; line
call    CTrcSaveLocation
mov     r8, cs:func_48
mov     rcx, cs:hdl      ; hdl
lea     rdx, aSDpreadmemvalu ; "%s: DpReadMemValue (%d)"
mov     r9d, ebx
call    DpTrcErr
call    DpUnlock

如果ctl_level的值大于或等于程序预设的某个阈值，那么程序将会按照相应的详细程度记录dev_wo、 dev_disp等dev-日志文件。

在使用TYPEINFODUMP程序把PDB文件转换为文本文件之后，我们使用grep指令搜索与密码有关的函数名称：

cat     "disp+work.pdb.d" | grep FUNCTION | grep -i password

上述指令的返回结果是：

FUNCTION rcui::AgiPassword::DiagISelection
FUNCTION ssf_password_encrypt
FUNCTION ssf_password_decrypt
FUNCTION password_logon_disabled
FUNCTION dySignSkipUserPassword
FUNCTION migrate_password_history
FUNCTION password_is_initial
FUNCTION rcui::AgiPassword::IsVisible
FUNCTION password_distance_ok
FUNCTION get_password_downwards_compatibility
FUNCTION dySignUnSkipUserPassword
FUNCTION rcui::AgiPassword::GetTypeName
FUNCTION 'rcui::AgiPassword::AgiPassword'::'1'::dtor$2
FUNCTION 'rcui::AgiPassword::AgiPassword'::'1'::dtor$0
FUNCTION 'rcui::AgiPassword::AgiPassword'::'1'::dtor$1
FUNCTION usm_set_password
FUNCTION rcui::AgiPassword::TraceTo
FUNCTION days_since_last_password_change
FUNCTION rsecgrp_generate_random_password
FUNCTION rcui::AgiPassword::`scalar deleting destructor'
FUNCTION password_attempt_limit_exceeded
FUNCTION handle_incorrect_password
FUNCTION 'rcui::AgiPassword::`scalar deleting destructor''::'1'::dtor$1
FUNCTION calculate_new_password_hash
FUNCTION shift_password_to_history
FUNCTION rcui::AgiPassword::GetType
FUNCTION found_password_in_history
FUNCTION `rcui::AgiPassword::`scalar deleting destructor''::'1'::dtor$0
FUNCTION rcui::AgiObj::IsaPassword
FUNCTION password_idle_check
FUNCTION SlicHwPasswordForDay
FUNCTION rcui::AgiPassword::IsaPassword
FUNCTION rcui::AgiPassword::AgiPassword
FUNCTION delete_user_password
FUNCTION usm_set_user_password
FUNCTION Password_API
FUNCTION get_password_change_for_SSO
FUNCTION password_in_USR40
FUNCTION rsec_agrp_abap_generate_random_password

根据提示信息，接下来我们在调试信息里搜索关键词“password”和“locked”。略加分析之后，笔者发现password_attempt_limit_exceeded()函数会调用关键字符串“user was locked by subsequently failed password logon attempts”。

这个函数还会在日志文件里记录“password logon attempt will be rejected immediately (preventing dictionary attacks)”“failed-logon lock: expired (but not removed due to ‘read-only’ operation)”以及“failed-logon lock: expired => removed”。

进一步的研究表明，这个函数就是登录保护函数。它会被密码验证函数——chckpass()函数调用。

首先要验证上述推测是否正确。使用笔者开发的tracer程序进行分析：

tracer64.exe -a:disp+work.exe bpf=disp+work.exe!chckpass,args:3,unicode
PID=2236|TID=2248|(0) disp+work.exe!chckpass (0x202c770, L"Brewered1 ", 0x41) (called from ↙
    ↘ 0x1402f1060 (disp+work.exe!usrexist+0x3c0))
PID=2236|TID=2248|(0) disp+work.exe!chckpass -> 0x35

调用逻辑是syssigni()→DyISigni()→dychkusr()→usrexist()→chckpass()。

数字0x35是chckpass()函数返回的错误信息编号：

.text:00000001402ED567 loc_1402ED567:                         ; CODE XREF: chckpass+B4
.text:00000001402ED567               mov    rcx, rbx          ; usr02
.text:00000001402ED56A               call   password_idle_check
.text:00000001402ED56F               cmp    eax, 33h
.text:00000001402ED572               jz     loc_1402EDB4E
.text:00000001402ED578               cmp    eax, 36h
.text:00000001402ED57B               jz     loc_1402EDB3D
.text:00000001402ED581               xor    edx, edx          ; usr02_readonly
.text:00000001402ED583               mov    rcx, rbx          ; usr02
.text:00000001402ED586               call   password_attempt_limit_exceeded
.text:00000001402ED58B               test   al, al
.text:00000001402ED58D               jz     short loc_1402ED5A0
.text:00000001402ED58F               mov    eax, 35h
.text:00000001402ED594               add    rsp, 60h
.text:00000001402ED598               pop    r14
.text:00000001402ED59A               pop    r12
.text:00000001402ED59C               pop    rdi
.text:00000001402ED59D               pop    rsi
.text:00000001402ED59E               pop    rbx
.text:00000001402ED59F               retn

然后进行试验：

tracer64.exe -a:disp+work.exe bpf=disp+work.exe!password_attempt_limit_exceeded,args:4,unicode, rt:0

PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded (0x202c770, 0, 0x257758, 0) ↙
    ↘ (called from 0x1402ed58b (disp+work.exe!chckpass+0xeb))
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded -> 1
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded (0x202c770, 0, 0, 0) (called ↙
    ↘ from 0x1402e9794 (disp+work.exe!chngpass+0xe4))
PID=2744|TID=360|(0) disp+work.exe!password_attempt_limit_exceeded -> 1
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0

此后我们就可以进行登录了。

顺便提一下，如果忘记密码的话，可以把chckpass()函数的返回值强制改为0，那样它就不会进行密码验证了：

tracer64.exe -a:disp+work.exe bpf=disp+work.exe!chckpass,args:3,unicode,rt:0

PID=2744|TID=360|(0) disp+work.exe!chckpass (0x202c770, L"bogus ", 0x41) (called from 0x1402f1060 ↙
    ↘ (disp+work.exe!usrexist+0x3c0))
PID=2744|TID=360|(0) disp+work.exe!chckpass -> 0x35
PID=2744|TID=360|We modify return value (EAX/RAX) of this function to 0

在分析password_attemp_limit_exceeded()函数时，我们可以看到函数的前几行指令是：

lea     rcx, aLoginFailed_us ; "login/failed_user_auto_unlock"
call    sapgparam
test    rax, rax
jz      short loc_1402E19DE
movzx   eax, word ptr [rax]
cmp     ax, 'N'
jz      short loc_1402E19D4
cmp     ax, 'n'
jz      short loc_1402E19D4
cmp     ax, '0'
jnz     short loc_1402E19DE

很显然，sapgparam()函数的作用是获取配置参数。整个程序有1768处指令调用这个函数。据此推测，只要追踪这个函数的调用关系，就可以分析特定参数对整个程序的影响。

不得不说，SAP要比Oracle RDBMS亲切得多。前者提供的函数名等信息远比后者清晰。不过disp+work程序具有C++程序的特征，莫非官方最近重新编写了它的源程序？

[1]　关于SAP采用的密码传输和指令传输技术，请参见笔者的博客：http://blog.yurichev.com/ node/44http://blog.yurichev.com/node/47。

[2]　http://www.farmanager.com/。

[3]　本章演示的程序是SAP GUI v720 for Win32，其版本号为7200,1,0,9009。如果版本不同，那么相关偏移量应当会是不同值。

[4]　这个函数名称是笔者命名的。

[5]　https://msdn.microsoft.com/en-us/library/tb2sfw2z(VS.80).aspx。

[6]　返回环境变量的标准C函数。

[7]　这属于thiscall类型的函数。有关thiscall类型的函数，可参见本书51.1.1节。

[8]　http://conus.info/utils/SAP_pkt_decompr.txt。

[9]　http://www.debuginfo.com/tools/typeinfodump.html。

第81章 Oracle RDBMS

81.1　V$VERSION表

Oracle RDBMS 11.2是个规模庞大的数据库系统。其主程序oracle.exe包含近124000个函数。相比之下，Windows 7 x86的内核ntoskrnl.exe只有近11000函数；Linux 3.9.8的内核（默认编译/带有默认驱动程序）包含的函数也不过31000个左右。

本章首先演示一个最简单的Oracle查询指令。我们可通过下述指令查询Oracle RDBMS数据库的版本信息：

SQL> select * from V$VERSION;

上述指令的返回结果如下：

BANNER
--------------------------------------------------------------------------------

Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production
PL/SQL Release 11.2.0.1.0 - Production
CORE     11.2.0.1.0　     Production
TNS for 32-bit Windows: Version 11.2.0.1.0 - Production
NLSRTL Version 11.2.0.1.0 - Production

第一个问题就来了：字符串“V$VERSION”存储在Oracle RDBMS的什么地方？

在Win32版本的oracle.exe程序里不难发现这个字符串。但是在Linux平台的文件里，函数名称和全局变量名都会走样。因此，即使在Linux版的Oracle RDBMS里找到了正确的对象（.o）文件，挖掘相应的处理函数也会花费更多的时间。

在Linux版程序的文件里，包含字符串“V$VERSION”的文件是kqf.o。这个文件在Oracle的库文件目录lib/libserver11.a之中。

kqf.o文件在定义数据表kqfviw的时候，调用了字符串“V$VERSION”。

指令清单81.1　kqf.o

.rodata:0800C4A0 kqfviw         dd  0Bh                ; DATA XREF: kqfchk:loc_8003A6D
.rodata:0800C4A0                                       ; kqfgbn+34
.rodata:0800C4A4                dd  offset _2__STRING_10102_0 ; "GV$WAITSTAT"
.rodata:0800C4A8                dd  4
.rodata:0800C4AC                dd  offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C4B0                dd  3
.rodata:0800C4B4                dd  0
.rodata:0800C4B8                dd  195h
.rodata:0800C4BC                dd  4
.rodata:0800C4C0                dd  0
.rodata:0800C4C4                dd 0FFFFC1CBh
.rodata:0800C4C8                dd  3
.rodata:0800C4CC                dd  0
.rodata:0800C4D0                dd  0Ah
.rodata:0800C4D4                dd  offset _2__STRING_10104_0 ; "V$WAITSTAT"
.rodata:0800C4D8                dd  4
.rodata:0800C4DC                dd  offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C4E0                dd  3
.rodata:0800C4E4                dd  0
.rodata:0800C4E8                dd  4Eh
.rodata:0800C4EC                dd  3
.rodata:0800C4F0                dd  0
.rodata:0800C4F4                dd  0FFFFC003h
.rodata:0800C4F8                dd  4
.rodata:0800C4FC                dd  0
.rodata:0800C500                dd  5
.rodata:0800C504                dd  offset _2__STRING_10105_0 ; "GV$BH"
.rodata:0800C508                dd  4
.rodata:0800C50C                dd  offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C510                dd  3
.rodata:0800C514                dd  0
.rodata:0800C518                dd  269h
.rodata:0800C51C                dd  15h
.rodata:0800C520                dd  0
.rodata:0800C524                dd  0FFFFC1EDh
.rodata:0800C528                dd  8
.rodata:0800C52C                dd  0
.rodata:0800C530                dd  4
.rodata:0800C534                dd  offset _2__STRING_10106_0 ; "V$BH"
.rodata:0800C538                dd  4
.rodata:0800C53C                dd  offset _2__STRING_10103_0 ; "NULL"
.rodata:0800C540                dd  3
.rodata:0800C544                dd  0
.rodata:0800C548                dd  0F5h
.rodata:0800C54C                dd  14h
.rodata:0800C550                dd  0
.rodata:0800C554                dd  0FFFFC1EEh
.rodata:0800C558                dd  5
.rodata:0800C55C                dd  0

在分析Oracle RDBMS的内部文件时，很多人都会奇怪“为什么函数名称和全局变量名称都那么诡异？”这大概是因为Oracle是20世纪80年代的古典作品吧。那个时代C语言编译器都遵循的ANSI标准：函数名称和变量名称不得超出6个字符（linker的局限），即“外部标识符以前6个字符为准”的规则。[1]

名字以V$-开头的数据视图，多数（很有可能是全部）都由这个文件的kqfviw表定义。这些V$视图都是内容固定视图（fixed Views）。从表面看来，这些数据具有显著的循环周期。因此，我们可以初步判断，kqfviw表的每个元素都由12个32位字段构成。借助IDA程序，我们可以轻易地再现出这种12字段的数据结构，套用到整个数据表。在Oracle RDBMS v11.2里，总共有1023个固定视图。即，这个文件可能描述了1023个预定义的视图。本章稍后讨论这个数字。

关于视图中的各字段、及各字段对应的数据，并没有多少资料可寻。虽然我们发现第一个数字就是数据库图的名称（没有最末的那个零字节）、而且这个规律适用于全部的数据元素，但是这种信息的作用不大。

我们还查到了一个叫作“V$FIXED_VIEW_DEFINITION”的固定视图[2]，它能够检索所有固定视图的信息。顺便提一下，这个表有1023个元素，正好对应预定义视图的总数。

SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='V$VERSION';

VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------

V$VERSION
select  BANNER from GV$VERSION where inst_id = USERENV('Instance')

可见，对于GV$VERSION而言，V$VERSION是thunk view（形实转换视图）：

SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='GV$VERSION';

VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------

GV$VERSION
select inst_id, banner from x$version

另外，在Oracle数据库里，那些官方文档没有介绍的、以X$开头的数据表同样是记载系统信息的服务表。因为这些以X$开头的表由Oracle程序控制并动态更新的数据表，所以数据库用户不能修改它们。

如果我们在文件kqf.o里搜索文本“select BANNER from GV$VERSION where inst_id = USERENV('Instance')”，那么就会发现它在kqfvip表里。

指令清单81.2　kqf.o

.rodata:080185A0 kqfvip      dd  offset _2__STRING_11126_0 ; DATA XREF: kqfgvcn+18
.rodata:080185A0                                          ; kqfgvt+F
.rodata:080185A0                                          ; "select inst_id, decode(indx,1,'data ↙
     ↘ bloc" ...
.rodata:080185A4             dd  offset kqfv459_c_0
.rodata:080185A8             dd  0
.rodata:080185AC             dd  0

...

.rodata:08019570             dd  offset _2__STRING_11378_0 ; "select BANNER from GV$VERSION ↙
     ↘  where in "...
.rodata:08019574             dd  offset kqfv133_c_0
.rodata:08019578             dd  0
.rodata:0801957C             dd  0
.rodata:08019580             dd  offset _2__STRING_11379_0 ; "select inst_id,decode(bitand(↙
     ↘ cfflg,1),0"...
.rodata:08019584             dd  offset kqfv403_c_0
.rodata:08019588             dd  0
.rodata:0801958C             dd  0
.rodata:08019590             dd  offset _2__STRING_11380_0 ; "select STATUS , NAME, ↙
     ↘ IS_RECOVERY_DEST"...
.rodata:08019594             dd  offset kqfv199_c_0

这个表的每个元素由4个字段构成。而且它同样包含了1023个元素。第二个字段指向了另一个表——也就是与表名称相对应的固定视图。V$VERSION的表格只有2个元素，第一个是6（后面字符串的长度），第二个是BANNER字符串。此后是终止符——零字节和C语言字符null。

指令清单81.3　kqf.o

.rodata:080BBAC4 kqfv133_c_0       dd  6                            ; DATA XREF: .rodata:08019574
.rodata:080BBAC8                   dd  offset _2__STRING_5017_0 ; "BANNER"
.rodata:080BBACC                   dd  0
.rodata:080BBAD0                   dd  offset _2__STRING_0_0

因此可见，综合kqfviw和kqfvip表的各项信息，我们可以获悉某个固定视图都含有哪些可被查询的字段。

基于上述分析结果，笔者编写了一个专门导出Linux Oracle数据库系统表的小程序——oracle_tables[3]。用它导出V$VERSION时，可得到如下所示的各项信息。

指令清单81.4　Result of oracle tables

kqfviw_element.viewname: [V$VERSION] ?: 0x3 0x43 0x1 0xffffc085 0x4
kqfvip_element.statement: [select  BANNER from GV$VERSION where inst_id = USERENV('Instance')]
kqfvip_element.params:
[BANNER]

指令清单81.5　Result of oracle tables

kqfviw_element.viewname: [GV$VERSION] ?: 0x3 0x26 0x2 0xffffc192 0x1
kqfvip_element.statement: [select inst_id, banner from x$version]
kqfvip_element.params:
[INST_ID] [BANNER]

固定视图GV$VERSION比V$VERSION多出了一个“instance”字段，除此以外两者相同。因此，我们只要专心研究数据表X$VERSION就可举一反三地理解另一个表。与其他名字以X$-开头的数据表一样，这个表也没有资料可查。但是，我们可以直接对其进行检索：

SQL> select * from x$version;

ADDR              INDX      INST_ID
----------  -------------  ----------
BANNER
--------------------------------------------------------------------------------

0DBAF574            0            1
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production

...

这个表的字段名里有ADDR和INDX。

继续使用IDA分析kqf.o的时候，我们会发现在kqftab表里有一个指向X$VERSION字符串的指针。

指令清单81.6　kqf.o

.rodata:0803CAC0                  dd  9                        ; element number 0x1f6
.rodata:0803CAC4                  dd  offset _2__STRING_13113_0 ; "X$VERSION"
.rodata:0803CAC8                  dd  4
.rodata:0803CACC                  dd  offset _2__STRING_13114_0 ; "kqvt"
.rodata:0803CAD0                  dd  4
.rodata:0803CAD4                  dd  4
.rodata:0803CAD8                  dd  0
.rodata:0803CADC                  dd  4
.rodata:0803CAE0                  dd  0Ch
.rodata:0803CAE4                  dd  0FFFFC075h
.rodata:0803CAE8                  dd  3
.rodata:0803CAEC                  dd  0
.rodata:0803CAF0                  dd  7
.rodata:0803CAF4                  dd  offset _2__STRING_13115_0 ; "X$KQFSZ"
.rodata:0803CAF8                  dd  5
.rodata:0803CAFC                  dd  offset _2__STRING_13116_0 ; "kqfsz"
.rodata:0803CB00                  dd  1
.rodata:0803CB04                  dd  38h
.rodata:0803CB08                  dd  0
.rodata:0803CB0C                  dd  7
.rodata:0803CB10                  dd  0
.rodata:0803CB14                  dd  0FFFFC09Dh
.rodata:0803CB18                  dd  2
.rodata:0803CB1C                  dd  0

上述指令中有很多处数据都引用了以X$-开头的数据表名称。很显然，这些名字都是Oracle数据库的数据表名称。鉴于公开资料没有这些信息，笔者还不能理解字符串“kqvt”的实际含义。“kq-”前缀的指令，多数是与Kernel（内核）和query（查询）有关的指令。不过，至于“v是否是version的缩写”、“t是否是type的缩写”，这些猜测都无法证明。

另外，kqf.o文件里还记录了类似的数据表名称。

指令清单81.7　kqf.o

.rodata:0808C360 kqvt_c_0         kqftap_param <4, offset _2__STRING_19_0, 917h, 0, 0, 0, 4, 0, ↙
     ↘  0>
.rodata:0808C360                                         ; DATA XREF: .rodata:08042680
.rodata:0808C360                                         ; "ADDR"
.rodata:0808C384                  kqftap_param <4, offset _2__STRING_20_0, 0B02h, 0, 0, 0, 4, 0, ↙
     ↘  0>;"INDX"
.rodata:0808C3A8                  kqftap_param <7, offset _2__STRING_21_0, 0B02h, 0, 0, 0, 4, 0, ↙
     ↘  0>;"INST_ID"
.rodata:0808C3CC                  kqftap_param <6, offset _2__STRING_5017_0, 601h, 0, 0, 0, 50h,↙
     ↘  0, 0> ; "BANNER"
.rodata:0808C3F0                  kqftap_param <0, offset _2__STRING_0_0, 0, 0, 0, 0, 0, 0, 0>

这些信息可以解释X$VERSION表中的所有字段。在kqftap表中，唯一一个引用这个表的指令如下所示。

指令清单81.8　kqf.o

.rodata:08042680            kqftap_element <0, offset kqvt_c_0, offset kqvrow, 0> ; ↙
     ↘  element 0x1f6

值得关注的是，这个元素是表中第502个（0x1f6）元素。它就像kqftab表中指向X$VERSION字符串的指针一般。数据表kqftap和kqftab之间的关系，很可能像kqfvip和kqfviw之间的关系那样是某种互补关系。我们还在其中找到了指向kqvrow() 函数的函数指针。我们最终挖掘到了有价值的信息！

笔者把上述各表的有关信息也添加到了自制的oracle系统表查询工具——oracle_tables里[4]。用它检索X$VERSION后，可得如下所示的各项信息。

指令清单81.9　Result of oracle tables

kqftab_element.name: [X$VERSION] ?: [kqvt] 0x4 0x4 0x4 0xc 0xffffc075 0x3
kqftap_param.name=[ADDR] ?: 0x917 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INDX] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INST_ID] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[BANNER] ?: 0x601 0x0 0x0 0x0 0x50 0x0 0x0
kqftap_element.fn1=kqvrow
kqftap_element.fn2=NULL

借助笔者自创的tracer程序，我们不难发现：在查询X$VERSION表时，这个函数被连续调用了6次（由qerfxFetch() 函数）。

为了查看具体执行了哪些指令，我们以cc模式运行tracer程序：

tracer -a:oracle.exe bpf=oracle.exe!_kqvrow,trace:cc

_kqvrow_   proc near

var_7C     = byte ptr -7Ch
var_18     = dword ptr -18h
var_14     = dword ptr -14h
Dest       = dword ptr -10h
var_C      = dword ptr -0Ch
var_8      = dword ptr -8
var_4      = dword ptr -4
arg_8      = dword ptr  10h
arg_C      = dword ptr  14h
arg_14     = dword ptr  1Ch
arg_18     = dword ptr  20h

; FUNCTION CHUNK AT .text1:056C11A0 SIZE 00000049 BYTES

           push   ebp
           mov    ebp, esp
           sub    esp, 7Ch
           mov    eax, [ebp+arg_14] ; [EBP+1Ch]=1
           mov    ecx, TlsIndex    ; [69AEB08h]=0
           mov    edx, large fs:2Ch
           mov    edx, [edx+ecx*4] ; [EDX+ECX*4]=0xc98c938
           cmp    eax, 2            ; EAX=1
           mov    eax, [ebp+arg_8] ; [EBP+10h]=0xcdfe554
           jz     loc_2CE1288
           mov    ecx, [eax]       ; [EAX]=0..5
           mov    [ebp+var_4], edi ; EDI=0xc98c938

loc_2CE10F6:  ; CODE XREF: _kqvrow_+10A
              ; _kqvrow_+1A9
           cmp       ecx, 5            ; ECX=0..5
           ja        loc_56C11C7
           mov       edi, [ebp+arg_18] ; [EBP+20h]=0
           mov       [ebp+var_14], edx ; EDX=0xc98c938
           mov       [ebp+var_8], ebx ; EBX=0
           mov       ebx, eax          ; EAX=0xcdfe554
           mov       [ebp+var_C], esi ; ESI=0xcdfe248

loc_2CE110D: ; CODE XREF: _kqvrow_+29E00E6
           mov      edx, ds:off_628B09C[ecx*4] ; [ECX*4+628B09Ch]= 0x2ce1116, 0x2ce11ac, 0x2ce11db↙
     ↘ , 0x2ce11f6, 0x2ce1236, 0x2ce127a
           jmp       edx                ; EDX=0x2ce1116, 0x2ce11ac, 0x2ce11db, 0x2ce11f6, 0x2ce1236, ↙
     ↘ 0x2ce127a

loc_2CE1116: ; DATA XREF: .rdata:off_628B09C
           push      offset aXKqvvsnBuffer ; "x$kqvvsn buffer"
           mov       ecx, [ebp+arg_C] ; [EBP+14h]=0x8a172b4
           xor       edx, edx
           mov       esi, [ebp+var_14] ; [EBP-14h]=0xc98c938
           push      edx               ; EDX=0
           push      edx               ; EDX=0
           push      50h
           push      ecx               ; ECX=0x8a172b4
           push      dword ptr [esi+10494h] ;[ESI+10494h]=0xc98cd58
           call      _kghalf           ; tracing nested maximum level (1) reached, skipping this ↙
    ↘ CALL
           mov       esi, ds:__imp__vsnnum ; [59771A8h]=0x61bc49e0
           mov       [ebp+Dest], eax ; EAX=0xce2ffb0
           mov       [ebx+8], eax    ; EAX=0xce2ffb0
           mov       [ebx+4], eax    ; EAX=0xce2ffb0
           mov       edi, [esi]      ; [ESI]=0xb200100
           mov       esi, ds:__imp__vsnstr ; [597D6D4h]=0x65852148, "- Production"
           push      esi               ; ESI=0x65852148, "- Production"
           mov       ebx, edi          ; EDI=0xb200100
           shr       ebx, 18h          ; EBX=0xb200100
           mov       ecx, edi          ; EDI=0xb200100
           shr       ecx, 14h          ; ECX=0xb200100
           and       ecx, 0Fh          ; ECX=0xb2
           mov       edx, edi          ; EDI=0xb200100
           shr       edx, 0Ch          ; EDX=0xb200100
           movzx     edx, dl           ; DL=0
           mov       eax, edi          ; EDI=0xb200100
           shr       eax, 8            ; EAX=0xb200100
           and       eax, 0Fh          ; EAX=0xb2001
           and       edi, 0FFh         ; EDI=0xb200100
           push      edi               ; EDI=0
           mov       edi, [ebp+arg_18] ; [EBP+20h]=0
           push      eax               ; EAX=1
           mov       eax, ds:__imp__vsnban ; [597D6D8h]=0x65852100, "Oracle Database 11g ↙
     ↘ Enterprise Edition Release %d.%d.%d.%d.%d %s"
           push     edx               ;  EDX=0
           push     ecx               ;  ECX=2
           push     ebx               ;  EBX=0xb
           mov       ebx, [ebp+arg_8] ; [EBP+10h]=0xcdfe554
           push     eax               ; EAX=0x65852100, "Oracle Database 11g Enterprise Edition ↙
     ↘ Release %d.%d.%d.%d.%d %s"
           mov      eax, [ebp+Dest] ; [EBP-10h]=0xce2ffb0
           push     eax             ; EAX=0xce2ffb0
           call     ds:__imp__sprintf ; op1=MSVCR80.dll!sprintf tracing nested maximum level (1) ↙
     ↘ reached, skipping this CALL
           add      esp, 38h
           mov      dword ptr [ebx], 1

loc_2CE1192: ; CODE XREF: _kqvrow_+FB
             ; _kqvrow_+128 ...
           test     edi, edi         ; EDI=0
           jnz      __VInfreq__kqvrow
           mov      esi, [ebp+var_C] ; [EBP-0Ch]=0xcdfe248
           mov      edi, [ebp+var_4] ; [EBP-4]=0xc98c938
           mov      eax, ebx         ; EBX=0xcdfe554
           mov      ebx, [ebp+var_8] ; [EBP-8]=0
           lea      eax, [eax+4]     ; [EAX+4]=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 – Production↙
     ↘ ", "Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - Production", "PL/SQL ↙
     ↘ Release 11.2.0.1.0 - Production", "TNS for 32-bit Windows: Version 11.2.0.1.0 - ↙
     ↘ Production"

loc_2CE11A8: ; CODE XREF: _kqvrow_+29E00F6
           mov      esp, ebp
           pop      ebp
           retn                        ; EAX=0xcdfe558

loc_2CE11AC: ; DATA XREF: .rdata:0628B0A0
           mov      edx, [ebx+8]     ; [EBX+8]=0xce2ffb0, "Oracle Database 11g Enterprise Edition ↙
     ↘ Release 11.2.0.1.0-  Production"
           mov      dword ptr [ebx], 2
           mov      [ebx+4], edx     ; EDX=0xce2ffb0, "Oracle Database 11g Enterprise Edition ↙
     ↘ Release 11.2.0.1.0 - Production"
           push     edx              ; EDX=0xce2ffb0, "Oracle Database 11g Enterprise Edition ↙
     ↘ Release 11.2.0.1.0 - Production"
           call     _kkxvsn          ; tracing nested maximum level (1) reached, skipping this ↙
     ↘ CALL
           pop      ecx
           mov      edx, [ebx+4]      ; [EBX+4]=0xce2ffb0, "PL/SQL Release 11.2.0.1.0 - Production"
           movzx    ecx, byte ptr [edx]  ; [EDX]=0x50
           test     ecx, ecx          ; ECX=0x50
           jnz      short loc_2CE1192
           mov      edx, [ebp+var_14]
           mov      esi, [ebp+var_C]
           mov      eax, ebx
           mov      ebx, [ebp+var_8]
           mov      ecx, [eax]
           jmp      loc_2CE10F6

loc_2CE11DB: ; DATA XREF: .rdata:0628B0A4
           push     0
           push     50h
           mov      edx, [ebx+8]     ; [EBX+8]=0xce2ffb0, "PL/SQL Release 11.2.0.1.0 - Production"
           mov      [ebx+4], edx     ; EDX=0xce2ffb0,"PL/SQL Release 11.2.0.1.0 - Production"
           push     edx              ; EDX=0xce2ffb0, "PL/SQL Release 11.2.0.1.0 - Production"
           call     _lmxver          ; tracing nested maximum level (1) reached, skipping this ↙
     ↘ CALL
           add      esp, 0Ch
           mov      dword ptr [ebx], 3
           jmp      short loc_2CE1192

loc_2CE11F6: ; DATA XREF: .rdata:0628B0A8
           mov      edx, [ebx+8]     ; [EBX+8]=0xce2ffb0
           mov      [ebp+var_18], 50h
           mov      [ebx+4], edx     ; EDX=0xce2ffb0
           push     0
           call     _npinli          ; tracing nested maximum level (1)reached, skipping this ↙
     ↘ CALL
           pop      ecx
           test     eax, eax         ; EAX=0
           jnz      loc_56C11DA
           mov      ecx, [ebp+var_14] ; [EBP-14h]=0xc98c938
           lea      edx, [ebp+var_18] ; [EBP-18h]=0x50
           push     edx                ; EDX=0xd76c93c
           push     dword ptr [ebx+8] ; [EBX+8]=0xce2ffb0
           push     dword ptr [ecx+13278h] ; [ECX+13278h]=0xacce190
           call     _nrtnsvrs         ; tracing nested maximum level (1) reached, skipping this ↙
     ↘ CALL
           add      esp, 0Ch

loc_2CE122B: ; CODE XREF: _kqvrow_+29E0118
           mov      dword ptr [ebx], 4
           jmp      loc_2CE1192

loc_2CE1236: ; DATA XREF: .rdata:0628B0AC
           lea      edx, [ebp+var_7C] ; [EBP-7Ch]=1
           push     edx               ; EDX=0xd76c8d8
           push     0
           mov      esi, [ebx+8]     ; [EBX+8]=0xce2ffb0, "TNS for 32-bit Windows: Version ↙
     ↘ 11.2.0.1.0 - Production"
           mov      [ebx+4], esi     ; ESI=0xce2ffb0, "TNS for 32-bit Windows: Version 11.2.0.1.0 ↙
     ↘ - Production"
           mov      ecx, 50h
           mov      [ebp+var_18],  ecx ; ECX=0x50
           push     ecx               ; ECX=0x50
           push     esi               ; ESI=0xce2ffb0, "TNS for 32-bit Windows: Version 11.2.0.1.0 ↙
     ↘ - Production"
           call     _lxvers           ; tracing nested maximum level (1) reached, skipping this ↙
     ↘ CALL
           add      esp, 10h
           mov      edx, [ebp+var_18  ; [EBP-18h]=0x50
           mov      dword ptr [ebx], 5
           test    edx, edx          ; EDX=0x50
           jnz      loc_2CE1192
           mov      edx, [ebp+var_14]
           mov      esi, [ebp+var_C]
           mov       eax, ebx
           mov      ebx, [ebp+var_8]
           mov      ecx, 5
           jmp      loc_2CE10F6

loc_2CE127A: ; DATA XREF: .rdata:0628B0B0
           mov      edx, [ebp+var_14] ; [EBP-14h]=0xc98c938
           mov      esi, [ebp+var_C] ; [EBP-0Ch]=0xcdfe248
           mov      edi, [ebp+var_4] ; [EBP-4]=0xc98c938
           mov      eax, ebx         ; EBX=0xcdfe554
           mov      ebx, [ebp+var_8] ; [EBP-8]=0

loc_2CE1288: ; CODE XREF: _kqvrow_+1F
           mov      eax, [eax+8]     ; [EAX+8]=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 - Production"
           test     eax, eax         ; EAX=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 - Production"
           jz       short loc_2CE12A7
           push     offset aXKqvvsnBuffer ; "x$kqvvsn buffer"
           push     eax                ; EAX=0xce2ffb0, "NLSRTL Version 11.2.0.1.0 - Production"
           mov      eax, [ebp+arg_C]   ; [EBP+14h]=0x8a172b4
           push     eax                ; EAX=0x8a172b4
           push    dword ptr [edx+10494h]    ; [EDX+10494h]=0xc98cd58
           call     _kghfrf            ; tracing nested maximum level (1) reached, skipping this ↙
    ↘ CALL
          add       esp, 10h

loc_2CE12A7: ; CODE XREF: _kqvrow_+1C1
           xor      eax, eax
           mov      esp, ebp
           pop      ebp
           retn                        ; EAX=0
_kqvrow_   endp

不难看出，该函数从外部获取行号信息，然后按照下述顺序组装、返回字符串。

String 1
String 2
String 3
String 4
String 5
Using vsnstr, vsnnum, vsnban global variables. Calling sprintf().
Calling kkxvsn().
Calling lmxver().
Calling npinli(), nrtnsvrs().
Calling lxvers().

Oracle按照上述次序依次调用相应函数，从而获取各个模块的版本信息。

81.2　X$KSMLRU表

官方文件《Diagnosing and Resolving Error ORA-04031》[5]特别提到了这个数据表：

Oracle能够记录内存池内发生的、强制释放其他对象的内存占用情况。负责记录这种情况的数据表是固定表x$ksmlru。它可用来诊断内存异常消耗的具体原因。

如果内存池里发生了大量对象周期性释放的情况，那么这种问题会增加数据库的响应时间。而且当这些对象再次被加载到内存池时，这一现象还会增加库缓存（library cache）互锁的概率。

固定表 x$ksmlru 具有一个特性：只要出现了检索表的人为操作，那么这个表内的数据就会被立刻清空。此外，该数据表只会存储内存占用最大的前几项记录。“查询后立刻清空”的设定，是为了凸显那些先前并不那么耗费资源的内存分配情况。也就是说，每次检索所对应的时间段都是不同的。正因如此，数据库用户应当妥善保管该表的查询结果。

换句话说，查询这个表不是问题，问题是查询后它会被立即清空。那么，清空表的具体原因是什么？既然kqftab表和kqftap表含有X$-表的全部信息，我们可以继续使用前文介绍的oracle_tables进行分析。在oracle_tables的返回结果里，我们看到：在制备X$KSMLRU表的元素时，oracle调用了ksmlrs() 函数。

指令清单81.10　Result of oracle tables

kqftab_element.name: [X$KSMLRU] ?: [ksmlr] 0x4 0x64 0x11 0xc 0xffffc0bb 0x5
kqftap_param.name=[ADDR] ?: 0x917 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INDX] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INST_ID] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[KSMLRIDX] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[KSMLRDUR] ?: 0xb02 0x0 0x0 0x0 0x4 0x4 0x0
kqftap_param.name=[KSMLRSHRPOOL] ?: 0xb02 0x0 0x0 0x0 0x4 0x8 0x0
kqftap_param.name=[KSMLRCOM] ?: 0x501 0x0 0x0 0x0 0x14 0xc 0x0
kqftap_param.name=[KSMLRSIZ] ?: 0x2 0x0 0x0 0x0 0x4 0x20 0x0
kqftap_param.name=[KSMLRNUM] ?: 0x2 0x0 0x0 0x0 0x4 0x24 0x0
kqftap_param.name=[KSMLRHON] ?: 0x501 0x0 0x0 0x0 0x20 0x28 0x0
kqftap_param.name=[KSMLROHV] ?: 0xb02 0x0 0x0 0x0 0x4 0x48 0x0
kqftap_param.name=[KSMLRSES] ?: 0x17 0x0 0x0 0x0 0x4 0x4c 0x0
kqftap_param.name=[KSMLRADU] ?: 0x2 0x0 0x0 0x0 0x4 0x50 0x0
kqftap_param.name=[KSMLRNID] ?: 0x2 0x0 0x0 0x0 0x4 0x54 0x0
kqftap_param.name=[KSMLRNSD] ?: 0x2 0x0 0x0 0x0 0x4 0x58 0x0
kqftap_param.name=[KSMLRNCD] ?: 0x2 0x0 0x0 0x0 0x4 0x5c 0x0
kqftap_param.name=[KSMLRNED] ?: 0x2 0x0 0x0 0x0 0x4 0x60 0x0
kqftap_element.fn1=ksmlrs
kqftap_element.fn2=NULL

tracer程序可以印证这个结果：每次查询X$KSMLRU表时，Oracle都会调用这个函数。

另外，我们还看到ksmsplu_sp()函数和ksmsplu_jp()函数都引用了ksmsplu()函数。即，无论是执行ksmsplu_sp()函数、还是执行ksmsplu_jp()函数，最后都会调用ksmsplu()函数。在ksmsplu()结束之前，它调用了memset()函数。

指令清单81.11　ksm.o

…
.text:00434C50 loc_434C50:                               ; DATA XREF: .rdata:off_5E50EA8
.text:00434C50                   mov       edx, [ebp-4]
.text:00434C53                   mov       [eax], esi
.text:00434C55                   mov       esi, [edi]
.text:00434C57                   mov       [eax+4], esi
.text:00434C5A                   mov       [edi], eax
.text:00434C5C                   add       edx, 1
.text:00434C5F                   mov       [ebp-4], edx
.text:00434C62                   jnz       loc_434B7D
.text:00434C68                   mov       ecx, [ebp+14h]
.text:00434C6B                   mov       ebx, [ebp-10h]
.text:00434C6E                   mov       esi, [ebp-0Ch]
.text:00434C71                   mov       edi, [ebp-8]
.text:00434C74                   lea       eax, [ecx+8Ch]
.text:00434C7A                   push      370h             ; Size
.text:00434C7F                   push      0                ; Val
.text:00434C81                   push      eax              ; Dst
.text:00434C82                   call      __intel_fast_memset
.text:00434C87                   add       esp, 0Ch
.text:00434C8A                   mov       esp, ebp
.text:00434C8C                   pop       ebp
.text:00434C8D                   retn
.text:00434C8D _ksmsplu          endp

含有memset（block，0，size）的构造函数通常用于清空内存区域。如果我们阻止它调用这个memset() 函数，那么将发生什么情况？

为此，我们在程序向memset()函数传递参数的0x434C7A处设置断点、令调试程序tracer在此刻将程序计数器（PC，即EIP）调整为0x434C8A，从而使程序“跳过”清除内存的memset()函数。可以说，这种“调试”相当于令程序在0x434C7A处无条件转移到 0x434C8A。相关的tracer指令如下：

tracer -a:oracle.exe bpx=oracle.exe!0x00434C7A,set(eip,0x00434C8A)

请注意：上述地址仅对Win32版本的Oracle RDBMS 11.2有效。

经上述调试指令启动Oracle以后，无论查询X$ KSMLRU表多少次，这个表都不会被清空了。当然，不要在投入实用的业务服务器上进行这种测试。

或许这种调试的用处不大，或许这种修改有悖实用性原则。不过，当我们要查找特定的指令时，我们可以采用这样的调试步骤！

81.3　V$TIMER表

固定视图V$TIMER算得上是更新最频繁的视图之一了。

V$TIME以百分之一秒为单位、记录实际运行时间。这个值以计时原点开始测算，因此具体数值与操作系统相关。它会在4字节溢出时（大约历经497天后）循环，重新变为0。

上述内容摘自官方文档。[6]

比较有趣的是：Win32版本的Oracle程序和Linux版本的程序，返回的时间戳竟然是不同的。我们能否找到生成返回值的函数呢？

下述操作表明，时间信息最终取自X$KSUTM表：

SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='V$TIMER';

VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------

V$TIMER
select  HSECS from GV$TIMER where inst_id = USERENV('Instance')

SQL> select * from V$FIXED_VIEW_DEFINITION where view_name='GV$TIMER';

VIEW_NAME
------------------------------
VIEW_DEFINITION
--------------------------------------------------------------------------------

GV$TIMER
select inst_id,ksutmtim from x$ksutm

不过kqftab/kqftap表没有引用生成这项数值的函数。

指令清单81.12　Result of oracle tables

kqftab_element.name: [X$KSUTM] ?: [ksutm] 0x1 0x4 0x4 0x0 0xffffc09b 0x3
kqftap_param.name=[ADDR] ?: 0x10917 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INDX] ?: 0x20b02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[INST_ID] ?: 0xb02 0x0 0x0 0x0 0x4 0x0 0x0
kqftap_param.name=[KSUTMTIM] ?: 0x1302 0x0 0x0 0x0 0x4 0x0 0x1e
kqftap_element.fn1=NULL
kqftap_element.fn2=NULL

当我们搜索字符串KSUTMTIM时，我们看到了下述函数：

kqfd_DRN_ksutm_c proc near                 ; DATA XREF: .rodata:0805B4E8

arg_0             = dword ptr   8
arg_8             = dword ptr   10h
arg_C             = dword ptr   14h

                  push     ebp
                  mov      ebp, esp
                  push     [ebp+arg_C]
                  push     offset ksugtm
                  push     offset _2__STRING_1263_0 ; "KSUTMTIM"
                  push     [ebp+arg_8]
                  push     [ebp+arg_0]
                  call     kqfd_cfui_drain
                  add      esp, 14h
                  mov      esp, ebp
                  pop      ebp
                  retn
kqfd_DRN_ksutm_c endp

而数据表kqfd_tab_registry_0引用了kqfd_DRN_ksutm_c()函数：

dd offset _2__STRING_62_0 ; "X$KSUTM"
dd offset kqfd_OPN_ksutm_c
dd offset kqfd_tabl_fetch
dd 0
dd 0
dd offset kqfd_DRN_ksutm_c

打开Linux x86版本的这个文件，可看到如下所示的代码。

指令清单81.13　ksu.o

ksugtm               proc near

var_1C               = byte ptr  -1Ch
arg_4                = dword ptr  0Ch

                     push    ebp
                     mov     ebp, esp
                     sub     esp, 1Ch
                     lea     eax, [ebp+var_1C]
                     push    eax
                     call    slgcs
                     pop     ecx
                     mov     edx, [ebp+arg_4]
                     mov     [edx], eax
                     mov     eax, 4
                     mov     esp, ebp
                     pop     ebp
                     retn
ksugtm               endp

在Win32版本的程序里，相应文件的有关指令几乎相同。

这是我们寻找的函数吗？我们通过下述指令验证一下：

tracer -a:oracle.exe bpf=oracle.exe!_ksugtm,args:2,dump_args:0x4

然后在SQL*Plus里执行以下指令：

SQL> select * from V$TIMER;

      HSECS
----------
  27294929

SQL> select * from V$TIMER;

      HSECS
----------
  27295006

SQL> select * from V$TIMER;

      HSECS
----------
  27295167

指令清单81.14　tracer output

TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!__VInfreq__qerfxFetch↙
     ↘ +0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9                                           "8.                 "
TID=2428|(0) oracle.exe!_ksugtm () -> 0x4 (0x4)
Argument 2/2 difference
00000000: D1 7C A0 01                                     ".|..               "
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!__VInfreq__qerfxFetch↙
     ↘ +0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9                                           "8.                  "
TID=2428|(0) oracle.exe!_ksugtm () -> 0x4 (0x4)
Argument 2/2 difference
00000000: 1E 7D A0 01
TID=2428|(0) oracle.exe!_ksugtm (0x0, 0xd76c5f0) (called from oracle.exe!__VInfreq__qerfxFetch↙
     ↘ +0xfad (0x56bb6d5))
Argument 2/2
0D76C5F0: 38 C9                                           "8.                   "
TID=2428|(0) oracle.exe!_ksugtm () -> 0x4 (0x4)
Argument 2/2 difference
00000000: BF 7D A0 01                                     ".}..                 "

上述数据和我们在SQL*Plus看到的数据完全一样。它是函数的第二个参数。

然后我们再来分析Linux x86程序里的slgcs() 函数：

slgcs                proc near

var_4                = dword ptr -4
arg_0                = dword ptr  8

                     push     ebp
                     mov      ebp, esp
                     push     esi
                     mov      [ebp+var_4], ebx
                     mov      eax, [ebp+arg_0]
                     call     $+5
                     pop      ebx
                     nop                       ; PIC mode
                     mov      ebx, offset _GLOBAL_OFFSET_TABLE_
                     mov      dword ptr [eax], 0
                     call     sltrgatime64     ; PIC mode
                     push     0
                     push     0Ah
                     push     edx
                     push     eax
                     call     __udivdi3        ; PIC mode
                     mov      ebx, [ebp+var_4]
                     add      esp, 10h
                     mov      esp, ebp
                     pop      ebp
                     retn
slgcs                endp

这个函数调用了sltrgatime64()，然后把返回值除以10。[7]

在Win32版本的程序里，这个函数则是：

_slgcs              proc near                  ; CODE XREF: _dbgefgHtElResetCount+15
                                               ; _dbgerRunActions+1528

                    db       66h
                    nop
                    push     ebp
                    mov      ebp, esp
                    mov      eax, [ebp+8]
                    mov      dword ptr [eax], 0
                    call     ds:__imp__GetTickCount@0 ; GetTickCount()
                    mov      edx, eax
                    mov      eax, 0CCCCCCCDh
                    mul      edx
                    shr      edx, 3
                    mov      eax, edx
                    mov      esp, ebp
                    pop      ebp
                    retn
_slgcs              endp

Win32的结果就是GetTickCount() 函数返回值的十分之一。[8]

这就是Oracle在Win32下和Linux x86下返回不同结果的根本原因——它调用了完全不同的操作系统函数。

“call kqfd_cfui_drain”里有个“drain”。这个关键字有“表中的某个列取自特定函数的返回值”的含义。

前面介绍过的oracle_tables工具能够处理kqfd_tab_registry_0。因此，我们可以用它分析“列”的值与特定函数之间的关联关系：

[X$KSUTM] [kqfd_OPN_ksutm_c] [kqfd_tabl_fetch] [NULL] [NULL] [kqfd_DRN_ksutm_c]
[X$KSUSGIF] [kqfd_OPN_ksusg_c] [kqfd_tabl_fetch] [NULL] [NULL] [kqfd_DRN_ksusg_c]

上述信息中的OPN代表“Open”和“DRN”。DRN当然还是“drain”的意思。

[1]　1988年的ANSI标准请可参见笔者的摘录：http://yurichev.com/ref/ Draft%20ANSI%20C%20Standard%20(ANSI%20X3J11-88-090)%20(May%2013, %201988).txt。作为对比，微软的标识符标准可参阅https://msdn.microsoft.com/en-us/library/e7f8y25b.aspx。

[2]　笔者通过挖掘kqfviw和kqfvip表里的数据，最终发现了这个视图的信息。

[3]　http://yurichev.com/oracle_tables.html。

[4]　http://yurichev.com/oracle_tables.html。

[5]　http://www.oralab.net/METANOTES/DIAGNOSING%20AND%20RESOLVING%20ORA-04031%20ERROR.htm。

[6]　http://docs.oracle.com/cd/B28359_01/server.111/b28320/dynviews_3104.htm。

[7]　有关除法运算的有关细节，请参见本书第41章。

[8]　有关GetTickCount() 函数，请参见MSDN：https://msdn.microsoft.com/en-us/library/ windows/desktop/ms724408(v=vs.85).aspx。

第82章 汇编指令与屏显字符

82.1　EICAR

多数反病毒软件都用EICAR进行自检。EICAR是一个可以在MS-DOS平台上运行的应用程序。它仅在屏幕上显示“EICAR-STANDARD-ANTIVIRUS-TEST-FILE！”这样一个字符串。[1]

EICAR最重要的特点是：它的每个字节都是可以在屏幕上显示出来的ASCII字符串。我们在文本编译器里粘贴下列字符串，即可生成EICAR文件：

X5O!P%@AP[4\PZX54(P^)7CC]7]$EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$H+H*

EICAR文件的汇编指令如下：

; initial conditions: SP=0FFFEh, SS:[SP]=0
0100 58               pop      ax
; AX=0, SP=0
0101 35 4F 21         xor      ax, 214Fh
; AX = 214Fh and SP = 0
0104 50               push     ax
; AX = 214Fh, SP = FFFEh and SS:[FFFE] = 214Fh
0105 25 40 41         and      ax, 4140h
; AX = 140h, SP = FFFEh and SS:[FFFE] = 214Fh
0108 50               push     ax
; AX = 140h, SP = FFFCh, SS:[FFFC] = 140h and SS:[FFFE] = 214Fh
0109 5B               pop      bx
; AX = 140h, BX = 140h, SP = FFFEh and SS:[FFFE] = 214Fh
010A 34 5C            xor      al, 5Ch
; AX = 11Ch, BX = 140h, SP = FFFEh and SS:[FFFE] = 214Fh
010C 50               push     ax
010D 5A               pop      dx
; AX = 11Ch, BX = 140h, DX = 11Ch, SP = FFFEh and SS:[FFFE] = 214Fh
010E 58               pop      ax
; AX = 214Fh, BX = 140h, DX = 11Ch and SP = 0
010F 35 34 28         xor      ax, 2834h
; AX = 97Bh, BX = 140h, DX = 11Ch and SP = 0
0112 50               push     ax
0113 5E               pop      si
; AX = 97Bh, BX = 140h, DX = 11Ch, SI = 97Bh and SP = 0
0114 29 37            sub      [bx], si
0116 43               inc      bx
0117 43               inc      bx
0118 29 37            sub      [bx], si
011A 7D 24            jge      short near ptr word_10140
011C 45 49 43 ...     db 'EICAR-STANDARD-ANTIVIRUS-TEST-FILE!$'
0140 48 2B word_10140        dw 2B48h  ; CD 21 (INT 21) will be here
0142 48 2A                   dw 2A48h  ; CD 20 (INT 20) will be here
0144 0D                      db  0Dh
0145 0A                      db  0Ah

笔者在上述代码里追加了各种注释，以介绍执行指令后各寄存器和栈的状态。

本质上说，这个程序的关键指令（下文简称“核心指令”）只有：

B4 09     MOV AH, 9
BA 1C 01  MOV DX, 11Ch
CD 21     INT 21h
CD 20     INT 20h

INT 21的第9号功能（AH寄存器）的作用是显示字符串。系统中断从DS:DX获取字符串的指针，然后把它输出在屏幕上。另外，这种字符串必须以“$”字符结尾。当今的操作系统显然继续兼容了这种指令。不过这种指令属于CP/M（Control Program for Microcomputers），来自比MS-DOS还要古老的磁盘操作系统。

由此可见，EICAR的主要功能是：

向寄存器（AH和DX）传递预定值。

在内存中准备INT 21和INT 20的opcode。

执行INT 21和INT 20。

严格地讲，核心指令的opcode基本都不是屏显字符。EICAR采用“拼凑”指令的方法，把核心指令凑成了可存储在字符串里的屏显字符组合。这项“拼凑”技术还普遍应用于shellcode。

有关“可用屏显字符表示的opcode”，请参见本书附录A.6.5。

[1]　请参见https://en.wikipedia.org/wiki/EICAR\_test\_file。

第83章 实例演示

编写Demo程序不仅要求编程人员具备熟练的数学技巧、卓越的计算机绘图水平，而且还非常考验他们手写x86代码的基本功。

83.1　10PRINT CHR$(205.5+RND(1));:GOTO 10

本节介绍的程序都是MS-DOS环境下的.COM程序。

在参考资料［a12］里，我们找到了一个非常简单的随机图案生成程序。虽然它的功能只是不停地在屏幕上打印斜杠和反斜杠，但是这两种字符最终可构成一种几何图案，如图83.1所示。

图83.1　一种随机几何图案

在16位的x86平台上，这种算法非常多。

83.1.1　Trixter的42字节程序

Trixter在他的网站上[1]公开了具备42字节大小的程序。笔者把它摘录出来，并标注上了自己的注释：

00000000: B001         mov         al,1      ; set 40x25 video mode
00000002: CD10         int         010
00000004: 30FF         xor         bh,bh     ; set video page for int 10h call
00000006: B9D007       mov         cx,007D0  ; 2000 characters to output
00000009: 31C0         xor         ax,ax
0000000B: 9C           pushf                 ; push flags
; get random value from timer chip
0000000C: FA           cli                   ; disable interrupts
0000000D: E643         out         043,al    ; write 0 to port 43h
; read 16-bit value from port 40h
0000000F: E440         in          al,040
00000011: 88C4         mov         ah,al
00000013: E440         in          al,040
00000015: 9D           popf                  ; enable interrupts by restoring IF flag
00000016: 86C4         xchg        ah,al
; here we have 16-bit pseudorandom value
00000018: D1E8         shr         ax,1
0000001A: D1E8         shr         ax,1
; CF currently have second bit from the value
0000001C: B05C         mov         al,05C ;'\'
; if CF=1, skip the next instruction
0000001E: 7202         jc          000000022
; if CF=0, reload AL register with another character
00000020: B02F         mov         al,02F ;'/'
; output character
00000022: B40E         mov         ah,00E
00000024: CD10         int         010
00000026: E2E1         loop        000000009 ; loop 2000 times
00000028: CD20         int         020       ; exit to DOS

实际上，上述程序里的伪随机数是Intel 8253计时器芯片（硬件）回传过来的时间信息。它选用了零号计时器，而时钟决定这个计时器每秒递增18.2次。

向0x43端口发送零字节，相当于发送了“选定#0号（通用）计数器”“计数器的输出持续可读”和“采用二进制计数（返回值是二进制数字，而非BCD码）”这三条指令。[2]

当程序执行POPF指令时，CPU恢复IF标识的同时会恢复终端功能。

在使用IN指令读取数据时，返回值必须写到AL寄存器里，所以后面出现了数据交换指令xchg。

83.1.2　笔者对Trixter算法的改进：27字节

这个程序并没有使用计时器查询确切时间，而是用它来生成伪随机数。因此，我们没有必要屏蔽系统中断。此外，我们只需要返回值的低8位数据，所以读这低8位数据即可。

笔者对Trixter的程序稍作精简，把它改进为27字节的程序：

00000000: B9D007   mov     cx,007D0 ; limit output to 2000 characters
00000003: 31C0     xor     ax,ax    ; command to timer chip
00000005: E643     out     043,al
00000007: E440     in      al,040   ; read 8-bit of timer
00000009: D1E8     shr     ax,1     ; get second bit to CF flag
0000000B: D1E8     shr     ax,1
0000000D: B05C     mov     al,05C    ; prepare '\'
0000000F: 7202     jc      000000013
00000011: B02F     mov     al,02F    ; prepare '/'
; output character to screen
00000013: B40E     mov     ah,00E
00000015: CD10     int     010
00000017: E2EA     loop    000000003
; exit to DOS
00000019: CD20     int     020

83.1.3　从随机地址读取随机数

MS-DOS系统完全没有内存保护技术的概念。换句话说，应用程序可以任意访问内存地址。不仅如此，在使用LODSB指令从DS:SI读取单字节数据的时候，即使程序没有预先给寄存器赋值也没有问题——它会从任意地址读取一个字节！

Trixter甚至在其网页里[3]推荐不初始化相关寄存器就直接使用LODSB指令。

原文同时建议使用SCASB指令替代LODSB指令，因为前者可以在读取数据的同时，根据数据直接设置标识位。

此外，使用DOS系统的syscall INT 29h还能对程序进行进一步精简。这个中断可以在屏幕上输出AL寄存器里的字符。

Peter Ferrie和Andrey“hermlt”Baranovich分别写出了11字节和10字节的程序。[4]

指令清单83.1　Andrey“herm1t”Baranovich: 11 bytes

00000000: B05C        mov        al,05C     ;'\'
; read AL byte from random place of memory
00000002: AE          scasb
; PF = parity(AL - random_memory_byte) = parity(5Ch - random_memory_byte)
00000003: 7A02        jp         000000007
00000005: B02F        mov        al,02F     ;'/'
00000007: CD29        int        029        ; output AL to screen
00000009: EBF5        jmp        000000000  ; loop endlessly

SCASB指令计算“AL-[随机地址]”，并设置相应标识位。JP指令比较少见，触发JP转移的条件是奇偶标识位PF为1（偶数）。在这个程序里，输出的字符不再由随机字节的某个比特位决定，而是由这个字节的各个比特位共同决定。因此，这个程序的散列程度有望更好一些。

如果使用x86未公开的SALC指令（即SETALC）、单指令完成“SET AL CF”，那么整个程序还可以更短。这个指令最初出现在NEC v20 CPU上。用自然语言解释的话，它的功能就是“有CF标识位填充AL寄存器”若CF为1，则AL的值将会是0xFF；否则AL的值就是零。受到SALC适用性的影响，任8086/8088平台上这个程序应该跑不起来。

指令清单83.2　Peter Ferrie: 10 bytes

; AL is random at this point
00000000: AE         scasb
; CF is set according subtracting random memory byte from AL.
; so it is somewhat random at this point
00000001: D6         setalc
; AL is set to 0xFF if CF=1 or to 0 if otherwise
00000002: 242D       and          al,02D ;'-'
; AL here is 0x2D or 0
00000004: 042F       add          al,02F ;'/'
; AL here is 0x5C or 0x2F
00000006: CD29       int          029       ; output AL to screen
00000008: EBF6       jmps         000000000 ; loop endlessly

因此，完全有可能彻底抛弃条件转移指令。反斜杠（“\”）和斜杠（“/”）的ASCII值分别是0x5C和0x2F。余下的问题就是：可否根据CF的（伪随机）状态把AL寄存器的值设置为0x5C和0x2F。

实际上解决方法十分简单：无论AL的值是0还是0xFF，我们把它和0x2D进行“与”运算，即可得到0和0x2D。再把这个值与0x2F相加，就得到了0x2F和0x5C。然后把AL寄存器里的值打印到屏幕上即可。

83.1.4　其他

应当注意的是：在DOSBox、Windows NT主机、甚至是MS-DOS主机上运行同一个程序，看到的图案都可能是不同的。影响程序结果的因素有：模拟器对Intel 8253计时器的不同模拟方式、寄存器的不同初始值，以及其他因素。

83.2　曼德博集合

多少年来，编程人员不懈地钻研曼德博集合[5]的各种算法。本文将要介绍的，是Sir_Lagsalot在2009年发表的曼德博集合的Demo程序[6]。这个程序由30个16位x86指令构成，文件大小仅为64字节。

它绘制的图案如图83.2所示。

图83.2　曼德博集合Demo程序绘制的图案

本节将介绍这个程序的工作原理。

83.2.1　理论

复数

复数是二元有序实数对，由实部（Re()）和虚部（Im()）两部分构成。在复数概念的二维空间里，任意复数的实部和虚部都可表示为二维坐标，从而把该复数表示为平面的一个点。

本节用到的复数运算的基本法则有：

加法：(a+bi)+(c+di)=(a+c)+(b+d)i。

即：

Re(sum) = Re(a) + Re(b)

Im(sum) = Im(a) + Im(b)

乘法：(a + bi)(c + di) = (ac − bd) + (bc + ad)i。

即：

Re(product) = Re(a) · Re(c)−Re(b) · Re(d)

Im(product) = Im(b) · Im(c) + Im(a) · Im(d)

平方：(a+bi)² = (a+bi)(a+bi)=(a² −b²)+(2ab)i。

即：

Re(square) = Re(a)²−Im(a)²

Im(square) = 2 · Re(a) · Im(a)

曼德博集合的绘制方法

曼德博集合可以由复二次多项式来定义：对于由复数Z() 构成的递归序列z_{n+1}=z_{n}^{2}+c来说，不同的参数c可能使序列的绝对值逐渐发散到无限大，也可能收敛在有限的区域内。曼德博集合就是使序列不延伸至无限大的所有复数c的集合。

简单地说，普通程序的做法大致如下：[7]

把屏幕划分为像限/取值区域。

将每个坐标点视为一个c值，并验证它是否属于曼德布洛特集合。

验证各坐标的方法如下：

将每个坐标点视为一个复数参数c。

描述该点的复数值。

计算复数值的平方。

计算上述值与初始值的矢量和[8]。

判断上述结果是否逃逸。如果逃逸则立刻终断。

在一定的迭代次数（n）内，进行复数二项式z_{n+1}=z_{n}^{2}+c的迭代。

如果这个点所代表的c值不会使递归序列的值逃逸到无限大，那么就在屏幕上用某种颜色标注这个点。

如果这个点所代表的c值会使递归序列的值逃逸，则：

（黑白构图）不给这个点着色。

（彩色构图）把逃逸时的迭代次数转换成某种颜色，并用这种颜色给这个点着色。因此，彩色曼德博集合图上的颜色，描述的是该点的逃逸速度。

笔者编写了两个程序，分别以复数（宏观参数）和代数二项式（表达形式）两种角度实现上述算法。

指令清单83.3　For complex numbers

def check_if_is_in_set(P):
    P_start=P
    iterations=0

    while True:
         if (P>bounds):
              break
         P=P^2+P_start
        if iterations > max_iterations:
             break
        iterations++
     return iterations

# black-white
for each point on screen P:
    if check_if_is_in_set (P) < max_iterations:
         draw point

# colored
for each point on screen P:
    iterations = if check_if_is_in_set (P)
    map iterations to color
    draw color point

当参数为复数时，就要使用代数二项式“代入”上述参数，并应用前文介绍过的复数计算法则。

指令清单83.4　For integer numbers

def check_if_is_in_set(X, Y):
    X_start=X
    Y_start=Y
    iterations=0

    while True:
         if (X^2 + Y^2 > bounds):
             break
         new_X=X^2 - Y^2 + X_start
         new_Y=2*X*Y + Y_start
         if iterations > max_iterations:
             break
         iterations++

    return iterations

# black-white
for X = min_X to max_X:
    for Y = min_Y to max_Y:
        if check_if_is_in_set (X,Y) < max_iterations:
             draw point at X, Y

# colored
for X = min_X to max_X:
    for Y = min_Y to max_Y:
        iterations = if check_if_is_in_set (X,Y)
        map iterations to color
        draw color point at X,Y

维基百科的网站介绍了一种以C＃语言实现的曼德博集合算法[9]。不过，那个源程序只能用符号显示迭代次数的信息。笔者把它改得更为直观了一些，让它能够显示出序列的迭代次数[10]：

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Mnoj
{
     class Program
     {
          static void Main(string[] args)
          {
               double realCoord, imagCoord;
               double realTemp, imagTemp, realTemp2, arg;
               int iterations;
               for (imagCoord = 1.2; imagCoord >= -1.2; imagCoord -= 0.05)
               {
                    for (realCoord = -0.6; realCoord <= 1.77; realCoord += 0.03)
                    {
                         iterations = 0;
                         realTemp = realCoord;
                         imagTemp = imagCoord;
                         arg = (realCoord * realCoord) + (imagCoord * imagCoord);
                         while ((arg < 2*2) && (iterations < 40))
                         {
                              realTemp2 = (realTemp * realTemp) - (imagTemp * imagTemp) - realCoord;
                              imagTemp = (2 * realTemp * imagTemp) - imagCoord;
                              realTemp = realTemp2;
                              arg = (realTemp * realTemp) + (imagTemp * imagTemp);
                              iterations += 1;
                         }
                         Console.Write("{0,2:D} ", iterations);
                    }
                    Console.Write("\n");
               }
               Console.ReadKey();
          }
     }
}

运行上述程序，可得到文件：http://beginners.re/examples/mandelbrot/result.txt

。

这个程序限定迭代次数的上限为40。输出结果中的“40”表示在40次迭代之内序列仍然收敛；而40以内的数字（比方说是n），则表示在该点的c值在迭代n次之后令序列逃逸。

除此之外，http://demonstrations.wolfram.com/MandelbrotSetDoodle/

公开了一个精彩的demo程序。它能够用彩色线条显示指定c值产生的递归序列zn。如果彩线落在灰色圆圈（模为2）之外，那么该点代表的c值就不属于曼德博集合。

首先，笔者在黄色区域之内选取一个c值，观察它产生的递归序列，如图83.3所示。

图83.3　在黄色区域之内选取c值

上述线条是收敛的。这表明笔者选取的c值属于曼德博集合。

然后，笔者在黄色区域之外选取c值。线条立刻混乱、甚至超出边界。如图83.4所示。

图83.4　在黄色区域之外选取c值

这种图案是发散的。这表示刚才选取的c值不属于曼德博集合。

这个网站还开发了另一款曼德博集合的研究程序，有兴趣的读者可访问：http://demonstrations. wolfram.com/IteratesForTheMandelbrotSet/

。

83.2.2　demo程序

本节将要介绍的这个曼德博集合的demo程序，仅由30条指令、64个字节构成。虽然它的算法与前文的算法一致，但是它用到了很多编程技巧。

整个程序的源代码是公开的。为了便于读者阅读，笔者添加了一些注释。

指令清单83.5　程序源代码+注释

 1  ; X is column on screen
 2  ; Y is row on screen
 3
 4
 5  ; X=0, Y=0                X=319, Y=0
 6  ; +------------------------------->
 7  ; |
 8  ; |
 9  ; |
10  ; |
11  ; |
12  ; |
13  ; v
14  ; X=0, Y=199             X=319, Y=199
15
16
17  ; switch to VGA 320*200*256 graphics mode
18  mov al,13h
19  int 10h
20  ; initial BX is 0
21  ; initial DI is 0xFFFE
22  ; DS:BX (or DS:0) is pointing to Program Segment Prefix at this moment
23  ; ... first 4 bytes of which are CD 20 FF 9F
24  les ax,[bx]
25  ; ES:AX=9FFF:20CD
26
27  FillLoop:
28  ; set DX to 0. CWD works as: DX:AX = sign_extend(AX).
29  ; AX here 0x20CD (at startup) or less then 320 (when getting back after loop),
30  ; so DX will always be 0.
31  cwd
32  mov ax,di
33  ; AX is current pointer within VGA buffer
34  ; divide current pointer by 320
35  mov cx,320
36  div cx
37  ; DX (start_X) - remainder (column: 0..319); AX - result (row: 0..199)
38  sub ax,100
39  ; AX=AX-100, so AX (start_Y) now is in range -100..99
40  ; DX is in range 0..319 or 0x0000..0x013F
41  dec dh
42  ; DX now is in range 0xFF00..0x003F (-256..63)
43
44  xor bx,bx
45  xor si,si
46  ; BX (temp_X)=0; SI (temp_Y)=0
47
48  ; get maximal number of iterations
49  ; CX is still 320 here, so this is also maximal number of iteration
50  MandelLoop:
51  mov bp,si       ; BP = temp_Y
52  imul si,bx      ; SI = temp_X*temp_Y
53  add si,si       ; SI = SI*2= (temp_X*temp_Y)*2
54  imul bx,bx      ; BX = BX^2= temp_X^2
55  jo MandelBreak  ; overflow?
56  imul bp,bp      ; BP = BP^2= temp_Y^2
57  jo MandelBreak  ; overflow?
58  add bx,bp       ; BX = BX+BP = temp_X^2 + temp_Y^2
59  jo MandelBreak  ; overflow?
60  sub bx,bp       ; BX = BX-BP = temp_X^2+temp_Y^2 - temp_Y^2 = temp_X^2
61  sub bx,bp       ; BX = BX-BP = temp_X^2 - temp_Y^2
62
63  ; correct scale:
64  sar bx,6        ; BX=BX/64
65  add bx,dx       ; BX=BX+start_X
66  ; now temp_X = temp_X^2 - temp_Y^2 + start_X
67  sar si,6        ; SI=SI/64
68  add si,ax       ; SI=SI+start_Y
69  ; now temp_Y = (temp_X*temp_Y)*2 + start_Y
70
71  loop MandelLoop
72
73  MandelBreak:
74  ; CX=iterations
75  xchg ax,cx
76  ; AX=iterations. store AL to VGA buffer at ES:[DI]
77  stosb
78  ; stosb also increments DI, so DI now points to the next point in VGA buffer
79  ; jump always, so this is eternal loop here
80  jmp FillLoop

这个程序的算法是：

切换到分辨率为320×200/256色的VGA模式。320×200 = 64000（0xFA00）。256色的每个像素都是单字节的数据，所以缓冲区大小就是0xFA00字节。应用程序使用ES:DI寄存器对即可对像素进行寻址。

VGA图形缓冲区的段地址要存储在ES寄存器里，所以ES寄存器的值必须是0xA000。但是向ES寄存器传递0xA000的指令至少要占用4个字节（PUSH 0A000h/POP ES）。有关6位MS-DOS系统的内存模型，可参见第94章的详细介绍。

假设BX寄存器的值为零、程序段前缀（Program Segment Prefix）位于第0号地址处，那么2字节的LES AX，[BX] 指令就会在AX寄存器里存储0x20CD、在ES寄存器里存储0x9FFF。也就是说，这个程序会在图形缓冲区之前输出16个像素（字节）。但是因为该程序的运行平台是MS-DOS，而MS-DOS没有实现内存保护技术，所以这种问题不会引发程序崩溃。不过，屏幕右侧多出了一个16像素宽的红色条带，整个图像向左移动了16个像素。这就是在程序里节省2字节空间的代价。

单个循环处理全部像素。在处理图像问题时，一般的程序都会使用两个循环：一个循环遍历x坐标、另一个循环遍历y坐标。不过，在VGA图形缓存区里对某个像素进行定位时，双循环的程序必须通过乘法运算才能寻址。为此，这个程序的作者决定使用单循环的数据结构，通过除法运算获取当前点的坐标。经过转换以后，程序坐标的取值范围是：x～［−256，63］，y～［−100，99］。正因如此，整个图像的中心点偏右。虽然直接把x的值减去160就可以使x的取值范围变成［−160，159］，从而校准图像中心，但是“SUB DX，160”的指令占用4个字节，而作者的“DEC DH”（相当于SUB DX，0x100）只用两个字节。图像中心偏右算是节省文件空间的另一个代价吧。

检测当前点是否属于曼德博集合。检测算法和前文相同。

以CX寄存器作为循环计数器进行LOOP循环。作者没有明确地给循环计数器赋值，而是让CX寄存器继续沿用第35行的数值（320）。或许，数值越大越精确吧。这同样可以节省文件空间。

使用IMUL指令。因为操作符是有符号数，所以乘法指令不是MUL。同理，为了把原点坐标0，0调整到屏幕中心区域，在转换坐标时（除法）使用到的是SAR指令（带符号右移），而不是SHR指令。

简化逃逸检测的算法。常规算法检测的是坐标，即一对坐标值。而作者分三次检测了溢出问题：两个求平方操作和一次加法运算的溢出。事实正是如此，我们使用的是16位寄存器，寄存器内的数值不会超过［−32768，+32767］。在计算机进行有符号数的乘法运算时，只要坐标的值大于32767，那么该点的复数就不会属于曼德博集合。

后面再次出现的SAR指令（相当于除以64）设置了64级素灰度。调色版的灰度值越大，对比度就越高、图像就越清晰；反之，灰度越集中，对比度就越低、图像就越模糊。

程序执行到MandelBreak标签的情况分为两种：一种情况是循环结束时CX = 0这就说明，该点属于曼德博集合；另一种情况是程序发生溢出、此时CX存储着非零值。这个程序把CX的低8位（即CL）写到图形缓冲区里。在默认调色板中，0代表黑色，所以属于曼德博集合的坐标点都会显示纯黑色像素。当然，我们确实能够在绘图之前把调色板设置得更个性化一些，不过那种程序就不会只有64字节了！

这个程序采用的是无限循环，不能正常退出。如果还要判断循环终止或者进行互动响应，那么程序文件还要更大。

此外，这个程序的优化技巧同样值得一提：

使用单字节的CWD指令清空DX寄存器。相比之下，“XOR DX，DX”占用2个字节，而“MOV DX，0”则占用3个字节。

使用单字节的“XCHG AX，CX”完成双字节“MOV AX，CX”的操作。毕竟后面不再使用AX寄存器，所以交换数据完全不会造成问题。

因为程序没有对DI寄存器（图形缓冲区的位置）进行初始化赋值，所以它在启动时的初始值为0xFFFE（寄存器初始值由操作系统决定）。不过这无伤大雅，这个程序只要求DI的值保持在［0，0xFFFF］之间，软件用户也看不到屏幕区域之外的点（在320×200×256的图像缓冲区里，最后一个像素的地址是0xF9FF）。也就是说，因为这个程序前后衔接得严丝合缝，所以不管DI寄存器也没问题。否则，编程人员还要添加把DI寄存器置零、以及检测图形缓冲区结束边界的指令。

83.2.3　笔者的改进版

指令清单83.6　My“fixed”version

 1 org 100h
 2 mov al, 13h
 3 int 10h
 4
 5 ; set palette
 6 mov dx, 3c8h
 7 mov al, 0
 8 out dx, al
 9 mov cx, 100h
10 inc  dx
11 l00:
12 mov  al, cl
13 shl  ax, 2
14 out  dx, al ; red
15 out  dx, al ; green
16 out  dx, al ; blue
17 loop l00
18
19 push 0a000h
20 pop es
21
22 xor di, di
23
24 FillLoop:
25 cwd
26 mov ax, di
27 mov cx, 320
28 div cx
29 sub ax, 100
30 sub dx, 160
31
32 xor bx, bx
33 xor si, si
34
35 MandelLoop:
36 mov bp, si
37 imul si, bx
38 add si, si
39 imul bx, bx
40 jo MandelBreak
41 imul   bp, bp
42 jo MandelBreak
43 add bx, bp
44 jo MandelBreak
45 sub bx, bp
46 sub bx, bp
47
48 sar bx, 6
49 add bx, dx
50 sar si, 6
51 add si, ax
52
53 loop MandelLoop
54
55 MandelBreak:
56 xchg ax, cx
57 stosb
58 cmp di, 0FA00h
59 jb FillLoop
60
61 ; wait for keypress
62 xor ax, ax
63 int 16h
64 ; set text video mode
65 mov ax, 3
66 int 10h
67 ; exit
68 int 20h

笔者修正了上一个程序的缺陷：使用了平滑过渡的灰度调色板；把整个图形全部输出到了图形缓冲区里（第19、20行））；使图像中心与屏幕中心重合（第30行）；绘图结束后等待键盘敲击再退出程序（第58～68行）。不过，整个程序大了近一倍：它由54条指令构成，文件大小也增长到了105字节。该程序的绘图如图83.5所示。

图83.5　笔者的改进版程序的绘图

[1]　http://trixter.oldskool.org/2012/12/17/maze-generation-in-thirteen-bytes/。

[2]　实际上，8位控制字相当于四条指令。因为另一个指令没有在本程序发挥作用，所以本文没有进行介绍。

[3]　http://trixter.oldskool.org/2012/12/17/maze-generation-in-thirteen-bytes/。

[4]　请参照http://pferrie.host22.com/misc/10print.htm。

[5]　还被译作曼德布洛特集合，英文原文是Mandelbrot set。

[6]　http://www.pouet.net/prod.php?which=53287。

[7]　微软较为全面地介绍了数学方面和编程方面的细节，建议读者阅读：https://msdn.microsoft. com/zh-cn/library/jj635753%28v=vs.85%29.aspx。

[8]　实际算法都令z₀=0，因此前几步运算在计算z₁。请参考前面推荐的微软文档。

[9]　https://en.wikipedia.org/wiki/Mandelbrot_set。

[10]　修改版程序的下载地址是：http://beginners.re/examples/mandelbrot/dump_ iterations.exe。

第九部分
文件分析

第84章 基于XOR的文件加密

84.1　Norton Guide：单字节XOR加密实例

在MS-DOS时代，Norton Guide（http://en.wikipedia.org/wiki/Norton_Guides

）风靡一时。它是一款驻留内存的TSR程序，可与编程语言的编辑程序整合，提供超文本形式的参考信息。

Norton Guide的数据库文件是.ng文件。一看便知，这种文件经过加密处理。如图84.1所示。

图84.1　Very typical look

为什么说它是加密文件而非压缩文件呢？我们可以看到数值为0x1A的字节（右箭头字符）多次出现，而压缩文件则不会发生这种情况。我们还看到了大量有拉丁字符的片段，只是这些字符串不可自然解释。

因为文件多次出现了0x1A，所以我们按照加密文件进行处理，并且假设该文件是经异或/XOR加密的密文。我们可在Hiew里清楚地看到，当使用0x1A对每个字节进行异或运算时，文件里出现了亲切的英语文字。如图84.2所示。

图84.2　Hiew XORing with 0x1A

对每个字节进行XOR运算是一种最初级的加密方法。不要因为其简单就瞧不起这种分析方法，实际的分析工作中就是会频繁遇到这种技术。

现在，我们大体理解了0x1A出现次数很多的原因了。在进行异或运算之后，数值为0的明文字节会演变为数值为0x1A的密文。

应当注意：加密的常量会因文件而异。在解密单字节XOR加密的密文时，我们应当尝试0～255之间的每个数，再分析一下密文文件被解密成什么样子。有关Norton Guide的文件格式，请参见：http://www.davep.org/norton-guides/file-format/

。

信息熵

在加密领域，信息熵（entropy）属于重要的信息指标。它有一个重要特性：加密前后的明文和密文，其信息熵不变。本节将介绍使用Wolfram Mathematica 10来计算信息熵的具体方法。

指令清单84.1　Wolfram Mathematica 10

In[1]:= input = BinaryReadList["X86.NG"];

In[2]:= Entropy[2, input] // N
Out[2]= 5.62724

In[3]:= decrypted = Map[BitXor[#, 16^^1A] &, input];

In[4]:= Export["X86_decrypted.NG", decrypted, "Binary"];

In[5]:= Entropy[2, decrypted] // N
Out[5]= 5.62724

In[6]:= Entropy[2, ExampleData[{"Text", "ShakespearesSonnets"}]] // N
Out[6]= 4.42366

上述各指令分别用于加载文件、计算信息熵、解密、保存和计算明文的信息熵（熵不变）。Mathematica还提供了知名的英文片段以供人们进行分析。我选取了莎士比亚的十四行韵律诗进行分析，其信息熵与前一个例子基本相同。我们分析的英文语句，其信息熵与莎士比亚的语言相似。对英文原文进行单字节的XOR加密之后，其信息熵与原文相同。

但是，如果加密单元大于一个字节，那么信息熵就是另外一种情况了。

本节分析的英文原文，可在下述地址下载：http://beginners.re/examples/Norton_guide/ X86.NG

。

其他

Wolfram Mathematica计算的熵以自然指数e为基数，而UNIX的ent工具[1]则以2为基数。所以上例明确指定“以2为基数”，以使得Mathematica的计算结果与ent工具的计算结果相同。

84.2　4字节XOR加密实例

即使XOR算法采用多字节密钥，例如说4字节密钥，分析方法也没有什么两样。本节以32位Windows Server 2008的Kernel32.dll为例进行说明。源文件如图84.3所示。

图84.3　源文件

以4字节密钥进行加密，可得到图84.4所示的结果。

图84.4　文件密文

通过观察文件，就可以看到一组循环出现的4字节字符串。实际上这并不困难，因为PE文件的文件头中含有大量的零字节，所以我们可以直接看到密钥。

在16进制编辑器里，PE文件头大体如图84.5所示。

图84.5　PE文件头

加密之后，如图84.6所示。

图84.6　加密后的PE文件头

观察可得4字节密钥：8C 61 D2 63。使用这个消息块即可对文件解密。

此处不得不提PE文件的几个特点：

① PE文件头里含有大量的零字节。

② 所有的PE字段都向分页边界——4096字节对齐，用零字节填补空缺；所以每个字段之后肯定存在大量的零字节。

用零来实现边界对齐的文件格式并不罕见。很多科学计算软件及工程类软件都采用了这种文件格式。

有兴趣的读者可研究一下本例的文件。它们的下载地址是：http://beginners.re/examples/XOR_ 4byte/

。

84.3　练习题

请尝试解密下列链接中的密文。

http://go.yurichev.com/17353

[1]　官方网站为http://www.fourmilab.ch/random/。

第85章 Millenium游戏的存档文件

“Millenium Return to Earth”是一款古老的DOS游戏（1991年问世）。玩家可在游戏中挖矿、修建战舰、在其他星球上作战，等等。有兴趣的读者可以体验一下它：http://thehouseofgames.org/index.php?t=10&id=110

。

和其他的游戏程序一样，这个游戏也有游戏存档的功能。现在我们来分析一下它的存档文件。

游戏中有“矿”的概念。挖矿的速度因星球而异，在某些星球上快些，而在另一些星球上慢些。另外，在游戏的设定中，矿产的种类也有差异化的设定。在图85.1中，您可看到游戏存盘时的挖矿进度。

图85.1　Mine：状态1

我保存了这一时刻的游戏状态。存盘文件总计9538字节。

然后，我又在游戏里挖了几“天”的矿。挖矿进度如图85.2所示。

图85.2　Mine：状态2

此时我再次保存了游戏状态。

现在，我们使用DOS/Windows的FC程序来比较两个存档文件的差别：

...> FC /b 2200save.i.v1 2200SAVE.I.V2
Comparing files 2200save.i.v1 and 2200SAVE.I.V2
00000016: 0D 04
00000017: 03 04
0000001C: 1F 1E
00000146: 27 3B
00000BDA: 0E 16
00000BDC: 66 9B
00000BDE: 0E 16
00000BE0: 0E 16
00000BE6: DB 4C
00000BE7: 00 01
00000BE8: 99 E8
00000BEC: A1 F3
00000BEE: 83 C7
00000BFB: A8 28
00000BFD: 98 18
00000BFF: A8 28
00000C01: A8 28
00000C07: D8 58
00000C09: E4 A4
00000C0D: 38 B8
00000C0F: E8 68
...

上述内容是比对结果的部分内容。两个文件的不同之处还有很多，但是其余内容不如这些信息那样富有代表性。

在第一次存盘时，我持有14个单位的hydrogen和102个单位的oxygen。在第二次存盘时，相应的持有量变为22和155个单位。如果程序把这两个值存储在存档文件中，那么我们应当可以在存档里找到它。实际情况正是如此。在存档文件的0xBDA处，第一个存档文件的值为0x0E（14），在第二个存档文件的值为0x16（22）。地址0xBDA存储的应当是hygrogen的量。然后，在文件的0xBDC处，两个值分别为0x66（102）和0x9B（155）。地址0xBDC存储的应当是oxygen的值。

您可以自己把玩一下这个游戏，分析存档文件的具体格式。您还可以下载我用的游戏存档：http:// beginners.re/examples/millennium_DOS_game/

。

使用Hiew打开第二次存档的存档文件，可以看到有关矿石的持有量。如图85.3所示。

图85.3　Hiew：状态1

这个值无疑是16位数值：在DOS时代的16位软件程序里，int型数据就是16位数据，这并不意外。

验证一下我们的推测是否正确。把这个地址的值（hydrogen）改为1234（0x4D2），如图85.4所示。

图85.4　Hiew：把数值修改为1234（0x04D2）

然后打开游戏、加载存档中的进度，看看矿产持有量。如图85.5所示。

图85.5　Let’s check for hydrogen value

以上信息表明，我们的推测是正确的。

为了快速通关，我们把所有矿产的持有量都改成最大值，如图85.6所示。

图85.6　Hiew：把各项都修改为最大值

0xFFFF是65535。改动之后，我们就是资源大亨了。如图85.7所示。

图85.7　所有资源都变为65535

在进行了几个游戏日的奋斗之后——哎？部分资源变少了。如图85.8所示。

图85.8　Resource variables overflow

这就发生了数值溢出。游戏开发人员可能没有想到玩家会持有这么多的矿产，所以未做溢出检测。但是挖矿就会增加矿产，超过数据最大值之后、数据溢出了。这样看来，要是我当初没那么贪心就好了，或许吧。

这款游戏的存档文件里还有很多数值，本文不再一一分析。

这属于一种简单的游戏作弊方法。只要玩家略微改动一下存档文件，他们就可以获得很高的游戏分值。

本书63.4节详细介绍了各种文件及内存快照的比较方法。

第86章 Oracle的.SYM文件

在程序崩溃的时候，Oracle RDBMS会把大量信息写到日志文件（log）里。日志文件会记录数据栈的使用情况，如下所示。

----- Call Stack Trace -----
calling              call      entry                  argument values in hex
location             type      point                  (? means dubious value)
-------------------- -------- ---------------------- -------------------
_kqvrow()                      00000000
_opifch2()+2729      CALLptr   00000000               23D4B914 E47F264 1F19AE2
                                                      EB1C8A8 1
_kpoal8()+2832       CALLrel   _opifch2()             89 5 EB1CC74
_opiodr()+1248       CALLreg   00000000               5E 1C EB1F0A0
_ttcpip()+1051       CALLreg   00000000               5E 1C EB1F0A0 0
_opitsk()+1404       CALL???   00000000               C96C040 5E EB1F0A0 0 EB1ED30
                                                      EB1F1CC 53E52E 0 EB1F1F8
_opiino()+980        CALLrel   _opitsk()              00
_opiodr()+1248       CALLreg   00000000               3C 4 EB1FBF4
_opidrv()+1201       CALLrel   _opiodr()              3C 4 EB1FBF4 0
_sou2o()+55          CALLrel   _opidrv()              3C 4 EB1FBF4
_opimai_real()+124   CALLrel   _sou2o()               EB1FC04 3C 4 EB1FBF4
_opimai()+125        CALLrel   _opimai_real()         2 EB1FC2C
_OracleThreadStart@  CALLrel   _opimai()              2 EB1FF6C 7C88A7F4 EB1FC34 0
4()+830                                               EB1FD04
77E6481C             CALLreg   00000000               E41FF9C 0 0 E41FF9C 0 EB1FFC4
00000000             CALL???   00000000

既然是编译器生成的程序，那么Oracle的可执行程序里必定会有调试信息、带有符号（symbol）信息的映射文件、或者是相似的信息。

在Windows NT版的Oracle RDBMS中，其可执行文件里存在着与.SYM文件有关的符号信息。可惜，官方不会公开.SYM文件的文件格式。固然Oracle可以使用纯文本文件，但是如此一来还要对其进行多次转换，性能必然大打折扣。

我们从最短的文件orawtc8.sym入手，试着分析这种格式的文件。Oracle 8.1.7的动态库文件orawtc8.dll之中，存在着这个.SYM文件的符号信息。对于oracle数据库的程序来说，版本越旧、功能模块的文件就越小。

使用Hiew打开上述文件，可以看到如图86.1所示的界面。

图86.1　使用Hiew打开整个文件

参照其他.SYM文件，可知这种格式的文件头（及文件尾部）都有OSYM字样。据此判断，这个字符串可能是某种文件签名。

大体来说，这种文件的格式是“OSYM + 某些二进制数据 + 以0做结束符的字符串 + OSYM”。很明显，这些文件中的字符串应当是函数名和全局变量名。

如图86.2所示，字符串OSYM的位置较为固定。

图86.2　OSYM signature and text strings

接下来，我把这个文件中的整个字符串部分（不包含尾部的OSYM签名字符串）复制了出来，并单独存储为一个文件strings_block。然后使用UNIX的strings和wc工具统计它字符串的数量：

strings strings_block | wc -l
66

可见它包含66个文本字符串。我们先把这个数字记下来。

通常来说，无论它是字符串、还是其他什么类型的数据，数据的总数往往会出现在二进制文件的其他部分。这次的分析过程再次印证了这个规律，我们可以在文件的开始部分、OSYM之后看到66（0x42）：

$ hexdump -C orawtc8.sym
00000000  4f 53 59 4d 42 00 00 00  00 10 00 10 80 10 00 10  |OSYMB...........|
00000010  f0 10 00 10 50 11 00 10  60 11 00 10 c0 11 00 10  |....P...`.......|
00000020  d0 11 00 10 70 13 00 10  40 15 00 10 50 15 00 10  |....p...@...P...|
00000030  60 15 00 10 80 15 00 10  a0 15 00 10 a6 15 00 10  |`...............|
....

当然，0x42不是一个byte型数据，很可能是个以小端字节序存储的32位数据。正因如此，0x42之后排列着3个以上的零字节。

判断它是32位数据的依据是什么？Oracle RDBMS的符号文件可能非常大。以版本号为10.2.0.4的Oracle主程序为例，它的oracle.sym包含有0x3A38E（即238478）个符号。16位的数据类型不足以表达这个数字。

分析过其他.SYM文件之后，我更加确定了上述猜测：在32位的OSYM签名之后的数据，就是反映文本字符串数量的数据。

这也是多数二进制文件的常规格式：文件头通常包含程序签名和文件中的某种信息。

接下来，我们分析一下文件中的二进制部分。我把文件中第8字节（字符串计数器之后）到字符串之间的内容存储为另外一个文件（binary_block）。然后再使用Hiew打开这个新文件，如图86.3所示。

图86.3　Binary block

这个文件的模式逐渐清晰了起来。为了便于理解，我在图中添加了几条分割线，如图86.4所示。

图86.4　Binary block

多数的hex编辑器每行都显示16个字节，Hiew也不例外。所以，在Hiew的窗口里，每行信息对应着4个32位数据。

文件中的数据凸显了它的这种特征：在地址0x104之前的数据都是0x1000xxx形式的数据（请注意小端字节序），数据都以0x10、0x00字节开头；以0x108开始的数据，都是0x0000xxxx型的数据，都以两个零字节开头。

我们把这些数据整理为32位的数组，代码如下所示。

指令清单86.1　第一列是地址

$ 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的阵列。字符串的总量正好是66。那么，到底是每个字符串符号对应了2个32位数据，还是说这2个32位数据完全就是两个互不相干数组呢？我们继续分析。

以0x1000开头的值可能是某种地址。毕竟.SYM文件是为.DLL文件服务的，而且Win32 DLL文件的默认基址是0x10000000，代码的起始地址通常是0x10001000。

使用IDA工具打开orawtc8.dll文件，可以看到它的基址不是默认地址。尽管如此，我们可以看到它的第一个函数的对应代码为：

.text:60351000 sub_60351000     proc near
.text:60351000
.text:60351000 arg_0            = dword ptr  8
.text:60351000 arg_4            = dword ptr  0Ch
.text:60351000 arg_8            = dword ptr  10h
.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”！它不就是在.SYM文件的字符串区里的第一个字符串嘛！可见，这个函数的名字应该就是“ax_reg”。

上述DLL文件的第二个函数是：

.text:60351080 sub_60351080     proc near
.text:60351080
.text:60351080 arg_0            = dword ptr  8
.text:60351080 arg_4            = dword ptr  0Ch
.text:60351080
.text:60351080                  push    ebp
.text:60351081                  mov     ebp, esp
.text:60351083                  mov     eax, dword_60353018
.text:60351088                  cmp     eax, 0FFFFFFFFh
.text:6035108B                  jnz     short loc_603510CF
.text:6035108D                  mov     ecx, hModule
.text:60351093                  xor     eax, eax
.text:60351095                  cmp     ecx, 0FFFFFFFFh
.text:60351098                  mov     dword_60353018, eax
.text:6035109D                  jnz     short loc_603510B1
.text:6035109F                  call    sub_603510F0
.text:603510A4                  mov     ecx, eax
.text:603510A6                  mov     eax, dword_60353018
.text:603510AB                  mov     hModule, ecx
.text:603510B1
.text:603510B1 loc_603510B1:                             ; CODE XREF: sub_60351080+1D
.text:603510B1                  test    ecx, ecx
.text:603510B3                  jbe     short loc_603510CF
.text:603510B5                  push    offset aAx_unreg ; "ax_unreg"
.text:603510BA                  push    ecx              ; hModule
.text:603510BB                  call    ds:GetProcAddress
...

“ax_unreg”是字符串区域里的第二个字符串。第二个函数的起始地址是0x60351080，而在SYM文件里二进制区域的第二个数值正是10001080。据此推测，文件里的这个值应该就是相对地址，只不过，这个相对地址的基地址不是默认的DLL基址罢了。

简短截说，在.SYM文件中那个66×2的数据里，前半部分66个数值是DLL文件里的函数地址。它们也可能是函数里某个标签的相对地址。那么，由0x0000开头的、余下的66个值表达的是什么信息呢？这些数据的取值区间是［0，0x3f8］。它不像是位域的值，只是某种递增序列。关键问题是：每个值的最后一个数之间没有什么明确关系，它也不像是某种地址信息——地址的值应该是4、8或0x10的整数倍。

不妨直接问问您自己：如果您是研发人员，还要在这个文件里写什么数据？即便是瞎猜，也会猜得八九不离十：目前还缺少文本字符串（函数名）在文件里的地址信息。简单验证可知，的确如此，这些数值与字符串的第一个字母的地址存在对应关系。

大功告成。

此外，我还写了一段把.SYM文件中的函数名加载到IDA脚本的程序，以便.idc脚本文件自动解析函数的函数名：

#include <stdio.h>
#include <stdint.h>
#include <io.h>
#include <assert.h>
#include <malloc.h>
#include <fcntl.h>
#include <string.h>

int main (intargc, char *argv[])
{
          uint32_t sig, cnt, offset;
          uint32_t *d1, *d2;
          int      h, i, remain, file_len;
          char    *d3;
          uint32_t array_size_in_bytes;

          assert (argv[1]); // file name
          assert (argv[2]); // additional offset (if needed)

          // additional offset
          assert (sscanf (argv[2], "%X", &offset)==1);

          // get file length
          assert ((h=open (argv[1], _O_RDONLY | _O_BINARY, 0))!=-1);
          assert ((file_len=lseek (h, 0, SEEK_END))!=-1);
          assert (lseek (h, 0, SEEK_SET)!=-1);

          // read signature
          assert (read (h, &sig, 4)==4);
          // read count
          assert (read (h, &cnt, 4)==4);

          assert (sig==0x4D59534F); // OSYM

          // skip timedatestamp (for 11g)
          //_lseek (h, 4, 1);

          array_size_in_bytes=cnt*sizeof(uint32_t);

          // load symbol addresses array
          d1=(uint32_t*)malloc (array_size_in_bytes);
          assert (d1);
          assert (read (h, d1, array_size_in_bytes) == array_size_in_bytes);

          // load string offsets array
          d2=(uint32_t*)malloc (array_size_in_bytes);
          assert (d2);
          assert (read (h, d2, array_size_in_bytes) ==array_size_in_bytes);

          // calculate strings block size
          remain=file_len-(8+4)-(cnt*8);

          // load strings block
          assert (d3=(char*)malloc (remain));
          assert (read (h, d3, remain)==remain);

          printf ("#include <idc.idc>\n\n");
          printf ("static main() {\n"};

          for (i=0; i<cnt; i++)
                    printf ("\tMakeName(0x%08X, \"%s\");\n", offset + d1[i], &d3[d2[i]]);

          printf (")\n");

          close (h);
          free (d1); free (d2); free (d3);
};

使用这个脚本以后，我们可以看到：

#include <idc.idc>

static main() {
          MakeName(0x60351000, "_ax_reg");
          MakeName(0x60351080, "_ax_unreg");
          MakeName(0x603510F0, "_loaddll");
          MakeName(0x60351150, "_wtcsrin0");
          MakeName(0x60351160, "_wtcsrin");
          MakeName(0x603511C0, "_wtcsrfre");
          MakeName(0x603511D0, "_wtclkm");
          MakeName(0x60351370, "_wtcstu");
...
}

如需下载本章用到的oracle文件，请访问：http://beginners.re/examples/oracle/SYM/

。

此外，我们来研究一下Win64下的64位oracle RDBMS。64位程序的指针肯定就是64位数据了吧！这种情况下，8字节数据的数据特征就更为明显了。如图86.5所示。

图86.5　RDBMS for Win64的.SYM文件（示例）

可见，数据表的所有元素都是64位数据，字符串偏移量也不例外。此外，大概是为了区别不同的操作系统，文件的签名改成了OSYMAM64。

如需让IDA自动加载.SYM文件中的函数名，可参考我的样本程序：https://github.com/dennis714/ porg/blob/master/oracle_sym.c

。

第87章 Oracle的.MSDB文件

在解决问题时，如果解是已知的，那么你总会有章可循。

《墨菲定律—精确的法则》

Oracle的.MSDB文件是一种含有错误信息和相应错误编号的二进制文件。本章将与您共同研究它的文件格式，尝试解读其中的原始数据。

虽然Oracle RDBMS提供专门的、文本格式的错误信息文件，但是并非每个.MSB文件里都有相应的、文本格式的错误信息文件，所以有时需要把二进制文件和信息文本进行关联分析。

过滤掉ORAUS.MSG的注释以后，文件开头部分的内容如下所示：

00000, 00000, "normal, successful completion"
00001, 00000, "unique constraint (%s.%s) violated"
00017, 00000, "session requested to set trace event"
00018, 00000, "maximum number of sessions exceeded"
00019, 00000, "maximum number of session licenses exceeded"
00020, 00000, "maximum number of processes (%s) exceeded"
00021, 00000, "session attached to some other process;cannot switch session"
00022, 00000, "invalid session ID; access denied"
00023, 00000, "session references process private memory;cannot detach session"
00024, 00000, "logins from more than one process not allowed in single-process mode"
00025, 00000, "failed to allocate %s"
00026, 00000, "missing or invalid session ID"
00027, 00000, "cannot kill current session"
00028, 00000, "your session has been killed"
00029, 00000, "session is not a user session"
00030, 00000, "User session ID does not exist."
00031, 00000, "session marked for kill"

...

其中，第一个数字是错误编号，第二个数字可能是某种特殊的标识信息。

现在，我们打开ORAUS.MSB的二进制文件，然后找到这些字符串，如图87.1所示。

图87.1　Hiew：第一个消息块

文本字符串之间掺杂着二进制的数据。简单分析之后，可知文件的主体部分可分为多个固定长度的信息块，每个信息块的大小是0x200（512）字节。

首先查看第一个信息块的数据，如图87.2所示。

图87.2　Hiew：第一个消息块

可以看到第一条错误信息的文本内容。此外，我们还注意到错误信息之间没有零字节；也就是说，这些字符串不是以零字节分割的C语言字符串。作为一种替代机制，文件中必须有某个数据记录字符串的长度。

然后我们来找找它的错误代码。参照ORAUS.MSG文件起始部分的错误编号，我们在.msb文件中找到取值为错误编号的几个字节：0，1，17（0x11），18（0x12），19（0x13），20（0x14），21（0x15），22（0x16），23（0x17），24（0x18）……笔者在这个信息块里找到了这些数字，并且在图87.2里用红线标出它们。相邻两个错误代码之间的空间周期是6个字节。这意味着每条错误信息可能都占用6个字节。

第一个16位值（0xA即10）代表着每个信息块包含的错误信息的总数——其他信息块的调查结果印证了这一猜想。稍微想一下就知道错误信息（文本字符串）的长度不会是一致的。这种字符串有长有短，但是信息块的尺寸却是固定的。所以，程序无法事先知道每个信息块装载了多少个文本字符串。

刚才讲过，这些字符串不是以零字节分割的C语言字符串，其字符串长度肯定位于文件中的其他什么地方。字符串“normal，successful completion”有29（0x1D）字节，另一个字符串“unique constraint（%s.%s）violated”有34个（0x22）字节。但是在这个信息块里，我们找不到0x1d或者0x22。

一般来说，Oracle RDBMS应当需要确定每个字符串在信息块之中的相对位置。第一个字符串“normal，successful completion”在整个文件中的绝对位置是0x1444，在信息块中的相对位置是0x44。同理可得第二个字符串“unique constraint（%s.%s）violated”的位置0x1461和0x61。这两个数（0x44和0x61）并不陌生！在信息块中的前几个字节里就有这些数字。

综上所述，我们分析出了各个信息块里6字节非文本信息的格式：

16位错误代码。

16位的零（可能含有其他的标识信息）。

16位的字符串地址信息，用于标记字符串在当前信息块的相对位置。

这些猜想可以被事实验证。在信息块中的最后一个6字节的、错误信息为“dummy”的信息块，它的错误编号为零、起始位置指向最后一个错误信息的最后一个字符之后的位置。或许这个凑数的字符串偏改量用于标记上一个字符串的结束符？至此为止，我们可以根据6字节数据的信息，索引指定的错误编号，从而获取文本字符串的起始位置。我们还知道源程序会根据下一个6字节数据块推算本字符串的文本长度。这样一来，我们可以确定字符串的界限。这种文件格式不必存储字符串的长度，因而十分节省空间。我们可能无法判断它最终能压缩多少文件空间，但是这无疑是一种不错的思路。

此后，我们返回来分析.MSB文件的文件头信息。信息的总数如图87.3中红线部分所示。

图87.3　Hiew：文件头

对其他.MSB文件进行了验证之后，我确信所有的推测都准确无误。虽然文件头中还富含其他信息，但是我们的目标（供调试工具调用）已经达成，故而本文不再分析那些数据。除非我们要编写一个.MSB文件的封装程序，否则就不需要理解其他数据的用途。

如图87.4所示，在文件头之后还有一个16位数值构成的数据表。

图87.4　Hiew：last_errnos表

如图87.4中的红线所示，这些数据的数据宽度可以直接观察出来。在导出这些数据时，我发现这些16位数据就是每个信息块里最后一个错误信息的错误编号。

可见，Oracle RDBMS通过这部分数据进行快速检索的过程大体如下：

加载last_errnos（随便起的名字）数据表。这个数据表包含每个信息块里的错误信息总数。

依据错误编号找到相应的信息块。此处假设各信息块中的信息以错误代码的增序排列。

加载相应的信息块。

逐一检索6字节的索引信息。

通过当前6字节数据找到字符串的第一个字符的位置。

通过下一个6字节数据找到最后一个字符的位置。

加载这个区间之内的全部字符。

我编写了一个展开.MSB信息的C语言程序，有兴趣的读者可通过下述网址下载：http://beginners. re/examples/oracle/MSB/oracle_msb.c

。

本例还用到了Oracle RDBMS 11.1.06的两个文件，如需下载请访问：

go.yurichev.com/17214。

go.yurichev.com/17215。

总结

对于现在的计算机系统来说，本章介绍的这种方法可能已经落伍了。恐怕只有那些在20世纪80年代中期做过大型工程、时刻讲究内存和磁盘的利用效率的老古董才会制定这样严谨的文件格式。无论怎样，这部分内容颇具代表性。我们可以在不分析Oracle RDBMS代码的前提下理解它的专用文件的文件格式。

第十部分
其他

第88章 npad

“npad”指令是一种汇编宏，用于把下一个指令标签的首地址向指定边界对齐。

被npad指令对齐的标签，通常都是需要被多次跳转到的地址标签。例如，在各种循环体起始地址处的标签之前，我们经常可以看到npad指令。它可通过对齐内存地址、内存总线或缓存线等手段，提高CPU加载数据（或指令代码）的访问效率。

下面这段代码摘自MSVC的文件listing.inc。

顺便提一下，这都是NOP指令的变种。虽然这些指令没有实际的操作意义，但是它们可以占用不同的空间。

出于CPU性能的考虑，下述代码没有使用多条NOP指令，而是使用了单条指令。

;; LISTING.INC
;;
;; This file contains assembler macros and is included by the files created
;; with the -FA compiler switch to be assembled by MASM (Microsoft Macro
;; Assembler).
;;
;; Copyright (c) 1993-2003, Microsoft Corporation. All rights reserved.

;; non destructivenops
npad macro size
if size eq 1
   nop
else
  if size eq 2
    mov edi, edi
  else
  if size eq 3
     ; lea ecx, [ecx+00]
     DB 8DH, 49H, 00H
  else
    if size eq 4
     ; lea esp, [esp+00]
     DB 8DH, 64H, 24H, 00H
   else
     if size eq 5
       add eax, DWORD PTR 0
     else
      if size eq 6
        ; lea ebx, [ebx+00000000]
       DB 8DH, 9BH, 00H, 00H, 00H, 00H
      else
        if size eq 7
          ; lea esp, [esp+00000000]
          DB 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H
         else
           if size eq 8
            ; jmp .+8; .npad 6
            DB 0EBH, 06H, 8DH, 9BH, 00H, 00H, 00H, 00H
           else
           if size eq 9
            ; jmp .+9; .npad 7
            DB 0EBH, 07H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H
           else
            if size eq 10
             ; jmp .+A; .npad 7; .npad 1
            DB 0EBH, 08H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 90H
           else
            if size eq 11
             ; jmp .+B; .npad 7; .npad 2
             DB 0EBH, 09H, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8BH, 0FFH
            else
             if size eq 12
              ; jmp .+C; .npad 7; .npad 3
              DB 0EBH, 0AH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 49H, 00H
             else
              if size eq 13
               ; jmp .+D; .npad 7; .npad 4
               DB 0EBH, 0BH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 64H, 24H, 00H
              else
               if size eq 14
               ; jmp .+E; .npad 7; .npad 5
               DB 0EBH, 0CH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 05H, 00H, 00H, 00H, 00H
              else
               if size eq 15
                ; jmp .+F; .npad 7; .npad 6
                DB 0EBH, 0DH, 8DH, 0A4H, 24H, 00H, 00H, 00H, 00H, 8DH, 9BH, 00H, 00H, 00H, 00H
               else
                %out error: unsupported npad size
                .err
               endif
              endif
             endif
            endif
           endif
          endif
         endif
        endif
       endif
      endif
     endif
    endif
   endif
  endif
 endif
endm

第89章 修改可执行文件

89.1　文本字符串

除了那些经过加密存储的字符串以外，我们可以使用任何一款的hex editor直接编辑C字符串。即使那些不了解机器码和可执行文件具体格式的人，也可以使用这项技术直接编辑可执行程序中的字符串。修改后的字符串，其长度不得大于原来字符串的长度，否则可能覆盖其他的数据甚至是其他指令。在MS-DOS盛行的时代，人们普遍使用这种方式直接用译文替换软件中的外文文字。至少在20世纪80年代和90年代的前苏联，这种技术十分流行。所以，那个时代也出现了各种古灵精怪的超短缩写：预定长度的字符串存储空间可能容纳不下完整的译文，所以软件翻译人员不得不绞尽脑汁压缩译文的长度。

在修改Delphi程序的字符串时，有时还要调整字符串的长度。

89.2　x86指令

修改可执行文件中汇编指令的方式有以下几种：

禁用某些指令。此时只要使用0x90（NOP）替换相应的汇编指令即可。

禁用条件转移指令。在修改74 xx（JZ）这样的条件转移指令时，我们可以直接把转移指令的2个字节替换为两个0x90（NOP），也可以把第二个字节（jump offset）替换为0，即把偏移量固定为0。

强制程序进行跳转。有些时候，我们需要把条件转移指令替换为跳转指令，强制其进行跳转。此时，把opcde的第一个字节替换为JMP的0xEB即可。

禁用某函数。只要把函数的第一个指令替换为RETN（0xC3），那么它就不会运行。只要程序的调用约定不是stdcall（第64章第2节），那么这种修改方法都不会有问题。在修改遵循stdcall约定的函数时，修改人员首先要注意函数参数的数量（可查阅原函数的RETN 指令），然后使用带有16位参数的RETN（0xC2）指令替换函数的第一条指令。

某些情况下，被禁用的函数必须返回0或1。此时可使用“MOV EAX，0”或“MOV EAX，1”进行处理。直接使用这两条指令的opcode进行替换时，您会发现其opcode较长。这种情况下就可以使用“XOR EAX，EAX”（0x31 0xC0 两个字节）或XOR EAX，EAX /INC EAX （0x31 0xC0 0x40三个字节）进行替换。

很多软件采用了防范修改的技术。这种功能通常都由“读取可执行文件代码”和“校验和（checksum）检验”两个步骤分步实现。即是说，要实现防修改机制，程序首先要读取（加载到内存里的）程序文件。我们可设置断点，解析其读取内存函数的具体地址。

tracer工具可以满足这种调试需求。它具有BPM（内存断点）功能。

在修改程序时，不得修改PE可执行文件的relocs（参见本书的68.2.6节）。Windows的加载程序会使用新的代码覆盖这部分代码。如果使用Hiew打开可执行程序，会发现这部分代码以灰色显示（请参考图7.12）。万不得已的时候，您可使用跳转指令绕过reclos，否则就要编辑relocs的数据表。

第90章 编译器内部函数

编译器内部函数（compiler intrinsic）是与特定编译器有关的函数，并非寻常的库函数。在编译库函数时，编译器会调用（call）这个函数；而在编译内部函数时，编译器会使用对应的机器码进行直译。内部函数通常是与特定CPU特定指令集有关的伪函数。

例如，C/C++语言里没有循环移位运算指令，而多数CPU硬件支持这种指令。为了便于编程人员使用这种指令，MSVC推出了有关的伪函数_rotl() 和_rotr()。在编译这两个函数时，编译器会直接使用x86指令集中ROL/ROR指令的opcode进行替换。

此外，为了方便程序代码调用SSE指令，MSVC还推出了一些内部函数。

如需查询所有的MSVC内部函数，请查阅MSDN网站：http://msdn.microsoft.com/en-us/library/ 26td21ds.aspx

。

第91章 编译器的智能短板

Intel C++ 10.1（在Linux x86平台上编译Oracle RDBMS 11.2的编译器）有时候会生成两个连续的JZ指令。实际上第二条JZ指令不会被执行、没有实际意义。

指令清单91.1　kdli.o from libserver11.a

.text:08114CF1                     loc_8114CF1: ; CODE XREF: __PGOSF539_kdlimemSer+89A
.text:08114CF1                                  ; __PGOSF539_kdlimemSer+3994
.text:08114CF1 8B 45 08                mov     eax, [ebp+arg_0]
.text:08114CF4 0F B6 50 14             movzx   edx, byte ptr [eax+14h]
.text:08114CF8 F6 C2 01                test    dl, 1
.text:08114CFB 0F 85 17 08 00 00       jnz     loc_8115518
.text:08114D01 85 C9                   test    ecx, ecx
.text:08114D03 0F 84 8A 00 00 00       jz      loc_8114D93
.text:08114D09 0F 84 09 08 00 00       jz      loc_8115518
.text:08114D0F 8B 53 08                mov     edx, [ebx+8]
.text:08114D12 89 55 FC                mov     [ebp+var_4], edx
.text:08114D15 31 C0                   xor     eax, eax
.text:08114D17 89 45 F4                mov     [ebp+var_C], eax
.text:08114D1A 50                      push    eax
.text:08114D1B 52                      push    edx
.text:08114D1C E8 03 54 00 00          call    esp, 8
.text:08114D21 83 C4 08                add     esp, 8

上述文件中，另有一处也存在这种问题。

指令清单91.2　from the same code

.text:0811A2A5                     loc_811A2A5: ; CODE XREF: kdliSerLengths+11C
.text:0811A2A5                                  ; kdliSerLengths+1C1
.text:0811A2A5 8B 7D 08                mov     edi, [ebp+arg_0]
.text:0811A2A8 8B 7F 10                mov     edi, [edi+10h]
.text:0811A2AB 0F B6 57 14             movzx   edx, byte ptr [edi+14h]
.text:0811A2AF F6 C2 01                test    dl, 1
.text:0811A2B2 75 3E                   jnz     short loc_811A2F2
.text:0811A2B4 83 E0 01                and     eax, 1
.text:0811A2B7 74 1F                   jz      short loc_811A2D8
.text:0811A2B9 74 37                   jz      short loc_811A2F2
.text:0811A2BB 6A 00                   push    0
.text:0811A2BD FF 71 08                push    dwordptr [ecx+8]
.text:0811A2C0 E8 5F FE FF FF          call    len2nbytes

这些问题可能属于编译器的bug。但是它们生成的程序不会受到该bug的影响，所以可能被测试人员遗漏了下来。本书的19.2.4节、39.3节、47.7节、18.7节、12.4.1节、19.5.2节中都演示了这种问题。

本文演示了这些编译器问题，以证明编译器确实可能出现匪夷所思的奇怪行为。如果遇到了这种现象，读者不必绞尽脑汁地去琢磨“编译器为什么生成这种诡异代码”。

第92章 OpenMP

OpenMP是一种相对简单的、实现多线程并发功能的编程API。

本章将以加密学意义上的非重复随机数nonce为例，演示OpenMP的应用方法。下面这段代码把nonce和不加密的明文进行串联（即添加），以进行非可逆加密，从而增加截获、破解密文的难度。此外，Bitcion协议约定，在某个阶段中通过nonce使消息块的hash包含特定长度的、连续的零。这种机制又叫作“prove of system”（系统验证）（https://en.wikipedia.org/wiki/Proof-of-work_system

），即参与通信的系统通过这种机制证明它已经采取了精密而耗时的计算。

虽然下面这段代码和Bitcoin没有直接关系，但是功能颇为类似。它会向字符串“hello，word!_”添加一个数字，使得“hello，word!_<数字>”的SHA512 hash包含三个或三个以上的0字节。

假设穷举的区间为［0，INT32最大数−1］（即0～0x7FFFFFFE/2147483646)。

整个算法并不复杂：

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <time.h>
#include "sha512.h"

int found=0;
int32_t checked=0;

int32_t* __min;
int32_t* __max;

time_t start;

#ifdef __GNUC__
#define min(X,Y) ((X) < (Y) ? (X) : (Y))
#define max(X,Y) ((X) > (Y) ? (X) : (Y))
#endif

void check_nonce (int32_t nonce)
{
          uint8_t buf[32];
          struct sha512_ctx ctx;
          uint8_t res[64];

          // update statistics
          int t=omp_get_thread_num();

          if (__min[t]==-1)
                    __min[t]=nonce;
          if (__max[t]==-1)
                    __max[t]=nonce;

          __min[t]=min(__min[t], nonce);
          __max[t]=max(__max[t], nonce);

          // idle if valid nonce found
          if (found)
                    return;

          memset (buf, 0, sizeof(buf));
          sprintf (buf, "hello, world!_%d", nonce);

          sha512_init_ctx (&ctx);
          sha512_process_bytes (buf, strlen(buf), &ctx);
          sha512_finish_ctx (&ctx, &res);
          if (res[0]==0 && res[1]==0 && res[2]==0)
          {
                    printf ("found (thread %d): [%s]. seconds spent=%d\n", t, buf, time(NULL)-start);
                    found=1;
          };
          #pragma omp atomic
          checked++;

          #pragma omp critical
          if ((checked % 100000)==0)
                    printf ("checked=%d\n", checked);
};

int main()
{
          int32_t i;
          int threads=omp_get_max_threads();
          printf ("threads=%d\n", threads);

          __min=(int32_t*)malloc(threads*sizeof(int32_t));
          __max=(int32_t*)malloc(threads*sizeof(int32_t));
          for (i=0; i<threads; i++)
                    __min[i]=__max[i]=-1;

          start=time(NULL);

          #pragma omp parallel for
          for (i=0; i<INT32_MAX; i++)
                    check_nonce (i);

          for (i=0; i<threads; i++)
                    printf ("__min[%d]=0x%08x __max[%d]=0x%08x\n", i, __min[i], i, __max[i]);

          free(__min); free(__max);
};

函数check_nonce() 有3个作用：向字符串添加数字、使用SHA512算法计算新字符串的hash、检查hash中是否有3个为0的字节。

这段代码中较为重要的部分是：

          #pragma omp parallel for
          for (i=0; i<INT32_MAX; i++)
                   check_nonce (i);

这个程序确实不复杂。如果没有#pragma，程序会从0依次穷举到INT32的最大值（0x7fffffff，即2147483647），依次用check_nonce() 函数验证。加上#pragma之后，编译器会添加特定的代码把整个区间划分为若干子区间，充分利用CPU的多核进行并行运算。[1]

我们可以通过下述指令，使用MSVC 2012进行编译[2]：

cl openmp_example.c sha512.obj /openmp /O1 /Zi /Faopenmp_example.asm

GCC对应的编译指令为：

gcc -fopenmp 2.c sha512.c -S -masm=intel

92.1　MSVC

MSVC 2012生成的主循环的指令如下所示。

指令清单92.1　MSVC 2012

          push    OFFSET _main$omp$1
          push    0
          push    1
          call    __vcomp_fork
          add     esp, 16                              ; 00000010H

所有以vcomp开头的函数都是与OpenMP有关的函数，通过vcomp*.dll进行加载。它将发起一组线程进行并行计算。

具体来说，_main$omp$1的汇编代码如下所示。

指令清单92.2　MSVC 2012

$T1 = -8                                                 ; size = 4
$T2 = -4                                                 ; size = 4
_main$omp$1 PROC                                         ; COMDAT
        push    ebp
        mov     ebp,  esp
        push    ecx
        push    ecx
        push    esi
        lea     eax, DWORD PTR $T2[ebp]
        push    eax
        lea     eax, DWORD PTR $T1[ebp]
        push    eax
        push    1
        push    1
        push    2147483646                               ; 7ffffffeH
        push    0
        call    __vcomp_for_static_simple_init
        mov     esi, DWORD PTR $T1[ebp]
        add     esp, 24                                  ; 00000018H
        jmp     SHORT $LN6@main$omp$1
$LL2@main$omp$1:
        push    esi
        call    _check_nonce
        pop     ecx
        inc     esi
$LN6@main$omp$1:
        cmp     esi, DWORD PTR $T2[ebp]
        jle     SHORT $LL2@main$omp$1
        call    __vcomp_for_static_end
        pop     esi
        leave
        ret     0
_main$omp$1 ENDP

这个函数会启动n个并发线程，其中n就是CPU核芯（cores）的总数。函数vcomp_for_static_ simple_init()计算当前线程里for() 结构体的区间，而区间的间隔则由当前线程的总数决定。循环计数器的起始值和结束值分别存储于局部变量$T1和$T2。细心的读者可能注意到函数vcomp_for_ static_simple_init() 的一个参数为0x7ffffffeh（即2147483646）——它是整个循环体的迭代次数，最终会被n整除。

接下来程序发起了调用函数check_nonce() 的循环，完成余下的工作。

我在源代码的check_nonce() 函数中有意添加了统计代码，用来统计参数被调用的次数。

整个程序的运行结果如下：

threads=4
...
checked=2800000
checked=3000000
checked=3200000
checked=3300000
found (thread 3): [hello, world!_1611446522]. seconds spent=3
__min[0]=0x00000000 __max[0]=0x1fffffff
__min[1]=0x20000000 __max[1]=0x3fffffff
__min[2]=0x40000000 __max[2]=0x5fffffff
__min[3]=0x60000000 __max[3]=0x7ffffffe

计算结果的前3个字节确实是零：

C:\...\sha512sum test
000000f4a8fac5a4ed38794da4c1e39f54279ad5d9bb3c5465cdf57adaf60403
df6e3fe6019f5764fc9975e505a7395fed780fee50eb38dd4c0279cb114672e2 *test

在笔者的4核Intel Xeon E3-1220 3.10Ghz CPU上运行这个程序，总耗时大约2～3秒。我们可以在任务管理器中看到5个线程：1个主线程和4个子线程。虽然理论上它还有精简的空间，但是我没有对程序源代码进行深度优化。深度优化应当可以大幅度提升它的运行效率。另外，因为我的CPU有4个内核，所以它发起了4个子线程——这完全符合OpenMP规范。

通过程序输出的统计数据，我们可以清楚地观察到整个穷举空间被划分为大致相等的四个部分。严格地说，考虑到最后一个比特位，这四个区间确实并非完全相等。

OpenMP还提供了保障模块crtomic（原子性）的Prugms指令。顾名思义，被标记为原子性的代码不会被拆分为多个子线程运行。我们来看看这段代码：

          #pragma omp atomic
          checked++;

          #pragma omp critical
          if ((checked % 100000)==0)
                    printf ("checked=%d\n", checked);

经MSVC 2012编译，上述代码对应的汇编指令如下所示。

指令清单92.3　MSVC 2012

         push     edi
         push     OFFSET _checked
         call     __vcomp_atomic_add_i4
; Line 55
         push     OFFSET _$vcomp$critsect$
         call     __vcomp_enter_critsect
         add      esp, 12                                    ; 0000000cH
; Line 56
         mov      ecx, DWORD PTR _checked
         mov      eax, ecx
         cdq
         mov      esi, 100000                                ; 000186a0H
         idiv     esi
         test     edx, edx
         jne      SHORT $LN1@check_nonc
; Line 57
         push     ecx
         push     OFFSET ??_C@_0M@NPNHLIOO@checked?$DN?$CFd?6?$AA@
         call     _printf
         pop      ecx
         pop      ecx
$LN1@check_nonc:
         push     DWORD PTR _$vcomp$critsect$
         call     __vcomp_leave_critsect
         pop      ecx

封装在vcomp*.dll里的函数vcomp_atomic_add_i4() 只是一个使用了LOCK XADD指令的小函数。[3]

函数vcomp_enter_critsect() 调用的是Win32 API函数EnterCriticalSection()[4]。

92.2　GCC

经GCC 4.8.1生成的程序，其统计结果和上面的程序一样。所以GCC分割区间的方法与MSVC相同。

指令清单92.4　GCC 4.8.1

          mov       edi, OFFSET FLAT:main._omp_fn.0
          call      GOMP_parallel_start
          mov       edi, 0
          call      main._omp_fn.0
          call      GOMP_parallel_end

由GCC编译生成的程序，会发起3个新的线程，原有线程扮演第4进程的角色。所以，总体上GCC的进程数是4，MSVC的进程数是5。

其中，函数main._omp_fn.0的代码如下所示。

指令清单92.5　GCC 4.8.1

main._omp_fn.0:
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 40
        mov     QWORD PTR [rbp-40], rdi
        call    omp_get_num_threads
        mov     ebx, eax
        call    omp_get_thread_num
        mov     esi, eax
        mov     eax, 2147483647 ; 0x7FFFFFFF
        cdq
        idiv    ebx
        mov     ecx, eax
        mov     eax, 2147483647 ; 0x7FFFFFFF
        cdq
        idiv    ebx
        mov     eax, edx
        cmp     esi, eax
        jl      .L15
.L18:
        imul    esi, ecx
        mov     edx, esi
        add     eax, edx
        lea     ebx, [rax+rcx]
        cmp     eax, ebx
        jge     .L14
        mov     DWORD PTR [rbp-20], eax
.L17:
        mov     eax, DWORD PTR [rbp-20]
        mov     edi, eax
        call    check_nonce
        add     DWORD PTR [rbp-20], 1
        cmp     DWORD PTR [rbp-20], ebx
        jl      .L17
        jmp     .L14
.L15:
        mov     eax, 0
        add     ecx, 1
        jmp     .L18
.L14:
        add     rsp, 40
        pop     rbx
        pop     rbp
        ret

上述指令清晰地显示出：程序通过调用函数omp_get_num_threads() 和另一个函数omp_get_ thread_num() 获取当前线程的总数以及当前线程的编号，然后分割循环体。之后，它再运行check_nonce()。

GCC在代码中直接使用LOCK ADD指令，而MSVC则是调用另一个DLL文件中的独立函数。

指令清单92.6　GCC 4.8.1

        lock add         DWORD PTR checked[rip], 1
        call    GOMP_critical_start
        mov     ecx, DWORD PTR checked[rip]
        mov     edx, 351843721
        mov     eax, ecx
        imul    edx
        sar     edx, 13
        mov     eax, ecx
        sar     eax, 31
        sub     edx, eax
        mov     eax, edx
        imul    eax, eax, 100000
        sub     ecx, eax
        mov     eax, ecx
        test    eax, eax
        jne     .L7
        mov     eax, DWORD PTR checked[rip]
        mov     esi, eax
        mov     edi, OFFSET FLAT:.LC2 ; "checked=%d\n"
        mov     eax, 0
        call    printf
.L7:
        call    GOMP_critical_end

以GOMP开头的函数来自于GNU OpenMP Library。您可在GitHub下载到它的源文件（https:// github.com/gcc-mirror/gcc/tree/master/libgomp

）。不过，微软的vcomp*.dll文件没有源代码可查。

[1]　本例仅为示范性说明。在实际情况下，使用OpenMP技术往往更为困难、复杂。

[2]　sha512.（c|h）和u64.h的源文件可照搬OpenSSL的库文件：http://www.openssl.org/ source/。

[3]　有关LOCK前缀的详细说明，请参见本书附录A.6.1。

[4]　请参见本书68.4节。

第93章 安腾指令

就市场来讲，安腾（Itanium）处理器几乎是失败产品。但是它的Intel Itanium（IA64）架构非常值得研究。乱序执行（OOE）CPU理念，侧重于让CPU重新划分指令的片段和顺序，再把重组后的指令组分派到并联计算单位进行并行计算。而英特尔（Intel）安腾架构推出的并行计算技术（Explicitly Parallel Instruction Code，EPIC），则主张让编译器在编译的早期阶段实现指令分组。

厂商推出了配合这种并行计算技术的编译器。不过，这些编译器因异常复杂而颇受争议。

本章从Linux内核（3.2.0.4）摘录了部分IA64指令。这段程序用于实现某加密机制。其源代码如下所示。

指令清单93.1　Linux kernel 3.2.0.4

#define TEA_ROUNDS                32
#define TEA_DELTA                 0x9e3779b9

static void tea_encrypt(struct crypto_tfm *tfm, u8 *dst, const u8 *src)
{
          u32 y, z, n, sum = 0;
          u32 k0, k1, k2, k3;
          structtea_ctx *ctx = crypto_tfm_ctx(tfm);
          const __le32 *in = (const __le32 *)src;
          __le32 *out = (__le32 *)dst;

          y = le32_to_cpu(in[0]);
          z = le32_to_cpu(in[1]);

          k0 = ctx->KEY[0];
          k1 = ctx->KEY[1];
          k2 = ctx->KEY[2];
          k3 = ctx->KEY[3];

          n = TEA_ROUNDS;

          while (n-- > 0) {
                   sum += TEA_DELTA;
                   y += ((z << 4) + k0) ^ (z + sum) ^ ((z >> 5) + k1);
                   z += ((y << 4) + k2) ^ (y + sum) ^ ((y >> 5) + k3);
          }

          out[0] = cpu_to_le32(y);
          out[1] = cpu_to_le32(z);
}

其编译结果如下所示。

指令清单93.2　Linux Kernel 3.2.0.4 for Itanium 2（McKinley）

0090|                                  tea_encrypt:
0090|08 80 80 41 00 21                     adds r16 = 96, r32     // ptr to ctx->KEY[2]
0096|80 C0 82 00 42 00                     adds r8 = 88, r32      // ptr to ctx->KEY[0]
009C|00 00 04 00                           nop.i 0
00A0|09 18 70 41 00 21                     adds r3 = 92, r32      // ptr to ctx->KEY[1]
00A6|F0 20 88 20 28 00                     ld4 r15 = [r34], 4     // load z
00AC|44 06 01 84                           adds r32 = 100, r32;;  // ptr to ctx->KEY[3]
00B0|08 98 00 20 10 10                     ld4 r19 = [r16]        // r19=k2
00B6|00 01 00 00 42 40                     mov r16=r0             // r0一直是0
00BC|00 08 CA 00                           mov.i r2 = ar.lc       // 保存lc
00C0|05 70 00 44 10 10 9E FF FF FF 7F 20   ld4 r14 = [r34]        // load y
00CC|92 F3 CE 6B                           movl r17 = 0xFFFFFFFF9E3779B9;; // TEA_DELTA
00D0|08 00 00 00 01 00                     nop.m 0
00D6|50 01 20 20 20 00                     ld4 r21 = [r8]         // r21=k0
00DC|F0 09 2A 00                           mov.i ar.lc = 31       //TEA_ROUNDS=32
00E0|0A A0 00 06 10 10                     ld4 r20 = [r3];;       // r20=k1
00E6|20 01 80 20 20 00                     ld4 r18 = [r32]        // r18=k3
00EC|00 00 04 00                           nop.i 0
00F0|
00F0|                                  loc_F0:
00F0|09 80 40 22 00 20                     add r16 = r16, r17  //r16=sum, r17=TEA_DELTA
00F6|D0 71 54 26 40 80                     shladd r29 = r14, 4, r21 // r14=y, r21=k0
00FC|A3 70 68 52                           extr.u r28 = r14, 5, 27;;
0100|03 F0 40 1C 00 20                     add r30 = r16, r14
0106|B0 E1 50 00 40 40                     add r27 = r28, r20;;     // r20=k1
010C|D3 F1 3C 80                           xor r26 = r29, r30;;
0110|0B C8 6C 34 0F 20                     xor r25 = r27, r26;;
0116|F0 78 64 00 40 00                     add r15 = r15, r25       // r15=z
011C|00 00 04 00                           nop.i 0;;
0120|00 00 00 00 01 00                     nop.m 0
0126|80 51 3C 34 29 60                     extr.u r24 = r15, 5, 27
012C|F1 98 4C 80                           shladd r11 = r15, 4, r19 // r19=k2
0130|0B B8 3C 20 00 20                     add r23 = r15, r16;;
0136|A0 C0 48 00 40 00                     add r10 = r24, r18       // r18=k3
013C|00 00 04 00                           nop.i 0;;
0140|0B 48 28 16 0F 20                     xor r9 = r10, r11;;
0146|60 B9 24 1E 40 00                     xor r22 = r23, r9
014C|00 00 04 00                           nop.i 0;;
0150|11 00 00 00 01 00                     nop.m 0
0156|E0 70 58 00 40 A0                     add r14 = r14, r22
015C|A0 FF FF 48                           br.cloop.sptk.few loc_F0;;
0160|09 20 3C 42 90 15                     st4 [r33] = r15, 4    // store z
0166|00 00 00 02 00 00                     nop.m 0
016C|20 08 AA 00                           mov.i ar.lc = r2;;    // restore lc legister
0170|11 00 38 42 90 11                     st4 [r33] = r14       // store y
0176|00 00 00 02 00 80                     nop.i 0
017C|08 00 84 00                           br.ret.sptk.many b0;;

上述IA64指令很有特点。

首先，每3条指令构成一个指令字（instruction bundles）。每个指令字的长度都是16字节即128位，由1个5位的模版字段和3个41位微操作指令构成。IDA把这些指令组分为（6 + 6 + 4）字节的结构体，以便于调试人员进行区分。

除了含有停止位（stop bit）的指令之外，这些由3条微操作指令构成的指令字，通常都由CPU并行处理。

据称，Intel和HP的开发人员针对常见指令进行了模式划分，从而推出了指令字类型（即指令模版）的概念——声明指令字运算资源的模版字段。CPU依此把指令字区分为12种基本类型（basic bundle types），基本类型又分为带停止位的版本和不带停止位的版本。举例来说，第0类指令字叫作MII类指令字，依次由内存读写微操作指令（M）和两条整数运算的微操作指令（II）构成；最后一类指令，即0x1d类指令字，又叫作MFB类指令字，依次由内存读写微操作指令（M）、浮点数运算微操作指令（F）、分支（转移）微操作指令（B）构成。

如果编译器在指令字的指令位（instruction slot）上编排不了相应的微操作指令，那么它可能在这些指令位上安插空操作指令nop。您可能注意到了，本文中的nop指令分为“nop，i”和“nop，m”。i代表该nop指令占用整数（integer）处理单元，属于整数运算型微操作指令；m代表它占用内存处理单元，属于内存操作型微操作指令。在人工编写汇编语言时，编辑程序会自动插入相应的nop指令。

IA64汇编指令的特性不止这些。该平台的指令字还可进行分组，构成指令组（instruction group）。指令组可由任意个连续运算的指令字和一个含有停止位的指令字构成，是一个可并行执行的指令集合。在实际应用中，安腾2处理器可以并行执行2路指令字，即同时处理6个微操作指令。

这就要求指令字中的各微操作指令和每个指令组的各指令字之间互不干扰，即不存在数据竞争。如果存在数据竞争、形成脏数据，那么运算结果不可控（undefined）。

在IDA中，微操作指令之后的两个分号（；；）表示该指令有停止位。可见，［90-ac］及［b0-bc］分别属于可并行执行的两个指令组，它们之间不存在互扰。下一组则是［b0-cc］。

另外，在10c处的指令，以及下一条位于110处的指令都有停止位。这就意味着CPU会在与其他指令隔绝的情况下运行这两个指令，这种运行模式就和常规的CISC/复杂指令集的执行方式完全相同了。这是由于后续指令，即110处的指令需要前一条指令的运行结果（R26）寄存器，所以不能并行处理这两条指令。很明显，此时编译器不能找到更好的并行处理手段，无法更有效地利用CPU，所以在此添加了2个停止位和多个NOP指令。虽说编译器在智能方面很不成熟，但是人工的IA64汇编编程也丝毫不轻松：程序员要手动完成指令字分组的工作。

要图省事的话，程序员可以给每条指令添加停止位，不过这将大幅度地浪费安腾处理器的运算性能。

Linux内核的源代码中，就有一些经典的、手写IA64汇编代码。有兴趣的读者可参考：http://lxr. free-electrons.com/source/arch/ia64/lib/

。

有关IA64汇编语言人工编程的具体方法，可参见Mike Burrell撰写的专业论著《Writing Effcient Itanium 2 Assembly Code》（http://phrack.org/issues/57/5.html

。

第94章 8086的寻址方式

在MS-DOS及Win16（参见本书78.3节和53.5节）平台的16位应用程序中，我们可以看到程序指针由两个16位值构成。这是什么情况？不得不说这是MS-DOS和8086的另一大怪异的特色。

虽然8086/8088属于16位CPU，但是它却有20位的RAM地址空间（内存总线有20个引脚）；即，它可直接寻址的存储空间只有1MB。这1MB的外部内存空间又被划分为RAM（最大640kB）、ROM、显卡内存、EMS卡，等等。

16位的8086/8088 CPU实际上由8080 CPU发展而来。8080 CPU的地址空间只有16位，所以可直接控制的内存只有64KB。大概是8086的设计者认为64KB空间不够用，而且8086还要兼容8080平台的程序，所以就把20位/1MB的内存划分为若干个段使用。这就是早期玩具级的虚拟内存技术的思路。而8086的寄存器又只是16位寄存器，为了进行更大范围（20位）的寻址，它就得借助新推出的段寄存器。从此CPU就有了CS、DS、ES和SS寄存器。20位的内存指针由短寄存器和地址寄存器对（DS:BX）混合计算而来：

举例来说，过去IBM PC兼容的主机，其显卡（EGA、VGA）的显存都只有64KB。要读写显存，就要在某个段寄存器里（例如DS）写入0xA000。如此一来，程序就可使用DS:0～DS:0xFFFF访问整个显存。虽然地址总线是20位的，超过了16位寄存器的表达范围，但是CPU可借助段寄存器毫无障碍地访问0xA0000～0AFFFF。

程序还可能直接访问固定的内存地址，例如0x1234，但是操作系统加载应用程序到起始地址却不是固定的。段寄存器的出现，解决了这种问题——它可由段寄存器进行相对寻址，应用程序不必关心自己到底被加载到了什么RAM地址上。

实际上，MS-DOS系统下的指针由段地址和段内地址构成，可由两个16位的数值表示。20位地址总线足以满足这种寻址方式的需要。不过，程序员就需要重新计算内存地址了：他们要不停地考虑空间和效率的平衡，仔细规划数据栈的分配情况。

另外，8086的寻址方式决定了每个内存块不能大于64KB。

80286平台仍然继承了段寄存器（segment registers），只是用途不同而已。

在支持更大RAM的80386 CPU问世时，市面上流行的仍然是MS-DOS。更有一大批叫作DOS extenders的DOS粉丝在Windows系统问世以后继续坚守DOS阵地。他们甚至开发出各种像模像样的OS系统。这种系统不仅实现了CPU保护模式的切换功能，而且大幅度地改进了内存API，可继续支持MS-DOS的应用程序。著名的有：DOS/4GW（游戏巨作DOOM就是面向它编译的）、Phar Lap和PMODE。

在Win32之前，16位的Windows 3.x仍然沿用了这种寻址方式。

第95章 基本块重排

95.1　PGO的优化方式

PGO是Profile-guided optimization的缩写，中文有“配置文件引导的优化”等译法。经PGO方式优化以后，程序中的某些基本块（basic block）（所谓基本块，指的是程序里顺序执行的语句序列。基本块由第一个语句构成入口，由最后一个语句构成出口。在执行程序时，不可从入口以外进入该基本块（被跳入），也不可从出口以外的地址跳出该基本块。）可能会被调整到可执行文件的任意位置。

很明显，函数中的程序代码存在执行频率的差异。例如，循环语句一类代码的执行频率必然很高，而错误报告、异常处理之类代码的执行频率较低。

在使用PGO时，编译器首先会生成一种可记录运行细节的特殊程序。而后，研发人员通过试运行的手段收集该程序的各项统计信息。最后，编译器根据这些统计信息对可执行文件进行调整和优化，把执行几率较小的基本块挪到其他地方。

在经PGO优化后的程序里，频繁执行的函数代码会被调整得更为紧凑。PGO优化了条件跳转的性能，提高了CPU分支预测的准确率。这些特性均有助于提升程序性能。

Oracle是由Intel C++编译器生成的程序。本文收录了Oracle中orageneric11.dll（Win32）的部分代码。

指令清单95.1　orageneric11.dll（Win32）

                  public _skgfsync
_skgfsync         proc near

; address 0x6030D86A

                  db       66h
                  nop
                  push     ebp
                  mov      ebp,  esp
                  mov      edx, [ebp+0Ch]
                  test     edx, edx
                  jz       short loc_6030D884
                  mov      eax, [edx+30h]
                  test     eax, 400h
                  jnz      __VInfreq__skgfsync  ; write to log
continue:
                  mov      eax, [ebp+8]
                  mov      edx, [ebp+10h]
                  mov      dword ptr [eax], 0
                  lea      eax, [edx+0Fh]
                  and      eax, 0FFFFFFFCh
                  mov      ecx, [eax]
                  cmp      ecx, 45726963h
                  jnz      error               ; exit with error
                  mov      esp, ebp
                  pop      ebp
                  retn
_skgfsync         endp

...

; address 0x60B953F0
__VInfreq__skgfsync:
                  mov      eax, [edx]
                  test     eax, eax
                  jz       continue
                  mov      ecx, [ebp+10h]
                  push     ecx
                  mov      ecx, [ebp+8]
                  push     edx
                  push     ecx
                  push     offset ... ; "skgfsync(se=0x%x, ctx=0x%x, iov=0x%x)\n"
                  push     dword ptr [edx+4]
                  call     dword ptr [eax] ; write to log
                  add      esp, 14h
                  jmp      continue
error:
                  mov      edx, [ebp+8]
                  mov      dword ptr [edx], 69AAh ; 27050 "function called with invalid FIB/IOV structure"
                  mov      eax, [eax]
                  mov      [edx+4], eax
                  mov      dword ptr [edx+8], 0FA4h ; 4004
                  mov      esp, ebp
                  pop      ebp
                  retn
; END OF FUNCTION CHUNK FOR _skgfsync

上述两个基本块的地址相距9MB左右。

在这个文件中，所有的不常用函数都位于DLL文件的尾部。这部分不常用函数都被Intel C++编译器打上了VInfreq前缀。例如，我们看到函数尾部的部分代码用于记录log文件（大概用于错误、警告和异常处理）。因为Oracle开发人员在试运行期间收集统计信息时，它的执行概率较低（甚至没被执行过），所以它们被标注上了__VInfreq前缀。最终，这个日志基本块把控制流返回给位于“热门地区”的函数代码。

程序里另外一处“不常用”的区间是返回错误代码27050的基本块。

在Linux ELF环境下，Intel C++编译器会在ELF文件里通过.hot/.unlikely标记“热门”/“冷门”基本块。

以逆向工程的角度来看，这些信息可用来辨别函数的核心部分和异常处理部分。

第十一部分
推荐阅读

第96章 参考书籍

96.1　Windows

Mark E. Russinovich､ David A. Solomon与Alex Ionescu合著的《Windows Internals: Including Windows Server 2008 and Windows Vista, Fifth Edition》2009。

96.2　C/C++

《ISO/IEC 14882:2011 (C++ 11 standard)》。

此外，读者可参见http://go.yurichev.com/17275

（2013）。

96.3　x86/x86-64

Intel出版的《Intel 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes》，其中的1，2A，2B，2C，3A，3B和3C章。本书作者将其收录为http://go.yurichev. com/17283

（2013）。

AMD出版的《AMD64 Architecture Programmer’s Manual》。本书作者将其收录为http://go. yurichev.com/17284

（2013）。

96.4　ARM

请参见本书作者收集的ARM手册：http://go.yurichev.com/17024

。

96.5　加密学

Bruce Schneier撰写的《Applied Cryptography: Protocols, Algorithms, and Source Code in C》（1994年）。

第97章 博客

97.1　Windows平台

微软：Raymond Chen (http://blogs.msdn.com/b/oldnewthing/)。

nynaeve.net (http://www.nynaeve.net/)。

第98章 其他内容

reddit.com有两个非常出色的逆向工程相关板块，请参见：

Reverse Engineering（http://www.reddit.com/r/ReverseEngineering/）。

REMath逆向工程与数学的综合板块（http://www.reddit.com/r/remath）。

Stack Exchange网站同样有一个著名的逆向工程板块：

http://reverseengineering.stackexchange.com/。

FreeNode（IRC）的#re频道是专门讨论逆向工程的主题聊天室。

第十二部分
练习题

除非文中有单独的提问，否则本卷题目的默认问题都是：

请用一句话描述这个程序的功能。

请把这个函数还原为C/C++语言的源程序。

在解答题目时，您可以通过Google等搜索引擎查找线索。但是，不借助搜索引擎的乐趣会更多一些。

另外，您还可以在本书的附录里查找相关提示。

第99章 初等难度练习题

这种难度的题目通常可以直接心算。

99.1　练习题1.4

下列程序使用了密码保护机制，请找到程序指定的密码。

喜欢举一反三的读者，还可以修改可执行程序来改变程序的密码。在修改密码的时候，建议您同时调整密码的长度，并摸索最短密码到底可以有多短。

此外，单独一个字符串就可以令程序崩溃。请创建这种字符串。

Win32（go.yurichev.com/17166）。

Linux x86（go.yurichev.com/17167）。

Mac OS X（go.yurichev.com/17168）。

MIPS（go.yurichev.com/17169）。

第100章 中等难度练习题

要解答这个难度的题目，您可能会用到文本编辑器或者纸笔。

100.1　练习题2.1

100.1.1　Optimizing MSVC 2010 x86

__real@3fe0000000000000 DQ 03fe0000000000000r
__real@3f50624dd2f1a9fc DQ 03f50624dd2f1a9fcr

_g$   = 8
tv132 = 16
_x$ = 16
f1 PROC
         fld      QWORD PTR _x$[esp-4]
         fld      QWORD PTR __real@3f50624dd2f1a9fc
         fld      QWORD PTR __real@3fe0000000000000
         fld      QWORD PTR _g$[esp-4]
$LN2@f1:
         fld      ST(0)
         fmul     ST(0), ST(1)
         fsub     ST(0), ST(4)
         call     __ftol2_sse
         cdq
         xor      eax, edx
         sub      eax, edx
         mov      DWORD PTR tv132[esp-4], eax
         fild     DWORD PTR tv132[esp-4]
         fcomp    ST(3)
         fnstsw   ax
         test     ah, 5
         jnp      SHORT $LN19@f1
         fld      ST(3)
         fdiv     ST(0), ST(1)
         faddp    ST(1), ST(0)
         fmul     ST(0), ST(1)
         jmp      SHORT $LN2@f1
$LN19@f1:
         fstp     ST(3)
         fstp     ST(1)
         fstp     ST(0)
         ret      0
f1 ENDP

__real@3ff0000000000000 DQ 03ff0000000000000r

_x$ = 8
f2 PROC
         fld      QWORD PTR _x$[esp-4]
         sub      esp, 16
         fstp     QWORD PTR [esp+8]
         fld1
         fstp     QWORD PTR [esp]
         call     f1
         add      esp, 16
         ret      0
f2 ENDP

100.1.2　Optimizing MSVC 2012 x64

__real@3fe0000000000000 DQ 03fe0000000000000r
__real@3f50624dd2f1a9fc DQ 03f50624dd2f1a9fcr
__real@3ff0000000000000 DQ 03ff0000000000000r

x$ = 8
f        PROC
         movsdx   xmm2, QWORD PTR __real@3ff0000000000000
         movsdx   xmm5, QWORD PTR __real@3f50624dd2f1a9fc
         movsdx   xmm4, QWORD PTR __real@3fe0000000000000
         movapd   xmm3, xmm0
         npad     4
$LL4@f:
         movapd   xmm1, xmm2
         mulsd    xmm1, xmm2
         subsd    xmm1, xmm3
         cvttsd2si eax, xmm1
         cdq
         xor      eax, edx
         sub      eax, edx
         movd     xmm0, eax
         cvtdq2pd xmm0, xmm0
         comisd   xmm5, xmm0
         ja       SHORT $LN18@f
         movapd   xmm0, xmm3
         divsd    xmm0, xmm2
         addsd    xmm0, xmm2
         movapd   xmm2, xmm0
         mulsd    xmm2, xmm4
         jmp      SHORT $LL4@f
$LN18@f:
         movapd   xmm0, xmm2
         ret       0
f        ENDP

100.2　练习题2.4

下面这道题目摘自MSVC 2010，是标准的库函数。

100.2.1　Optimizing MSVC 2010

PUBLIC    _f
_TEXT    SEGMENT
_arg1$ = 8              ;size=4
_arg2$ = 12             ;size=4
_f    PROC
     push    esi
     mov     esi, DWORD PTR _arg1$[esp]
     push    edi
     mov     edi, DWORD PTR _arg2$[esp+4]
     cmp     BYTE PTR [edi], 0
     mov     eax, esi
     je      SHORT $LN7@f
     mov     dl, BYTE PTR [esi]
     push    ebx
     test    dl, dl
     je      SHORT $LN4@f
     sub     esi, edi
     npad    6 ; align next label
$LL5@f:
     mov     ecx, edi
     test    dl, dl
     je      SHORT $LN2@f
$LL3@f:
     mov     dl, BYTE PTR [ecx]
     test    dl, dl
     je      SHORT $LN14@f
     movsx   ebx, BYTE PTR [esi+ecx]
     movsx   edx, dl
     sub     ebx, edx
     jne     SHORT $LN2@f
     inc     ecx
     cmp     BYTE PTR [esi+ecx], bl
     jne     SHORT $LL3@f
$LN2@f:
     cmp    BYTE PTR [ecx], 0
     je     SHORT $LN14@f
     mov    dl, BYTE PTR [eax+1]
     inc    eax
     inc    esi
     test   dl, dl
     jne    SHORT $LL5@f
     xor    eax, eax
     pop    ebx
     pop    edi
     pop    esi
     ret    0
_f     ENDP
_TEXT     ENDS
END

100.2.2　GCC 4.4.1

                  public f
f                 proc near

var_C             = dword ptr -0Ch
var_8             = dword ptr -8
var_4             = dword ptr -4
arg_0             = dword ptr  8
arg_4             = dword ptr  0Ch

                  push    ebp
                  mov     ebp, esp
                  sub     esp, 10h
                  mov     eax, [ebp+arg_0]
                  mov     [ebp+var_4], eax
                  mov     eax, [ebp+arg_4]
                  movzx   eax, byte ptr [eax]
                  test    al, al
                  jnz     short loc_8048443
                  mov     eax, [ebp+arg_0]
                  jmp     short locret_8048453

loc_80483F4:
                  mov     eax, [ebp+var_4]
                  mov     [ebp+var_8], eax
                  mov     eax, [ebp+arg_4]
                  mov     [ebp+var_C], eax
                  jmp     short loc_804840A

loc_8048402:
                  add     [ebp+var_8], 1
                  add     [ebp+var_C], 1

loc_804840A:
                  mov     eax, [ebp+var_8]
                  movzx   eax, byte ptr [eax]
                  test    al, al
                  jz      short loc_804842E
                  mov     eax, [ebp+var_C]
                  movzx   eax, byte ptr [eax]
                  test    al, al
                  jz      short loc_804842E
                  mov     eax, [ebp+var_8]
                  movzx   edx, byte ptr [eax]
                  mov     eax, [ebp+var_C]
                  movzx   eax, byte ptr [eax]
                  cmp     dl, al
                  jz      short loc_8048402

loc_804842E:
                  mov     eax, [ebp+var_C]
                  movzx   eax, byte ptr [eax]
                  test    al, al
                  jnz     short loc_804843D
                  mov     eax, [ebp+var_4]
                  jmp     short locret_8048453

loc_804843D:
                  add     [ebp+var_4], 1
                  jmp     short loc_8048444

loc_8048443:
                  nop

loc_8048444:
                  mov     eax, [ebp+var_4]
                  movzx   eax, byte ptr [eax]
                  test    al, al
                  jnz     short loc_80483F4
                  mov     eax, 0

locret_8048453:
                  leave
                  retn
f                 endp

100.2.3　Optimizing Keil（ARM mode）

          PUSH     {r4,lr}
          LDRB     r2,[r1,#0]
          CMP      r2,#0
          POPEQ    {r4,pc}
          B        |L0.80|
|L0.20|
          LDRB     r12,[r3,#0]
          CMP      r12,#0
          BEQ      |L0.64|
          LDRB     r4,[r2,#0]
          CMP      r4,#0
          POPEQ    {r4,pc}
          CMP      r12,r4
          ADDEQ    r3,r3,#1
          ADDEQ    r2,r2,#1
          BEQ      |L0.20|
          B        |L0.76|
|L0.64|
          LDRB     r2,[r2,#0]
          CMP      r2,#0
          POPEQ    {r4,pc}
|L0.76|
          ADD      r0,r0,#1
|L0.80|
          LDRB     r2,[r0,#0]
          CMP      r2,#0
          MOVNE    r3,r0
          MOVNE    r2,r1
          MOVEQ    r0,#0
          BNE      |L0.20|
          POP      {r4,pc}

100.2.4　Optimizing Keil（Thumb mode）

          PUSH     {r4,r5,lr}
          LDRB     r2,[r1,#0]
          CMP      r2,#0
          BEQ      |L0.54|
          B        |L0.46|
|L0.10|
          MOVS     r3,r0
          MOVS     r2,r1
          B        |L0.20|
|L0.16|
          ADDS     r3,r3,#1
          ADDS     r2,r2,#1
|L0.20|
          LDRB     r4,[r3,#0]
          CMP      r4,#0
          BEQ      |L0.38|
          LDRB     r5,[r2,#0]
          CMP      r5,#0
          BEQ      |L0.54|
          CMP      r4,r5
          BEQ      |L0.16|
          B        |L0.44|
|L0.38|
          LDRB     r2,[r2,#0]
          CMP      r2,#0
          BEQ      |L0.54|
|L0.44|
          ADDS     r0,r0,#1
|L0.46|
          LDRB     r2,[r0,#0]
          CMP      r2,#0
          BNE      |L0.10|
          MOVS     r0,#0
|L0.54|
          POP      {r4,r5,pc}

100.2.5　Optimizing GCC 4.9.1（ARM64）

指令清单100.1　Optimizing GCC 4.9.1（ARM64）

func:
          ldrb    w6, [x1]
          mov     x2, x0
          cbz     w6, .L2
          ldrb    w2, [x0]
          cbz     w2, .L24
.L17:
          ldrb    w2, [x0]
          cbz     w2, .L5
          cmp     w6, w2
          mov     x5, x0
          mov     x2, x1
          beq     .L18
          b        .L5
.L4:
          ldrb    w4, [x2]
          cmp     w3, w4
          cbz     w4, .L8
          bne     .L8
.L18:
          ldrb    w3, [x5,1]!
          add     x2, x2, 1
          cbnz    w3, .L4
.L8:
          ldrb    w2, [x2]
          cbz     w2, .L27
.L5:
          ldrb    w2, [x0,1]!
          cbnz    w2, .L17
.L24:
          mov     x2, 0
.L2:
          mov     x0, x2
          ret
.L27:
          mov     x2, x0
          mov     x0, x2
          ret

100.2.6　Optimizing GCC 4.4.5（MIPS）

指令清单100.2　Optimizing GCC 4.4.5（MIPS）（IDA）

f:
                  lb       $v1, 0($a1)
                  or       $at, $zero
                  bnez     $v1, loc_18
                  move     $v0, $a0
locret_10:                                     # CODE XREF: f+50
                                               # f+78
                  jr       $ra
                  or       $at, $zero
loc_18:                                        # CODE XREF: f+8
                  lb       $a0, 0($a0)
                  or       $at, $zero
                  beqz     $a0, locret_94
                  move     $a2, $v0
loc_28:                                        # CODE XREF: f+8C
                  lb       $a0, 0($a2)
                  or       $at, $zero
                  beqz     $a0, loc_80
                  or       $at, $zero
                  bne      $v1, $a0, loc_80
                  move     $a3, $a1
                  b        loc_60
                  addiu    $a2, 1
loc_48:                                        # CODE XREF: f+68
                  lb       $t1, 0($a3)
                  or       $at, $zero
                  beqz     $t1, locret_10
                  or       $at, $zero
                  bne      $t0, $t1, loc_80
                  addiu    $a2, 1
loc_60:                                        # CODE XREF: f+40
                  lb       $t0, 0($a2)
                  or       $at, $zero
                  bnez     $t0, loc_48
                  addiu    $a3, 1
                  lb       $a0, 0($a3)
                  or       $at, $zero
                  beqz     $a0, locret_10
                  or       $at, $zero
loc_80:                                        # CODE XREF: f+30
                                               # f+38 ...
                  addiu    $v0, 1
                  lb       $a0, 0($v0)
                  or       $at, $zero
                  bnez     $a0, loc_28
                  move     $a2, $v0
locret_94:                                     # CODE XREF: f+20
                  jr       $ra
                  move     $v0, $zero

100.3　练习题2.6

100.3.1　Optimizing MSVC 2010

PUBLIC     _f
; Function compile flags: /Ogtpy
_TEXT     SEGMENT
_k0$ = -12              ;size = 4
_k3$ = -8               ;size = 4
_k2$ = -4               ;size = 4
_v$ = 8                 ;size = 4
_k1$ = 12               ;size = 4
_k$ = 12                ;size = 4
_f     PROC

     sub     esp, 12     ; 0000000cH
     mov     ecx, DWORD PTR _v$[esp+8]
     mov     eax, DWORD PTR [ecx]
     mov     ecx, DWORD PTR [ecx+4]
     push    ebx
     push    esi
     mov     esi, DWORD PTR _k$[esp+16]
     push    edi
     mov     edi, DWORD PTR [esi]
     mov     DWORD PTR _k0$[esp+24], edi
     mov     edi, DWORD PTR [esi+4]
     mov     DWORD PTR _k1$[esp+20], edi
     mov     edi, DWORD PTR [esi+8]
     mov     esi, DWORD PTR [esi+12]
     xor     edx, edx
     mov     DWORD PTR _k2$[esp+24], edi
     mov     DWORD PTR _k3$[esp+24], esi
     lea     edi, DWORD PTR [edx+32]
$LL8@f:
     mov     esi, ecx
     shr     esi, 5
     add     esi, DWORD PTR _k1$[esp+20]
     mov     ebx, ecx
     shl     ebx, 4
     add     ebx, DWORD PTR _k0$[esp+24]
     sub     edx, 1640531527   ; 61c88647H
     xor     esi, ebx
     lea     ebx, DWORD PTR [edx+ecx]
     xor     esi, ebx
     add     eax, esi
     mov     esi, eax
     shr     esi, 5
     add     esi, DWORD PTR _k3$[esp+24]
     mov     ebx, eax
     shl     ebx, 4
     add     ebx, DWORD PTR _k2$[esp+24]
     xor     esi, ebx
     lea     ebx, DWORD PTR [edx+eax]
     xor     esi, ebx
     add     ecx, esi
     dec     edi
     jne     SHORT $LL8@f
     mov     edx, DWORD PTR _v$[esp+20]
     pop     edi
     pop     esi
     mov     DWORD PTR [edx], eax
     mov     DWORD PTR [edx+4], ecx
     pop     ebx
     add     esp, 12              ; 0000000cH
     ret     0
_f     ENDP

100.3.2　Optimizing Keil（ARM mode）

         PUSH     {r4-r10,lr}
         ADD      r5,r1,#8
         LDM      r5,{r5,r7}
         LDR      r2,[r0,#4]
         LDR      r3,[r0,#0]
         LDR      r4,|L0.116|
         LDR      r6,[r1,#4]
         LDR      r8,[r1,#0]
         MOV      r12,#0
         MOV      r1,r12
|L0.40|
         ADD      r12,r12,r4
         ADD      r9,r8,r2,LSL #4
         ADD      r10,r2,r12
         EOR      r9,r9,r10
         ADD      r10,r6,r2,LSR #5
         EOR      r9,r9,r10
         ADD      r3,r3,r9
         ADD      r9,r5,r3,LSL #4
         ADD      r10,r3,r12
         EOR      r9,r9,r10
         ADD      r10,r7,r3,LSR #5
         EOR      r9,r9,r10
         ADD      r1,r1,#1
         CMP      r1,#0x20
         ADD      r2,r2,r9
         STRCS    r2,[r0,#4]
         STRCS    r3,[r0,#0]
         BCC      |L0.40|
         POP      {r4-r10,pc}
|L0.116|
         DCD      0x9e3779b9

100.3.3　Optimizing Keil（Thumb mode）

         PUSH     {r1-r7,lr}
         LDR      r5,|L0.84|
         LDR      r3,[r0,#0]
         LDR      r2,[r0,#4]
         STR      r5,[sp,#8]
         MOVS     r6,r1
         LDM      r6,{r6,r7}
         LDR      r5,[r1,#8]
         STR      r6,[sp,#4]
         LDR      r6,[r1,#0xc]
         MOVS     r4,#0
         MOVS     r1,r4
         MOV      lr,r5
         MOV      r12,r6
         STR      r7,[sp,#0]
|L0.30|
         LDR      r5,[sp,#8]
         LSLS     r6,r2,#4
         ADDS     r4,r4,r5
         LDR      r5,[sp,#4]
         LSRS     r7,r2,#5
         ADDS     r5,r6,r5
         ADDS     r6,r2,r4
         EORS     r5,r5,r6
         LDR      r6,[sp,#0]
         ADDS     r1,r1,#1
         ADDS     r6,r7,r6
         EORS     r5,r5,r6
         ADDS     r3,r5,r3
         LSLS     r5,r3,#4
         ADDS     r6,r3,r4
         ADD      r5,r5,lr
         EORS     r5,r5,r6
         LSRS     r6,r3,#5
         ADD      r6,r6,r12
         EORS     r5,r5,r6
         ADDS     r2,r5,r2
         CMP      r1,#0x20
         BCC      |L0.30|
         STR      r3,[r0,#0]
         STR      r2,[r0,#4]
         POP      {r1-r7,pc}
|L0.84|
         DCD      0x9e3779b9

100.3.4　Optimizing GCC 4.9.1（ARM64）

指令清单100.3　Optimizing GCC 4.9.1（ARM64）

f:
         ldr      w3, [x0]
         mov      w4, 0
         ldr      w2, [x0,4]
         ldr      w10, [x1]
         ldr      w9, [x1,4]
         ldr      w8, [x1,8]
         ldr      w7, [x1,12]
.L2:
         mov      w5, 31161
         add      w6, w10, w2, lsl 4
         movk     w5, 0x9e37, lsl 16
         add      w1, w9, w2, lsr 5
         add      w4, w4, w5
         eor      w1, w6, w1
         add      w5, w2, w4
         mov      w6, 14112
         eor      w1, w1, w5
         movk     w6, 0xc6ef, lsl 16
         add      w3, w3, w1
         cmp      w4, w6
         add      w5, w3, w4
         add      w6, w8, w3, lsl 4
         add      w1, w7, w3, lsr 5
         eor      w1, w6, w1
         eor      w1, w1, w5
         add      w2, w2, w1
         bne      .L2
         str      w3, [x0]
         str      w2, [x0,4]
         ret

100.3.5　Optimizing GCC 4.4.5（MIPS）

指令清单100.4　Optimizing GCC 4.4.5（MIPS）（IDA）

f:
                 lui     $t2, 0x9E37
                 lui     $t1, 0xC6EF
                 lw      $v0, 0($a0)
                 lw      $v1, 4($a0)
                 lw      $t6, 0xC($a1)
                 lw      $t5, 0($a1)
                 lw      $t4, 4($a1)
                 lw      $t3, 8($a1)
                 li      $t2, 0x9E3779B9
                 li      $t1, 0xC6EF3720
                 move    $a1, $zero
loc_2C:                                         # CODE XREF: f+6C
                 addu    $a1, $t2
                 sll     $a2, $v1, 4
                 addu    $t0, $a1, $v1
                 srl     $a3, $v1, 5
                 addu    $a2, $t5
                 addu    $a3, $t4
                 xor     $a2, $t0, $a2
                 xor     $a2, $a3
                 addu    $v0, $a2
                 sll     $a3, $v0, 4
                 srl     $a2, $v0, 5
                 addu    $a3, $t3
                 addu    $a2, $t6
                 xor     $a2, $a3, $a2
                 addu    $a3, $v0, $a1
                 xor     $a2, $a3
                 bne     $a1, $t1, loc_2C
                 addu    $v1, $a2
                 sw      $v1, 4($a0)
                 jr      $ra
                 sw      $v0, 0($a0)

100.4　练习题2.13

下述程序采用了一种加密算法。这种算法的名称是什么？

100.4.1　Optimizing MSVC 2012

_in$ = 8                                            ; size = 2
_f       PROC
         movzx    ecx, WORD PTR _in$[esp-4]
         lea      eax, DWORD PTR [ecx*4]
         xor      eax, ecx
         add      eax, eax
         xor      eax, ecx
         shl      eax, 2
         xor      eax, ecx
         and      eax, 32                           ; 00000020H
         shl      eax, 10                           ; 0000000aH
         shr      ecx, 1
         or       eax, ecx
         ret      0
_f       ENDP

100.4.2　Keil（ARM mode）

f PROC
          EOR     r1,r0,r0,LSR #2
          EOR     r1,r1,r0,LSR #3
          EOR     r1,r1,r0,LSR #5
          AND     r1,r1,#1
          LSR     r0,r0,#1
          ORR     r0,r0,r1,LSL #15
          BX      lr
          ENDP

100.4.3　Keil（Thumb mode）

f PROC
          LSRS    r1,r0,#2
          EORS    r1,r1,r0
          LSRS    r2,r0,#3
          EORS    r1,r1,r2
          LSRS    r2,r0,#5
          EORS    r1,r1,r2
          LSLS    r1,r1,#31
          LSRS    r0,r0,#1
          LSRS    r1,r1,#16
          ORRS    r0,r0,r1
          BX      lr
          ENDP

100.4.4　Optimizing GCC 4.9.1（ARM64）

f:
          uxth    w1, w0
          lsr     w2, w1, 3
          lsr     w0, w1, 1
          eor     w2, w2, w1, lsr 2
          eor     w2, w1, w2
          eor     w1, w2, w1, lsr 5
          and     w1, w1, 1
          orr     w0, w0, w1, lsl 15
          ret

100.4.5　Optimizing GCC 4.4.5（MIPS）

指令清单100.5　Optimizing GCC 4.4.5（MIPS）（IDA）

f:
                 andi    $a0, 0xFFFF
                 srl     $v1, $a0, 2
                 srl     $v0, $a0, 3
                 xor     $v0, $v1, $v0
                 xor     $v0, $a0, $v0
                 srl     $v1, $a0, 5
                 xor     $v0, $v1
                 andi    $v0, 1
                 srl     $a0, 1
                 sll     $v0, 15
                 jr      $ra
                 or      $v0, $a0

100.5　练习题2.14

下面这段程序采用了另一种著名算法。函数把两个输入变量输出为一个返回值。

100.5.1　MSVC 2012

_rt$1 = −4                                        ;size=4
_rt$2 = 8                                         ;size=4
_x$ = 8                                           ;size=4
_y$ = 12                                          ;size=4
?f@@YAIII@Z PROC                                  ; f
        push    ecx
        push    esi
        mov     esi, DWORD PTR _x$[esp+4]
        test    esi, esi
        jne     SHORT $LN7@f
        mov     eax, DWORD PTR _y$[esp+4]
        pop     esi
        pop     ecx
        ret     0
$LN7@f:
        mov     edx, DWORD PTR _y$[esp+4]
        mov     eax, esi
        test    edx, edx
        je      SHORT $LN8@f
        or      eax, edx
        push    edi
        bsf     edi, eax
        bsf     eax, esi
        mov     ecx, eax
        mov     DWORD PTR _rt$1[esp+12], eax
        bsf     eax, edx
        shr     esi, cl
        mov     ecx, eax
        shr     edx, cl
        mov     DWORD PTR _rt$2[esp+8], eax
        cmp     esi, edx
        je      SHORT $LN22@f
$LN23@f:
        jbe     SHORT $LN2@f
        xor     esi, edx
        xor     edx, esi
        xor     esi, edx
$LN2@f:
        cmp     esi, 1
        je      SHORT $LN22@f
        sub     edx, esi
        bsf     eax, edx
        mov     ecx, eax
        shr     edx, cl
        mov     DWORD PTR _rt$2[esp+8], eax
        cmp     esi, edx
        jne     SHORT $LN23@f
$LN22@f:
        mov     ecx, edi
        shl     esi, cl
        pop     edi
        mov     eax, esi
$LN8@f:
        pop     esi
        pop     ecx
        ret     0
?f@@YAIII@Z ENDP

100.5.2　Keil（ARM mode）

||f1|| PROC
        CMP      r0,#0
        RSB      r1,r0,#0
        AND      r0,r0,r1
        CLZ      r0,r0
        RSBNE    r0,r0,#0x1f
        BX       lr
        ENDP

f PROC
        MOVS     r2,r0
        MOV      r3,r1
        MOVEQ    r0,r1
        CMPNE    r3,#0
        PUSH     {lr}
        POPEQ    {pc}
        ORR      r0,r2,r3
        BL       ||f1||
        MOV      r12,r0
        MOV      r0,r2
        BL       ||f1||
        LSR      r2,r2,r0
|L0.196|
        MOV      r0,r3
        BL       ||f1||
        LSR      r0,r3,r0
        CMP      r2,r0
        EORHI    r1,r2,r0
        EORHI    r0,r0,r1
        EORHI    r2,r1,r0
        BEQ      |L0.240|
        CMP      r2,#1
        SUBNE    r3,r0,r2
        BNE      |L0.196|
|L0.240|
        LSL      r0,r2,r12
        POP      {pc}
        ENDP

100.5.3　GCC 4.6.3 for Raspberry Pi（ARM mode）

f:
        subs     r3, r0, #0
        beq      .L162
        cmp      r1, #0
        moveq    r1, r3
        beq      .L162
        orr      r2, r1, r3
        rsb      ip, r2, #0
        and      ip, ip, r2
        cmp      r2, #0
        rsb      r2, r3, #0
        and      r2, r2, r3
        clz      r2, r2
        rsb      r2, r2, #31
        clz      ip, ip
        rsbne    ip, ip, #31
        mov      r3, r3, lsr r2
        b        .L169
.L171:
        eorhi    r1, r1, r2
        eorhi    r3, r1, r2
        cmp      r3, #1
        rsb      r1, r3, r1
        beq      .L167
.L169:
        rsb      r0, r1, #0
        and      r0, r0, r1
        cmp      r1, #0
        clz      r0, r0
        mov      r2, r0
        rsbne    r2, r0, #31
        mov      r1, r1, lsr r2
        cmp      r3, r1
        eor      r2, r1, r3
        bne      .L171
.L167:
        mov      r1, r3, asl ip
.L162:
        mov      r0, r1
        bx       lr

100.5.4　Optimizing GCC 4.9.1（ARM64）

指令清单100.6　Optimizing GCC 4.9.1（ARM64）

f:
        mov      w3, w0
        mov      w0, w1
        cbz      w3, .L8
        mov      w0, w3
        cbz      w1, .L8
        mov      w6, 31
        orr      w5, w3, w1
        neg      w2, w3
        neg      w7, w5
        and      w2, w2, w3
        clz      w2, w2
        sub      w2, w6, w2
        and      w5, w7, w5
        mov      w4, w6
        clz      w5, w5
        lsr      w0, w3, w2
        sub      w5, w6, w5
        b        .L13
.L22:
        bls      .L12
        eor      w1, w1, w2
        eor      w0, w1, w2
.L12:
        cmp      w0, 1
        sub      w1, w1, w0
        beq      .L11
.L13:
        neg      w2, w1
        cmp      w1, wzr
        and      w2, w2, w1
        clz      w2, w2
        sub      w3, w4, w2
        csel     w2, w3, w2, ne
        lsr      w1, w1, w2
        cmp      w0, w1
        eor      w2, w1, w0
        bne      .L22
.L11:
        lsl      w0, w0, w5
.L8:
        ret

100.5.5　Optimizing GCC 4.4.5（MIPS）

指令清单100.7　Optimizing GCC 4.4.5（MIPS）（IDA）

f:

var_20            = -0x20
var_18            = -0x18
var_14            = -0x14
var_10            = -0x10
var_C             = -0xC
var_8             = -8
var_4             =-4

                  lui     $gp, (__gnu_local_gp >> 16)
                  addiu   $sp, -0x30
                  la      $gp, (__gnu_local_gp & 0xFFFF)
                  sw      $ra, 0x30+var_4($sp)
                  sw      $s4, 0x30+var_8($sp)
                  sw      $s3, 0x30+var_C($sp)
                  sw      $s2, 0x30+var_10($sp)
                  sw      $s1, 0x30+var_14($sp)
                  sw      $s0, 0x30+var_18($sp)
                  sw      $gp, 0x30+var_20($sp)
                  move    $s0, $a0
                  beqz    $a0, loc_154
                  move    $s1, $a1
                  bnez    $a1, loc_178
                  or      $s2, $a1, $a0
                  move    $s1, $a0

loc_154:                                      # CODE XREF: f+2C
                  lw      $ra, 0x30+var_4($sp)
                  move    $v0, $s1
                  lw      $s4, 0x30+var_8($sp)
                  lw      $s3, 0x30+var_C($sp)
                  lw      $s2, 0x30+var_10($sp)
                  lw      $s1, 0x30+var_14($sp)
                  lw      $s0, 0x30+var_18($sp)
                  jr      $ra
                  addiu   $sp, 0x30

loc_178:                                      # CODE XREF: f+34
                  lw      $t9, (__clzsi2 & 0xFFFF)($gp)
                  negu    $a0, $s2
                  jalr    $t9
                  and     $a0, $s2
                  lw      $gp, 0x30+var_20($sp)
                  bnez    $s2, loc_20C
                  li      $s4, 0x1F
                  move    $s4, $v0
loc_198:                                      # CODE XREF: f:loc_20C
                  lw      $t9, (__clzsi2 & 0xFFFF)($gp)
                  negu    $a0, $s0
                  jalr    $t9
                  and     $a0, $s0
                  nor     $v0, $zero, $v0
                  lw      $gp, 0x30+var_20($sp)
                  srlv    $s0, $v0
                  li      $s3, 0x1F
                  li      $s2, 1
loc_1BC:                                     # CODE XREF: f+F0
                  lw      $t9, (__clzsi2 & 0xFFFF)($gp)
                  negu    $a0, $s1
                  jalr    $t9
                  and     $a0, $s1
                  lw      $gp, 0x30+var_20($sp)
                  beqz    $s1, loc_1DC
                  or      $at, $zero
                  subu    $v0, $s3, $v0
loc_1DC:                                      # CODE XREF: f+BC
                  srlv    $s1, $v0
                  xor     $v1, $s1, $s0
                  beq     $s0, $s1, loc_214
                  sltu    $v0, $s1, $s0
                  beqz    $v0, loc_1FC
                  or      $at, $zero
                  xor     $s1, $v1
                  xor     $s0, $s1, $v1
loc_1FC:                                    # CODE XREF: f+D8
                  beq     $s0, $s2, loc_214
                  subu    $s1, $s0
                  b       loc_1BC
                  or      $at, $zero
loc_20C:                                    # CODE XREF: f+78
                  b       loc_198
                  subu    $s4, $v0
loc_214:                                    # CODE XREF: f+D0
                                            # f:loc_1FC
                  lw      $ra, 0x30+var_4($sp)
                  sllv    $s1, $s0, $s4
                  move    $v0, $s1
                  lw      $s4, 0x30+var_8($sp)
                  lw      $s3, 0x30+var_C($sp)
                  lw      $s2, 0x30+var_10($sp)
                  lw      $s1, 0x30+var_14($sp)
                  lw      $s0, 0x30+var_18($sp)
                  jr      $ra
                  addiu   $sp, 0x30

100.6　练习题2.15

这个程序实现了一种著名的算法。请问，这个算法的名称是什么？

在x86平台上，程序使用FPU进行运算；而在x64平台上，程序使用的是SIMD指令集。这属于正常现象，详细介绍请参见本书第27章。

100.6.1　Optimizing MSVC 2012 x64

__real@412e848000000000 DQ 0412e848000000000r     ; 1e+006
__real@4010000000000000 DQ 04010000000000000r     ;4
__real@4008000000000000 DQ 04008000000000000r     ;3
__real@3f800000 DD 03f800000r                     ;1

tmp$1 = 8
tmp$2 = 8
f       PROC
        movsdx  xmm3, QWORD PTR __real@4008000000000000
        movss   xmm4, DWORD PTR __real@3f800000
        mov     edx, DWORD PTR ?RNG_state@?1??get_rand@@9@9
        xor     ecx, ecx
        mov     r8d, 200000                               ; 00030d40H
        npad    2 ; align next label
$LL4@f:
        imul    edx, 1664525                              ; 0019660dH
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        imul    edx, 1664525                              ; 0019660dH
        bts     eax, 30
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     DWORD PTR tmp$2[rsp], eax
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        bts     eax, 30
        movss   xmm0, DWORD PTR tmp$2[rsp]
        mov     DWORD PTR tmp$1[rsp], eax
        cvtps2pd xmm0, xmm0
        subsd   xmm0, xmm3
        cvtpd2ps xmm2, xmm0
        movss   xmm0, DWORD PTR tmp$1[rsp]
        cvtps2pd xmm0, xmm0
        mulss   xmm2, xmm2
        subsd   xmm0, xmm3
        cvtpd2ps xmm1, xmm0
        mulss   xmm1, xmm1
        adds    xmm1, xmm2
        comiss  xmm4, xmm1
        jbe     SHORT $LN3@f
        inc     ecx
$LN3@f:
        imul    edx, 1664525                              ; 0019660dH
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        imul    edx, 1664525                              ; 0019660dH
        bts     eax, 30
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     DWORD PTR tmp$2[rsp], eax
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        bts     eax, 30
        movss   xmm0, DWORD PTR tmp$2[rsp]
        mov     DWORD PTR tmp$1[rsp], eax
        cvtps2pd xmm0, xmm0
        subsd   xmm0, xmm3
        cvtpd2ps xmm2, xmm0
        movss   xmm0, DWORD PTR tmp$1[rsp]
        cvtps2pd xmm0, xmm0
        mulss   xmm2, xmm2
        subsd   xmm0, xmm3
        cvtpd2ps xmm1, xmm0
        mulss   xmm1, xmm1
        adds    xmm1, xmm2
        comiss  xmm4, xmm1
        jbe     SHORT $LN15@f
        inc     ecx
$LN15@f:
        imul    edx, 1664525                              ; 0019660dH
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        imul    edx, 1664525                              ; 0019660dH
        bts     eax, 30
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     DWORD PTR tmp$2[rsp], eax
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        bts     eax, 30
        movss   xmm0, DWORD PTR tmp$2[rsp]
        mov     DWORD PTR tmp$1[rsp], eax
        cvtps2pd xmm0, xmm0
        subsd   xmm0, xmm3
        cvtpd2ps xmm2, xmm0
        movss   xmm0, DWORD PTR tmp$1[rsp]
        cvtps2pd xmm0, xmm0
        mulss   xmm2, xmm2
        subsd   xmm0, xmm3
        cvtpd2ps xmm1, xmm0
        mulss   xmm1, xmm1
        adds    xmm1, xmm2
        comiss  xmm4, xmm1
        jbe     SHORT $LN16@f
        inc     ecx
$LN16@f:
        imul    edx, 1664525                              ; 0019660dH
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        imul    edx, 1664525                              ; 0019660dH
        bts     eax, 30
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     DWORD PTR tmp$2[rsp], eax
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        bts     eax, 30
        movss   xmm0, DWORD PTR tmp$2[rsp]
        mov     DWORD PTR tmp$1[rsp], eax
        cvtps2pd xmm0, xmm0
        subsd   xmm0, xmm3
        cvtpd2ps xmm2, xmm0
        movss   xmm0, DWORD PTR tmp$1[rsp]
        cvtps2pd xmm0, xmm0
        mulss   xmm2, xmm2
        subsd   xmm0, xmm3
        cvtpd2ps xmm1, xmm0
        mulss   xmm1, xmm1
        adds    xmm1, xmm2
        comiss  xmm4, xmm1
        jbe     SHORT $LN17@f
        inc     ecx
$LN17@f:
        imul    edx, 1664525                              ; 0019660dH
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        imul    edx, 1664525                              ; 0019660dH
        bts     eax, 30
        add     edx, 1013904223                           ; 3c6ef35fH
        mov     DWORD PTR tmp$2[rsp], eax
        mov     eax, edx
        and     eax, 8388607                              ; 007fffffH
        bts     eax, 30
        movss   xmm0, DWORD PTR tmp$2[rsp]
        mov     DWORD PTR tmp$1[rsp], eax
        cvtps2pd xmm0, xmm0
        subsd   xmm0, xmm3
        cvtpd2ps xmm2, xmm0
        movss   xmm0, DWORD PTR tmp$1[rsp]
        cvtps2pd xmm0, xmm0
        mulss   xmm2, xmm2
        subsd   xmm0, xmm3
        cvtpd2ps xmm1, xmm0
        mulss   xmm1, xmm1
        addss   xmm1, xmm2
        comiss  xmm4, xmm1
        jbe     SHORT $LN18@f
        inc     ecx
$LN18@f:
        dec     r8
        jne     $LL4@f
        movd    xmm0, ecx
        mov     DWORD PTR ?RNG_state@?1??get_rand@@9@9, edx
        cvtdq2ps xmm0, xmm0
        cvtps2pd xmm1, xmm0
        mulsd   xmm1, QWORD PTR __real@4010000000000000
        divsd   xmm1, QWORD PTR __real@412e848000000000
        cvtpd2ps xmm0, xmm1
        ret     0
f       ENDP

100.6.2　Optimizing GCC 4.4.6 x64

f1:
        mov     eax, DWORD PTR v1.2084[rip]
        imul    eax, eax, 1664525
        add     eax, 1013904223
        mov     DWORD PTR v1.2084[rip], eax
        and     eax, 8388607
        or      eax, 1073741824
        mov     DWORD PTR [rsp-4], eax
        movss   xmm0, DWORD PTR [rsp-4]
        subss   xmm0, DWORD PTR .LC0[rip]
        ret
f:
        push    rbp
        xor     ebp, ebp
        push    rbx
        xor     ebx, ebx
        sub     rsp, 16
.L6:
        xor     eax, eax
        call    f1
        xor     eax, eax
        movss   DWORD PTR [rsp], xmm0
        call    f1
        movss   xmm1, DWORD PTR [rsp]
        mulss   xmm0, xmm0
        mulss   xmm1, xmm1
        lea     eax, [rbx+1]
        addss   xmm1, xmm0
        movss   xmm0, DWORD PTR .LC1[rip]
        ucomiss xmm0, xmm1
        cmova   ebx, eax
        add     ebp, 1
        cmp     ebp, 1000000
        jne     .L6
        cvtsi2ss        xmm0, ebx
        unpcklps        xmm0, xmm0
        cvtps2pd        xmm0, xmm0
        mulsd   xmm0, QWORD PTR .LC2[rip]
        divsd   xmm0, QWORD PTR .LC3[rip]
        add     rsp, 16
        pop     rbx
        pop     rbp
        unpcklpd        xmm0, xmm0
        cvtpd2ps        xmm0, xmm0
        ret
v1.2084:
        .long   305419896
.LC0:
        .long   1077936128
.LC1:
        .long   1065353216
.LC2:
        .long   0
        .long   1074790400
.LC3:
        .long   0
        .long   1093567616

100.6.3　Optimizing GCC 4.8.1 x86

f1:
        sub     esp, 4
        imul    eax, DWORD PTR v1.2023, 1664525
        add     eax, 1013904223
        mov     DWORD PTR v1.2023, eax
        and     eax, 8388607
        or      eax, 1073741824
        mov     DWORD PTR [esp], eax
        fld     DWORD PTR [esp]
        fsub    DWORD PTR .LC0
        add     esp, 4
        ret
f:
        push    esi
        mov     esi, 1000000
        push    ebx
        xor     ebx, ebx
        sub     esp, 16
.L7:
        call    f1
        fstp    DWORD PTR [esp]
        call    f1
        lea     eax, [ebx+1]
        fld     DWORD PTR [esp]
        fmul    st, st(0)
        fxch    st(1)
        fmul    st, st(0)
        faddp   st(1), st
        fld1
        fucomip st, st(1)
        fstp    st(0)
        cmova   ebx, eax
        sub     esi, 1
        jne     .L7
        mov     DWORD PTR [esp+4], ebx
        fild    DWORD PTR [esp+4]
        fmul    DWORD PTR .LC3
        fdiv    DWORD PTR .LC4
        fstp    DWORD PTR [esp+8]
        fld     DWORD PTR [esp+8]
        add     esp, 16
        pop     ebx
        pop     esi
        ret
v1.2023:
        .long   305419896
.LC0:
        .long   1077936128
.LC3:
        .long   1082130432
.LC4:
        .long   1232348160

100.6.4　Keil（ARM模式）：面向Cortex-R4F CPU的代码

f1      PROC
        LDR      r1,|L0.184|
        LDR      r0,[r1,#0]  ; v1
        LDR      r2,|L0.188|
        VMOV.F32 s1,#3.00000000
        MUL      r0,r0,r2
        LDR      r2,|L0.192|
        ADD      r0,r0,r2
        STR      r0,[r1,#0]  ; v1
        BFC      r0,#23,#9
        ORR      r0,r0,#0x40000000
        VMOV     s0,r0
        VSUB.F32 s0,s0,s1
        BX       lr
        ENDP

f       PROC
        PUSH     {r4,r5,lr}
        MOV      r4,#0
        LDR      r5,|L0.196|
        MOV      r3,r4
|L0.68|
        BL       f1
        VMOV.F32 s2,s0
        BL       f1
        VMOV.F32 s1,s2
        ADD      r3,r3,#1
        VMUL.F32 s1,s1,s1
        VMLA.F32 s1,s0,s0
        VMOV     r0,s1
        CMP      r0,#0x3f800000
        ADDLT    r4,r4,#1
        CMP      r3,r5
        BLT      |L0.68|
        VMOV     s0,r4
        VMOV.F64 d1,#4.00000000
        VCVT.F32.S32 s0,s0
        VCVT.F64.F32 d0,s0
        VMUL.F64 d0,d0,d1
        VLDR     d1,|L0.200|
        VDIV.F64 d2,d0,d1
        VCVT.F32.F64 s0,d2
        POP      {r4,r5,pc}
        ENDP

|L0.184|
        DCD      ||.data||
|L0.188|
        DCD      0x0019660d
|L0.192|
        DCD      0x3c6ef35f
|L0.196|
        DCD      0x000f4240
|L0.200|
        DCFD     0x412e848000000000 ; 1000000

        DCD      0x00000000
        AREA ||.data||, DATA, ALIGN=2
v1
        DCD      0x12345678

100.6.5　Optimizing GCC 4.9.1（ARM64）

指令清单100.8　Optimizing GCC 4.9.1（ARM64）

f1:
        adrp    x2, .LANCHOR0
        mov     w3, 26125
        mov     w0, 62303
        movk    w3, 0x19, lsl 16
        movk    w0, 0x3c6e, lsl 16
        ldr     w1, [x2,#:lo12:.LANCHOR0]
        fmov    s0, 3.0e+0
        madd    w0, w1, w3, w0
        str     w0, [x2,#:lo12:.LANCHOR0]
        and     w0, w0, 8388607
        orr     w0, w0, 1073741824
        fmov    s1, w0
        fsub    s0, s1, s0
        ret
mail_function:
        adrp    x7, .LANCHOR0
        mov     w3, 16960
        movk    w3, 0xf, lsl 16
        mov     w2, 0
        fmov    s2, 3.0e+0
        ldr     w1, [x7,#:lo12:.LANCHOR0]
        fmov    s3, 1.0e+0

.L5:
        mov     w6, 26125
        mov     w0, 62303
        movk    w6, 0x19, lsl 16
        movk    w0, 0x3c6e, lsl 16
        mov     w5, 26125
        mov     w4, 62303
        madd    w1, w1, w6, w0
        movk    w5, 0x19, lsl 16
        movk    w4, 0x3c6e, lsl 16
        and     w0, w1, 8388607
        add     w6, w2, 1
        orr     w0, w0, 1073741824
        madd    w1, w1, w5, w4
        fmov    s0, w0
        and     w0, w1, 8388607
        orr     w0, w0, 1073741824
        fmov    s1, w0
        fsub    s0, s0, s2
        fsub    s1, s1, s2
        fmul    s1, s1, s1
        fmadd   s0, s0, s0, s1
        fcmp    s0, s3
        csel    w2, w2, w6, pl
        subs    w3, w3, #1
        ben     .L5
        scvtf   s0, w2
        str     w1, [x7, #:lo12:.LANCH0R]]
        fmov    d1, 4.0e+0
        fcvt    d0, s0
        fmul    d0, d0, d1
        ldr     d1, .LC0
        fdiv    d0, d0, d1
        fcvt    s0, d0
        ret
.LC0:
        .word   0
        .word   1093567616
.V1:
        .word   1013904223
.V2:
        .word   1664525
.LANCH0R0: = . + 0
v3.3095
        .word   305419896

100.6.6　Optimizing GCC 4.4.5（MIPS）

指令清单100.9　Optimizing GCC 4.4.5（MIPS）（IDA）

f1:
        mov     eax, DWORD PTR v1.2084[rip]
        imul    eax, eax, 1664525
        add     eax, 1013904223
        mov     DWORD PTR v1.2084[rip], eax
        and     eax, 8388607
        or      eax, 1073741824
        mov     DWORD PTR [rsp-4], eax
        movss   xmm0, DWORD PTR [rsp-4]
        subss   xmm0, DWORD PTR .LC0[rip]
        ret
f:
        push    rbp
        xor     ebp, ebp
        push    rbx
        xor     ebx, ebx
        sub     rsp, 16
.L6:
        xor   eax, eax
        call  f1
        xor   eax, eax
        movss DWORD PTR [rsp], xmm0
        call  f1
        movss xmm1, DWORD PTR [rsp]
        mulss xmm0, xmm0
        mulss xmm1, xmm1
        lea   eax, [rbx+1]
        addss xmm1, xmm0
        movss xmm0, DWORD PTR .LC1[rip]
        ucomiss xmm0, xmm1
        cmova ebx, eax
        add   ebp, 1
        cmp   ebp, 1000000
        jne   .L6
        cvtsi2ss       xmm0, ebx
        unpcklps       xmm0, xmm0
        cvtps2pd       xmm0, xmm0
        mulsd xmm0, QWORD PTR .LC2[rip]
        divsd xmm0, QWORD PTR .LC3[rip]
        add   rsp, 16
        pop   rbx
        pop   rbp
        unpcklpd       xmm0, xmm0
        cvtpd2ps       xmm0, xmm0
        ret
v1.2084:
        .long 305419896
.LC0:
        .long 1077936128
.LC1:
        .long 1065353216
.LC2:
        .long 0
        .long 1074790400
.LC3:
        .long 0
        .long 1093567616

100.7　练习题2.16

这个题目是一个著名的函数。请问它的计算结果是什么？如果输入了4和2，该程序会出现栈溢出问题。为什么会这样，代码里有错误么？

100.7.1　Optimizing MSVC 2012 x64

m$ = 48
n$ = 56
f       PROC
$LN14:
        push    rbx
        sub     rsp, 32
        mov     eax, edx
        mov     ebx, ecx
        test    ecx, ecx
        je      SHORT $LN11@f
$LL5@f:
        test    eax, eax
        jne     SHORT $LN1@f
        mov     eax, 1
        jmp     SHORT $LN12@f
$LN1@f:
        lea     edx, DWORD PTR [rax-1]
        mov     ecx, ebx
        call    f
$LN12@f:
        dec     ebx
        test    ebx, ebx
        jne     SHORT $LL5@f
$LN11@f:
        inc     eax
        add     rsp, 32
        pop     rbx
        ret     0
f       ENDP

100.7.2　Optimizing Keil（ARM mode）

f PROC
        PUSH    {r4,lr}
        MOVS    r4,r0
        ADDEQ   r0,r1,#1
        POPEQ   {r4,pc}
        CMP     r1,#0
        MOVEQ   r1,#1
        SUBEQ   r0,r0,#1
        BEQ     |L0.48|
        SUB     r1,r1,#1
        BL      f
        MOV     r1,r0
        SUB     r0,r4,#1
|L0.48|
        POP     {r4,lr}
        B       f
        ENDP

100.7.3　Optimizing Keil（Thumb mode）

f PROC
        PUSH    {r4,lr}
        MOVS    r4,r0
        BEQ     |L0.26|
        CMP     r1,#0
        BEQ     |L0.30|
        SUBS    r1,r1,#1
        BL      f
        MOVS    r1,r0
|L0.18|
        SUBS    r0,r4,#1
        BL      f
        POP     {r4,pc}
|L0.26|
        ADDS    r0,r1,#1
        POP     {r4,pc}
|L0.30|
        MOVS    r1,#1
        B       |L0.18|
        ENDP

100.7.4　Non-optimizing GCC 4.9.1（ARM64）

指令清单100.10　Non-optimizing GCC 4.9.1（ARM64）

f:
        stp     x29, x30, [sp, -48]!
        add     x29, sp, 0
        str     x19, [sp,16]
        str     w0, [x29,44]
        str     w1, [x29,40]
        ldr     w0, [x29,44]
        cmp     w0, wzr
        bne     .L2
        ldr     w0, [x29,40]
        add     w0, w0, 1
        b       .L3
.L2:
        ldr     w0, [x29,40]
        cmp     w0, wzr
        bne     .L4
        ldr     w0, [x29,44]
        sub     w0, w0, #1
        mov     w1, 1
        bl      ack
        b       .L3
.L4:
        ldr     w0, [x29,44]
        sub     w19, w0, #1
        ldr     w0, [x29,40]
        sub     w1, w0, #1
        ldr     w0, [x29,44]
        bl      ack
        mov     w1, w0
        mov     w0, w19
        bl      ack
.L3:
        ldr     x19, [sp,16]
        ldp     x29, x30, [sp], 48
        ret

100.7.5　Optimizing GCC 4.9.1（ARM64）

开启优化模式之后，GCC生成的代码反而更长了。这是为什么？

指令清单100.11　Optimizing GCC 4.9.1（ARM64）

ack:
        stp     x29, x30, [sp, -160]!
        add     x29, sp, 0
        stp     d8, d9, [sp,96]
        stp     x19, x20, [sp,16]
        stp     d10, d11, [sp,112]
        stp     x21, x22, [sp,32]
        stp     d12, d13, [sp,128]
        stp     x23, x24, [sp,48]
        stp     d14, d15, [sp,144]
        stp     x25, x26, [sp,64]
        stp     x27, x28, [sp,80]
        cbz     w0, .L2
        sub     w0, w0, #1
        fmov    s10, w0
        b       .L4
.L46:
        fmov    w0, s10
        mov     w1, 1
        sub     w0, w0, #1
        fmov    s10, w0
        fmov    w0, s13
        cbz     w0, .L2
.L4:
        fmov    s13, s10
        cbz     w1, .L46
        sub     w1, w1, #1
        fmov    s11, s10
        b       .L7
.L48:
        fmov    w0, s11
        mov     w1, 1
        sub     w0, w0, #1
        fmov    s11, w0
        fmov    w0, s14
        cbz     w0, .L47
.L7:
        fmov    s14, s11
        cbz     w1, .L48
        sub     w1, w1, #1
        fmov    s12, s11
        b       .L10
.L50:
        fmov    w0, s12
        mov     w1, 1
        sub     w0, w0, #1
        fmov    s12, w0
        fmov    w0, s15
        cbz     w0, .L49
.L10:
        fmov    s15, s12
        cbz     w1, .L50
        sub     w1, w1, #1
        fmov    s8, s12
        b       .L13
.L52:
        fmov    w0, s8
        mov     w1, 1
        sub     w0, w0, #1
        fmov    s8, w0
        fmov    w0, s9
        cbz     w0, .L51
.L13:
        fmov    s9, s8
        cbz     w1, .L52
        sub     w1, w1, #1
        fmov    w22, s8
        b       .L16
.L54:
        mov     w1, 1
        sub     w22, w22, #1
        cbz     w28, .L53
.L16:
        mov     w28, w22
        cbz     w1, .L54
        sub     w1, w1, #1
        mov     w21, w22
        b       .L19
.L56:
        mov     w1, 1
        sub     w21, w21, #1
        cbz     w24, .L55
.L19:
        mov     w24, w21
        cbz     w1, .L56
        sub     w1, w1, #1
        mov     w20, w21
        b       .L22
.L58:
        mov     w1, 1
        sub     w20, w20, #1
        cbz     w25, .L57
.L22:
        mov     w25, w20
        cbz     w1, .L58
        sub     w1, w1, #1
        mov     w26, w20
        b       .L25
.L60:
        mov     w1, 1
        sub     w26, w26, #1
        cbz     w27, .L59
.L25:
        mov     w27, w26
        cbz     w1, .L60
        sub     w1, w1, #1
        mov     w19, w26
        b       .L28
.L62:
        mov     w23, w19
        mov     w1, 1
        sub     w19, w19, #1
        cbz     w23, .L61
.L28:
        add     w0, w19, 1
        cbz     w1, .L62
        sub     w1, w1, #1
        mov     w23, w19
        sub     w19, w19, #1
        bl      ack
        mov     w1, w0
        cbnz    w23, .L28
.L61:
        add     w1, w1, 1
        sub     w26, w26, #1
        cbnz    w27, .L25
.L59:
        add     w1, w1, 1
        sub     w20, w20, #1
        cbnz    w25, .L22
.L57:
        add     w1, w1, 1
        sub     w21, w21, #1
        cbnz    w24, .L19
.L55:
        add     w1, w1, 1
        sub     w22, w22, #1
        cbnz    w28, .L16
.L53:
        fmov    w0, s8
        add     w1, w1, 1
        sub     w0, w0, #1
        fmov    s8, w0
        fmov    w0, s9
        cbnz    w0, .L13
.L51:
        fmov    w0, s12
        add     w1, w1, 1
        sub     w0, w0, #1
        fmov    s12, w0
        fmov    w0, s15
        cbnz    w0, .L10
.L49:
        fmov    w0, s11
        add     w1, w1, 1
        sub     w0, w0, #1
        fmov    s11, w0
        fmov    w0, s14
        cbnz    w0, .L7
.L47:
        fmov    w0, s10
        add     w1, w1, 1
        sub     w0, w0, #1
        fmov    s10, w0
        fmov    w0, s13
        cbnz    w0, .L4
.L2:
        add     w0, w1, 1
        ldp     d8, d9, [sp,96]
        ldp     x19, x20, [sp,16]
        ldp     d10, d11, [sp,112]
        ldp     x21, x22, [sp,32]
        ldp     d12, d13, [sp,128]
        ldp     x23, x24, [sp,48]
        ldp     d14, d15, [sp,144]
        ldp     x25, x26, [sp,64]
        ldp     x27, x28, [sp,80]
        ldp     x29, x30, [sp], 160
        ret

100.7.6　Non-optimizing GCC 4.4.5（MIPS）

指令清单100.12　Non-optimizing GCC 4.4.5（MIPS）（IDA）

f:                                          # CODE XREF: f+64
                                            # f+94 ...

var_C          = -0xC
var_8          = -8
var_4          = -4
arg_0          = 0
arg_4          = 4

               addiu   $sp, -0x28
               sw      $ra, 0x28+var_4($sp)
               sw      $fp, 0x28+var_8($sp)
               sw      $s0, 0x28+var_C($sp)
               move    $fp, $sp
               sw      $a0, 0x28+arg_0($fp)
               sw      $a1, 0x28+arg_4($fp)
               lw      $v0, 0x28+arg_0($fp)
               or      $at, $zero
               bnez    $v0, loc_40
               or      $at, $zero
               lw      $v0, 0x28+arg_4($fp)
               or      $at, $zero
               addiu   $v0, 1
               b       loc_AC
               or      $at, $zero

loc_40:                                     # CODE XREF: f+24
               lw      $v0, 0x28+arg_4($fp)
               or      $at, $zero
               bnez    $v0, loc_74
               or      $at, $zero
               lw      $v0, 0x28+arg_0($fp)
               or      $at, $zero
               addiu   $v0, -1
               move    $a0, $v0
               li      $a1, 1
               jal     f
               or      $at, $zero
               b       loc_AC
               or      $at, $zero

loc_74:                                     # CODE XREF: f+48
               lw      $v0, 0x28+arg_0($fp)
               or      $at, $zero
               addiu   $s0, $v0, -1
               lw      $v0, 0x28+arg_4($fp)
               or      $at, $zero
               addiu   $v0, -1
               lw      $a0, 0x28+arg_0($fp)
               move    $a1, $v0
               jal     f
               or      $at, $zero
               move    $a0, $s0
               move    $a1, $v0
               jal     f
               or      $at, $zero

loc_AC:                                    # CODE XREF: f+38
                                           # f+6C
               move    $sp, $fp
               lw      $ra, 0x28+var_4($sp)
               lw      $fp, 0x28+var_8($sp)
               lw      $s0, 0x28+var_C($sp)
               addiu   $sp, 0x28
               jr      $ra
               or      $at, $zero

100.8　练习题2.17

下列程序向stdout输出信息，而且每次输出的结果还不一样。请问它输出的是什么信息？

请下载编译后的可执行文件:

Linux x64（go.yurichev.com/17170）。

Mac OS X（go.yurichev.com/17171）。

Linux MIPS（go.yurichev.com/17172）。

Win32（go.yurichev.com/17173）。

Win64（go.yurichev.com/17174）。

可能有个别版本的Windows无法执行这个程序。如果发生这种情况，请下载MSVC 2012 redist.（http://www.microsoft.com/en-us/download/details.aspx?id=30679

）。

100.9　练习题2.18

下列程序会验证密码。请找到它的密码。

另外，它可以接受的密码不是唯一的。请尽可能地多列举一些密码。

您还可以对它进行修改，改变程序的密码：

Win32（go.yurichev.com/17175）。

Linux x86（go.yurichev.com/17176）。

Mac OS X（go.yurichev.com/17177）。

Linux MIPS（go.yurichev.com/17178）。

100.10　练习题2.19

这组题目和练习题2.18的练习内容相同：

Win32（go.yurichev.com/17179）。

Linux x86（go.yurichev.com/17180）。

Mac OS X（go.yurichev.com/17181）。

Linux MIPS（go.yurichev.com/17182）。

100.11　练习题2.20

下列程序向stdout输出信息。请问它输出的是什么信息？

Linux x64（go.yurichev.com/17183）。

Mac OS X（go.yurichev.com/17184）。

Linux ARM Raspberry Pi（go.yurichev.com/17185）。

Linux MIPS（go.yurichev.com/17186）。

Win64（go.yurichev.com/17187）。

第101章 高难度练习题

这种难度题目的解答时间会很长。解答时间可能长达一整天。

101.1　练习题3.2

下列可执行程序实现了某种著名的加密机制。请问它实现的是什么算法？

Windows x86（go.yurichev.com/17188）。

Linux x86（go.yurichev.com/17189）。

Mac OS X（x64）（go.yurichev.com/17190）。

Linux MIPS（go.yurichev.com/17191）。

101.2　练习题3.3

下列程序可打开并读取某个文件，而后计算某种值并在屏幕上输出浮点数。请问它实现的是什么功能？

Windows x86（go.yurichev.com/17192）。

Linux x86（go.yurichev.com/17193）。

Mac OS X（x64）（go.yurichev.com/17194）。

Linux MIPS（go.yurichev.com/17195）。

101.3　练习题3.4

这是一个用密码加、解密文件的工具。虽然我们找到了密文，但是找不到加密密码。此外，我们还知道原文是英文的文本文件。虽然程序采用了较强的加密机制，但是它存在严重的功能缺陷。这种缺陷大大降低了解密的难度。

请找到程序的缺陷，并把密文还原为明文。

Windows x86（go.yurichev.com/17196）。

密文下载地址：http://go.yurichev.com/17197。

101.4　练习题3.5

下列程序实现了版权保护机制。它会读取key文件，核对其中的用户名和序列号。

本题的任务分为两个：

（低难度）使用tracer或别的debugger，强制程序认可篡改过的key文件。

（中等难度）修改用户名，但是不得修改可执行程序。

程序的下载地址如下：

Windows x86（go.yurichev.com/17198）。

Linux x86（go.yurichev.com/17199）。

Mac OS X（x64）（go.yurichev.com/17200）。

Linux MIPS（go.yurichev.com/17201）。

Key文件（go.yurichev.com/17202）。

101.5　练习题3.6

下列程序属于羽量级的web服务器程序。虽然它支持静态文件，但是不支持cgi等动态脚本。这个程序里有4个以上的安全漏洞。找到这些漏洞，并且想办法利用它们攻陷服务器。

Windows x86（go.yurichev.com/17203）。

Linux x86（go.yurichev.com/17204）。

Mac OS X（x64）（go.yurichev.com/17205）。

101.6　练习题3.8

下列程序实现了著名的数据压缩算法。或许是因为原作者在输入代码时敲错了按键，它的解压缩功能存在问题。我们能够在执行过程中看到它的bug。

压缩之前的原文件：go.yurichev.com/17206。

压缩之后的压缩包：go.yurichev.com/17207。

解压之后的（故障）文件：go.yurichev.com/17208。

请找到程序中的bug。如果可能的话，还请修改可执行文件，修补这个bug。

Windows x86（go.yurichev.com/17209）。

Linux x86（go.yurichev.com/17210）。

Mac OS X（x64）（go.yurichev.com/17211）。

第102章 Crackme/Keygenme

有关软件知识产权保护措施的相关分析，请参见：http://crackmes.de/users/yonkie/

。

附录A　x86

A.1　数据类型

16位（8086/80286）、32位（80386等）和64位系统常用的数据类型有：

byte（字节）：8位数据。声明字节型数组和变量的汇编伪指令是DB。计算机使用寄存器的低8位空间存储字节型数据。也就是说，字节型数据通常存储在（寄存器助记符）AL/BL/CL/DL/AH/BH/ CH/DH/SIL/DIL/R*L里。

word（字）：16位数据。声明字型数组和变量的汇编伪指令是DW。计算机使用寄存器的16位空间存储word型数据。也就是说，字型数据通常存储在（寄存器助记符）AX/BX/CX/DX/SI/DI/R*W里。

dword/double word：32位数据。声明DWord型数组和变量的汇编伪指令是DD。x86 CPU的标准寄存器及x64 CPU寄存器的32位空间都可存储DWord型数据。16位应用程序则用寄存器对来存储DWord型数据。

qword/quad word：64位数据。声明QWord型数组和变量的汇编伪指令是DQ。32位系统使用一对寄存器来存储QWord型数据。

tbyte（10字节型）：80位，即10字节数据。符合IEEE 754标准的FPU寄存器都采用这种类型的数据。

paragraph（16字节型）：这种类型的数据主要出现在MS-DOS操作系统的程序里。

Windows API所定义的各种同名的数据类型（包括BYTE WORD DWORD）及其数据存储空间，同样遵循上述标准。

A.2　通用寄存器

x86-64的CPU可以直接调用多数通用寄存器的8位（byte）和16位（word）存储空间。通用寄存器有前向兼容的特性（可兼容最早的8080CPU）。早期的8位CPU（例如8080）可以用一对8位寄存器存储一个16位数据。如此一来，面向早期8080平台的程序就可以照常访问16位寄存器的低8位空间、高8位空间，还能够把这两个寄存器的数据当作一个整体的16位寄存器使用。x86平台的这种前向兼容的特性，或许是为了方便人们在不同平台上移植程序。而采用RISC精简指令集的CPU则通常没有这种特性。

此外，x86-64 CPU上有R开头的寄存器，而80386以后的CPU都有E开头的寄存器。可见，R-寄存器属于64位寄存器，而E-寄存器属于32位寄存器。

x86-64 CPU还比x86 CPU多了8个通用寄存器，即R8～R15寄存器。

在Intel官方手册中，这些寄存器的低8位空间（byte）的助记符带有“L”后缀（例如R8L），而IDA程序则给它们加上了后缀“B”（例如R8B）。

A.2.1　RAX/EAX/AX/AL

7

6

5

4

3

2

1

0

RAXx64

EAX

AX

AH

AL

又称累加寄存器（Accumulator）。函数的返回值通常保存在这个寄存器里。

A.2.2　RBX/EBX/BX/BL

7

6

5

4

3

2

1

0

RBXx64

EBX

BX

BH

BL

A.2.3　RCX/ECX/CX/CL

7

6

5

4

3

2

1

0

RCXx64

ECX

CX

CH

CL

又称计数器（Counter）。“REP”开头的指令、位移运算（SHL/SHR/RxL/RxR）通常都会影响这个寄存器的状态。

A.2.4　RDX/EDX/DX/DL

7

6

5

4

3

2

1

0

RDXx64

EDX

DX

DH

DL

A.2.5　RSI/ESI/SI/SIL

7

6

5

4

3

2

1

0

RSIx64

ESI

SI

SILx64

又称源寄存器（Source），是REP MOVSx和REP CMPSx指令默认的数据源。

A.2.6　RDI/EDI/DI/DIL

7

6

5

4

3

2

1

0

RDIx64

EDI

DI

DILx64

又称目标寄存器（Destination），是REP MOVSx、REP STOSx指令默认的目标寄存器。

A.2.7　R8/R8D/R8W/R8L

7

6

5

4

3

2

1

0

R8

R8D

R8W

R8L

A.2.8　R9/R9D/R9W/R9L

7

6

5

4

3

2

1

0

R9

R9D

R9W

R9L

A.2.9　R10/R10D/R10W/R10L

7

6

5

4

3

2

1

0

R10

R10D

R10W

R10L

A.2.10　R11/R11D/R11W/R11L

7

6

5

4

3

2

1

0

R11

R11D

R11W

R11L

A.2.11　R12/R12D/R12W/R12L

7

6

5

4

3

2

1

0

R12

R12D

R12W

R12L

A.2.12　R13/R13D/R13W/R13L

7

6

5

4

3

2

1

0

R13

R13D

R13W

R13L

A.2.13　R14/R14D/R14W/R14L

7

6

5

4

3

2

1

0

R14

R14D

R14W

R14L

A.2.14　R15/R15D/R15W/R15L

7

6

5

4

3

2

1

0

R15

R15D

R15W

R15L

A.2.15　RSP/ESP/SP/SPL

7

6

5

4

3

2

1

0

RSP

ESP

SP

SPL

SP是栈指针Stack Pointer的缩写。在初始化之后，它是当前栈地址的指针。

A.2.16　RBP/EBP/BP/BPL

7

6

5

4

3

2

1

0

RBP

EBP

BP

BPL

帧指针（Frame Pointer），通常是局部变量的指针。在调用函数时，它也常常用来传递参数。有关这个寄存器的详细介绍，请参照本书的7.1.2节。

A.2.17　RIP/EIP/IP

7

6

5

4

3

2

1

0

RIPx64

EIP

IP

指令指针instruction pointer应当总是指向接下来将要执行的那条指令。正常情况下，无法直接干预它的值。但是，下述指令可以等效地实现调整指令指针的功能：

MOV EAX,  ...
JMP EAX
或者：
PUSH Value
RET

A.2.18　段地址寄存器CS/DS/ES/SS/FS/GS

CS/DS/SS/ES分别代表Code Segment代码段寄存器、Data Segment数据段寄存器、Stack Segment堆栈段寄存器和Extra Segment附加段寄存器。

在Win32系统里，FS附加段寄存器（Extra Segment Register）承担TLS（线程本地存储/Thread Local Storage）的角色；而在Linux系统里，GS（另一个附加段寄存器）承担这个角色。早期，这两个寄存器用于实现段式寻址；而现在，它们用于提供更为快速的TLS和TIB（线程信息块/ThreadInformationBlock）功能。有关段地址寄存器的详细介绍，请参见本书第94章。

A.2.19　标识寄存器

标识寄存器即Eflags。

Bit位（及掩码）

缩写（及含义）

描　　述

0（1）

CF（进/借位）

除了常规计算指令之外，专门操作CF的指令还有CLC/STC/CMC

2（4）

PF（奇偶标识位）

参见17.7.1节

4（0x10）

AF（辅助进/借位标识）

6（0x40）

ZF（零标识位）

ZF用来反映运算结果是否为0。如果运算结果为0，则其值为1，否则其值为0

7（0x80）

SF（符号位）

8（0x100）

TF（追踪标识）

当追踪标志TF被置为1时，CPU进入单步执行方式，即每执行一条指令，产生一个单步中断请求。这种方式主要用于程序的调试

9（0x200）

IF（中断允许标识）

中断允许标志用来决定CPU是否响应CPU外部的可屏蔽中断发出的中断请求。CLI/STI指令可对它进行赋值

10（0x400）

DF（方向标识）

决定在执行串操作指令（REP MOVSx、REP CMPSx、REP LODSx和REP SCASx）时有关指针寄存器发生调整的方向。CLD/STD指令可对它进行赋值

11（0x800）

OF（溢出标识）

12,13（0x3000）

IOPL（I/O特权标识）⁸⁰²⁸⁶

14（0x4000）

NT（嵌套任务标志）⁸⁰²⁸⁶

16（0x10000）

RF（重启标识）⁸⁰³⁸⁶

重启动标识用来控制是否接受调试。如果它的值为1，那么CPU将忽略DRx中的硬件断点调试功能

17（0x20000）

VM（虚拟8086方式标志）⁸⁰³⁸⁶

18（0x40000）

AC（对准校验方式位）⁸⁰⁴⁸⁶

19（0x80000）

VIF（虚拟中断标志）Pentium

20（0x100000）

VIP（虚拟中断未决标志）Pentium

21（0x200000）

ID（标识标志）Pentium

其余的标识位都是保留标识位。

A.3　FPU寄存器

FPU栈由8个80位寄存器构成，这8个寄存器分别叫作ST（0）～ST（7）。IDA把ST（0）显示为ST。FPU寄存器用于存储符合IEEE 754标准的long double型数据。这种数据的格式如下表所示。

第79位

第78-64位

第63位

第62-0位

符号位

指数位

整数位

尾数（小数）位

A.3.1　控制字寄存器（16位）

FPU的控制字（Control Word）用于控制FPU的行为。

位

缩写（及含义）

描　　述

0

IM（无效操作掩码）

1

DM（操作数规格异常掩码）

2

ZM（除数为0的掩码）

3

OM（上溢/溢出掩码）

4

UM（下溢/溢出掩码）

5

PM（精度异常掩码）

7

IEM（异常中断位/软件处理控制位）

第0～5位掩码控制功能的总开关，现在的FPU已经不可对其赋值。若IEM为0，则由FPU处理所有的异常信息，从而对软件屏蔽了所有的错误信息。默认值为1

8，9

PC（精度控制）

00：IEEE单精度24位（REAL4）
01：保留
10：IEEE双精度53位（REAL8）
11：IEEE扩展双精度64位（REAL10）

10，11

RC（舍入控制）

00：就近舍入（默认）
01：向−∞舍入
10：向+∞舍入
11：截断（向零舍入）

12

IC（无限/∞控制位）

0：按照unsigned处理±∞（初始态）
1：按照signed处理∞

若PM、UM、OM、ZM、DM、IM字段（第0～5位）设置为1，则由FPU处理异常信息（对软件屏蔽了错误信息）；若某位设置为0，则FPU将会在遇到相应异常时进行中断、释放异常信息给应用程序，程序在处理之后再把控制权返还给FPU。

A.3.2　状态字寄存器（16位）

FPU的状态寄存器又称Fstate，属于只读寄存器。

位序

缩写（及含义）

描 述

15

B（忙）

1：FPU正在进行运算
0：FPU可进行下次运算

14

C3（条件代码位C3）

13，12，11

TOP（栈顶指针）

ST（0）使用的物理寄存器

10

C2（条件代码位C2）

9

C1（条件代码位C1）

8

C0（条件代码位C0）

7

IR（中断请求）

6

SF（栈异常）

5

P（精度）

4

U（下溢）

3

O（上溢）

2

Z（运算结果为零）

1

D（操作数规格异常）

0

I（无效操作）

状态位SF、P、U、O、Z、D、I用于异常反馈。

有关C3、C2、C1、C0的更详细介绍，请参见本书的17.7.1节。

在软件使用st（x）时，FPU会计算x与栈顶指针序号的和，必要的时候还会再计算8的模（余数），以此确定栈指针的物理寄存器地址。

A.3.3　标记字寄存器（16位）

标志字寄存器总共16位。每2位为一组，表示FPU数据寄存器的使用情况。

位　　序

描　　述

15，14

Tag（7）

13，12

Tag（6）

11，10

Tag（5）

9，8

Tag（4）

7，6

Tag（3）

5，4

Tag（2）

3，2

Tag（1）

1，0

Tag（0）

Tag（x）存储着FPU物理寄存器R（x）[1]的状态码。

其各值的代表含义是：

00：该寄存器存储着非零的值。

01：该寄存器存储的值为零。

10：寄存器的值为特殊的值，NAN、∞或者无效操作数。

11：寄存器为空。

A.4　SIMD寄存器

A.4.1　MMX寄存器

MMX寄存器由8个64位寄存器（MM0～MM7）组成。

A.4.2　SSE与AVX寄存器

SSE都有XMM0～XMM7这8个128位寄存器，x86-64系统还有额外的8个寄存器（XMM8～XMM15）。

而支持AVX指令集的CPU，它们把XMM＊寄存器扩充为256位寄存器。

A.5　FPU调试寄存器

调试寄存器（Debugging registers）用于实现基于硬件的断点控制。

DR0为第1个断点的地址（线性地址）。

DR1为第2个断点的地址。

DR2为第3个断点的地址。

DR3为第4个断点的地址。

DR6为调试状态寄存器。在调试过程异常时，它负责报告产生异常的原因。

DR7为用于控制断点调试。

A.5.1　DR6规格

位序（掩码）

描　　述

0（1）

B0：触发了断点DR0

1（2）

B1：触发了断点DR1

2（4）

B2：触发了断点DR2

3（8）

B3：触发了断点DR3

13（0x2000）

BD：仅在DR7的GD为1的情况下有效。只有当下一条指令要访问到某一个调试寄存器的时候，BD位才被置位（1）

14（0x4000）

BS：当进行单步调试的时候，即EFLAGS的TF标识位被置位的时候，BS才被置位。单步调试具有最高的调试优先级，不受其他标识位影响

15（0x8000）

BT：任务切换标识位

单步调试断点是在执行一条指令之后发生的断点。设置EFLAGS（附录A.2.19）的TF标识，即可实现单步调试。

A.5.2　DR7规格

DR7用于控制断点类型。

位序（掩码）

描　　述

0（1）

L0：在当前任务的DR0处设置断点

1（2）

G0：在所有任务中都设置DR0的断点

2（4）

L1：在当前任务的DR1处设置断点

3（8）

G1：在所有任务中都设置DR1的断点

4（0x10）

L2：在当前任务的DR2处设置断点

5（0x20）

G2：在所有任务中都设置DR2的断点

6（0x40）

L3：在当前任务的DR3处设置断点

7（0x80）

G3：在所有任务中都设置DR3的断点

8（0x100）

LE：P6以及P6以后的处理器不支持这个标识位。如果被置位，那么FPU将会在当前任务中追踪精确的数据断点

9（0x200）

GE：P6以及P6以后的处理器不支持这个标识位。如果被置位，那么FPU将会在所有任务中追踪精确的数据断点

13（0x2000）

GD：如果置位，那么当MOV指令修改DRx寄存器的值时，FPU将进行进行异常处理

16，17（0x3000）

断点DR0的触发条件

18，19（0xC000）

断点DR0的断点长度

20，21（0x30000）

断点DR1的触发条件

22，23（0xC0000）

断点DR1的断点长度

24，25（0x300000）

断点DR2的触发条件

26，27（0xC00000）

断点DR2的断点长度

28，29（0x3000000）

断点DR3的触发条件

30，31（0xC000000）

断点DR3的断点长度

其中，断点DRx的触发条件又分为：

00：执行指令。

01：数据的写操作。

10：读写I/O（User mode 下不可用）。

11：读写数据。

可见，FPU断点的触发条件里没有“读取数据”这一项。

FPU断点长度的规格如下：

00：1个字节。

01：2个字节。

10：32位系统中未定义，64位系统中代表8字节。

11：4个字节。

A.6　指令

标记为（M）的指令通常都不是编译器生成的指令。这种指令或许属于手写出来的汇编代码，或许属于编译器的内部指令（参见本书第90章）。

本节仅列举那些常见指令。如果需要查看完整的指令说明，请参见《Intel® 64 and IA-32 Architectures Software Developer’s Manual Combined Volumes:1，2A，2B，2C，3A，3B，and 3C》和《AMD64 Architecture Programmer’s Manual》。

我们是不是也要记住指令的opcode呢？除非您专门从事给代码打补丁的工作（参见本书第89章第2节），否则没那种必要。

A.6.1　指令前缀

LOCK：数据总线封锁前缀。在执行LOCK作前缀的汇编指令时，它可起到独占数据总线的作用。简单地说，在执行这种指令时，多处理器的其他CPU都将停下来、等该指令执行结束。这种指令常见于各种关键系统、（硬件）信号量和互斥锁。

禁止协处理器修改数据总线上的数据，起到独占总线的作用。该指令的执行不影响任何标志位。它常作为ADD、AND、BTR、BTS、CMPXCHG、OR、XADD、XOR指令的前缀。本书的第68章第4节详细介绍了这种指令。

REP：与MOVSx和STOSx指令结合使用，以循环的方式进行数据复制及数据存储。在执行REP指令时，CX/ECX/RCX寄存器里存储的值将作为隐含的循环计数器。有关MOVSx和STOSx指令的详细说明，请参见附录A.6.2。

REP指令属于DF敏感指令。DF标识位决定了它的操作方向。

REPE/REPNE：（又称为REPZ/REPNZ）与CMPSx和SCASx指令结合使用，以循环的方式进行数值比较。在执行这种指令时，CX/ECX/RCX 寄存器里存储的值将作为隐含的循环计数器。当ZF标识位为0（REPE），或ZF标识位为1（REPNE）时，它将终止循环过程。

有关CMPSx和SCASx的详细描述，请参见附录A.6.2和A.6.3。

REPE/REPNE指令属于DF敏感指令。DF标识位决定了它的操作方向。

A.6.2　常见指令

ADC（进位加法运算）：在进行加法运算时，会把 CF 标识位代表的进位加入和中。它常见于较大数值的加法运算。例如，在32位系统进行64位数值的加法运算时，会组合使用ADD和ADC指令，如下所示：

; 64位值的运算：val2= val1 + val2.
; .lo 代表低32位，.hi代表高32位。
ADD val1.lo, val2.lo
ADC val1.hi,val2.hi；会使用上一条指令设置的CF

本书的第24章有更为详细的使用案例。

ADD：加法运算指令。

AND：逻辑“与”运算指令。

CALL：调用其他函数。相当于“PUSH（CALL之后的返回地址）；JMP label”。

CMP：比较数值、设置标识位。虽然它的运算过程确是减法运算，但是SUB指令保存运算结果（差）、而CMP指令不保存运算结果。

DEC：递减运算。它不影响CF标识位。

IMUL：有符号数的乘法运算指令。

INC：递增运算。它不影响CF标识位。

JCXZ，JECXZ，JRCXZ（M）：当CX/ECX/RCX=0时跳转。

JMP：跳转到指定地址。相应的opcode中含有转移偏移量（jump offset）。

Jcc：条件转移指令。cc是condition code的缩写。

JAE即JNC：（unsigned）在大于或等于的情况下进行跳转；转移条件是CF=0。

JA即JNBE：（unsigned）在大于的情况下进行跳转；转移条件是CF=0且ZF=0。

JBE：（unsigned）在小于或等于的条件下进行跳转；转移条件是CF=1或ZF=1。

JB即JC：（unsigned）在小于的情况下进行跳转；转移条件是CF=1

JC即JB：在小于的情况下进行跳转；转移条件是CF=1。

JE即JZ：在相等的情况下进行跳转；转移条件是ZF=1。

JGE：（signed）在大于或等于的情况下进行跳转；转移条件是SF=OF。

JG：（signed）在大于的情况下进行跳转；转移条件是ZF=0且SF=OF。

JLE：（signed）在小于或等于的情况下进行跳转；转移条件是ZF=1或SF≠OF。

JL：（signed）在小于的条件下进行跳转；转移条件是SF≠OF。

JNAE即JC：（unsigned）在小于（不大于且不相等）的情况下进行跳转；转移条件是CF=1。

JNA：（unsigned）在不大于的情况下进行跳转；转移条件是CF=1或ZF=1。

JNBE：（unsigned）在大于的情况下进行转移；转移条件是CF=0且ZF=0。

JNB即JNC：（unsigned）在不小于的情况下进行跳转；转移条件是CF=0。

JNC即JAE：等同于JNB；转移条件是CF=0。

JNE即JNZ：在不相等的情况下进行跳转；转移条件是ZF=0。

JNGE：（signed）在不大于且不等于的情况下进行跳转；转移条件是SF≠OF。

JNG：（signed）在不大于的情况下进行跳转；转移条件是ZF=1或SF≠OF。

JNLE：（signed）在不大于且不相等的情况下进行跳转；转移条件是ZF=0且SF=OF。

JNL：（signed）在不小于的情况下进行跳转；转移条件是SF=OF。

JNO：在不溢出的情况下进行跳转；转移条件是OF=0。

JNS：在SF标识位为0的情况下进行跳转。

JNZ即JNE：在不大于且不等于的情况下进行跳转；转移条件是ZF=0

JO：在溢出的情况下进行跳转；转移条件是OF=1。

JPO：在PF标识位为零的情况下进行跳转。

JP即JPE：在PF标识位为1的情况下进行跳转。

JS：在SF标识位为1的情况下进行跳转。

JZ即JE：在操作数相等的情况下进行跳转；转移条件是ZF=1。

LAHF：标识位读取指令。它把标识位复制到AH寄存器。数权关系如下表所示。

7

6

5

4

3

2

1

0

SF

ZF

AF

PF

CF

LEAVE：等效于“MOV ESP，EBP”“POP EBP”指令的组合。即，这条指令释放当前子程序在堆栈中的局部变量，恢复栈指针（stack pointer/ESP）和EBP寄存器的初始状态。

LEA：有效（偏移）地址传送指令。

这个指令并非调用寄存器的值，也不会进行地址以外的求值运算。它可利用数组地址、元素索引号和元素空间进行混合运算，求得某个元素的有效地址。

所以，MOV和LEA指令有巨大的差别：MOV指令会把操作数的值当作地址、而后对这个地址的值进行读写操作；而LEA就对操作数的地址进行直接处理。

因此，LEA指令也经常用于各种常规计算。

LEA指令有一个重要的特点——它不会影响CPU标识位的状态。对于OOE（乱序方式执行的指令）处理器来说，这一特性有利于大幅度降低数据依赖性。

int f(int a, int b)
{
         return a*8+b;
};

使用MSVC 2010（启用优化功能）编译上述程序，可得到：

指令清单A.1　优化MSVC 2010

_a$ = 8                                            ; size = 4
_b$ = 12                                           ; size = 4
_f      PROC
        mov     eax, DWORD PTR _b$[esp-4]
        mov     ecx, DWORD PTR _a$[esp-4]
        lea     eax, DWORD PTR [eax+ecx*8]
        ret     0
_f      ENDP

Intel C++编译器生成的汇编代码更为烦琐。例如，在编译下述源代码时：

int f1(int a)
{
       return a*13;
};

Intel C++生成的汇编代码为：

指令清单A.2　Intel C++2011

_f1     PROC NEAR
        mov       ecx, DWORD PTR [4+esp]          ; ecx = a
        lea       edx, DWORD PTR [ecx+ecx*8]      ; edx = a*9
        lea       eax, DWORD PTR [edx+ecx*4]      ; eax = a*9 + a*4 = a*13
        ret

即使如此，两条LEA指令的执行效率仍然超过了单条IMUL指令。

MOVSB/MOVSW/MOVSD/MOVSQ：复制8位单字节数据（Byte）/16位Word数据（Word）/32位双字型数据（Dword）/64位四字型数据（Qword）的指令。默认情况下，它将把SI/ESI/RSI寄存器里的值当作源操作数的地址，目标操作数的地址将取自DI/EDI/RDI寄存器。

在与REP前缀组合使用时，它会把CX/ECX/RCX作为循环控制变量进行循环操作。这种情况下，它就像C语言中的memcpy() 函数那样工作。如果编译器在编译阶段能够确定每个模块的大小，编译器通常使用REP MOVSx指令以内连函数的形式实现memcpy()。

例如，memcpy（EDI，ESI，15）等效于：

; copy 15 bytes from ESI to EDI
CLD          ; set direction to "forward"
MOV ECX, 3
REP MOVSD    ; copy 12 bytes
MOVSW        ; copy 2 more bytes
MOVSB        ; copy remaining byte

在复制15字节的内容时，从寄存器读取的操作效率来看，上述代码的效率要高于15次数据读写（MOVSB）的操作效率。

MOVSX：以符号扩展的方法实现signed型数据的类型转换（参见本书15.1.1节）。

MOVZX：以用零扩展的方法实现unsigned型数据的类型转换（参见本书15.1.1节）。

MOV：数据传送指令。“MOV”这个名字与“MOVE”（移动）拼写相似，不过它的功能是复制数据而非移动数据。在某些平台上，这条指令的名字是“LOAD”或者某个类似的名字。

值得一提的是，当使用MOV指令给32位寄存器的低16位赋值时，寄存器的高16位不会发生变化。而使用MOV指令给64位寄存器的低32位赋值时，寄存器的高32位会被清零。

64位寄存器的高32位被自动清零的特性，可能是为了在x86-64系统上兼容32位程序而有意这样设计的。

MUL：unsigned型数据的乘法运算指令。

NEG：求补指令（并非补码计算指令）。NEG op可得到–op。

NOP：NOP指令。在x86平台上的opcode是0x90。这个opcode和XCHG EAX，EAX的空操作指令相同。这即是说，x86平台没有NOP专用的汇编指令，而RISC平台上NOP有专用的汇编指令。有关这个指令的详细介绍，请参见本书的第88章。

编译器可能会使用 NOP 指令进行 16 字节边界对齐。此外，在手工修改程序时，人们也会使用 NOP指令进行指令替换，用于屏蔽条件转移之类的汇编指令。

NOT：求反指令/逻辑“非”运算指令。

OR：逻辑“或”运算指令。

POP：出栈指令。它从SS：［ESP］中取值，再执行ESP=ESP+4（或8）的操作。

PUSH：入栈指令。它先进行ESP=ESP–4（或8），再向地址SS：［ESP］存储数据。

RET：子程序返回函数，相当于POP tmp或JMP tmp。

实际上RET是汇编语言的宏。在Windows和*NIX环境中，它会被解释为RETN（“return near”）；在MS-DOS的寻址方式里，它被解释为RETF（参见本书第94章）。

RET指令可以有操作数。在这种情况下，它等同于POP tmp、ADD ESP op1及JMP tmp。在符合调用约定stdcall的程序里，每个函数最后的RET指令通常都有相应的操作数。有关细节请参见本书的64.2。

SAHF：标识传送指令。它把AH寄存器的值，复制到CPU到标识上。对应的数权关系如下表所示。

7

6

5

4

3

2

1

0

SF

ZF

AF

PF

CF

SBB：借位减法运算指令。计算出操作数间的差值之后，如果CF标识位为1，则将差值递减。SBB指令常见于大型数据的减法运算。例如，在32位系统上计算2个64位数据的差值时，编译器通常组合使用SUB和SBB指令：

; 计算两个 64位数的差值；val1＝val1-val2
; .lo 代表低 32 位； .hi 代表高32位
SUB val1.lo, val2.lo
SBB val1.hi, val2.hi ; 使用了前一个指令设置的CF标识位

本书第24章还有更详细的介绍。

SCASB/SCASW/SCASD/SCASQ（M）：比较8位单字节数据（Byte）/16位Word数据（Word）/32位双字型数据（Dword）/64位四字型数据（Qword）的指令。它将AX/EAX/RAX寄存器里的值当作源操作数，另一个操作数则取自DI/EDI/RDI寄存器。在比较结果之后再设置标识位，设置标识位的方式和CMP指令相同。

这些指令通常与REPNE指令前缀组合使用。在这种情况下，组合指令将把寄存器AX/EAX/RAX里存储的值当作关键字，在缓冲区里进行搜索。此时，REPNE里的NE就意味着：如果值不相等，则继续进行比较（搜索）。

这种指令常常用于实现strlen() 函数，以判明ASCIIZ型字符串的长度。例如：

lea      edi, string
mov      ecx, 0FFFFFFFFh ; 扫描2³²−1 bytes, 即, 接近“无限”
xor      eax, eax        ; 0作终止符
repne scasb
add      edi, 0FFFFFFFFh ; 修正

; 现在，EDI寄存器的值指向里ASCIIZ字符串的最后一个字符

; 接下来将计算字符串的长度
; 目前 ECX = -1-strlen

not      ecx
dec      ecx

; 此后，ECX的值是字符串的长度值

如果AX/EAX/RAX里存储的值不同，那么这个函数的功能就和标准C函数memchr() 一致，即搜索特定byte。

SHL：逻辑左移指令。

SHR：逻辑右移指令。

指令常用于乘以/除以2n乘除运算。此外，它们还常见于字段处理的各种实践方法（请参见本书第19章）。

SHRD op1，op2，op3：双精度右移指令。把op2右移op3位，移位引起的空缺位由op1的相应位进行补足。详细介绍，请参见本书第24章。

STOSB/STOSW/STOSD/STOSQ：8位单字节数据（Byte）/16位Word数据（Word）/32位双字型数据（Dword）/64位四字型数据（Qword）的输出指令。它把AX/EAX/RAX的值当作源操作数，存储到DI/EDI/RDI为目的串地址指针所寻址的存储器单元中去。指针DI将根据DF的值进行自动调整。

在与REP前缀组合使用时，它将使用CX/ECX/RCX寄存器作循环计数器、进行多次循环；工作方式与C语言到memset() 函数相似。如果编译器能够在早期阶段确定区间大小，那么将通过REP MOVSx指令实现memset() 函数，从而减少代码碎片。

例如，memset（EDI，0xAA，15）对应的汇编指令是：

;在EDI中存储15个 0xAA
CLD                     ; set direction to "forward"
MOV EAX, 0AAAAAAAAh
MOV ECX, 3
REP STOSD              ; write 12 bytes
STOSW                  ; write 2 more bytes
STOSB                  ; write remaining byte

在复制15字节的内容时，从寄存器读取的操作效率来看，上述代码的效率要高于15次数据读写（REP STOSB）的操作效率。

SUB：减法运算指令。常见的“SUB寄存器，寄存器”指令可进行寄存器清零。

TEST：测试指令。在设置标识位方面，它和AND指令相同，但是它不存储逻辑与的运算结果。详细介绍请参见本书的第19章。

XCHG：数据交换指令。

XOR op1，op2：逻辑异或运算指令。常见的“XOR reg，reg”用于寄存器清零。

XOR指令普遍用于“翻转”特定比特位。无论以哪个操作符作为源数据，只要另外一个操作符（参照数据）的指定位为1，那么运算结果里的那一位数据都是源数据相应位的非值。

输入A

输入B

运 算 结 果

0

0

0

0

1

1

1

0

1

1

1

0

另外，如果参照数据的对应位为0，那么运算结果里的那一位数据都是源数据相应位的原始值。这是XOR操作非常重要的特点，应当熟练掌握。

A.6.3　不常用的汇编指令

BSF：顺向位扫描指令。详情请参见本书25.2节。

BSR：逆向位扫描指令。

BSWAP：重新整理字节次序的指令。它以字节为单位逆序重新排列字节序，用于更改数据的字节序。

BTC：位测试并取反的指令。

BTR：位测试并清零的指令。

BTS：位测试并置位的指令。

BT：位测试指令。

CBW/CWD/CWDE/CDQ/CDQE：signed型数据的类型转换指令：

CBW：把AL中的字节（byte）型数据转换为字（word）型数据，存储于AX寄存器。

CWD：把AX中的字（word）型数据转换为双字（Dword）型数据、存储于DX-AX寄存器对。

CWDE：把AX中的字（word）型数据转换为双字字（Dword）型数据，存储于DAX寄存器。

CDQ：把EAX中的双字（word）型数据转换为四字（Qword）型数据，存储于EDX：EAX寄存器对。

CDQE（x64指令）：把EAX中对双字（Dword）型数据转换为四字（Qword）型数据，并存储于RAX寄存器。

上述五个指令均能正确处理signed型数据中对符号位、对高位进行正确的填补。详情请参见本书的第24章第5节。

CLD清除DF标识位。

CLI（M）：清除IF标识位。

CMC（M）：变换CF标识位。

CMOVcc：条件赋值指令。如果满足相应的条件代码（cc），则进行赋值。有关条件代码cc的各种代表意，请参见前文A.6.2对Jcc的详细说明。

CMPSB/CMPSW/CMPSD/CMPSQ（M）：比较8位单字节数据（Byte）/16位Word数据（Word）/32位双字型数据（Dword）/64位四字型数据（Qword）的指令。它将SI/ESI/RSI寄存器里的值当作源操作数的地址，另一个操作数的地址则取自DI/EDI/RDI寄存器。在比较结果之后再设置标识位，设置标识位的方式和CMP指令相同。

这些指令通常与指令前缀REP组合使用。在这种情况下，组合指令将CX/ECX/RCX寄存器的值当作循环计数器进行多次循环比较，直至ZF标识位为零。也就是说，它也常用作字符比较（或搜索）。

它的工作方式和C语言的memcmp() 函数相同。

以Windows NT内核（WindowsResearchKernel v1.2）为例，base\ntos\rtl\i386\ movemem.asm代码如下所示。

指令清单A.3　base\ntos\rtl\i386\movemem.asm

; ULONG
; RtlCompareMemory (
;     IN PVOID Source1,
;     IN PVOID Source2,
;     IN ULONG Length
;     )
;
; Routine Description:
;
;     This function compares two blocks of memory and returns the number
;     of bytes that compared equal.
;
; Arguments:
;
;     Source1 (esp+4) - Supplies a pointer to the first block of memory to
;         compare.
;
;     Source2 (esp+8) - Supplies a pointer to the second block of memory to
;         compare.
;
;     Length (esp+12) - Supplies the Length, in bytes, of the memory to be
;         compared.
;
; Return Value:
;
;     The number of bytes that compared equal is returned as the function
;     value. If all bytes compared equal, then the length of the original
;     block of memory is returned.
;
;--

RcmSource1       equ      [esp+12]
RcmSource2       equ      [esp+16]
RcmLength        equ      [esp+20]

CODE_ALIGNMENT
cPublicProc _RtlCompareMemory,3
cPublicFpo 3,0

         push   esi                           ; save registers
         push   edi
         cld                                  ; clear direction
         mov    esi,RcmSource1                ; (esi) -> first block to compare
         mov    edi,RcmSource2                ; (edi) -> second block to compare

;
;     Compare dwords, if any.
;

rcm10:  mov     ecx,RcmLength                ; (ecx) = length in bytes
        shr     ecx,2                        ; (ecx) = length in dwords
        jz      rcm20                        ; no dwords, try bytes
        repe    cmpsd                        ; compare dwords
        jnz     rcm40                        ; mismatch, go find byte

;
;  Compare residual bytes, if any.
;

rcm20:  mov     ecx,RcmLength               ; (ecx) = length in bytes
        and     ecx,3                       ; (ecx) = length mod 4
        jz      rcm30                       ; 0 odd bytes, go do dwords
        repe    cmpsb                       ; compare odd bytes
        jnz     rcm50                       ; mismatch, go report how far we got

;
;  All bytes in the block match.
;

rcm30:  mov      eax,RcmLength              ; set number of matching bytes
        pop      edi                        ; restore registers
        pop      esi                        ;
        stdRET   _RtlCompareMemory

;
;  When we come to rcm40, esi (and edi) points to the dword after the
;  one which caused the mismatch.  Back up 1 dword and find the byte.
;  Since we know the dword didn't match, we can assume one byte won't.
;

rcm40:  sub     esi,4                       ; back up
        sub     edi,4                       ; back up
        mov     ecx,5                       ; ensure that ecx doesn't count out
        repe    cmpsb                       ; find mismatch byte

;
;  When we come to rcm50, esi points to the byte after the one that
;  did not match, which is TWO after the last byte that did match.
;

rcm50:  dec     esi                         ; back up
        sub     esi,RcmSource1              ; compute bytes that matched
        mov     eax,esi                     ;
        pop     edi                         ; restore registers
        pop     esi                         ;
        stdRET  _RtlCompareMemory

stdENDP _RtlCompareMemory

当内存块的尺寸是4的倍数时，这个函数将使用32位字型数据的比较指令CMPSD，否则使用逐字比较指令CMPSB。

CPUID：返回CPU信息的指令。详情请参见本书的21.6.1节。

DIV：无符号型数据的除法指令。

IDIV：有符号型数据的除法指令。

INT（M）：INT x的功能相当于16位系统中的PUSHF；CALL dwordptr[x*4]。在MS-DOS中，INT指令普遍用于系统调用（syscall）。它调用AX/BX/CX/DX/SI/DI寄存器里的中断参数，然后跳转到中断向量表[2]。因为INT的opcode很短（2字节），而且通过中断调用MS-DOS服务的应用程序不必去判断系统服务的入口地址，所以INT指令曾盛行一时。中断处理程序通过使用IRET指令即可返回程序的控制流。

最常被调用的MS-DOS中断是第0x21号中断，它负责着大量的API接口。有关MS-DOS各中断的完整列表，请参见Ralf Brown撰写的《The x86 Interrupt List》[3]。

在MS-DOS之后，早期的Linux和Windows（参见本书第66章）系统仍然使用INT指令进行系统调用。近些年来，它们逐渐使用SYSENTER或SYSCALL指令替代了INT指令。

INT 3（M）：这条指令有别于其他的INT指令。它的opcde只有1个字节，即（0xCC），普遍用于程序调试。一般来说，调试程序就是在需要进行调试的断点地址写上0xCC（opcode替换）。当被调试程序执行 INT3 指令而导致异常时，调试器就会捕捉这个异常从而停在断点处，然后将断点处的指令恢复成原来指令。

在Windows NT系统里，当CPU执行这条指令时，系统将会抛出EXCEPTION_BREAKPOINT异常。如果运行了主机调试程序/ host debugger，那么这个调试事件将会被主机调试程序拦截并处理；否则，Windows系统将会调用系统上注册了的某个调试器/system debugger进行响应。如果安装了MSVS（Microsoft Visual Studio），在执行INT3时，Windows可能会启动MSVS的debugger，继而调试这个进程。这种调试方法改变了原程序的指令，容易被软件检测到。人们开发出了很多反调试技术，通过检查加载代码的完整性防止他人进行逆向工程的研究。

MSVC编译器有INT3对应的编译器内部函数——__debugbreak()。[4]

Kernel32.dll里还有win32的系统函数DebugBreak()，专门执行INT 3。[5]

IN（M）：数据输入指令，用于从外设端口读取数据。这个指令常用于OS驱动程序和MS-DOS的应用程序。详细介绍请参见本书的78.3节。

IRET：在MS-DOS环境中调用INT中断之后，IRET指令负责返还中断处理程序（interrupt handler）。它相当于POP tmp；POPF；JMP tmp。

LOOP（M）：递减计数器CX/ECX/RCX，在计数器不为零的情况下进行跳转。

OUT（M）：数据输出指令，用于向外设端口传输数据。这个指令常用于OS驱动程序和MS-DOS的应用程序。详细介绍请参见本书的78.3节。

POPA（M）：从数据栈中读取（恢复）（R|E）DI、（R|E）SI、（R|E）BP、（R|E）BX、（R|E）DX、（R|E）CX、（R|E）AX寄存器的值。

POPCNT：它的名称是“population count”的缩写。该指令一般翻译为“位1计数”。既是说，它负责统计有多少个“为1的位”。它的英文外号称为“hamming weight”和“NSA指令”。这种外号来自于下述轶闻（Bruce Schneier《Applied Cryptography：Protocols，Algorithms，and Source Code in C》1994.）：

This branch of cryptography is fast-paced and very politically charged. Most designs are secret; a majority of military encryptions systems in use today are based on LFSRs. In fact, most Cray computers (Cray 1, Cray X-MP, Cray Y-MP) have a rather curious instruction generally known as “population count.” It counts the 1 bits in a register and can be used both to efficiently calculate the Hamming distance between two binary words and to implement a vectorized version of a LFSR. I’ve heard this called the canonical NSA instruction, demanded by almost all computer contracts.

更多信息请参阅参考资料［Sch94］。

POPF：从数据栈中读取标识位，即恢复EFLAGS寄存器。

PUSHA（M）：把（R|E）AX、（R|E）CX、（R|E）DX、（R|E）BX、（R|E）BP、（R|E）SI、（R|E）DI寄存器的值，依次保存在数据栈里。

PUSHF：把标识位保存到数据栈里，即存储EFLAGS寄存器的值。

RCL（M）：带进位的循环左移指令，通过CF标识位实现。

RCR（M）：带进位的循环右移指令，通过CF标识位实现。

ROL/ROR（M）：循环左/右移。

几乎所有的CPU都有这些循环位移指令。但是C/C++语言里没有相应的操作指令，所以它们的编译器不会生成这些指令。

为了便于编程人员使用这些指令，至少MSVC提供了相应的伪函数（complier intrinsics）_rotl() 和_rotr()，可直接翻译这些指令。详情请参见：http://msdn.microsoft.com/en-us/library/ 5cc576c4.aspx

。

SAL：算术左移指令，等同于逻辑左移SHL指令。

SAR：算术右移指令。

本指令通常用于对带符号数减半的运算中，因而在每次右移时，它保持最高位（符号位）不变，并把最低位右移至CF中。

SETcc op：在条件表达式cc为真的情况下，将目标操作数设置为1；否则设置目标操作数为0。这里的目标操作数指向一个字节寄存器（也就是８位寄存器）或内存中的一个字节。状态码后缀（cc）指代条件表达式，可参见附录A.6.2的有关介绍。

STC（M）：设置CF标识位的指令。

STD（M）：设置DF标识位的指令。编译器不会生成这种指令，因此十分罕见。我们可以在Windows的内核文件ntoskrnl.exe中找到这条指令，也可以在手写的内存复制的汇编代码里看到它。

STI（M）：设置IF标识位的指令。

SYSCALL：（AMD）系统调用指令（参见本书第66章）。

SYSENTER：（Intel）系统调用指令（参见本书第66章）。

UD2（M）：未定义的指令，会产生异常信息，多用于软件测试。

A.6.4　FPU指令

FPU指令有很多带有“-R”或“-P”后缀的派生指令。带有R后缀的指令，其操作数的排列顺序与常规指令相反。带有P后缀的指令，在运行计算功能后，会从栈里抛出一个数据；而带有PP后缀的指令则最后抛出两个数据。P/PP后缀的指令可在计算后释放栈里存储的计算因子。

FABS：计算ST（0）绝对值的指令。ST(0)=fabs(ST(0))。

FADD op：单因子加法运算指令。ST(0)=op+ST(0)。

FADD ST（0），ST（i）：加法运算指令。ST(0)=ST(0)+ST(i)。

FADDP ST（1）：相当于ST(1)＝ST(0)+ST(1)；pop。求和之后，再从数据栈里抛出1个因子。即，使用计算求得的“和”替换计算因子。

FCHS：求负运算指令。ST(0)= −1xST(0)

FCOM：比较ST（0）和ST（1）。

FCOM op：比较ST（0）和op。

FCOMP：比较ST（0）和ST（1）；然后执行1次出栈操作。

FCOMPP：比较ST（0）和ST（1）；然后执行2次出栈操作。

FDIVR op：ST(0)=op/ST(0)。

FDIVR ST（i），ST（j）：ST(i)=ST(j)/ST(i)。

FDIVRP op：ST(0)=op/ST(0)，然后执行1次出栈操作。

FDIVRPP ST（i），ST（j）：ST(i)=ST(j)/ST(i)，然后执行2次出栈操作。

FDIV op：ST(0)=ST(0)/op。

FDIV ST（i），ST（j）：ST(i)=ST(i)/ST(j)。

FDIVP：ST(1)=ST(0)/ST(1)，然后执行1次出栈操作。即，被除数替换为商。

FILD op：将整数转化为长精度数据，并存入ST（0）的指令。

FIST op：将st（0）以整数保存到op。

FISTP op：将st（0）以整数保存到op，再从栈里抛出ST（0）。

FLD1：把1推送入栈。

FLDCW op：从16位的操作数op里提取FPU控制字（参见附录A.3）。

FLDZ：把0推送入栈。

FLD op：把op推送入栈。

FMUL op：ST(0)＝ST(0)*op。

FMUL ST（i），ST（j）：ST(i)=ST(i)*ST(j)。

FMULP op：ST(0)＝ST(0)*op；然后执行1次出栈操作。

FMULP ST（i），ST（j）：ST(i)=ST(i)*ST(j)；然后执行1次出栈操作。

FSINCOS：一次计算Sine和Cosine结果的指令。

调用指令时，ST（0）存储着角度参数tmp。ST(0)=sin(tmp)；PUSH ST(0)（即ST1存储Sin值）；之后再计算ST(0)=cos(tmp)。

FSQRT：ST(0)=\sqrt{ST(0)}。

FSTCW op：检查尚未处理的、未被屏蔽的浮点异常，再将FPU的控制字（参见附录A.3）保存到op。

FNSTCW op：将FPU的控制字（参见附录A.3）直接保存到op。

FSTSW op：检查尚未处理的、未被屏蔽的浮点异常，再将FPU的状态字（参见附录A.3）保存到op。

FNSTSW op：将状态字（参见附录A.3）直接保存到op。

FST op：保存实数ST(0) 到op。

FSTP op：将ST(0) 复制给op，然后执行1次出栈操作（ST(0)）。

FSUBR op：ST(0)=op−ST(0)。

FSUBR ST（0），ST（i）：ST(0)=ST(i)−ST(0)。

FSUBRP：ST(1)=ST(0)−ST(1)，然后执行1次出栈操作。即被减数替换为差。

FSUB op：ST(0)=ST(0)−op。

FSUB ST（0），ST（i）：ST(0)=ST(0)−ST(i)。

FUSBP ST（1）：ST(1)=ST(1)−ST(0)。

FUCOM ST（i）：比较ST（0）和ST（i）。

FUCOM：比较ST（0）和ST（1）。

FUCOMP：比较ST（0）和ST（1），然后执行1次出栈操作。

FUCOMPP：比较ST（0）和ST（1），然后从栈里抛出2个数据。

上述两个指令与FCOM的功能相似，但是它们在处理QNaN型数据时不会报错，仅在处理SNaN时进行异常汇报。

FXCH ST（i）：交换ST（0）和ST（i）的数据。

FXCH：交换ST（0）和ST（1）的数据。

A.6.5　可屏显的汇编指令（32位）

在构建Shellcode时（参见本书第82章），可能会用到下面这个速查表。

ASCII字符

16进制码

x86指令

0

30

XOR

1

31

XOR

2

32

XOR

3

33

XOR

4

34

XOR

5

35

XOR

7

37

AAA

8

38

CMP

9

39

CMP

:

3A

CMP

;

3B

CMP

<

3C

CMP

=

3D

CMP

?

3F

AAS

@

40

INC

A

41

INC

B

42

INC

C

43

INC

D

44

INC

E

45

INC

F

46

INC

G

47

INC

H

48

DEC

I

49

DEC

J

4A

DEC

K

4B

DEC

L

4C

DEC

M

4D

DEC

N

4E

DEC

O

4F

DEC

P

50

PUSH

Q

51

PUSH

R

52

PUSH

S

53

PUSH

T

54

PUSH

U

55

PUSH

V

56

PUSH

W

57

PUSH

X

58

POP

Y

59

POP

Z

5A

POP

[

5B

POP

\

5C

POP

]

5D

POP

^

5E

POP

_

5F

POP

`

60

PUSHA

a

61

POPA

f

66

32位运行模式下，把操作数切换为16位

g

67

32位运行模式下，把操作数切换为16位

h

68

PUSH

i

69

IMUL

j

6a

PUSH

k

6b

IMUL

p

70

JO

q

71

JNO

r

72

JB

s

73

JAE

t

74

JE

u

75

JNE

v

76

JBE

w

77

JA

x

78

JS

y

79

JNS

z

7A

JP

总之，存在对应ASCII字符的指令有AAA、AAS、CMP、DEC、IMUL、INC、JA、JAE、JB、JBE、JE、JNE、JNO、JNS、JO、JP、JS、POP、POPA、PUSH、PUSHA和XOR。

[1]　请注意，Tag（x）描述的不是FPU逻辑寄存器ST（x）的状态。

[2]　Interrupt Vector Table。它位于地址空间的开头部分，是实模式中断机制的重要组成部分，表中记录所有中断号对应的中断服务程序的内存地址。

[3]　http://www.cs.cmu.edu/~ralf/files.html。

[4]　编译器内部函数指compiler intrinsic，本书有详细介绍。它属于编译器有关的内部函数，基本不会被常规库函数调用。编译器为其生成特定的机械码，并非直接调用它的函数。内部函数常用于实现与特定CPU有关的伪函数。_debugbreak的介绍请参见http://msdn.microsoft.com/en-us/ library/f408b4et.aspx。

[5]　请参见http://msdn.microsoft.com/en-us/library/windows/desktop/ms679297%28v= vs.85%29.aspx。

附录B　ARM

B.1　术语

初代ARM处理器就是32位CPU。所以它的word型数据都是32位数据。这方面它和x86有所不同。

byte　　　 　8位数据。声明byte型数组和变量的指令是DB。

halfword　　16位数据。声明halfword型数组和变量的指令是DCW。

word　　　　32位数据。声明word型数组和变量的指令是DCD。

doubleword　64位数据。

quadword　　128位数据。

B.2　版本差异

ARMv4：开始支持Thumb模式的指令。

ARMv6：用于第一代iPhone、iPhone 3G（这些设备的处理器采用了Samsung 32位RISC ARM 1176IZ（F）-S，支持Thumb-2指令）。

ARMv7：实现了Thumb-2指令（2003）。用于iPhone 3GS、iPhone 4、iPad 1（ARM Crotex-A8）、iPad 2（Cortex-A9）和iPad 3。

ARMv7s：添加了新的指令。用于iPhone 5、iPhone 5c和iPad 4（Apple A6）。

ARMv8：64位CPU，又叫作ARM64、AArch64。用于iPhone 5S、iPad Air（Apple A7）。64位平台不支持Thumb模式，只支持ARM模式的指令（4字节指令）。

B.3　32位ARM（AArch32）

B.3.1　通用寄存器

R0。函数的返回结果通常通过R0传递。

R1～R12。通用寄存器GPRs。

R13。SP（Stack Pointer/栈指针）。

R14。LR（link register/链接寄存器）。

R15。PC（程序计数器）。

R0～R3也被称为临时寄存器（scratch registers）。它们通常用于传递函数的参数；在函数结束时，也不必恢复它们的初始状态。

B.3.2　程序状态寄存器/CPSR

CPSR的全称是Current Program Status Register，其规格如下表所示。

位　　序

描　　述

0～4

M—处理器模式控制位

5

T—Thumb模式控制位

6

F—FIQ禁止位

7

I—IRQ/中断禁止位

8

A—imprecise data abort disable

9

E—数据字节序状态位

10..15，25，26

IT—if–then状态位

16..19

GE—greater-than-or-equal-to

20..23

DNM—Do Not Modify

24

J—Java state

27

Q—sticky overflow表达式整体溢出（相对于各独立因子溢出）标识位

28

V—Overflow溢出标识位

29

C—进位/借位/扩展位

30

Z—零标识位

31

N—负号位/小于标识位

B.3.3　VFP（浮点）和NEON寄存器

0～31bits
32～64
65～96
97～127

Q0128bits

D064bits

D1

S032bits

S1

S2

S3

S寄存器为32位寄存器，用于存储单精度数。

D寄存器位64位寄存器，用于存储双精度数。

D寄存器和S寄存器的存储地址相通。程序可以通过S寄存器的助记符访问D寄存器的数据。虽然这种操作毫无实际意义，但是确实可行。

与之相似的是，NEON的Q寄存器是128位寄存器，与其他浮点数寄存器共用CPU的物理存储空间。

VFP里有32个S寄存器，即S0～S31。VFP v2里又新增了16个D寄存器，实际上这些D寄存器用的还是S0～S31的存储空间。

再后来，VFPv3（NEON或者叫作“Advanced SIMD”）再次添加了16个D寄存器，形成了D0～D31。这几个新增的寄存器，即D16～D31寄存器的存储空间，终于和S寄存器的存储空间相互独立。

NEON或Advanced SIMD也有16个128位Q寄存器，它们使用的是D寄存器（D0～D31）的存储空间。

B.4　64位ARM（AArch64）

通用寄存器

64位ARM的寄存器总数，是32位ARM的两倍之多。

X0。常用于传递函数运算结果。

X0～X7。传递函数参数。

X8。

X9～X15。临时寄存器，供被调用方函数使用，函数退出时无需恢复。

X16。

X17。

X18。

X19～X29。被调用方函数使用的寄存器，在函数退出时需要恢复原值。

X29。用作帧指针/FP（至少GCC把它当作FP）。

X30。链接寄存器/LR（link register）。

X31。这个寄存器一直为0，又称为XZR、“零寄存器”。它的32位（word型）部分还叫作WZR。

SP，64位系统有单独的栈指针寄存器，不再使用通用寄存器。

详细内容请参见官方的ARM资料《Procedure Call Standard for the ARM 64-bit Architecture （AArch64）》（http://infocenter.arm.com/help/topic/com.arm.doc.ihi0055b/IHI0055B_ aapcs64.pdf

）。

程序还可通过其W寄存器这样的助记符访问64位X寄存器的低32位空间。W寄存器和X寄存器的对应关系如下表所示。

高32位部分

低32位部分

X0

W0

B.5　指令

ARM64的部分指令存在对应的、带S后缀的指令。这个后缀代表着该指令会根据结果设置相应的标识位，而不带S后缀的指令就不设置标识位。以ADD和ADDS指令为例，前者只进行数学运算而不会设置标识位。这种功能划分，为ARM系统的预处理功能提供了可靠的保证，绝不会意外地影响到CMP指令和其他条件转移需要的各标识位。

Conditional codes速查表

代　　码

描　　述

标　识　位

EQ

相等

Z==1

NE

相异

Z==0

CS/HS

Carry置位/Unsigned，大于或等于

C==1

CC/LO

Carry为零/Unsigned，小于

C==0

MI

Minus，负数/小于

N==1

PL

Plus，正数或零/大于或等于

N==0

VS

溢出

V==1

VC

未溢出

V==0

HI

Unsigned大于

C==1 and Z==0

LS

Unsigned小于或等于

C==0 or Z==1

GE

Signed大于或等于

N==V

LT

Signed小于

N !=V

GT

Signed大于

Z==0 and N==V

LE

Signed小于或等于

Z==1 or N !=V

None/AL

Always

Any

附录C　MIPS

C.1　寄存器

MIPS遵循的调用约定是O32。

C.1.1　通用寄存器GPR

编　　号

别　　名

描　　述

$0

$ZERO

总是为零。给这个寄存器赋值到操作相当于NOP

$1

$AT

汇编宏和伪指令使用到临时寄存器

$2～$3

$V0～$V1

用于传递函数返回值

$4～$7

$A0～$A3

用于传递函数的参数

$8～$15

$T0～$T7

可供临时数据使用

$16～$23

$S0～$S7

可供临时数据使用，被调用方函数必须保全

$24～$25

$T8～$T9

可供临时数据使用

$26～$27

$K0～$K1

OS Kernel保留

$28

$GP

全局指针/Global Pointer，被调用方函数必须保全PIC code以外的值

$29

$SP

栈指针/Stack Pointer

$30

$FP

帧指针/Frame Pointer

$31

$RA

返回地址/Return Address

n/a

PC

PC

n/a

HI

专门存储商或积的高32位，可通过MFHI访问

n/a

LO

专门存储商或积的低32位，可通过MFLO访问

C.1.2　浮点寄存器FPR

名　　称

描　　述

$F0～$F1

函数返回值

$F2～$F3

未被使用

$F4～$F11

用于临时数据

$F12～$F15

函数的前两个参数

$F16～$F19

用于临时数据

$F20～$F31

用于临时数据，被调用方函数必须保全

C.2　指令

MIPS的指令分为以下3类：

R-Type，Register/寄存器类指令。此类指令操作3个寄存器，具有以下形式：

指令目标寄存器，源寄存器1，源寄存器2

当前两个操作数相同时，IDA可能会以以下形式进行显示：

指令目标寄存器/源寄存器1，源寄存器2

这种显示风格与x86汇编语言的Intel语体十分相似。

I-Type，Immediate/立即数类指令。涉及2个寄存器和1个立即数。

J-Type，Jump/转移指令。在MIPS转移指令的opcode里，共有26位空间可存储偏移量的信息。

转移指令

实现转移功能的指令可分为B开头的指令（BEQ，B等）和J开头的指令（JAL，JALR等）。这两种跳转指令的区别在哪里？

B-类转移指令属于I-type的指令。也就是说，B-指令的opcode里封装有16位立即数（偏移量）。而J和JAL属于J-type指令，它们的opocde里存有26位立即数。

简单地说，B开头的转移指令可以把转移条件（cc）封装到opcode里（B指令是“BEQ $ZERO, $ZERO, Label”的伪指令）。但是J开头的转移指令无法在opcode里封装转移条件表达式。

附录D　部分GCC库函数

名　　称

描　　述

__divdi3

有符号型数据的除法运算

__moddi3

有符号型数据的求模运算（余数）

__udivdi3

无符号型数据的除法运算

__umoddi3

无符号型数据的求模运算（余数）

附录E　部分MSVC库函数

在MSVC的库函数里，那些名称里带有“LL”的函数都是操作“long long”型数据、即64位数据的函数。

名　　称

描　　述

__alldiv

有符号数的除法运算

__allmul

乘法运算

__allrem

有符号数的求余运算

__allshl

左位移运算

__aulldiv

无符号数的除法运算

__aullrem

无符号数的求余运算

__aullshr

无符号数的右移运算

其中，乘法运算指令、左位移运算指令不区分有符号数和无符号数，所以此处的两条指令不再区分数据类型。

安装MSVS之后，你可以在库文件里找到上述函数的源代码。确切的文件位置是VC/crt/src/ intel/*.asm。

附录F　速查表

F.1　IDA

常用的IDA的快捷键如下表所示。

按　　键

作　　用

空格

切换显示方式

C

转换为代码

D

转换为数据

A

转换为字符

*

转换为数组

U

未定义

O

提取操作数的偏移量

H

把立即数转换为10进制数

R

把立即数转换为字符

B

把立即数转换为2进制数

Q

把立即数转换为16进制数

N

为标签重命名

？

计算器

G

跳转到地址

:

添加注释

Ctrl-X

查看当前函数、标签、变量的参考（显示栈）

X

查看当前函数、标签、变量的参考

Alt-I

搜索常量constant

Ctrl-I

再次搜索常量

Alt-B

搜索byte序列

Ctrl-B

再次搜索byte序列

Alt-T

搜索文本（包括指令中的文本）

Ctrl-T

再次搜索该文本

Alt-P

编辑当前函数

Enter

跳转到函数、变量等对象

Esc

返回

Num −

收缩（函数代码或区域代码）

Num +

展开

如果您明白函数的具体作用，就可以使用IDA的“收缩”功能把函数的代码隐藏起来。笔者编写的IDA脚本程序（https://github.com/yurichev/IDA_scripts

）可自动隐藏经常使用的内联代码。

F.2　OllyDbg

OllyDbg常用的快捷键如下表所示。

快　捷　键

作　　用

F7

单步步入/单步调试

F8

单步步过

F9

执行至断点处

Ctrl－F2

重新启动程序

F.3　MSVC选项

本书常用的MSVC选项如下表所示。

选　　项

作　　用

/O1

创建尺寸最小的文件

/Ob0

禁止內联展开

/Ox

启用最大优化

/GS-

禁用安全检查（缓冲区溢出）

/Fa(file)

创建汇编文件（设置文件名）

/Zi

生成完整的调试信息

/Zp(n)

使封装结构体向n字节边界对齐

/MD

令可执行程序使用MSVCR*.DLL

有关MSVC的详细介绍，请参见本书的55.1节。

F.4　GCC

本书用到的GCC选项如下表所示。

选　　项

作　　用

-Os

优化目标文件大小

-O3

打开所有-O2的优化选项

-regparm=

设定传递参数的寄存器的数量

-o file

指定输出的文件名

-g

产生带有调试信息的目标代码

-S

仅编译到汇编指令，不进行汇编和链接

-masm=intel

汇编指令采用intel语体

-fno-inline

禁止內联函数

F.5　GDB

本书用到的GDB指令如下表所示。

选　　项

作　　用

break filename.c:number

在源程序第n行处设置断点

break function

在函数入口处设置断点

break *address

在某地址设置断点

b

同break

p variable

显示变量的值

run

运行

r

同run

cont

继续运行

c

（同上）

bt

打印当前栈的所有信息

set disassembly-flavor intel

设置为intel语体

disas

查看当前函数程序的汇编指令

disas function

查看函数的汇编指令

disas function,+50
disas $eip,+0x10

查看函数的部分汇编指令
（同上）

disas/r

查看接下来的几条指令

info registers

查看opcode

info float

显示CPU各寄存器的值

info locals

显示FPU各寄存器的值

x/w ...

列出全部的局部变量（如果能识别出来）

x/w $rdi

读取内存，并显示为word型数据

x/10w ...

从RDI指定的地址读取数据，显示为word

x/s ...

读取并显示10个word型数据

x/i ...

读取内存并显示为字符串

x/10c ...

读取内存并显示为汇编代码

x/b ...

读取内存，并显示10个字符

x/h ...

读取内存，并显示为byte

x/g ...

读取并显示16位halfword型数据

finish

读取并显示giant words（64位）

next

执行到函数退出位置

step

单条语句执行指令，（不步入函数）

set step-mode on

单步步入的跟踪调试指令

frame n

打开step-mode模式

info break

切换栈帧

del n

查看断点

set args ...

删除断点

附录G　练习题答案

G.1　各章练习

第3章

题目1

对应章节：3.7.1节

MessageBeep (0xFFFFFFFF);  // A simple beep. If the sound card is not available, the sound is ↙
    ↘ generated using the speaker.

题目2

对应章节：3.7.2节

#include <unistd.h>

int main()
{
        sleep(2);
};

第5章

题目1

对应章节：5.5.1节

如果未启用MSVC的优化编译功能，程序显示的数字分别是EBP的值、RA和argc。在命令行中执行相应的程序即可进行验证。

如果启用了MSVC的优化编译功能，程序显示的数字分别来自：返回地址RA、argc和数组argv［］。

GCC会给main() 函数的入口分配16字节的地址空间，所以输出内容会有不同。

题目2

对应章节：5.5.2节

这些都是打印UNIX时间的程序，其源代码如下所示：

#include
#include

int main(int argc, char*argv[])
{
　　time_t vTime = time((time_t*)0);
　　printf("%d\n", vTime); // <-- %d 在 MSVC 会报 warnning, 因为 time_t 在 MSVC 默认是8字节长度类型

　　return 0;
}

第7章

题目1

对应章节：7.4.1节

Linux下的GCC将所有文本字符串信息放置到.rodata数据段，它是显式只读的（“只读的数据”）：

$ objdump –s 1

...
Contents of section .rodata:
 400600 01000200 52657375 6c743a20 25730a00  ....Result: %s..
 400610 48656c6c 6f2c2077 6f726c64 210a00    Hello, world!..

第13章

题目1

对应章节：13.5.1节

提示：函数printf() 只会被调用1次。

第14章

题目3

对应章节：14.4.3节

#include <stdio.h>

int main()
{
         for(int i = 100; i>0; --i)
         printf ("%d\n", i);
};

题目4

对应章节：14.4.4节

#include <stdio.h>

int main()
{
         int i;
         for (i=1; i<100; i=i+3)
                   printf ("%d\n", i);
};

第15章

题目1

对应章节：15.2.1节

这是一个统计C字符串中空格的程序，源代码如下：

int f(const char* aStr)
{
    const char* vEos = aStr;
    int vCount = 0;

    for(;!*vEos;++vEos)
    {
        if (*vEos == ' ') ++vCount;
    }

    return vCount;
}

第16章

题目1

对应章节：16.3.1节

int f(int a)
{
  return a*7;
}

第17章

题目2

对应章节：17.10.2节

计算5个双精度浮点数的平均值，源代码如下：

double f(double a1, double a2, double a3, double a4, double a5)
{
  return a1*a2*a3*a4*a5 / 5.0;
}

第18章

题目1

对应章节：18.9.1节

两个100x200矩阵的双精度加法运算程序，其源代码如下：

#define M    100
#define N    200
void s(double *a, double *b, double *c)
{
  for(int i=0;i<N;i++)
    for(int j=0;j<M;j++)
      *(c+i*M+j)=*(a+i*M+j) + *(b+i*M+j);
};

题目2

对应章节：18.9.2节

答案：两个矩阵（一个是100x200矩阵，另一个是100x300）的乘法运算程序，生成一个100x300的矩阵。其源代码如下所示：

#define M     100
#define N     200
#define P     300
void m(double *a, double *b, double *c)
{
  for(int i=0;i<M;i++)
    for(int j=0;j<P;j++)
    {
      *(c+i*M+j)=0;
      for (int k=0;k<N;k++) *(c+i*M+j)+=*(a+i*M+j) * *(b+i*M+j);
    }
};

题目3

对应章节：18.9.3节

double f(double array[50][120], int x, int y)
{
         return array[x][y];
};

题目4

对应章节：18.9.4节

int f(int array[50][60][80], int x, int y, int z)
{
        return array[x][y][z];
};

题目5

对应章节：18.9.5节

生成乘法九九表的程序

int tbl[10][10];

int main()
{
        int x, y;
        for (x=0; x<10; x++)
             for (y=0; y<10; y++)
                    tbl[x][y]=x*y;
};

第19章

题目1

对应章节：19.7.1节

这是一个改变32位数值字节序的函数，源代码如下：

unsigned int f(unsigned int a)
{
       return ((a>>24)&0xff) | ((a<<8)&0xff0000) | ((a>>8)&0xff00) | ((a<<24)&0xff000000);
};

题目2

对应章节：19.7.2节

这是一个把BCD封装的32位值转换为常规格式的函数，源代码如下：

#include <stdio.h>

unsigned int f(unsigned int a)
{
       int i=0;
       int j=1;
       unsigned int rt=0;
       for (;i<=28; i+=4, j*=10)
          rt+=((a>>i)&0xF) * j;
       return rt;
};
int main()
{
         // test
         printf ("%d\n", f(0x12345678));
         printf ("%d\n", f(0x1234567));
         printf ("%d\n", f(0x123456));
         printf ("%d\n", f(0x12345));
         printf ("%d\n", f(0x1234));
         printf ("%d\n", f(0x123));
         printf ("%d\n", f(0x12));
         printf ("%d\n", f(0x1));
};

题目3

对应章节：19.7.3节

#include <windows.h>

int main()
{
         MessageBox(NULL, "hello, world!", "caption",
                     MB_TOPMOST | MB_ICONINFORMATION | MB_HELP | MB_YESNOCANCEL);
};

题目4

对应章节：19.7.4节

这个函数把两个32位数相乘，返回64位的积。解答这种题目，根据输入输出进行判断的速度比较快。

#include <stdio.h>
#include <stdint.h>

// source code taken from
//http://www4.wittenberg.edu/academics/mathcomp/shelburne/comp255/notes/binarymultiplication.pdf

uint64_t mult (uint32_t m, uint32_t n)
{
    uint64_t p = 0; // initialize product p to 0
    while (n != 0) // while multiplier n is not 0
    {
      if (n & 1) // test LSB of multiplier
            p = p + m; // if 1 then add multiplicand m
        m = m << 1; // left shift multiplicand
        n = n >> 1; // right shift multiplier
    }
    return p;
}

int main()
{
    printf ("%d\n", mult (2, 7));
    printf ("%d\n", mult (3, 11));
    printf ("%d\n", mult (4, 111));
};

第21章

题目1

对应章节：21.7.1节

这个程序将显示文件所有人的user ID。其源代码如下所示：

#include <sys/types.h>
#include <sys/stat.h>
#include <time.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    struct stat sb;

    if (argc != 2)
    {
        fprintf(stderr, "Usage: %s <pathname>\n", argv[0]);
        return 0;
    }
    if (stat(argv[1], &sb) == -1)
    {
        // error
        return 0;
    }

    printf("%ld\n",(long) sb.st_uid);
}

题目2

对应章节：21.7.2节

提示：研究的侧重点应当放在Jcc、MOVSx和MOVZX指令。

#include <stdio.h>

struct some_struct
{
        int a;
        unsigned int b;
        float f;
        double d;
        char c;
        unsigned char uc;
};

void f(struct some_struct *s)
{
           if (s->a > 1000)
           {
                    if (s->b > 10)
                    {
                                      printf ("%f\n", s->f * 444 + s->d * 123);
                                      printf ("%c, %d\n", s->c, s->uc);
                    }
                    else
                    {
                                      printf ("error #2\n");
                    };
           }
           else
           {
                    printf ("error #1\n");
           };
};

第41章

题目1

对应章节：41.6节

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

第50章

题目1

对应章节：50.5.1节

源代码：

#include <stdio.h>

int is_prime(int number)
{
    int i;
    for (i=2; i<number; i++)
    {
      if (number % i == 0 && i != number)
             return 0;
    }
    return 1;
}

int main()
{
    printf ("%d\n", is_prime(137));
};

G.2　初级练习题

G.2.1　练习题1.1

这是一个返回2个数中最大值的函数。

G.2.2　练习题1.4

程序源代码如下：

#include <stdio.h>

int main()
{
    char buf[128];

    printf ("enter password:\n");
    if (scanf ("%s", buf)!=1)
       printf ("no password supplied\n");
    if (strcmp (buf, "metallica")==0)
       printf ("password is correct\n");
    else
       printf ("password is not correct\n");
};

G.3　中级练习题

G.3.1　练习题2.1

这个函数可根据牛顿法计算平方根，其算法摘自Harold Abelson, Gerald Jay Sussman, and Julie Sussman：《Structure and Interpretation of Computer Programs》（1996）。

程序源代码如下所示：

// algorithm taken from SICP book

#include <math.h>

double average(double a, double b)
{
    return (a + b) / 2.0;
}

double improve (double guess, double x)
{
    return average(guess, x/guess);
}

int good_enough(double guess, double x)
{
    double d = abs(guess*guess - x);
    return (d < 0.001);
};

double square_root(double guess, double x)
{
    while(good_enough(guess, x)==0)
     guess = improve(guess, x);
    return guess;
};

double my_sqrt(double x)
{
    return square_root(1, x);
};

int main()
{
    printf ("%g\n", my_sqrt(123.456));
};

G.3.2　练习题2.4

答案：函数strstr() 。其源代码如下所示：

char * strstr (
          const char * str1,
          const char * str2
          )
{
          char *cp = (char *) str1;
          char *s1, *s2;

          if ( !*str2 )
               return((char *)str1);

          while (*cp)
          {
                    s1 = cp;
                    s2 = (char *) str2;

                    while ( *s1 && *s2 && !(*s1-*s2) )
                         s1++, s2++;

                    if (!*s2)
                        return(cp);

                    cp++;
          }

          return(NULL);

}

G.3.3　练习题2.6

提示：使用google搜索程序中的常量。

答案：程序采用的是TEA（Tiny Encryption Algorithm）加密算法。

C语言源代码摘自http://en.wikipedia.org/wiki/Tiny_Encryption_Algorithm

：

void f (unsigned int* v, unsigned int* k) {
     unsigned int v0=v[0], v1=v[1], sum=0, i;            /* set up */
     unsigned int delta=0x9e3779b9;                      /* a key schedule constant */
     unsigned int k0=k[0], k1=k[1], k2=k[2], k3=k[3];    /* cache key */
     for (i=0; i < 32; i++) {                            /* basic cycle start */
          sum += delta;
          v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
          v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
     }                                                   /* end cycle */
     v[0]=v0; v[1]=v1;
}

G.3.4　练习题2.13

答案：线性反馈位移寄存器。详细资料请参见：https://en.wikipedia.org/wiki/Linear_feedback_ shift_register

。

程序源代码如下所示：

// reworked, from https://en.wikipedia.org/wiki/Linear_feedback_shift_register

#include <stdint.h>

uint16_t f(uint16_t in)
{
    /* taps: 16 14 13 11; feedback polynomial: x^16 + x^14 + x^13 + x^11 + 1 */
    unsigned bit  = ((in >> 0) ^ (in >> 2) ^ (in >> 3) ^ (in >> 5) ) & 1;
    return (in >> 1) | (bit << 15);
};

//#define C 0xACE1u
#define C 0xBADFu

int main(void)
{
    uint16_t lfsr = C;
    unsigned period = 0;

    do
    {
      lfsr=f(lfsr);
      printf ("period=0x%x, lfsr=0x%x\n", period, lfsr);
      ++period;
    } while(lfsr != C);

    return 0;
}

G.3.5　练习题2.14

程序采用的是计算最大公因数GCD的算法。源代码可参见：http://beginners.re/exercise-solutions/2/14/GCD.c

。

G.3.6　练习题2.15

这是一个使用蒙特卡罗法计算圆周率的程序，源代码可参见：http://beginners.re/exercise-solutions/2/15/monte.c

。

G.3.7　练习题2.16

阿克曼（Ackermann）函数（https://en.wikipedia.org/wiki/Ackermann_function

）。

int ack (int m, int n)
{
         if (m==0)
                 return n+1;
         if (n==0)
                 return ack (m-1, 1);
                 return ack(m-1, ack (m, n-1));
};

G.3.8　练习题2.17

程序采用了第110号初等元胞自动机的原理。这种算法的介绍可参见：https://en.wikipedia.org/ wiki/Rule_110

。

程序源代码可参见：http://beginners.re/exercise-solutions/2/17/CA.c

。

G.3.9　练习题2.18

程序源代码可参见：http://beginners.re/exercise-solutions/2/18/

。

G.3.10　练习题2.19

程序源代码可参见：http://beginners.re/exercise-solutions/2/19/

。

G.3.11　练习题2.20

提示：可参考Hint: On-Line Encyclopedia of Integer Sequences（OEIS））。

答案：考拉兹猜想。请参考源代码中的注释。

程序源代码可参见：http://beginners.re/exercise-solutions/2/20/collatz.c

。

G.4　高难度练习题

G.4.1　练习题3.2

程序采用的是表查询的简易算法。

源代码请参见：go.yurichev.com/17156。

G.4.2　练习题3.3

源代码请参见：go.yurichev.com/17157。

G.4.3　练习题3.4

源代码和解密后的文件请参见：go.yurichev.com/17158。

G.4.4　练习题3.5

提示：我们可以看到，用户名称的字符串并没有占用整个文件。在偏移量 0x7F 之前的、以零终止的字节被程序忽略了。

源代码请参见：go.yurichev.com/17159。

G.4.5　练习题3.6

源代码请参见：go.yurichev.com/17160。

结合其他练习，您可以掌握修补这个web服务器所有漏洞的方法。

G.4.6　练习题3.8

源代码请参见：go.yurichev.com/17161。

G.5　其他练习题

G.5.1　“扫雷（Windows XP）”

请参见本书第76章。

提示：留意边界字节（0x10）。

参考文献

[al12] Nick Montfort 等人撰写的《10 PRINT CHR$(205.5+RND(1)); : GOTO 10》，The MIT Press, 2012。笔者将之收录为http://go.yurichev.com/ 17286

。

[AMD13a] AMD在2013年发布的《AMD64 Architecture Programmer’s Manual》。笔者将之收录为：http://go.yurichev.com/17284

。

[AMD13b] AMD在2013年发布的《Software Optimization Guide for AMD Family 16h Processors》。笔者将之收录为：http://go.yurichev.com/17285

。

[App10] Apple在2010年发布的《iOS ABI Function Call Guide》。笔者将之收录为http://go.yurichev.com/17276

。

[ARM12] ARM在2012年发布的《ARM® Architecture Reference Manual, ARMv7-A and ARMv7-R edition》。

[ARM13a] ARM 在 2013 年发布的《ARM Architecture Reference Manual, ARMv8, for ARMv8-A architecture profile》。

[ARM13b] ARM在2013年发布的《ELF for the ARM 64-bit Architecture (AArch64)》。笔者将之收录为http://go.yurichev.com/17288

。

[ARM13c] ARM.在2013年发布的《Procedure Call Standard for the ARM 64-bit Architecture (AArch64)》。笔者将之收录为：http://go.yurichev. com/17287

。

[ASS96] Harold Abelson、Gerald Jay Sussman和Julie Sussman撰写的《Structure and Interpretation of Computer Programs》，1996。

[Bro] Ralf Brown《The x86 Interrupt List》。笔者将之收录为http://go.yurichev.com/17292

。

[Bur] Mike Burrell《Writing Effcient Itanium 2 Assembly Code》。笔者将之收录为http://go.yurichev.com/17265

。

[Cli] Marshall Cline《C++ FAQ》。笔者将之收录为: http://go.yurichev.com/17291

。

[Cor+09] Thomas H. Cormen等人编写的《Introduction to Algorithms, Third Edition. 3rd》The MIT Press, 2009。ISBN: 0262033844，9780262033848。

[Dij68] Edsger W. Dijkstra《Letters to the editor: go to statement considered harmful”》，发表于《Commun. ACM 11.3（1968年3月）》，pp. 147–148. ISSN: 0001-0782. DOI: 10.1145/362929.362947. 笔者将之收录为http://go.yurichev.com/17299

。

[Dol13] Stephen Dolan《mov is Turing-complete》，2013。笔者将之收录为http://go.yurichev.com/17269

。

[Dre07] Ulrich Drepper《What Every Programmer Should Know About Memory》，2007。笔者将之收录为http://go.yurichev.com/17341

。

[Dre13] Ulrich Drepper《ELF Handling For Thread-Local Storage》，2013。笔者将之收录为http://go.yurichev.com/17272

。

[Eic11] Jens Eickhoff《Onboard Computers, Onboard Software and Satellite Operations: An Introduction》，2011。

[Fog13a] AgnerFog《Optimizing software in C++: An optimization guide for Windows, Linux and Mac platforms》，2013。http://go. yurichev.com/17279

。

[Fog13b] AgnerFog《The microarchitecture of Intel, AMD and VIA CPUs/An optimization guide for assembly programmers and compiler makers》，2013。笔者将之收录为:http://go.yurichev.com/17278

。

[Fog14] Agner Fog《Calling conventions》，2014。http://go.yurichev.com/17280

[haq] papasutra of haquebright.《WRITING SHELLCODE FOR IA-64》。笔者将之收录为：http://go.yurichev.com/17340

。

[IBM00] IBM《PowerPC (tm) Microprocessor Family: The Programming Environments for 32-Bit Microprocessors》，2000。笔者将之收录为http://go.yurichev.com/17281

。

[Int13] Intel《Intel® 64 and IA-32 Architectures Software Developer’s Manual 》，2013，重点参考了其中的1，2A，2B，2C，3A，3B，3C卷。笔者将之收录为:http://go.yurichev.com/ 17283

。

[Int14] Intel《Intel® 64 and IA-32 Architectures Optimization Reference Manual》（2014年9月）。笔者将之收录为http://go.yurichev.com/17342

[ISO07] ISO《ISO/IEC 9899: TC3（C C99 standard）》（2007）。笔者将之收录为：http://go.yurichev.com/17274

[ISO13] ISO《ISO/IEC 14882:2011 (C++ 11 standard)》(2013)。笔者将之收录为：http://go.yurichev.com/17275

[Jav13] Java《The Java® Virtual Machine Specification Java SE 7 Edition》，2013年2月。笔者将之收录为：http://go.yurichev.com/17345

，以及http://go.yurichev.com/17346

[Ker88] Brian W. Kernighan《The C Programming Language.Ed. by Dennis M. Ritchie. 2nd. Prentice Hall Professional Technical Reference》, 1988. ISBN: 0131103709.

[Knu74] Donald E. Knuth《Structured Programming with go to Statements》，刊登于《ACM Comput. Surv. 6.4 (Dec. 1974)》. 笔者将之收录为:http://go.yurichev.com/17271

, pp. 261–301. ISSN: 0360-0300. DOI: 10.1145/356635. 356640. URL: http://go.yurichev.com/17300.

[Knu98] Donald E. Knuth《The Art of Computer Programming Volumes 1-3 Boxed Set》 2nd. Boston, MA, USA: Addison-Wesley Longman Publishing Co., Inc., 1998. ISBN: 0201485419.

[Loh10] Eugene Loh《The Ideal HPC Programming Language》，发表于《Queue 8.6 (June 2010)》, 30:30–30:38. ISSN: 1542-7730. DOI: 10.1145/1810226.1820518.。笔者将之收录为:http://go.yurichev.com/17298

。

[Ltd94] Advanced RISC Machines Ltd《The ARM Cookbook》，1994。笔者将之收录为：http://go.yurichev.com/17273

。

[Mit13] Michael Matz/Jan Hubicka/Andreas Jaeger/Mark Mitchell《System V Application Binary Interface. AMD64 Archi- tecture Processor Supplement》，2013。笔者将之收录为：http://go.yurichev.com/17295

。

[Mor80] Stephen P. Morse《The 8086 Primer》，1980。笔者将之收录为：http://go.yurichev. com/17351

。

[One96] Aleph One.《Smashing The Stack For Fun And Profit》，发表《Phrack（1996）》。笔者将之收录为:http://go.yurichev.com/17266

。

[Pie] Matt Pietrek《A Crash Course on the Depths of Win32TM Structured Exception Handling》发表于MSDN magazine。URL: http://go.yurichev.com/17293

.

[Pie02] Matt Pietrek《An In-Depth Look into the Win32 Portable Executable File Format》发表于《MSDN magazine（2002）》. URL:http://go.yurichev.com/17318

.

[Pre+07] William H. Press 等人《Numerical Recipes》，2007。

[RA09] Mark E. Russinovich and David A. Solomon with Alex Ionescu《Windows® Internals: Including Windows Server 2008 and Windows Vista, Fifth Edition》，2009。

[Ray03] Eric S. Raymond《The Art of UNIX Programming》，Pearson Education, 2003. ISBN: 0131429019。笔者将之收录为:http://go.yurichev.com/17277

。

[Rit79] Dennis M. Ritchie《The Evolution of the Unix Time-sharing System》，1979。

[Rit86] Dennis M. Ritchie《Where did ++ come from? (net.lang.c)》，1986。笔者将之收录为：http://go.yurichev.com/17296

[2013年整理]。

[Rit93] Dennis M. Ritchie《The development of the C language》，发表于《SIGPLAN Not. 28.3（Mar. 1993）》。笔者将之收录为http://go.yurichev.com/17264

, pp. 201–208. ISSN: 0362-1340. DOI: 10.1145/155360.155580. URL: http://go.yurichev.com/17297。

[RT74] D. M. Ritchie and K. Thompson《The UNIX Time Sharing System》，1974。笔者将之收录为http://go.yurichev.com/17270

。

[Sch94] Bruce Schneier《Applied Cryptography: Protocols, Algorithms, and Source Code in C》，1994。

[SK95] SunSoft Steve Zucker and IBM Kari Karhi《SYSTEM V APPLICATION BINARY INTERFACE: PowerPC Processor Supplement》，1995。笔者将之收录为http://go.yurichev. com/17282

。

[Sko12] Igor Skochinsky《Compiler Internals: Exceptions and RTTI》，2012。笔者将之收录为http://go.yurichev.com/17294

。

[Str13] BjarneStroustrup《The C++ Programming Language, 4th Edition》，2013。

[Swe10] Dominic Sweetman《See MIPS Run, Second Edition》，2010。

[War02] Henry S. Warren.《Hacker’s Delight》，Boston, MA, USA: Addison-Wesley Longman Publishing Co., Inc., 2002. ISBN: 0201914654。

[Yur12] Dennis Yurichev《Finding unknown algorithm using only input/output pairs and Z3 SMT solver》，发表于2012年。笔者将之收录为http://go.yurichev.com/17268

。

[Yur13] Dennis Yurichev《C/C++ programming language notes》，2013。笔者将之收录为http://go.yurichev.com/17289

。