IDE(Integrated Development Environment,集成开发环境)为软件开发提供了一站式服务,这是优点,同时也是缺点,这种一站式的服务使其占用了大量系统资源(以Visual Studio为例,安装VS往往需要占用10~20G甚至个更多的空间),启动/运行速度慢,需要较高的硬件配置,并且隐藏了底层(程序构建过程),有时候给程序维护带来困难。相比于IDE,Atom/Sublime Text/Notepad++/VS Code等文本编辑器,占用资源少,打开速度快,支持大量插件,程序员们能够将其打造成专属于自己的IDE,完全掌握代码的方方面面,深受广大程序员喜爱。
要想知道程序的构建中发生了什么,就要知道IDE在背后帮我们做了什么,一个完整的IDE集成了编辑器、编译器、汇编器和链接器等工具,这些工具的作用和功能如下:
| 工具 | 作用 | 功能 |
|---|---|---|
| 编辑器(editor) | 编写及编辑代码 | 编写源代码 |
| 编译器(compiler) | 检查用户代码的一些语法错误,将其编译成汇编代码 | 预编译,编译 |
| 汇编器(assembler) | 将汇编代码汇编成具有一定功能的目标代码 | 汇编 |
| 链接器(linker) | 将目标代码链接成可执行文件 | 链接 |
一个完整的程序从编写到跑起来一般需要进行这些步骤:源文件编辑–>预处理–>编译–>汇编–>链接,以下以Hello World为例简单介绍每个步骤。
源文件编辑
一个c++ hello world:
#include <iostream>
using namespace std;
int main()
{
cout<<"Hello World!"<<endl;
return 0;
}
预处理(Preprocessing)
g++ -o HelloWorld.i -E HelloWorld.cpp
在预处理过程中,计算机做了这些事情:
- 展开所有的宏定义,消除“#define”;
- 处理所有的预编译指令,如“#if”,”#ifdef”等;
- 处理“#include”预编译指令,将包含文件插入到该预编译的位置;
- 删除所有的注释“/**/”,“//”等;
- 添加行号和文件名标识,以便编译时编译器产生调试用的行号信息以及错误提醒。
这一步不会做出任何安全性和合法性检查。
编译(Compilig)
g++ -o HelloWorld.s -S Helloworld.i
编译过程生成汇编文件HelloWorld.s,这一步编译器将进行:
- 词法分析:将源代码文件的字符序列划分为一系列的记号;
- 语法分析器将对产生的记号进行语法分析,产生语法树,一步步判断如何执表达式操作;如果存在括号不匹配或者表达式错误,编译器会报告错误;
- 语义分析:由语法阶段完成的分析并没有赋予表达式或者其他实际的意义,如加减乘法,需要语义分析才能赋予其真正的意义;
- 代码优化:对代码进行优化处理;
- 汇总所有符号。
汇编(Assembling)
g++ -o Helloworld.o -C Helloworld.s
汇编过程中,生成二进制文件Helloworld.o,也就是机器语言对应的文件。
链接(Linking)
g++ -o Helloworld.exe Helloworld.cpp
链接指的是调用链接器ID来链接程序运行需要的目标文件,及其依赖的库文件,最后生成可执行文件。链接分为“静态链接”和“动态链接”,静态链接和动态链接分别链接静态库和动态库,静态库和动态库均为二进制文件,用于存放功能的函数实现。其中静态库在编译时会被访问(被集成),编译后可以删除,而动态库则在程序执行时被访问,编译时不要需要,不能删除。(win和linux下的动态库文件名后缀分别为.dll和.so)
- 静态链接:指在编译阶段直接把静态库加入到可执行文件中,这样生成的可执行文件相对较大.
- 动态链接:在链接阶段仅加入一些描述信息,可执行文件运行时再把相应动态库加载到内存.
Decompile
编译指的是将高级程序语言源码转换为可执行文件,反编译则是编译的逆过程。一般来说反编可以把可执行文件转化为汇编代码,但是通常不能变成高级语言源代码(也有一些例外,比如.Net Reflector能够反编译出C#源码)。
在一些情况下,比如源码丢失或者无法获取源码,却要修复exe的bug或者单纯仅仅只是修改(如启用/停用)程序中的某个方法或者功能,就可以通过反编译工具来实现,这相对于完全破解以获取软件的原理/算法/结构等等要简单得多。以下为一些网页上推荐的反编译工具:
- https://blog.csdn.net/zhangmiaoping23/article/details/80813161
- https://blog.csdn.net/lqw_6/article/details/78143919
以下为使用Ollydbg汇编分析调试器对HelloWorld.exe进行反汇编的部分结果:
Address Hex dump Command Comments
00401242 |> \8B1D F8814000 MOV EBX,DWORD PTR DS:[<&msvcrt._iob>]
00401248 |. 894424 04 MOV DWORD PTR SS:[ESP+4],EAX ; /mode
0040124C |. A3 0C404000 MOV DWORD PTR DS:[40400C],EAX ; |
00401251 |. 8B43 10 MOV EAX,DWORD PTR DS:[EBX+10] ; |
00401254 |. 890424 MOV DWORD PTR SS:[ESP],EAX ; |fd => [74AD4610] = 0
00401257 |. E8 382A0000 CALL <JMP.&msvcrt._setmode> ; \MSVCRT._setmode
0040125C |. A1 24704000 MOV EAX,DWORD PTR DS:[407024]
00401261 |. 894424 04 MOV DWORD PTR SS:[ESP+4],EAX ; /mode => [407024] = _O_RDONLY
00401265 |. 8B43 30 MOV EAX,DWORD PTR DS:[EBX+30] ; |
00401268 |. 890424 MOV DWORD PTR SS:[ESP],EAX ; |fd
0040126B |. E8 242A0000 CALL <JMP.&msvcrt._setmode> ; \MSVCRT._setmode
00401270 |. A1 24704000 MOV EAX,DWORD PTR DS:[407024]
00401275 |. 894424 04 MOV DWORD PTR SS:[ESP+4],EAX ; /mode => [407024] = _O_RDONLY
00401279 |. 8B43 50 MOV EAX,DWORD PTR DS:[EBX+50] ; |
0040127C |. 890424 MOV DWORD PTR SS:[ESP],EAX ; |fd
0040127F |. E8 102A0000 CALL <JMP.&msvcrt._setmode> ; \MSVCRT._setmode
上表中:
- 第一列是指令在内存中存放的地址;
- 第二列是汇编语言中的操作码,只有CPU能对它唯一识别,Ollydbg的作用就是根据操作码解读出易于人类阅读的汇编语言;
- 第三列为反汇编的目标了,即汇编代码;
- 第四列是Ollydbg提供的注释,可能是windows api,或者是一些可读的信息。
为了保护劳动成果,开发者们常常通过加壳来阻止反编译,加壳的目的是防止源代码被查看,常用的方式是在二进制的程序中植入一段代码,在运行的时候优先取得程序的控制权,阻止外部程序或软件对加壳程序的反汇编分析或者动态分析。
加壳本身只是提高了被反编译的难度,并不能杜绝反编译,例如一些反编译工具集成了了脱壳方法。事实上,只要是CPU能识别的语言,人类都能够识别,区别只是难度高低。完全杜绝反编译一般是不可能的,除非程序完全与用户隔离,比如运行在服务器上,各种防止反编译的方法仅仅只是提高被反编译的门槛。
相关资源
[1]. 程序编译的过程. https://www.jianshu.com/p/9a4d1dffa821
[2]. 在运行exe可执行文件前到底发生了什么?https://zhuanlan.zhihu.com/p/121063581
[3]. 推荐.Net、C# 逆向反编译四大工具利器. https://blog.csdn.net/zhangmiaoping23/article/details/80813161
[4]. EXE反编译方法及工具. https://blog.csdn.net/lqw_6/article/details/78143919
[5]. 从零开始的程序逆向之路. https://www.cnblogs.com/ichunqiu/p/9355253.html
附录 HelloWold编译过程中的中间文件
A. HelloWorld.cpp (源码文件,1kb)
#include <iostream>
using namespace std;
int main()
{
cout<<"Hello World!"<<endl;
return 0;
}
B. HelloWorld.i (预编译后文件,668kb,共25328行)
# 1 "HelloWorld.cpp"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "HelloWorld.cpp"
# 1 "c:\\mingw\\lib\\gcc\\mingw32\\9.2.0\\include\\c++\\iostream" 1 3
# 36 "c:\\mingw\\lib\\gcc\\mingw32\\9.2.0\\include\\c++\\iostream" 3
# 37 "c:\\mingw\\lib\\gcc\\mingw32\\9.2.0\\include\\c++\\iostream" 3
# 1 "c:\\mingw\\lib\\gcc\\mingw32\\9.2.0\\include\\c++\\mingw32\\bits\\c++config.h" 1 3
# 252 "c:\\mingw\\lib\\gcc\\mingw32\\9.2.0\\include\\c++\\mingw32\\bits\\c++config.h" 3
# 252 "c:\\mingw\\lib\\gcc\\mingw32\\9.2.0\\include\\c++\\mingw32\\bits\\c++config.h" 3
namespace std
{
typedef unsigned int size_t;
typedef int ptrdiff_t;
typedef decltype(nullptr) nullptr_t;
}
...
# 2 "HelloWorld.cpp" 2
# 2 "HelloWorld.cpp"
using namespace std;
int main()
{
cout<<"Hello World!"<<endl;
return 0;
}
C. HelloWorld.s (汇编文件,3kb,共116行)
.file "HelloWorld.cpp"
.text
.section .rdata,"dr"
__ZStL19piecewise_construct:
.space 1
.lcomm __ZStL8__ioinit,1,1
.def ___main; .scl 2; .type 32; .endef
LC0:
.ascii "Hello World!\0"
.text
.globl _main
.def _main; .scl 2; .type 32; .endef
_main:
LFB1515:
.cfi_startproc
leal 4(%esp), %ecx
.cfi_def_cfa 1, 0
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
.cfi_escape 0x10,0x5,0x2,0x75,0
movl %esp, %ebp
pushl %ecx
.cfi_escape 0xf,0x3,0x75,0x7c,0x6
subl $20, %esp
call ___main
movl $LC0, 4(%esp)
movl $__ZSt4cout, (%esp)
call __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc
movl $__ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_, (%esp)
movl %eax, %ecx
call __ZNSolsEPFRSoS_E
subl $4, %esp
movl $0, %eax
movl -4(%ebp), %ecx
.cfi_def_cfa 1, 0
leave
.cfi_restore 5
leal -4(%ecx), %esp
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1515:
.def ___tcf_0; .scl 3; .type 32; .endef
___tcf_0:
LFB1993:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $8, %esp
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitD1Ev
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1993:
.def __Z41__static_initialization_and_destruction_0ii; .scl 3; .type 32; .endef
__Z41__static_initialization_and_destruction_0ii:
LFB1992:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
cmpl $1, 8(%ebp)
jne L6
cmpl $65535, 12(%ebp)
jne L6
movl $__ZStL8__ioinit, %ecx
call __ZNSt8ios_base4InitC1Ev
movl $___tcf_0, (%esp)
call _atexit
L6:
nop
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1992:
.def __GLOBAL__sub_I_main; .scl 3; .type 32; .endef
__GLOBAL__sub_I_main:
LFB1994:
.cfi_startproc
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
subl $24, %esp
movl $65535, 4(%esp)
movl $1, (%esp)
call __Z41__static_initialization_and_destruction_0ii
leave
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
LFE1994:
.section .ctors,"w"
.align 4
.long __GLOBAL__sub_I_main
.ident "GCC: (MinGW.org GCC Build-20200227-1) 9.2.0"
.def __ZStlsISt11char_traitsIcEERSt13basic_ostreamIcT_ES5_PKc; .scl 2; .type 32; .endef
.def __ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_; .scl 2; .type 32; .endef
.def __ZNSolsEPFRSoS_E; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitD1Ev; .scl 2; .type 32; .endef
.def __ZNSt8ios_base4InitC1Ev; .scl 2; .type 32; .endef
.def _atexit; .scl 2; .type 32; .endef
D. HelloWorld.o (二进制文件,45kb,可执行文件HelloWorld.exe同为45kb)