资讯详情

资讯详情

EXE2BIN原理:DOS EXE到裸二进制的内存镜像转换

简介本资源是一份面向C语言初学者与DOS系统爱好者的技术学习材料提供经典工具EXE2BIN的完整C语言实现源码用于将DOS可执行文件.EXE剥离头信息、提取纯机器码并生成无格式二进制文件.BIN适用于嵌入式引导程序开发、ROM烧录准备及底层逆向分析等场景。压缩包共6个文件11KB含1个核心C源文件exe2com.c、3个说明类TXT文档含下载指引与功能说明、1个DOC格式技术文档及1个已编译的DOS可执行COM文件结构精简便于对照源码理解DOS文件格式解析、低级文件I/O与内存布局处理逻辑。目前已有248人学习下载读者可通过阅读源码掌握EXE头部结构解析、段地址计算、代码段提取等关键技能并借助配套说明文档快速复现编译与测试流程是深入理解早期PC平台程序加载机制与C语言系统编程实践的优质入门范例。1. EXE2BIN 不是文件格式转换器而是 DOS 时代内存加载逻辑的具象化实现很多人第一次看到EXE2BIN会下意识认为它只是把.EXE文件“去掉头”变成裸二进制——这种理解在技术表层成立但完全错过了它的设计本质。EXE2BIN 的核心任务是将 DOS 可执行文件中真正可被LOAD和CALL的代码段CS:IP与初始化数据段DS内容按实际运行时的内存布局线性拼接为一段连续、无重定位信息、可被 BIOS 或引导扇区直接jmp far跳转执行的原始字节流。它不处理.EXE中的重定位表、堆栈定义、覆盖段或 DOS 4.0 的扩展头它只信任header-e_cs、header-e_ip、header-e_ss、header-e_sp和header-e_lfanew若存在所共同描述的“加载后镜像”。这意味着用现代objcopy -O binary处理一个带重定位的.EXE大概率生成无法运行的.BIN而 EXE2BIN 源码里对e_cblp、e_cp、e_lfarlc等字段的精确计算恰恰是它能在 Turbo C 2.0 DOS 3.3 环境下稳定产出可烧录到 ROM 或加载到0x7C00的关键。它适合三类人正在逆向分析老游戏启动模块的固件工程师、需要在 QEMU 中复现真实 DOS 引导链的教学者以及想亲手触摸“程序如何从磁盘变成 CPU 指令”的 C 语言底层实践者。2. EXE2BIN 的源码结构解析从 DOS .EXE 文件头到裸二进制输出的四步映射EXE2BIN 的 C 源码虽小通常仅 1–2 个.c文件但其逻辑严格遵循 DOS MZ 格式规范Microsoft Linker 1.x/2.x 输出格式。它不是通用二进制提取器而是一个针对.EXE文件头字段进行硬编码解析的专用工具。理解其源码必须先厘清 DOS.EXE文件头IMAGE_DOS_HEADER中几个决定性字段的含义与联动关系。2.1 DOS EXE 文件头关键字段及其在 EXE2BIN 中的用途DOS.EXE文件以 64 字节的IMAGE_DOS_HEADER开头其中e_lfanew字段偏移 0x3C在 DOS 环境下始终为 0这是与 Windows PE 文件最根本的区别。EXE2BIN 源码中不会读取该字段而是直接依赖以下字段字段名偏移hex含义EXE2BIN 中如何使用e_cblp0x02最后一页字节数1–512计算文件总字节数filesize (e_cp - 1) * 512 e_cblpe_cp0x04文件总页数每页 512 字节同上用于校验输入文件完整性e_cparhdr0x08头部大小以 16 字节为单位header_size e_cparhdr * 16即跳过头部后才是代码/数据起始e_minalloc0x0C最小需分配的额外内存段通常忽略EXE2BIN 不做内存分配模拟e_maxalloc0x0E最大可分配内存段同上e_ss0x10初始 SS 值堆栈段写入.BIN文件头前 2 字节作为加载后 SSe_sp0x12初始 SP 值堆栈指针写入.BIN文件头第 3–4 字节作为加载后 SPe_ip0x14初始 IP 值入口点偏移写入.BIN文件头第 5–6 字节作为加载后 IPe_cs0x16初始 CS 值代码段写入.BIN文件头第 7–8 字节作为加载后 CSe_lfarlc0x18重定位表偏移DOS 下常为 0若非零EXE2BIN 通常报错或跳过重定位处理提示EXE2BIN 源码中几乎不会出现#include windows.h或IMAGE_NT_HEADERS相关定义。它只包含stdio.h、stdlib.h和string.h所有文件操作均使用fread()/fwrite()进行原始字节读写不调用任何 DOS 扩展 API如 INT 21h 的 4Bh 功能。这是它能在 Turbo C 1.5 编译器下运行的根本原因。2.2 源码主流程四阶段字节流构造典型 EXE2BIN 源码如exe2bin.c的main()函数执行流程可拆解为以下四个不可省略的阶段2.2.1 阶段一文件头合法性校验与基础参数提取FILE *fp fopen(argv[1], rb); if (!fp) { perror(Cannot open input file); return 1; } // 读取 DOS MZ 签名 unsigned char header[64]; if (fread(header, 1, 64, fp) ! 64) { fprintf(stderr, Read error: header too short\n); fclose(fp); return 1; } if (header[0] ! M || header[1] ! Z) { fprintf(stderr, Not a valid DOS EXE file\n); fclose(fp); return 1; } // 提取关键字段注意DOS 是小端序 unsigned int e_cblp (header[3] 8) | header[2]; // 0x02 unsigned int e_cp (header[5] 8) | header[4]; // 0x04 unsigned int e_cparhdr (header[9] 8) | header[8]; // 0x08 unsigned int e_ss (header[17] 8) | header[16]; // 0x10 unsigned int e_sp (header[19] 8) | header[18]; // 0x12 unsigned int e_ip (header[21] 8) | header[20]; // 0x14 unsigned int e_cs (header[23] 8) | header[22]; // 0x16这段代码的关键在于它不依赖任何结构体定义而是用硬编码偏移 手动字节拼接来读取字段。这是因为 Turbo C 的struct对齐规则与 DOS.EXE文件头物理布局不完全一致直接fread(dos_hdr, sizeof(dos_hdr), 1, fp)可能因填充字节导致错位。此处header[2]和header[3]的顺序正是 Intel x86 小端序LSB 在前的体现。2.2.2 阶段二计算有效载荷起始位置与长度DOS.EXE文件中代码和数据并非紧随文件头之后。它们位于header_size字节之后且可能跨越多个 512 字节页。EXE2BIN 必须精确计算出从哪一字节开始读取读多少字节unsigned int header_size e_cparhdr * 16; unsigned int file_size (e_cp - 1) * 512 e_cblp; // 定位到代码/数据起始跳过 header fseek(fp, header_size, SEEK_SET); // 分配缓冲区足够容纳全部代码数据不含 header unsigned char *payload malloc(file_size - header_size); if (!payload) { fprintf(stderr, Out of memory\n); fclose(fp); return 1; } size_t payload_len fread(payload, 1, file_size - header_size, fp); if (payload_len ! file_size - header_size) { fprintf(stderr, Truncated read: expected %u, got %zu\n, file_size - header_size, payload_len); free(payload); fclose(fp); return 1; }这里file_size - header_size是核心公式。e_cp表示整个文件占多少个 512 字节页e_cblp给出最后一页的实际字节数二者组合得出真实文件大小减去header_size即得到.EXE文件中“有效机器码数据”的总长度。这个长度直接决定了.BIN文件的主体大小。2.2.3 阶段三构造 BIN 文件头8 字节 DOS 兼容启动头.BIN文件并非纯裸代码。为兼容 DOS 加载器如DEBUG的L命令或某些引导加载器EXE2BIN 会在.BIN文件最开头写入 8 字节的“伪头”其内容就是e_ss,e_sp,e_ip,e_cs四个字段的原始值FILE *out fopen(argv[2], wb); if (!out) { perror(Cannot open output file); free(payload); fclose(fp); return 1; } // 写入 8 字节头SS, SP, IP, CS各 2 字节小端 fputc(e_ss 0xFF, out); fputc((e_ss 8) 0xFF, out); fputc(e_sp 0xFF, out); fputc((e_sp 8) 0xFF, out); fputc(e_ip 0xFF, out); fputc((e_ip 8) 0xFF, out); fputc(e_cs 0xFF, out); fputc((e_cs 8) 0xFF, out); // 写入 payload 主体 fwrite(payload, 1, payload_len, out); fclose(out); free(payload); fclose(fp);这 8 字节是.BIN文件能被DEBUG正确U反汇编或G执行的前提。DEBUG读取.BIN时会自动将前 2 字节作为 SS次 2 字节作为 SP再 2 字节为 IP最后 2 字节为 CS并据此设置寄存器后开始执行。没有这 8 字节.BIN就是一段无法定位入口的乱码。2.2.4 阶段四边界条件处理与错误反馈真实 EXE2BIN 源码中必然包含对异常情况的防御性检查这些检查直接决定了工具的鲁棒性e_cblp 0的处理DOS 规范允许e_cblp为 0此时e_cp应至少为 1表示文件大小恰为e_cp * 512。源码中需判断e_cblp 0 ? e_cp * 512 : (e_cp - 1) * 512 e_cblp。e_cparhdr * 16 64的处理极少数 DOS 工具生成的.EXE可能有扩展头此时e_cparhdr可能大于 4即header_size 64。标准 EXE2BIN 通常拒绝处理或尝试fseek(fp, 0, SEEK_SET); fread(extended_header, 1, header_size, fp)读取完整头。payload_len 0的处理当.EXE仅含头信息如某些 stub 或空程序payload_len为 0此时.BIN文件应只有 8 字节头。源码需允许此情况而非报错退出。这些细节在 Turbo C 2.0 的exe2bin.exe实际行为中均可验证用DEBUG加载一个仅含头的.EXEL命令后U会显示0000:0000处的指令正是那 8 字节头的解释结果。3. 在现代 Linux/macOS 环境下编译与验证 EXE2BIN 源码虽然 EXE2BIN 是为 DOS 设计的但其 C 源码本身是高度可移植的 ANSI C。在 Linux 或 macOS 上我们不仅能编译它更能用现代工具链对其进行深度验证从而确认其行为与原始 DOS 版本的一致性。3.1 使用 GCC 编译并生成可执行文件假设解压后的源码为exe2bin.c其内容符合上述四阶段逻辑。在 Ubuntu 22.04 或 macOS Ventura 上使用系统自带 GCC 即可编译gcc -stdc89 -Wall -Wextra -O2 -o exe2bin-linux exe2bin.c参数说明-stdc89强制使用 ANSI C 89 标准禁用 C99/C11 特性如//注释、inline确保与 Turbo C 兼容-Wall -Wextra开启全部警告暴露潜在的未初始化变量、类型转换问题-O2启用二级优化提升执行效率但不改变语义EXE2BIN 无循环依赖安全-o exe2bin-linux指定输出文件名避免与 DOS 原版混淆。编译成功后./exe2bin-linux即为可在 Linux 下运行的 EXE2BIN 工具。它接受两个参数输入.EXE文件路径和输出.BIN文件路径。3.2 构造测试用 DOS EXE 文件并验证转换结果要验证编译出的exe2bin-linux是否正确必须准备一个已知结构的 DOS.EXE文件。最可靠的方法是用 NASM 编写一个极简的 DOS COM 风格程序再用ld链接为.EXE; hello.asm org 0x100 bits 16 start: mov dx, msg mov ah, 09h int 21h mov ax, 4C00h int 21h msg db Hello from EXE2BIN!, 0Dh, 0Ah, $使用以下命令生成.EXEnasm -f bin -o hello.com hello.asm # 将 COM 转为最小 EXE添加标准 DOS 头 printf \x4d\x5a\x00\x00\x02\x00\x00\x00\x04\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x...... hello.exe # 实际中更推荐用 debug.com 或专门的 exe2com 工具生成标准 EXE注意手动构造.EXE头易出错。更推荐使用exe2com本压缩包中提及的工具将已知良好的.COM文件转为.EXE再用exe2bin-linux转回.BIN最后用xxd对比字节。3.3 使用 xxd 和 objdump 进行字节级与反汇编验证验证的核心是确认.BIN文件的前 8 字节与.EXE头中e_ss/e_sp/e_ip/e_cs完全一致且后续字节与.EXE中header_size之后的内容完全相同# 提取 EXE 头字段使用 Python 快速解析 python3 -c import sys with open(hello.exe, rb) as f: h f.read(64) ss h[0x10] (h[0x11] 8) sp h[0x12] (h[0x13] 8) ip h[0x14] (h[0x15] 8) cs h[0x16] (h[0x17] 8) print(fSS{ss:04X} SP{sp:04X} IP{ip:04X} CS{cs:04X}) # 查看 BIN 文件前 16 字节头 前几条指令 xxd -l 16 hello.bin # 反汇编 BIN 文件指定 16 位模式、起始地址 0x0000 objdump -b binary -m i8086 -D --adjust-vma0x0 hello.bin | head -n 20输出应显示xxd的前 8 字节00000000:行与 Python 计算出的SS/SP/IP/CS的小端序十六进制值完全匹配objdump的反汇编结果其第一条指令地址为00000000 .data且指令序列与原始.COM程序的org 0x100下的汇编逻辑一致如mov dx,0x10e等证明代码段被无损提取。若xxd显示前 8 字节错位或objdump报错Invalid instruction则说明源码中字节序处理有误或header_size计算错误——这正是阅读和调试 EXE2BIN 源码时最常踩的坑。4. 在 DOSBox 中运行原始 EXE2BIN 并对比行为差异现代编译的exe2bin-linux是验证工具但要真正理解其设计哲学必须回到 DOS 环境用原汁原味的exe2bin.exe由 Turbo C 编译执行并观察其与 DOS 系统的交互细节。DOSBox 是目前最可靠的 DOS 模拟环境它精确模拟了 8086/80286 CPU、BIOS 中断和文件系统。4.1 在 DOSBox 中配置 Turbo C 2.0 并编译 EXE2BIN首先从互联网归档如 WinWorld PC下载 Turbo C 2.0 安装包tc201.zip解压后挂载到 DOSBox# 在 DOSBox 配置文件 dosbox.conf 中添加 [autoexec] mount c c:\path\to\tc201 c: cd \tc tc启动 Turbo C IDE 后打开exe2bin.c选择Options → Compiler → Code Generation确保Memory Model设为Tiny因为 EXE2BIN 自身是.COM风格小程序无需段寻址Far Calls和Far Data均关闭。然后按AltF9编译F9连接生成exe2bin.exe。4.2 关键 DOS 特定行为观察INT 21h 与文件句柄在 DOS 下exe2bin.exe的文件操作不依赖 libc 的fopen()封装而是直接调用 DOS INT 21h 功能AH3DhOpen File以AL0只读打开输入.EXEAH3FhRead from Handle读取文件头和 payloadAH40hWrite to Handle写入.BIN文件AH3EhClose Handle关闭所有句柄。这些调用在 DOSBox 中可被DEBUG拦截观察debug exe2bin.exe -d 100 120 ; 查看代码段起始寻找 int 21h 指令 -t ; 单步执行观察 AX 寄存器值变化你会发现当AX被设为3D00h时紧接着就是int 21h这正是 DOS 打开文件的标准流程。而现代 Linux 版本的exe2bin-linux则完全绕过此层直接使用 POSIXopen()/read()/write()。这种差异不是缺陷而是平台适配的必然——EXE2BIN 的核心逻辑字段解析、字节拼接是跨平台的只是 I/O 接口不同。4.3 实际运行对比同一输入文件在 DOS 与 Linux 下的输出一致性准备一个真实 DOS 游戏的.EXE如COMMAND.COM或EDIT.COM的.EXE版本分别在 DOSBox 和 Linux 下运行转换# Linux 下 ./exe2bin-linux command.exe command-linux.bin # DOSBox 中假设已将 command.exe 复制到 C:\ C:\ exe2bin command.exe command-dos.bin然后在 Linux 下用cmp命令对比两个.BIN文件cmp command-linux.bin command-dos.bin如果输出为空说明二者完全一致。这证明尽管运行环境天差地别但 EXE2BIN 的算法逻辑是确定性的、与平台无关的。它的价值不在于“能在 DOS 上跑”而在于其源码是 DOS 文件格式规范的一份可执行注释——每一行fread()都对应一个 MZ 头字段每一个fputc()都是对加载器行为的精准模拟。5. 源码级调试技巧如何快速定位 EXE2BIN 的字段解析错误当你拿到一份未经验证的 EXE2BIN C 源码比如从downcode.com下载的.7z包第一步不是编译而是用hexdump和笔纸进行静态分析。以下是一套经过实战检验的三步定位法专治各类.BIN输出异常。5.1 第一步用 hexdump 快速提取并验证 DOS EXE 头字段对任意.EXE文件执行hexdump -C -n 64 program.exe | head -n 5输出类似00000000 4d 5a 90 00 03 00 00 00 04 00 00 00 ff ff 00 00 |MZ..............| 00000010 b8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00 |...............| 00000020 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................| 00000030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |................|对照上表手动计算e_cblp在0000000200 03→0x0300 768不对注意小端序00000002处是0000000003处是03所以e_cblp 0x0300 768e_cp在0000000400 04→0x0400 1024e_cparhdr在0000000800 04→0x0400 1024等等00000008是0400000009是00所以e_cparhdr 0x0004 4e_ss在00000010b8 00→0x00b8 184e_sp在0000001200 00→0x0000 0e_ip在0000001400 00→0x0000e_cs在0000001640 00→0x0040 64。将这些值代入源码中的计算公式看是否与file_size和header_size的实际值吻合。例如若e_cparhdr 4则header_size 4 * 16 64hexdump的前 64 字节即为头00000040开始就是 payload 起始。5.2 第二步在源码关键位置插入 printf 调试桩不要依赖 GDB 在 DOS 环境下调试太复杂。在 Linux 编译版中直接修改源码加入字段打印// 在提取字段后添加 printf(e_cblp%u e_cp%u e_cparhdr%u\n, e_cblp, e_cp, e_cparhdr); printf(e_ss%04X e_sp%04X e_ip%04X e_cs%04X\n, e_ss, e_sp, e_ip, e_cs); printf(header_size%u file_size%u\n, header_size, file_size);重新编译运行观察输出是否与hexdump手动计算值一致。若不一致问题必在字节序或偏移计算上。5.3 第三步用 dd 命令手工提取 payload 并与 BIN 主体比对这是最终验证手段。假设header_size 64file_size 2048则 payload 应为2048 - 64 1984字节位于.EXE文件偏移 64 处dd ifprogram.exe ofpayload-by-dd.bin bs1 skip64 count1984 # 生成的 payload-by-dd.bin 应与 exe2bin 输出的 .BIN 文件去掉前 8 字节后完全一致 tail -c 9 program.bin | cmp - payload-by-dd.bin若cmp输出为空则证明 EXE2BIN 的 payload 提取逻辑 100% 正确若有差异说明源码中fseek()或fread()的参数有误需检查fseek(fp, header_size, SEEK_SET)是否被执行以及payload_len是否等于file_size - header_size。这套方法不依赖任何 IDE 或高级调试器仅用命令行和基本 C 语言知识就能在 10 分钟内定位 90% 的 EXE2BIN 源码缺陷。它把抽象的“文件格式解析”还原为具体的“字节位置计算”这才是阅读和维护这类经典底层工具源码的正确姿势。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →