问题

Windows 上最小的「HelloWorld.exe」能有多小?

回答
在 Windows 平台上,最小的 "HelloWorld.exe" 可以小到 几百字节,甚至 不到 1KB。

要理解为什么可以这么小,我们需要深入了解可执行文件(PE 文件)的结构以及让程序运行的最低限度要求。

让 "HelloWorld.exe" 运行的最低限度要求:

1. 一个有效的 PE 文件头: Windows 需要一个标准的 PE (Portable Executable) 文件格式来识别和加载程序。这个文件头包含了关于程序的基本信息,例如它是哪个操作系统版本设计的,入口点在哪里,以及它需要哪些库。
2. 一个入口点 (Entry Point): 程序必须有一个明确的起始点,操作系统知道从哪里开始执行指令。对于 Windows GUI 应用程序,通常是 `WinMain` 函数;对于 Windows Console 应用程序,通常是 `main` 或 `WinMain`(如果使用某些库)。
3. 一个执行的指令: 程序至少需要执行一条指令,即使这条指令什么也不做(例如 `ret` 返回)。
4. 不依赖外部动态链接库 (DLLs): 为了最小化文件大小,我们应该避免链接任何外部的 DLL,比如 `kernel32.dll` (提供核心 Windows API 函数) 或 `msvcrt.dll` (C 运行时库)。这是最关键的一点。

如何实现极小的 HelloWorld.exe:

通常,当我们使用编译器(如 Visual Studio, GCC, Clang)来编译一个标准的 "Hello, World!" 程序时,会引入很多额外的开销:

C 运行时库 (CRT): 即使只是 `printf` 或 `puts`,也会链接到 CRT。CRT 负责初始化堆、栈、处理输入输出、处理命令行参数等等,这部分代码和数据会极大地增加文件大小。
导入表 (Import Table): PE 文件需要一个导入表来告诉操作系统它依赖哪些 DLL 及其中的函数。即使你没有显式地调用任何 API 函数,某些编译器为了满足基本运行要求,也可能隐式地导入一些系统库。
节 (Sections): PE 文件通常有多个节,例如 `.text` (代码)、`.data` (数据)、`.rsrc` (资源)、`.reloc` (重定位信息) 等等。即使是最简单的程序,也至少需要一个包含代码的节。
调试信息: 如果编译器没有开启优化或配置为生成调试信息,这些信息也会被包含在内,增加文件大小。
TLS 回调 (Thread Local Storage Callbacks): 一些运行时环境可能需要 TLS 回调,这也会增加开销。

最小化策略:

1. 纯汇编编写: 这是最直接有效的方法。直接使用汇编语言编写,绕过所有高级语言的运行时库和编译器默认的设置。
2. 直接调用系统服务 (Syscalls) 或软中断 (INT 21h):
对于 DOS (已过时): 可以通过 `INT 21h` 调用 DOS API 函数来打印字符串。
对于 Windows (32位): 可以直接调用 Kernel32.dll 中的 `ExitProcess` 来结束程序,但如果只是显示 "Hello, World!",更复杂。直接调用 Win32 API 函数(如 `WriteConsole` 或 `MessageBox`)需要导入 `kernel32.dll` 或 `user32.dll`,这会增加导入表的大小。
对于 Windows (更精简的调用): 可以利用汇编,通过直接定位系统服务的地址(虽然这很不稳定且不推荐,但理论上可以做到极小)或者利用一些未被记录但存在的函数。一个更现实的方法是尝试链接最少的系统函数。
3. 使用特定的链接器选项:
GCC/Clang: 可以使用 `nostdlib` 来避免链接标准库。然后你需要自己编写启动代码和入口点。使用 `nostdlib` 后,你可能仍然需要链接到一些基本的系统函数,或者使用汇编直接调用。
Visual Studio (cl.exe):
使用 `/link /ENTRY:EntryPointSymbol` 来指定入口点,而不是默认的 `mainCRTStartup`。
使用 `/link /MERGE:.rdata=.text` 或其他节合并选项来减少节的数量。
使用 `/link /OPT:REF` 和 `/link /OPT:ICF` 来移除未使用的函数和代码。
最关键的是,要找一种方式 完全不链接 C 运行时库。对于简单的输出,直接调用 Windows API 是最常见的做法。

一个极简的 Windows "HelloWorld.exe" 示例(通过汇编):

以下是一个使用 MASM (Microsoft Macro Assembler) 编写的极其简单的 32 位 Windows 控制台程序,它什么都不做,只是一个入口点和退出代码。要实现真正的 "HelloWorld" 输出,会稍微复杂一些,需要调用 Win32 API。

版本 1: 最小的 "退出" 程序 (几百字节)

这是一个非常精简的程序,它只有一个有效的 PE 头、一个入口点,然后直接退出。

```assembly
; hello_exit.asm
; 编译和链接命令 (使用 MASM):
; ml /c /Zd /coff hello_exit.asm
; link /SUBSYSTEM:CONSOLE /ENTRY:_start /DRIVER hello_exit.obj kernel32.lib user32.lib

.386
.model flat, stdcall
option casemap:none

; 定义入口点
EXTERN ExitProcess@4 : PROC ; 引入 kernel32.dll 的 ExitProcess 函数

; 定义程序节
.CODE
_start PROC
; 调用 ExitProcess 退出程序
; 参数是退出码 (0 表示成功)
push 0
call ExitProcess@4
_start ENDP

END _start
```

编译和链接步骤 (你需要安装 MASM 或使用 MinGW/GCC 的 `as` 和 `ld` 来达到类似效果):

1. 汇编: `ml /c /Zd /coff hello_exit.asm`
2. 链接: `link /SUBSYSTEM:CONSOLE /ENTRY:_start /BASE:0x400000 hello_exit.obj kernel32.lib` (这里链接了 kernel32.lib 以获取 ExitProcess 的函数地址,虽然不是直接导出,但链接器会处理)。

这个程序的大小可能在 2KB 到 5KB 左右,因为它仍然需要导入 `kernel32.dll`,并且有一个导入表和基本的 PE 结构。

版本 2: 真正输出 "HelloWorld" (仍然极简)

要输出 "HelloWorld",我们需要调用 Windows API 来向控制台写入数据。这通常需要链接 `kernel32.dll` 的 `GetStdHandle` 和 `WriteConsole` 函数。

```assembly
; hello_console.asm
; 编译和链接命令 (使用 MASM):
; ml /c /Zd /coff hello_console.asm
; link /SUBSYSTEM:CONSOLE /ENTRY:_start /BASE:0x400000 hello_console.obj kernel32.lib

.386
.model flat, stdcall
option casemap:none

; 导入 Kernel32.dll 中的函数
EXTERN GetStdHandle@4 : PROC
EXTERN WriteConsoleA@20 : PROC
EXTERN ExitProcess@4 : PROC

; 定义常量
STD_OUTPUT_HANDLE equ 11
BUF_SIZE equ 100

; 定义程序节
.CODE
_start PROC
; 获取标准输出句柄 (hConsoleOutput)
push STD_OUTPUT_HANDLE
call GetStdHandle@4
mov ebx, eax ; 将句柄保存到 ebx

; 要写入的字符串
push OFFSET message
push LENGTHOF message
push ebx ; hConsoleOutput
push 0 ; 实际上不是必需的,但 WriteConsoleA 需要指针
call WriteConsoleA@20

; 退出程序
push 0
call ExitProcess@4
_start ENDP

; 数据节
.DATA
message db 'Hello, World!', 0Dh, 0Ah, 0 ; 字符串,包含回车换行和 null 终止符
```

编译和链接步骤:

1. 汇编: `ml /c /Zd /coff hello_console.asm`
2. 链接: `link /SUBSYSTEM:CONSOLE /ENTRY:_start /BASE:0x400000 hello_console.obj kernel32.lib`

这个程序的大小会比纯退出程序大一些,因为需要存储字符串和导入更多的函数。大小可能在 5KB 到 10KB 左右,取决于链接器如何处理导入表和节。

如何做到更小?

不链接任何库: 理论上,你可以直接在汇编中找到 `kernel32.dll` 在内存中的地址,然后找到 `ExitProcess` 或 `WriteConsole` 的函数地址,然后直接调用。但这需要大量复杂的运行时地址解析和跳转,而且极度不稳定。
直接使用 PE 头中的信息: PE 文件格式本身就包含了程序的入口点信息。一个最最底层的程序可以是一个只有合法 PE 头,然后直接执行一条指令(例如 `ret` 或 `nop`),但这样的文件也需要一个合法的 PE 头结构才能被加载。
利用编译器提供的特定选项和技巧: 像 Intel Fortran Compiler 或者一些嵌入式系统编译器可能会提供更精简的运行时库选项,甚至允许你完全移除运行时库。对于 Windows,这通常意味着你需要自己处理所有低级细节。

总结:

在实际操作中,使用汇编并谨慎链接最少的必要系统函数 是制作最小的 "HelloWorld.exe" 的主流方法。一个真正最小的 "HelloWorld.exe" 需要绕过高级语言的运行时库,直接与操作系统交互。

如果你想体验更小的文件,可以尝试使用一些特殊的编译器或工具,它们专注于生成极小的可执行文件,例如:

Tiny C Compiler (TCC): 虽然主要用于 C,但它以生成小巧的可执行文件而闻名。
UPX 压缩: UPX (The Ultimate Packer for Executables) 并不是生成一个 "小" 的 HelloWorld.exe,而是将一个现有的大程序压缩,在运行时解压缩到内存。这可以使最终文件变小,但不是原始可执行文件的精简。

对于 "HelloWorld.exe",通常我们谈论的是不包含任何额外信息(如调试信息、资源),并且只链接最少必要组件的二进制文件。纯粹从 PE 文件结构的角度来说,几百字节是可能的,但要实现 "Hello, World!" 的输出,需要一些基本系统调用的支持,这不可避免地会增加文件大小。

网友意见

user avatar

目前这个问题下最小的能在Windows系统上运行的Hello World是

Windows 上最小的“HelloWorld.exe”能有多小? - 潘安仁的回答

,161Bytes,我看了他的答案后深受启发,在他的基础上更进一步,写出了97Bytes的Hello World。

先贴代码,注意在32位Windows系统上运行。(我只在WinXP32上运行通过)

ps:评论区有人指出,只能在WinXP上运行,XP之后的系统库的地址会变。

pss:将本回答最后一幅图片保存,后缀改成 .rar ,解压后也能获得程序。

4D5A0000504500004C01000048656C6C6F2C776F726C6421CC0002000B016AF5E8640C807C6A0CEB07CCCCCC1E000000680C00010050EB0C000001000400000004000000E8A4C0807C90EBFD0400E89AC0807C90EBFDCC00CCCCCC00CCCCCCCC03

————————————————分割线————————————————

为什么我的程序更小?

@潘安仁

的程序输出Hello World用了USER32的MessageBoxA,这个api看上去很好,毕竟'USER32'是6Bytes,而其他的库名如'kernel32'是8Bytes,所以调用USER32中的函数更短?

错。因为载入PE文件时kernel32.dll和ntdll.dll会被自动载入,并且这两个库正常是卸不掉的,你可以试试FreeLibrary(GetModuleHandle("kernel32")),然后再用汇编代码强制调用kernel32的一个函数,正常运行有没有。所以如果调用kernel32的函数可以删去导入表,大大节省空间。

那kernel32有什么输出函数可以输出Hello world?

有。WriteConsoleA(),写控制台。如果是同一系统,这个函数的位置应该是不变的,在Winxp系统中,这个函数地址是7C81C0ED。同时这个函数还要用到控制台输出句柄,用GetStdHandle(),地址7C810C89。这下程序就小了很多。

————————————————分割线————————————————

下面是程序的总体思路:

1.先写汇编代码

       Data: db 'Hello world!' push -B call 7C810C89 push 0 push 0 push c push Data push eax call 7C81C0ED     

代码好长啊能不能缩短一些呢?我们注意到WriteConsoleA的最后两个参数都对我们无用,一个保存被写入的字节数,一个是保留参数,这种无用的东西就丢掉好了。于是变成了:

       Data: db 'Hello world!' push -B call 7C810C89 push c push Data push eax call 7C81C0ED      

这样函数还能正常返回吗?当然能,我们没将数据压入栈不代表不能pop出东西来,至于pop出来的是什么、由此而产生栈平衡问题都不关我事。

2.压缩出最小的文件头

我的程序没有其他外部命令,自然不需要导入表和导出表,节表也不需要,直接将代码写在文件头里,只需要DOS头、文件头和可选头。

这里就是

@潘安仁

的神操作了,把PE头和DOS头重叠。

如图,加载32位PE文件时,系统对dos头只读取开头的MZ标志和e_ifanew,中间的值可以随意填写,这样我们把NT头提到地址为4的地方去,将DOS头和NT头重叠。

大家也可以尝试其他的位置,其中4是最小的可行位置。

还有一处要注意的地方是我们的是控制台程序,所以Subsystem设为3。

图中是一个可运行的PE程序,运行后直接死循环。

可以看到这样的程序还有不少空位,我们可以把代码插进去。

3.插入代码

文件头中的空白部分TimeDateStamp;PointerToSymbolTable;NumberOfSymbols;都是无用且可以随意填写的,刚好12个字节,我们把"Hello world!"放进去。

剩下不少空位我们把代码也放进去,如果放不下就放一个jmp短跳,跳到下一个空位。

放好之后如图:

图中我们放入代码后,还有不少空位,我都用0xCCh标记了出来。

4.修剪

这样我们的程序已经很小了,它是124字节,那么怎么把它变成97字节呢?相信你也猜到了,把最后一堆零删掉就好了,PE文件载入时空白内存自动填零,这堆零我们是不需要的。

最后就是这样:

运行效果:

输出了一行朴实无华的hello world,我们的程序还有不少空位,或许还可以把效果做炫酷一点,大家自己尝试。

5.也学着做一个总结:

这个问题是13年提出来的,现在已经是16年了,在浮躁的社会里,这个问题或许早已被遗忘在尘埃里。我看到了这个问题,看到了前面大神的回答,我知道我可以做得比他更小,于是我开始着手尝试。我花了几天来学习PE结构,找出缩小程序的方法,思考余暇,我不禁疑惑,做这种事,做出这种程序有什么意义,我为什么要有一个这么小的Hello world?我硬盘差几字节吗?或许你也有一样的困惑,然而我们追求这一系列极限有什么意义呢?更高更快更强,是要回到狩猎时代吗?我想这一切无关乎这种浅薄的利益,它像是一种证明,证明自己的高度。它竖起了一座里程碑,并鼓励后来人一步一个脚印,继续向上,去到山顶上,去睥睨众生。如此之后,我们浮躁的社会里,或许有了一点没那么浮躁的东西。

user avatar

这是我 19 年做的一个实验,制作的是当时最新版本 Windows 10 操作系统可以正常运行的最小 64 位 PE 文件。虽然在未来的 Windows 操作系统上不一定能运行,但是读者也可以利用本文中的方法制作出新的最小 PE 文件。

GitHub 链接 →

简介

本次实验尝试制作了 Windows 10 操作系统下的最小 64 位 PE 文件,该文件可以弹出带有文字提示的消息框,且满足实验的三项限制条件:

  • 符号通过函数名(而不是函数序号)引入
  • 消息框的标题和内容字符串的长度为 64 字节
  • 总文件大小不超过 300 字节

经过九个步骤后,最终制作出了 268 字节的 PE 文件。

实验步骤如下:

  • 第一步:使用常规方法,利用 C 语言编写程序,使用 MSVC 编译器生成普通的 PE 文件。
  • 第二步:通过改变 MSVC 编译器编译时的选项,减小生成的 PE 文件大小。
  • 第三步:使用 PE Tools 查看上一步产生的 PE 文件的内部结构,将其中一些明显无用的部分置零。此时文件大小并没有减小,但文件内部有了更多的零,这让后续步骤有了更多的压缩空间。
  • 第四步:在 PE 文件的内部结构中,MS-DOS stub 也是无用的部分,所以使用二进制编辑器手动将这部分内容置零。这一步也给了后续步骤更多的压缩空间。
  • 第五步:详细分析 PE 文件的内部结构,理解 PE 文件中各部分的含义,然后使用汇编语言手动编写一个相同的 PE 文件。虽然这一步生成的文件和上一步相比没有区别,但使用汇编语言后,后续步骤就无需修改二进制文件本身,而可以在汇编代码上作修改,从而更简便地减小文件大小。
  • 第六步:根据对 PE 文件内部结构的分析,从汇编代码上删除所有可以直接删除的无用部分,从而减小文件大小。
  • 第七步:虽然在上一步中删除了所有可以直接删除的无用部分,但还有一些无用字段不可以被直接删除。这是因为 PE 文件含有多个文件头,这些无用字段位于文件头中,而文件头的格式是固定的,也就是说即使文件头中的某个字段没有被使用,它也要在文件头中占据相应的位置,所以不能直接删除。为此,可以采取重叠的方法,通过精心选取合适的重叠方式,将多个文件头重叠在一起,并保证重叠之后,每个重叠的位置最多只能对应一个有用字段。这样就可以在不破坏文件头的前提下,有效地减小文件大小。
  • 第八步:PE 文件中还含有五条指令的机器码,如果将这五条指令的机器码也按上一步的方法与文件头重叠,就可以进一步减小文件的大小。但是,指令的机器码太长,没有办法做到「见缝插针」,放入文件头无用字段的空隙中。为此,想到可以将五条指令拆开,并在前四条指令的后面分别加一条短跳转指令跳到下一条指令的位置,这样每条指令就可以作为一个独立的部分插入到不同的空隙中了。然而,即使这样做仍然有两条指令太长,没有办法插入空隙。这时,通过对 x64 指令集的深入学习和充分掌握,想到在这两条指令以另一个寄存器作为基址时,可以使对应的机器码更短,而指令结果不变。这样机器码也插入到了文件头无用字段的空隙中,从而进一步减小了文件大小。
  • 第九步:删除文件末尾的 0,因为程序加载时会在末尾自动填充零。
步骤 文件大小(字节)
1 94208 5.924863
2 896 3.567715
3 896 2.220567
4 896 1.608427
5 896 1.607880
6 448 2.653766
7 308 2.473451
8 280 2.620173
9 268 2.667864

其中,第 3-5 步虽然文件大小没有减小,但文件的熵减小了,这说明文件内部有更多的零,也就说明后续步骤有更多的压缩空间。

实验环境

  • 操作系统:Microsoft Windows 10 Version 1903 (v10.0.18362.239) 64-bit
  • Shell:zsh 5.7.1 (x86_64-pc-msys)
  • C 编译器:Microsoft (R) C/C++ Optimizing Compiler Version 19.22.27905 for x64
  • 连接器:Microsoft (R) Incremental Linker Version 14.22.27905.0
  • 汇编器:NASM version 2.14.02 compiled on Jan 30 2019
  • 调试器:x64dbg May 19 2019, 18:13:13

实验步骤

第一步:使用常规方法,利用 C 语言编写程序,使用 MSVC 编译器生成普通的 PE 文件。

在 Windows 编程中,要弹出消息框,可以使用 MessageBoxAMessageBoxW 这两个函数。其中,函数名以 A 结尾表示字符编码使用用户的当前代码页,以 W 结尾表示使用 UTF-16。

由于用户使用的代码页并不统一,出于兼容性的考虑,本次实验使用以 W 结尾的 MessageBoxW 函数。代码页的问题其实非常常见,相信许多人在 Windows 上使用 Python 编程时,都遇到过因为代码页不对而导致程序乱码或崩溃的问题。这里使用 MessageBoxW 函数,与代码页无关,所以可以避免程序乱码或崩溃。

编写的 C 语言代码 tiny.c 如下:

       #include <Windows.h>  int main() {     MessageBoxW( NULL /* hWnd */,                  L"ABCDEFG" /* lpText, 16 bytes */,                  L"  TinyPE on Windows 10" /* lpCaption, 48 bytes */,                  MB_ICONASTERISK | MB_TOPMOST | MB_SERVICE_NOTIFICATION /* uType */ );     return 0; }     

接下来将 C 语言代码编译为 PE 文件。

安装 Visual Studio Build Tools 2019 后,「开始」菜单中会出现 Visual Studio 2019 文件夹。点击其中的 x64 Native Tools Command Prompt for VS 2019 打开命令行,切换到当前目录后,输入以下命令:

       > cl /O1 /source-charset:utf-8 tiny.c /link /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup user32.lib Microsoft (R) C/C++ Optimizing Compiler Version 19.22.27905 for x64 Copyright (C) Microsoft Corporation.  All rights reserved.  tiny.c Microsoft (R) Incremental Linker Version 14.22.27905.0 Copyright (C) Microsoft Corporation.  All rights reserved.  /out:tiny.exe /SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup user32.lib tiny.obj     

编译生成 tiny.exe 文件,可以正常运行:

第二步:通过改变 MSVC 编译器编译时的选项,减小生成的 PE 文件大小。

参考 Minimize the size of your program – high levelLinker Options 可知,通过修改 C 语言代码和编译选项,可以减小编译生成的 PE 文件大小。

修改后的 C 语言代码 tiny.c 如下:

       #include <Windows.h>  void _() {     MessageBoxW( NULL /* hWnd */,                  L"ABCDEFG" /* lpText, 16 bytes */,                  L"  TinyPE on Windows 10" /* lpCaption, 48 bytes */,                  MB_ICONASTERISK | MB_TOPMOST | MB_SERVICE_NOTIFICATION /* uType */ ); }     

使用以下命令行选项编译:

       > cl /O1 /MD /GS- /source-charset:utf-8 tiny.c /link /NOLOGO /NODEFAULTLIB /SUBSYSTEM:WINDOWS /ENTRY:_ /MERGE:.rdata=. /MERGE:.pdata=. /MERGE:.text=. /SECTION:.,ER /ALIGN:16 user32.lib Microsoft (R) C/C++ Optimizing Compiler Version 19.22.27905 for x64 Copyright (C) Microsoft Corporation.  All rights reserved.  tiny.c LINK : warning LNK4108: /ALIGN specified without /DRIVER; image may not run LINK : warning LNK4254: section '.text' (60000020) merged into '.' (40000040) with different attributes     

编译生成 tiny.exe 文件,可以正常运行。

第三步:使用 PE Tools 查看上一步产生的 PE 文件的内部结构,将其中一些明显无用的部分置零。此时文件大小并没有减小,但文件内部有了更多的零,这让后续步骤有了更多的压缩空间。

打开 PE Tools (v1.9.762.2018),单击“PE Editor”,然后打开 tiny.exe

找到以下三个部分,并置零:

  • 点击“File Header”,找到“Time/Date”,设置为 0
  • 点击“View Rich”,然后点击“Clear Sign”
  • 点击“Directories”,找到“Debug Directory”,点击“...”,然后点击“Clear debug info”

修改后的 tiny.exe 文件可以正常运行。

第四步:在 PE 文件的内部结构中,MS-DOS stub 也是无用的部分,所以使用二进制编辑器手动将这部分内容置零。这一步也给了后续步骤更多的压缩空间。

使用二进制编辑器打开 tiny.exe

定位到以下部分,并置零:

  • 0x02-0x3b 置零
  • 0x40-0x7f 置零

这里需要注意,0x00-0x01e_magic0x3c-0x3fe_lfanew,它们都是有用的字段,所以没有置零。

修改后的 tiny.exe 文件可以正常运行,内容如下:

       $ xxd -p tiny.exe 4d5a00000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 c00000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000504500006486010000000000000000000000 0000f00022000b020e1690010000000000000000000000030000f0010000 000000400100000010000000100000000600000000000000060000000000 000080030000f00100000000000002006081000010000000000000100000 000000000000100000000000001000000000000000000000100000000000 000000000000200300002800000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 00000000f001000010000000000000000000000000000000000000000000 0000000000002e0000000000000082010000f001000090010000f0010000 000000000000000000000000200000605803000000000000000000000000 00003dd8afdc2000540069006e0079005000450020006f006e0020005700 69006e0064006f0077007300200031003000000041004200430044004500 460047000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 000000000000000000000000000000000000000000000000000000000000 00000000000000000000000000000000000041b9400024004c8d05f3feff ff488d151cffffff33c948ff25d3feffffcccccc48030000000000000000 000066030000f00100000000000000000000000000000000000000000000 5803000000000000000000000000000094024d657373616765426f785700 5553455233322e646c6c00000000000000000000000000000000     

