《逆向工程权威指南》—第3章3.节x86

简介:

本节书摘来自异步社区《逆向工程权威指南》一书中的第3章3.节x86,作者【乌克兰】Dennis Yurichev(丹尼斯),更多章节内容可以访问云栖社区“异步社区”公众号查看。

第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语体里,每个运算操作符都需要声明操作数据的类型:
9-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”之类的指令。

相关文章
|
消息中间件 缓存 安全
抱歉,Xposed真的可以为所欲为——终 · 庖丁解码(下)
Xposed的使用不难,API也就那些,难点是: 逆向弄清楚Hook APP的方法调用流程,怎么调,参数都是干嘛的等。 经过反复练习,逆向Hook一个普通的APP(非企业级加固)写出可用的Xposed插件早已驾轻就熟(主要是磨时间),但有一个顾虑一直萦绕心间:不知道Xposed底层的具体实现原理。Tips:Xposed通常只能 Hook java层 及 应用资源的替换,有两个实现版本:4.4前的Dalvik虚拟机实现 和 5.0后ART虚拟机实现,本文针对后者进行分析,同时搭配 Android 5.1.1_r6 源码食用。
2192 0
SwiftUI—方便用户选择日期的DatePicker日期拾取器
SwiftUI—方便用户选择日期的DatePicker日期拾取器
2063 0
SwiftUI—方便用户选择日期的DatePicker日期拾取器
|
19天前
|
人工智能 安全 机器人
阿里云JVS Claw:三分钟实现“养虾自由”,下载就能用,免安装的OpenClaw
阿里云推出JVS Claw——零代码AI智能体平台:https://t.aliyun.com/U/IJbaxg 三分钟手机“养虾”,24小时待命、越用越聪明。支持多端同步、云端沙箱安全隔离、自进化技能体系。现启动“创意技能大赏”征集活动,邀全民共建智能体生态!
312 3
|
3月前
|
边缘计算 Serverless 数据库
Next.js+Vercel+Turso:全栈开发者的终极免费套餐,让数据库查询快10倍、成本降90%!
Turso是基于libSQL(SQLite开源分支)的边缘分布式数据库,支持全球35+节点自动复制、嵌入式本地副本、多写入并发及向量搜索。兼容SQLite生态,查询延迟降至5ms,成本降低90%,免费版即够用。完美适配Next.js/Vercel等全栈场景。
395 1
|
机器学习/深度学习 计算机视觉
RT-DETR改进策略【Neck】| ASF-YOLO 注意力尺度序列融合模块改进颈部网络,提高小目标检测精度
RT-DETR改进策略【Neck】| ASF-YOLO 注意力尺度序列融合模块改进颈部网络,提高小目标检测精度
488 3
RT-DETR改进策略【Neck】| ASF-YOLO 注意力尺度序列融合模块改进颈部网络,提高小目标检测精度
|
Linux 持续交付 开发工具
版本控制系统的选择:Git vs. Mercurial
【6月更文挑战第20天】Git vs. Mercurial: 两者都是流行的DVCS,Git由Linus Torvalds创建,以其速度和复杂分支管理著称,适合大型项目和有经验的开发者。Mercurial,由Matt Mackall开发,以其简洁命令行和易用性吸引初学者。Git社区更大,扩展更丰富,而Mercurial在某些场景下可能更直观。选择取决于项目需求、团队经验和偏好。
|
安全 NoSQL Linux
《ARM汇编与逆向工程 蓝狐卷 基础知识》
《ARM汇编与逆向工程 蓝狐卷 基础知识》
472 0
|
Java 数据库连接 API
十二.Spring源码剖析-Transactional 事务执行流程
上一篇《[Transactional源码解析](https://blog.csdn.net/u014494148/article/details/118398677)》我们介绍了Spring对Transactional的解析,也就是事务的初始化工作,这一篇我们接着来分析事务的执行流程。
|
自然语言处理
统一transformer与diffusion!Meta融合新方法剑指下一代多模态王者
【9月更文挑战第22天】该研究由Meta、Waymo及南加大团队合作完成,提出了一种名为Transfusion的新多模态模型,巧妙融合了语言模型与扩散模型的优点,实现了单一模型下的文本与图像生成和理解。Transfusion通过结合下一个token预测与扩散模型,在混合模态序列上训练单个Transformer,能够无缝处理离散和连续数据。实验表明,该模型在图像生成、文本生成以及图像-文本生成任务上表现出色,超越了DALL-E 2和SDXL等模型。不过,Transfusion仍面临计算成本高和图像理解能力有限等挑战,并且尚未涵盖音频和视频等其他模态。
482 2
使用npm install时遇到问题:npm ERR! code ERESOLVE
使用npm install时遇到问题:npm ERR! code ERESOLVE
549 1