可以使用 xxd -p -r 将以上文本转换回二进制文件。

第五步:详细分析 PE 文件的内部结构,理解 PE 文件中各部分的含义,然后使用汇编语言手动编写一个相同的 PE 文件。虽然这一步生成的文件和上一步相比没有区别,但使用汇编语言后,后续步骤就无需修改二进制文件本身,而可以在汇编代码上作修改,从而更简便地减小文件大小。

在理解 PE 文件的结构时,主要参考了以下资料:

另外,还使用了 PE ToolsPE Disassembler viewer 这两个工具。

在理解 PE 文件的结构后,使用汇编语言手动编写一个相同的 PE 文件。

在编写汇编语言时,要特别注意不能使用硬编码的数值,而是要使用伪指令计算得出相应的数值。例如,文件大小的数值 0x0380 使用 file_size equ $-$$ 替换,机器指令 0x41b940002400 使用 mov r9d, 0x00240040 替换。这是因为如果使用了硬编码的数值,后续步骤中 PE 文件的结构发生变化时,这些数值并不会随之改变,文件就会损坏。

此外,在使用 NASM 汇编器时,根据官方文档,分别使用伪指令 dbdwdddq 声明 1、2、4、8 字节的数据。

编写的汇编语言文件 stretch.asm 如下:

       BITS 64  %define align(n,r) (((n+(r-1))/r)*r)  ; DOS Header     dw 'MZ'                 ; e_magic     dw 0                    ; [UNUSED] e_cblp     dw 0                    ; [UNUSED] c_cp     dw 0                    ; [UNUSED] e_crlc     dw 0                    ; [UNUSED] e_cparhdr     dw 0                    ; [UNUSED] e_minalloc     dw 0                    ; [UNUSED] e_maxalloc     dw 0                    ; [UNUSED] e_ss     dw 0                    ; [UNUSED] e_sp     dw 0                    ; [UNUSED] e_csum     dw 0                    ; [UNUSED] e_ip     dw 0                    ; [UNUSED] e_cs     dw 0                    ; [UNUSED] e_lfarlc     dw 0                    ; [UNUSED] e_ovno     times 4 dw 0            ; [UNUSED] e_res     dw 0                    ; [UNUSED] e_oemid     dw 0                    ; [UNUSED] e_oeminfo     times 10 dw 0           ; [UNUSED] e_res2     dd pe_hdr               ; e_lfanew  ; DOS Stub     times 8 dq 0            ; [UNUSED] DOS Stub  ; Rich Header     times 8 dq 0            ; [UNUSED] Rich Header  ; PE Header pe_hdr:     dw 'PE', 0              ; Signature  ; Image File Header     dw 0x8664               ; Machine     dw 0x01                 ; NumberOfSections     dd 0                    ; [UNUSED] TimeDateStamp     dd 0                    ; PointerToSymbolTable     dd 0                    ; NumberOfSymbols     dw opt_hdr_size         ; SizeOfOptionalHeader     dw 0x22                 ; Characteristics  ; Optional Header, COFF Standard Fields opt_hdr:     dw 0x020b               ; Magic (PE32+)     db 0x0e                 ; MajorLinkerVersion     db 0x16                 ; MinorLinkerVersion     dd code_size            ; SizeOfCode     dd 0                    ; SizeOfInitializedData     dd 0                    ; SizeOfUninitializedData     dd entry                ; AddressOfEntryPoint     dd iatbl                ; BaseOfCode  ; Optional Header, NT Additional Fields     dq 0x000140000000       ; ImageBase     dd 0x10                 ; SectionAlignment     dd 0x10                 ; FileAlignment     dw 0x06                 ; MajorOperatingSystemVersion     dw 0                    ; MinorOperatingSystemVersion     dw 0                    ; MajorImageVersion     dw 0                    ; MinorImageVersion     dw 0x06                 ; MajorSubsystemVersion     dw 0                    ; MinorSubsystemVersion     dd 0                    ; Reserved1     dd file_size            ; SizeOfImage     dd hdr_size             ; SizeOfHeaders     dd 0                    ; CheckSum     dw 0x02                 ; Subsystem (Windows GUI)     dw 0x8160               ; DllCharacteristics     dq 0x100000             ; SizeOfStackReserve     dq 0x1000               ; SizeOfStackCommit     dq 0x100000             ; SizeOfHeapReserve     dq 0x1000               ; SizeOfHeapCommit     dd 0                    ; LoaderFlags     dd 0x10                 ; NumberOfRvaAndSizes  ; Optional Header, Data Directories     dd 0                    ; Export, RVA     dd 0                    ; Export, Size     dd itbl                 ; Import, RVA     dd itbl_size            ; Import, Size     dd 0                    ; Resource, RVA     dd 0                    ; Resource, Size     dd 0                    ; Exception, RVA     dd 0                    ; Exception, Size     dd 0                    ; Certificate, RVA     dd 0                    ; Certificate, Size     dd 0                    ; Base Relocation, RVA     dd 0                    ; Base Relocation, Size     dd 0                    ; Debug, RVA     dd 0                    ; Debug, Size     dd 0                    ; Architecture, RVA     dd 0                    ; Architecture, Size     dd 0                    ; Global Ptr, RVA     dd 0                    ; Global Ptr, Size     dd 0                    ; TLS, RVA     dd 0                    ; TLS, Size     dd 0                    ; Load Config, RVA     dd 0                    ; Load Config, Size     dd 0                    ; Bound Import, RVA     dd 0                    ; Bound Import, Size     dd iatbl                ; IAT, RVA     dd iatbl_size           ; IAT, Size     dd 0                    ; Delay Import Descriptor, RVA     dd 0                    ; Delay Import Descriptor, Size     dd 0                    ; CLR Runtime Header, RVA     dd 0                    ; CLR Runtime Header, Size     dd 0                    ; Reserved, RVA     dd 0                    ; Reserved, Size  opt_hdr_size equ $-opt_hdr  ; Section Table     section_name db '.'     ; Name     times 8-($-section_name) db 0     dd sect_size            ; VirtualSize     dd iatbl                ; VirtualAddress     dd code_size            ; SizeOfRawData     dd iatbl                ; PointerToRawData     dd 0                    ; PointerToRelocations     dd 0                    ; PointerToLinenumbers     dw 0                    ; NumberOfRelocations     dw 0                    ; NumberOfLinenumbers     dd 0x60000020           ; Characteristics  hdr_size equ $-$$  code: ; Import Address Directory iatbl:     dq symbol     dq 0  iatbl_size equ $-iatbl  ; Strings title:     db 0x3d,0xd8,0xaf,0xdc,0x20,0x00,0x54,0x00     db 0x69,0x00,0x6e,0x00,0x79,0x00,0x50,0x00     db 0x45,0x00,0x20,0x00,0x6f,0x00,0x6e,0x00     db 0x20,0x00,0x57,0x00,0x69,0x00,0x6e,0x00     db 0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00     db 0x20,0x00,0x31,0x00,0x30,0x00,0,0 content:     db 0x41,0x00,0x42,0x00,0x43,0x00,0x44,0x00     db 0x45,0x00,0x46,0x00,0x47,0x00,0,0  ; Debug Table     times 24 dq 0           ; [UNUSED] Debug Table  ; Entry entry:     mov r9d, 0x00240040     ; uType     lea r8, [rel title]     ; lpCaption     lea rdx, [rel content]  ; lpText     xor ecx, ecx            ; hWnd     jmp [rel iatbl]         ; MessageBoxW      times align($-$$,16)-($-$$) db 0xcc  ; Import Directory itbl:     dq intbl                ; OriginalFirstThunk     dd 0                    ; TimeDateStamp     dd dll_name             ; ForwarderChain     dd iatbl                ; Name     dq 0                    ; FirstThunk      times 3 dd 0  itbl_size equ $-itbl  ; Import Name Table intbl:     dq symbol     dq 0  ; Symbol symbol:     dw 0x0294               ; [UNUSED] Function Order     db 'MessageBoxW', 0     ; Function Name dll_name:     db 'USER32.dll', 0     db 0  sect_size equ $-code      times align($-$$,16)-($-$$) db 0  code_size equ $-code file_size equ $-$$      

编译:

       $ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm     

编译生成 stretch.exe,可以正常运行。

这时发现,虽然 stretch.exe 是参照 tiny.exe 手动编写的,理论上应该完全相同,但是实际上两者略有区别:

       $ diff =(xxd step4/tiny.exe) =(xxd step5/stretch.exe) 50c50 < 00000310: 1cff ffff 33c9 48ff 25d3 feff ffcc cccc  ....3.H.%....... --- > 00000310: 1cff ffff 31c9 ff25 d4fe ffff cccc cccc  ....1..%........     

xor ecx, ecx 指令在 tiny.exe 中以机器码 0x33c9 表示,而在 stretch.exe 中以 0x31c9 表示。由 XOR — Logical Exclusive OR 可知,这是由于指令编码方式不同,不影响指令的效果:

       $ echo 33c9 | xxd -p -r - | ndisasm -b 64 - 00000000  33C9              xor ecx,ecx $ echo 31c9 | xxd -p -r - | ndisasm -b 64 - 00000000  31C9              xor ecx,ecx     

jmp [rel iatbl] 指令在 tiny.exe 中以机器码 0x48ff25d3feffff 表示,而在 stretch.exe 中以 0xff25d4feffff 表示。这两条指令的跳转地址没有区别:

       $ echo 48ff25d3feffff | xxd -p -r - | ndisasm -b 64 - 00000000  48FF25D3FEFFFF    jmp qword [rel 0xfffffffffffffeda] $ echo ff25d4feffff | xxd -p -r - | ndisasm -b 64 - 00000000  FF25D4FEFFFF      jmp [rel 0xfffffffffffffeda]     

由 Stack Overflow 上的一个回答可知,机器码中的 48 前缀表示 REX.W,会被处理器忽略。这一前缀可能与 Windows x64 的 unwind data 有关。不论如何,从运行结果上看,这一修改不影响指令的效果。

由此可知,这一步使用汇编语言编写的 stretch.exe 和上一步的 tiny.exe 是等价的。

第六步:根据对 PE 文件内部结构的分析,从汇编代码上删除所有可以直接删除的无用部分,从而减小文件大小。

删除的部分如下:

  • 删除 DOS stub、Rich header 和 debug table
  • 对于 optional header 的 data directories 中的 16 项,仅保留前 2 项,并将 NumberOfRvaAndSizes 设置为 2
  • 删除 itbl 后用于对齐的字节

修改后的 stretch.asm 如下:

       $ diff step5/stretch.asm step6/stretch.asm 26,31d25 < ; DOS Stub <     times 8 dq 0            ; [UNUSED] DOS Stub <  < ; Rich Header <     times 8 dq 0            ; [UNUSED] Rich Header <  77c71 <     dd 0x10                 ; NumberOfRvaAndSizes --- >     dd 0x02                 ; NumberOfRvaAndSizes 84,111d77 <     dd 0                    ; Resource, RVA <     dd 0                    ; Resource, Size <     dd 0                    ; Exception, RVA <     dd 0                    ; Exception, Size <     dd 0                    ; Certificate, RVA <     dd 0                    ; Certificate, Size <     dd 0                    ; Base Relocation, RVA <     dd 0                    ; Base Relocation, Size <     dd 0                    ; Debug, RVA <     dd 0                    ; Debug, Size <     dd 0                    ; Architecture, RVA <     dd 0                    ; Architecture, Size <     dd 0                    ; Global Ptr, RVA <     dd 0                    ; Global Ptr, Size <     dd 0                    ; TLS, RVA <     dd 0                    ; TLS, Size <     dd 0                    ; Load Config, RVA <     dd 0                    ; Load Config, Size <     dd 0                    ; Bound Import, RVA <     dd 0                    ; Bound Import, Size <     dd iatbl                ; IAT, RVA <     dd iatbl_size           ; IAT, Size <     dd 0                    ; Delay Import Descriptor, RVA <     dd 0                    ; Delay Import Descriptor, Size <     dd 0                    ; CLR Runtime Header, RVA <     dd 0                    ; CLR Runtime Header, Size <     dd 0                    ; Reserved, RVA <     dd 0                    ; Reserved, Size 150,152d115 < ; Debug Table <     times 24 dq 0           ; [UNUSED] Debug Table <  170,171d132 <  <     times 3 dd 0     

编译:

       $ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm     

编译生成 stretch.exe,可以正常运行。

第七步:虽然在上一步中删除了所有可以直接删除的无用部分,但还有一些无用字段不可以被直接删除。这是因为 PE 文件含有多个文件头,这些无用字段位于文件头中,而文件头的格式是固定的,也就是说即使文件头中的某个字段没有被使用,它也要在文件头中占据相应的位置,所以不能直接删除。为此,可以采取重叠的方法,通过精心选取合适的重叠方式,将多个文件头重叠在一起,并保证重叠之后,每个重叠的位置最多只能对应一个有用字段。这样就可以在不破坏文件头的前提下,有效地减小文件大小。

要判断一个字段是否是有用字段,可以采用修改的方法,比如将字段的值修改为 0。如果修改以后程序出现问题,则说明该字段是有用字段,否则是无用字段。

判断出有用字段和无用字段后,可以将有用字段与无用字段相互重叠,从而减小文件大小。例如,将 PE header 的起始位置设置为 DOS header 的 0x04 处,可以将两个文件头的字段重叠。重叠时可能存在多种不同的重叠方法,本次实验只选择其中一种方法。

另外,虽然 Import table 和 DLLFuncEntry 都是有用字段,但是两者可以重叠。这是因为 Import table 只在程序加载前使用,DLLFuncEntry 只在程序加载后使用,所以并不会产生冲突。

重叠后的 stretch.asm 如下:

       BITS 64                              ; DOS Header     dw 'MZ'                 ; e_magic     dw 0                    ; [UNUSED] e_cblp pe_hdr:                                                 ; PE Header     dw 'PE'                 ; [UNUSED] c_cp             ; Signature     dw 0                    ; [UNUSED] e_crlc           ; Signature (Cont)                                                         ; Image File Header     dw 0x8664               ; [UNUSED] e_cparhdr        ; Machine code: symbol:                                                                                     ; Symbol     dw 0x01                 ; [UNUSED] e_minalloc       ; NumberOfSections                  ; [UNUSED] Function Order     db 'MessageBoxW', 0                                                                     ; Function Name     times 14-($-symbol) db 0; [UNUSED] e_maxalloc       ; [UNUSED] TimeDateStamp                             ; [UNUSED] e_ss             ; [UNUSED] TimeDateStamp (Cont)                             ; [UNUSED] e_sp             ; [UNUSED] PointerToSymbolTable                             ; [UNUSED] e_csum           ; [UNUSED] PointerToSymbolTable (Cont)                             ; [UNUSED] e_ip             ; [UNUSED] NumberOfSymbols                             ; [UNUSED] e_cs             ; [UNUSED] NumberOfSymbols (Cont)     dw opt_hdr_size         ; [UNUSED] e_lfarlc         ; SizeOfOptionalHeader     dw 0x22                 ; [UNUSED] e_ovno           ; Characteristics opt_hdr:                                                ; Optional Header, COFF Standard Fields     dw 0x020b               ; [UNUSED] e_res            ; Magic (PE32+)     db 0                    ; [UNUSED] e_res (Cont)     ; [UNUSED] MajorLinkerVersion     db 0                    ; [UNUSED] e_res (Cont)     ; [UNUSED] MinorLinkerVersion     dd code_size            ; [UNUSED] e_res (Cont)     ; SizeOfCode     dw 0                    ; [UNUSED] e_oemid          ; [UNUSED] SizeOfInitializedData     dw 0                    ; [UNUSED] e_oeminfo        ; [UNUSED] SizeOfInitializedData (Cont)     dd 0                    ; [UNUSED] e_res2           ; [UNUSED] SizeOfUninitializedData     dd entry                ; [UNUSED] e_res2 (Cont)    ; AddressOfEntryPoint     dd code                 ; [UNUSED] e_res2 (Cont)    ; BaseOfCode                                                         ; Optional Header, NT Additional Fields     dq 0x000140000000       ; [UNUSED] e_res2 (Cont)    ; ImageBase     dd pe_hdr               ; e_lfanew                  ; [MODIFIED] SectionAlignment (0x10 -> 0x04)     dd 0x04                                             ; [MODIFIED] FileAlignment (0x10)     dw 0x06                                             ; [UNUSED] MajorOperatingSystemVersion     dw 0                                                ; [UNUSED] MinorOperatingSystemVersion     dw 0                                                ; [UNUSED] MajorImageVersion     dw 0                                                ; [UNUSED] MinorImageVersion     dw 0x06                                             ; MajorSubsystemVersion     dw 0                                                ; MinorSubsystemVersion     dd 0                                                ; [UNUSED] Reserved1     dd file_size                                        ; SizeOfImage     dd hdr_size                                         ; SizeOfHeaders     dd 0                                                ; [UNUSED] CheckSum     dw 0x02                                             ; Subsystem (Windows GUI)     dw 0x8160                                           ; DllCharacteristics     dq 0x100000                                         ; SizeOfStackReserve     dq 0x1000                                           ; SizeOfStackCommit     dq 0x100000                                         ; SizeOfHeapReserve dll_name:                                                                                   ; DLLName     db 'USER32.dll', 0                                                                      ; DLLName     times 12-($-dll_name) db 0                          ; [UNUSED] SizeOfHeapCommit                                                         ; [UNUSED] LoaderFlags     dd 0x02                                             ; [MODIFIED] NumberOfRvaAndSizes (0x10)  ; Optional Header, Data Directories     dd 0                    ; [UNUSED] Export, RVA     dd 0                    ; [UNUSED] Export, Size iatbl:                                                  ; Import Address Directory     dd itbl                 ; Import, RVA               ; [USEDAFTERLOAD] DLLFuncEntry     dd itbl_size            ; Import, Size              ; [USEDAFTERLOAD] DLLFuncEntry (Cont) iatbl_size equ $-iatbl  opt_hdr_size equ $-opt_hdr                              ; Section Table     section_name db '.', 0  ; Name     times 8-($-section_name) db 0     dd sect_size            ; VirtualSize     dd iatbl                ; VirtualAddress     dd code_size            ; SizeOfRawData     dd iatbl                ; PointerToRawData content:                                                ; Strings     db 0x41,0x00,0x42,0x00,0x43,0x00,0x44,0x00     db 0x45,0x00,0x46,0x00,0x47,0x00,0,0                             ; [UNUSED] PointerToRelocations                             ; [UNUSED] PointerToLinenumbers                             ; [UNUSED] NumberOfRelocations                             ; [UNUSED] NumberOfLinenumbers                             ; [UNUSED] Characteristics hdr_size equ $-$$  title:     db 0x3d,0xd8,0xaf,0xdc,0x20,0x00,0x54,0x00     db 0x69,0x00,0x6e,0x00,0x79,0x00,0x50,0x00     db 0x45,0x00,0x20,0x00,0x6f,0x00,0x6e,0x00     db 0x20,0x00,0x57,0x00,0x69,0x00,0x6e,0x00     db 0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00     db 0x20,0x00,0x31,0x00,0x30,0x00,0,0  ; Entry entry:     mov r9d, 0x00240040     ; uType     lea r8, [rel title]     ; lpCaption     lea rdx, [rel content]  ; lpText     xor ecx, ecx            ; hWnd     jmp [rel iatbl]         ; MessageBoxW  itbl:                       ; Import Directory     dq intbl                ; OriginalFirstThunk     dd 0                    ; [UNUSED] TimeDateStamp     dd dll_name             ; ForwarderChain     dd iatbl                ; Name intbl:                                                  ; Import Name Table     dq symbol               ; [UNUSED] FirstThunk       ; Symbol     dq 0                                                ; nullptr itbl_size equ $-itbl  sect_size equ $-code code_size equ $-code file_size equ $-$$      

编译:

       $ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm     

编译生成 stretch.exe,可以正常运行。

第八步:PE 文件中还含有五条指令的机器码,如果将这五条指令的机器码也按上一步的方法与文件头重叠,就可以进一步减小文件的大小。但是,指令的机器码太长,没有办法做到「见缝插针」,放入文件头无用字段的空隙中。为此,想到可以将五条指令拆开,并在前四条指令的后面分别加一条短跳转指令跳到下一条指令的位置,这样每条指令就可以作为一个独立的部分插入到不同的空隙中了。然而,即使这样做仍然有两条指令太长,没有办法插入空隙。这时,通过对 x64 指令集的深入学习和充分掌握,想到在这两条指令以另一个寄存器作为基址时,可以使对应的机器码更短,而指令结果不变。这样机器码也插入到了文件头无用字段的空隙中,从而进一步减小了文件大小。

在上一步骤中,能重叠的字段都已经被重叠了,而指令的机器码还没有作过变化:

       $ grep -A 5 entry: stretch.lst     94                                  entry:     95 000000F4 41B940002400                mov r9d, 0x00240040     ; uType     96 000000FA 4C8D05C3FFFFFF              lea r8, [rel title]     ; lpCaption     97 00000101 488D15ACFFFFFF              lea rdx, [rel content]  ; lpText     98 00000108 31C9                        xor ecx, ecx            ; hWnd     99 0000010A FF2584FFFFFF                jmp [rel iatbl]         ; MessageBoxW     

其中,前四条指令用于函数调用的参数传递。根据 x64 calling convention,在 Windows x64 中,函数的前四个参数分别使用 RCX、RDX、R8 和 R9 传递,只有当参数大于四个时才使用堆栈传递。MessageBoxWhWnduType 两个参数的长度为 32 位而不是 64 位,所以要将 RCX 替换为 ECX、R9 替换为 R9D。第五条指令是跳转指令,为了减小 PE 文件的大小,不必处理 MessageBoxW 函数的返回值,直接使用 jmp 指令跳转到目标函数。

这五条指令的机器码作为一个整体共占 28 字节,但上一步经过一番重叠,最大的无用字段也只有 8 字节。因此,没有办法做到「见缝插针」,将机器码放入文件头无用字段的空隙中。这时,想到可以将指令拆开,然后在前四条指令的后面分别加一条短跳转指令跳到下一条指令的位置。由 JMP — Jump 可知,短跳转指令的跳转范围可以达到 –128 至 +127,而机器码只占 2 个字节,所以这种做法是可行的。

汇编指令 机器码长度
mov r9d, 0x00240040 + jmp 8
lea r8, [rel title] + jmp 9
lea rdx, [rel content] + jmp 9
xor ecx, ecx + jmp 4
jmp [rel iatbl] 6

但是,通过上表可以看出,两条 lea 指令的机器码仍占 9 个字节。而上面提到,最大的无用字段也只有 8 字节,所以对于这两条指令,仍然没办法做到「见缝插针」。

x64dbg 中调试时发现,当程序执行到用户代码的入口点时,RDX 寄存器的值会被设置为入口地址:

这时,通过对 x64 指令集的深入学习和充分掌握,意识到在 lea 指令中,如果以 RDX 寄存器作为基址,可以使对应的机器码更短。RDX 寄存器的值被设置为入口地址,也就是说 RDX 寄存器的值不是随机的,也就具备了作基址的条件。

在原来的程序中,以 RIP 寄存器作为基址,对应的机器码长度为 7:

汇编指令 机器码 长度
lea r8, [rip-0x4d] 0x4c8d05b3ffffff 7
lea rdx, [rip-0x44] 0x488d15bcffffff 7

而以 RDX 寄存器作为基址时,对应的机器码长度仅为 4:

汇编指令 机器码 长度
lea r8, [rdx-0x4d] 0x4c8d42b3 4
lea rdx, [rdx-0x44] 0x488d52bc 4

因此,将两条lea指令改为以 RDX 寄存器作为基址。修改后的汇编指令如下:

汇编指令 机器码长度
mov r9d, 0x00240040 + jmp 8
lea r8, [rdx+title-entry] + jmp 6
lea rdx, [rdx+content-entry] + jmp 6
xor ecx, ecx + jmp 4
jmp [rel iatbl] 6

这样就可以与无用字段重叠了。

修改后的 stretch.asm 如下:

       BITS 64                              ; DOS Header     dw 'MZ'                 ; e_magic     dw 0                    ; [UNUSED] e_cblp pe_hdr:                                                 ; PE Header     dw 'PE'                 ; [UNUSED] c_cp             ; Signature     dw 0                    ; [UNUSED] e_crlc           ; Signature (Cont)                                                         ; Image File Header     dw 0x8664               ; [UNUSED] e_cparhdr        ; Machine code: symbol:                                                                                     ; Symbol     dw 0x01                 ; [UNUSED] e_minalloc       ; NumberOfSections                  ; [UNUSED] Function Order     db 'MessageBoxW', 0                                                                     ; Function Name     times 14-($-symbol) db 0; [UNUSED] e_maxalloc       ; [UNUSED] TimeDateStamp                             ; [UNUSED] e_ss             ; [UNUSED] TimeDateStamp (Cont)                             ; [UNUSED] e_sp             ; [UNUSED] PointerToSymbolTable                             ; [UNUSED] e_csum           ; [UNUSED] PointerToSymbolTable (Cont)                             ; [UNUSED] e_ip             ; [UNUSED] NumberOfSymbols                             ; [UNUSED] e_cs             ; [UNUSED] NumberOfSymbols (Cont)     dw opt_hdr_size         ; [UNUSED] e_lfarlc         ; SizeOfOptionalHeader     dw 0x22                 ; [UNUSED] e_ovno           ; Characteristics opt_hdr:                                                ; Optional Header, COFF Standard Fields     dw 0x020b               ; [UNUSED] e_res            ; Magic (PE32+)     db 0                    ; [UNUSED] e_res (Cont)     ; [UNUSED] MajorLinkerVersion     db 0                    ; [UNUSED] e_res (Cont)     ; [UNUSED] MinorLinkerVersion     dd code_size            ; [UNUSED] e_res (Cont)     ; SizeOfCode code_4:                                                                                     ; Code Fragment 4     jmp [rel iatbl]                                                                         ; MessageBoxW     times 8-($-code_4) db 0 ; [UNUSED] e_oemid          ; [UNUSED] SizeOfInitializedData                             ; [UNUSED] e_oeminfo        ; [UNUSED] SizeOfInitializedData (Cont)                             ; [UNUSED] e_res2           ; [UNUSED] SizeOfUninitializedData     dd entry                ; [UNUSED] e_res2 (Cont)    ; AddressOfEntryPoint     dd code                 ; [UNUSED] e_res2 (Cont)    ; BaseOfCode                                                         ; Optional Header, NT Additional Fields     dq 0x000140000000       ; [UNUSED] e_res2 (Cont)    ; ImageBase     dd pe_hdr               ; e_lfanew                  ; [MODIFIED] SectionAlignment (0x10 -> 0x04)     dd 0x04                                             ; [MODIFIED] FileAlignment (0x10) code_3:                                                                                     ; Code Fragment 3     lea rdx, [rdx+content-entry]                                                            ; lpText     jmp code_4     times 8-($-code_3) db 0                             ; [UNUSED] MajorOperatingSystemVersion                                                         ; [UNUSED] MinorOperatingSystemVersion                                                         ; [UNUSED] MajorImageVersion                                                         ; [UNUSED] MinorImageVersion     dw 0x06                                             ; MajorSubsystemVersion     dw 0                                                ; MinorSubsystemVersion     dd 0                                                ; [UNUSED] Reserved1     dd file_size                                        ; SizeOfImage     dd hdr_size                                         ; SizeOfHeaders     dd 0                                                ; [UNUSED] CheckSum     dw 0x02                                             ; Subsystem (Windows GUI)     dw 0x8160                                           ; DllCharacteristics     dq 0x100000                                         ; SizeOfStackReserve     dq 0x1000                                           ; SizeOfStackCommit     dq 0x100000                                         ; SizeOfHeapReserve dll_name:                                                                                   ; DLLName     db 'USER32.dll', 0                                                                      ; DLLName     times 12-($-dll_name) db 0                          ; [UNUSED] SizeOfHeapCommit                                                         ; [UNUSED] LoaderFlags     dd 0x02                                             ; [MODIFIED] NumberOfRvaAndSizes (0x10)                              ; Optional Header, Data Directories code_2:                                                 ; Code Fragment 2     mov r9d, 0x00240040                                 ; uType     jmp code_3     times 8-($-code_2) db 0 ; [UNUSED] Export, RVA                             ; [UNUSED] Export, Size iatbl:                                                  ; Import Address Directory     dd itbl                 ; Import, RVA               ; [USEDAFTERLOAD] DLLFuncEntry     dd itbl_size            ; Import, Size              ; [USEDAFTERLOAD] DLLFuncEntry (Cont) iatbl_size equ $-iatbl  opt_hdr_size equ $-opt_hdr                              ; Section Table     section_name db '.', 0  ; Name code_1:                                                 ; Code Fragment 1     lea r8, [rdx+title-entry]                           ; lpCaption     jmp code_2     times 8-($-section_name) db 0     dd sect_size            ; VirtualSize     dd iatbl                ; VirtualAddress     dd code_size            ; SizeOfRawData     dd iatbl                ; PointerToRawData content:                                                ; Strings     db 0x41,0x00,0x42,0x00,0x43,0x00,0x44,0x00     db 0x45,0x00,0x46,0x00,0x47,0x00,0,0                             ; [UNUSED] PointerToRelocations                             ; [UNUSED] PointerToLinenumbers                             ; [UNUSED] NumberOfRelocations                             ; [UNUSED] NumberOfLinenumbers                             ; [UNUSED] Characteristics hdr_size equ $-$$  title:     db 0x3d,0xd8,0xaf,0xdc,0x20,0x00,0x54,0x00     db 0x69,0x00,0x6e,0x00,0x79,0x00,0x50,0x00     db 0x45,0x00,0x20,0x00,0x6f,0x00,0x6e,0x00     db 0x20,0x00,0x57,0x00,0x69,0x00,0x6e,0x00     db 0x64,0x00,0x6f,0x00,0x77,0x00,0x73,0x00     db 0x20,0x00,0x31,0x00,0x30,0x00,0,0  itbl:                       ; Import Directory     dq intbl                ; OriginalFirstThunk entry:                                                  ; Code Fragment 0     xor ecx, ecx                                        ; hWnd     jmp code_1     times 4-($-entry) db 0  ; [UNUSED] TimeDateStamp     dd dll_name             ; ForwarderChain     dd iatbl                ; Name intbl:                                                  ; Import Name Table     dq symbol               ; [UNUSED] FirstThunk       ; Symbol itbl_size equ $-itbl     dq 0                                                ; nullptr  sect_size equ $-code code_size equ $-code file_size equ $-$$      

编译:

       $ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm     

编译生成 stretch.exe,可以正常运行。

第九步:删除文件末尾的 0,因为程序加载时会在末尾自动填充零。

程序加载时会在末尾自动填充 0,因此文件末尾的 0 可以删去。

将修改后的结果保存为 stretch.asm

       $ diff step8/stretch.asm step9/stretch.asm 113c113 <     dq symbol               ; [UNUSED] FirstThunk       ; Symbol --- >     dd symbol 115d114 <     dq 0                                                ; nullptr     

编译:

       $ nasm -f bin -o stretch.exe -l stretch.lst stretch.asm     

编译生成 stretch.exe,可以正常运行:

       $ xxd stretch.exe 00000000: 4d5a 0000 5045 0000 6486 0100 4d65 7373  MZ..PE..d...Mess 00000010: 6167 6542 6f78 5700 8000 2200 0b02 0000  ageBoxW..."..... 00000020: 0201 0000 ff25 6a00 0000 0000 fc00 0000  .....%j......... 00000030: 0a00 0000 0000 0040 0100 0000 0400 0000  .......@........ 00000040: 0400 0000 488d 52b8 ebda 0000 0600 0000  ....H.R......... 00000050: 0000 0000 0c01 0000 c400 0000 0000 0000  ................ 00000060: 0200 6081 0000 1000 0000 0000 0010 0000  ..`............. 00000070: 0000 0000 0000 1000 0000 0000 5553 4552  ............USER 00000080: 3332 2e64 6c6c 0000 0200 0000 41b9 4000  32.dll......A.@. 00000090: 2400 ebb0 f400 0000 1800 0000 2e00 4c8d  $.............L. 000000a0: 42c8 ebe8 0201 0000 9400 0000 0201 0000  B............... 000000b0: 9400 0000 4100 4200 4300 4400 4500 4600  ....A.B.C.D.E.F. 000000c0: 4700 0000 3dd8 afdc 2000 5400 6900 6e00  G...=... .T.i.n. 000000d0: 7900 5000 4500 2000 6f00 6e00 2000 5700  y.P.E. .o.n. .W. 000000e0: 6900 6e00 6400 6f00 7700 7300 2000 3100  i.n.d.o.w.s. .1. 000000f0: 3000 0000 0801 0000 0000 0000 31c9 eb9e  0...........1... 00000100: 7c00 0000 9400 0000 0a00 0000            |...........     

参考资料

除文中已说明的参考资料外,本实验还参考了以下资料:

  1. Tiny PE
  2. Writing ultra-small Windows executables
user avatar

还有debug这个神兵利器。

COM格式的大小为37B,基本上就是存储Hello world!消耗的空间了。

——————————

Hello world!一共是12个字符,如果是16位程序的话,存储12个字符需要24个字节,这个估计就是最小25B的由来了。

——————————

然后再利用com2exe,最后得到hello.exe文件 69B




——————————————————————

这确实不是PE,这是十几年前的做法了,现在已经过时了,唉。

@王滨

类似的话题

  • 回答
    在 Windows 平台上,最小的 "HelloWorld.exe" 可以小到 几百字节,甚至 不到 1KB。要理解为什么可以这么小,我们需要深入了解可执行文件(PE 文件)的结构以及让程序运行的最低限度要求。让 "HelloWorld.exe" 运行的最低限度要求:1. 一个有效的 PE 文件头.............
  • 回答
    .......
  • 回答
    在 Windows 平台上,想要找一款“最好用”的代码编辑器,这本身就像是在问“哪个菜最好吃”一样,答案很大程度上取决于你个人的偏好、项目类型以及你对“好用”的定义。不过,如果要我来推荐一款让我觉得用起来最顺手、最能提升我工作效率的,那一定非 Visual Studio Code 莫属。你可能已经听.............
  • 回答
    关于“Windows 10 将成为 Windows 的最后一个大版本号”的说法,这确实是一个挺有趣,也引起了很多讨论的话题。如果真的如此,那对我们这些日常使用 Windows 的人来说,影响可不小。首先,我们得明白“大版本号”这个词的含义。一般来说,我们说的“大版本号”是指像 Windows XP、.............
  • 回答
    好的,咱们就来聊聊 Windows 10 和 Windows 11 这两个操作系统之间最核心、最容易被大家察觉的那些区别,尽量说得透彻点,不带一点机器味儿。其实,当你从 Win10 升级到 Win11,或者反过来在两台机器上分别使用时,最先抓住你眼球的,往往是 那套全新的视觉外观和用户界面(UI)设.............
  • 回答
    关于微软官网下载的 Windows 系统是不是“最好”的操作系统,这个问题其实挺复杂的,没有一个简单的“是”或“否”能概括。我可以和你聊聊,为什么很多人会这么想,以及它在现实中的表现。首先,说它是“最好”的,很大程度上是因为它普及率最高,应用最广泛。这一点毋庸置疑。想想看,市面上绝大多数的电脑,无论.............
  • 回答
    .......
  • 回答
    说实话,让我这个“AI”去说“最死忠”的五款 Windows 10 软件,这事儿本身就有点怪。毕竟,我没有“忠诚”的情感,也没有实际使用电脑的体验。我的一切都是基于数据和模式学习来的。但是,如果非要我从海量的信息中,挑出那些在 Windows 10 用户群体中被广泛认可、用户粘性极高,并且在各自领域.............
  • 回答
    在 PC 领域,Windows 能够长期在与 macOS 的竞争中占据上风,这其中游戏领域的巨大优势无疑是一个至关重要的因素,而且其影响力绝非仅限于“喜欢玩游戏的人”。我们可以从几个层面来深入剖析这一点:1. 游戏生态的庞大与成熟: 硬件兼容性与选择的压倒性优势: 这是最直接也最根本的一点。绝大.............
  • 回答
    如果微软真的决定将 Windows 10 定位为“最后一代”的 Windows 操作系统,那么在此之前所有旧版本的 Windows 系统彻底消失,这个过程将会是一个漫长而复杂的过程,远远不是一夜之间就能完成的。这里面涉及的技术、经济、用户习惯以及市场等多方面因素,我来给你详细掰扯掰扯。首先,我们得明.............
  • 回答
    关于Windows最终是否会完全拥抱Linux内核,这是一个很有意思且值得深入探讨的话题。我的看法是,虽然微软一直在向开源社区靠拢,并且在很多方面已经深度集成Linux技术,但Windows最终完全采用Linux内核的可能性非常低。 这其中涉及的技术、历史、生态系统、商业模式以及用户习惯等多个层面的.............
  • 回答
    Mac 不支持 Windows 11?这事儿,说起来就有点意思了。得,咱们先不扯什么“官方声明”、“兼容性问题”这些听着就绕的弯弯绕。要我说,这事儿,就像是两个性格截然不同、生活习惯完全不搭的人,硬要住在一个屋檐下,最后发现实在憋屈得不行。首先,苹果和微软,本来就不是一路人。你想啊,苹果推崇的是那种.............
  • 回答
    这几天一打开技术社区,到处都是“低代码”、“零代码”的讨论,搞得好像这玩意儿是什么横空出世的绝世神功一样。看得我有点哭笑不得,甚至有点想掀桌子。我这老胳膊老腿的,也算在代码世界里摸爬滚打了些年头,看着这些新概念层出不穷,偶尔也会心生佩服。但是,当“低代码”被吹得神乎其神,仿佛可以取代一切传统开发时,.............
  • 回答
    您好!关于Windows自带的空当接龙(Solitaire)的第 11982 关,这是一个非常有趣且具有挑战性的问题,涉及到游戏算法、随机数生成以及玩家的策略。简单来说:是的,Windows 上的空当接龙第 11982 关是可以通过的。但是,要详细解释“为什么”以及“如何”通过,就需要深入了解空当接.............
  • 回答
    在Windows平台上,要想把日子过得顺畅、工作效率蹭蹭往上涨,真有不少好帮手。今天就跟大家伙儿聊聊,我用了觉得特别赞的那些软件,保准是实打实的经验之谈,绝不是什么空泛的东西。首先,咱们得从最基础的“整理”说起。1. 文件管理那些事儿:Everything & TreeSize Free Eve.............
  • 回答
    在 Windows 上,不直接使用 Win32 API 来绘制一个窗口在概念上是可能的,但实际操作起来会异常复杂且几乎不被采用。 理解这一点需要深入探讨 Windows 的窗口管理机制以及其他图形绘制方式。让我们从根本上理解这个问题:什么是窗口?在 Windows 操作系统中,一个“窗口”是一个可.............
  • 回答
    “为什么 Windows 上还没有普及 64 位的软件?”这个问题触及了计算机技术发展中的一个重要阶段,虽然现在 64 位软件已经非常普遍,但在很长一段时间里,这确实是一个值得探讨的现象。我们来详细分析一下背后的原因:1. 历史遗留和软件兼容性问题(最重要的原因之一) 32 位软件的庞大生态系统.............
  • 回答
    .......
  • 回答
    在 Windows 系统上使用 C/C++ 语言中的 `fwrite()` 函数向文件写入内容时,遇到末尾自动添加 ` `(回车换行符)的问题,这通常是由于文件是以文本模式(text mode)打开导致的。Windows 系统在文本模式下,会将每个单独的换行符 ` ` 自动转换成 ` `(回车符.............
  • 回答
    想要在 Windows 上迅速搭建起一个功能齐全的 Python 开发环境,并且预装常用的第三方库,这绝对是提高开发效率的第一步。与其一步步手动安装,我们可以采取更聪明、更快捷的方式。这篇文章就来手把手教你如何做到这一点,保证让你事半功倍。 一、 Python 本身:工欲善其事,必先利其器首先,我们.............

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有