编译器构造:模拟器,汇编与反汇编

前言:

这个秋季,三碗饭会开始更新自己在秋季课程的笔记,主要包括Introduction to ML,可能还会有Complier Construction 以及 Computer Graphics.

本文将介绍Complier Construction中关于模拟器,汇编以及反汇编的知识,在下一章我们会从0写一个简易模拟器,以及汇编,反汇编程序。这门课如果没有兴趣,会非常无聊非常无聊,当然我一开始也觉得挺无聊的,毕竟搞AI Agent比这个有意思多,但我还是会尽量以简单的描述(避免复杂的解释),以及有意思的例子去讲这些知识。

前言

在浏览博客的时候看到一个挺有意思的思考,我们什么时候会需要去写编译器,或者反编译程序?

当然我要修学分写作业必须写lol

其他呢?

(1)有一种情况,你从网站下载了一个自动清理桌面的exe文件,通过工具分析,你知道这个文件的是用C写的,你觉得这个exe文件其实功能挺不错,但有些缺点,比如自动扫描磁盘效率低,你也找不到这个文件主人,于是你想修改这个文件,于是你用一个反编译工具把这个程序从EXE反编译为其原来汇编语言的伪代码(无法得到一模一样的源代码),这样你就可以对源代码进行修改和完善,然后再重新编译成EXE。

(2)第2个就比较"刑",我第一次听到也挺感兴趣。有一些软件你下载了运行需要注册,输入序列号才能使用。因为exe是CPU才能看懂的机器代码,这时候你就可以用到反编译技术,将其翻译为我们能看懂的汇编语言,然后你在代码中寻找判断是否注册的语句,进行修改就可以免费使用。相信大家或多或少在生活学习中有遇到这些情况。

1:编译器和反编译器

(1)编译器(Compiler)

编译器的核心用途就是把高级语言(C、Java、Haskell 等)翻译成机器可以执行的低级语言(汇编、目标文件、机器码)。典型场景:

我们自己写程序需要执行,所以要编译。

操作系统内核、驱动程序、数据库等系统级软件,都依赖高效的编译器。

(2)反编译(Decompiler)与反汇编(Disassembler)

反汇编:机器码 → 汇编。工具如 objdump,这是最常见的,因为机器码和汇编是一一对应的。

反编译:机器码 → 高级语言。比如 IDA Pro、Ghidra,结果往往不如源代码整洁,因为编译优化会丢失很多语义。

2:一个C++程序是如何运行的?

2.1 总体流程

源代码.cpp 预处理器 编译器 汇编器 链接器 可执行文件

2.2 预处理阶段

源代码 预处理器 头文件.h 宏定义 预处理后的代码

示例:

cpp 复制代码
// 输入
#include <iostream>
#define MAX 100
int main() { return MAX; }

// 输出
// iostream内容展开
int main() { return 100; }

2.3 编译阶段

预处理后代码 词法分析 语法分析 语义分析 中间代码 汇编代码

示例:

cpp 复制代码
// 输入
int add(int a, int b) {
    return a + b;
}

// 输出(汇编代码)
add:
    push rbp
    mov  rbp, rsp
    mov  eax, edi
    add  eax, esi
    pop  rbp
    ret

2.4 汇编阶段

汇编代码 汇编器 目标文件.o

目标文件内容:

复制代码
二进制格式:
+-------------+
| ELF头       |
+-------------+
| 代码段      |
+-------------+
| 数据段      |
+-------------+
| 重定位信息  |
+-------------+

2.5 链接阶段

目标文件1.o 链接器 目标文件2.o 库文件.lib 可执行文件

2.6 内存布局

高地址 栈区 ... 堆区 BSS段 数据段 代码段 低地址

2.7 运行时状态

程序加载 创建进程 分配内存 初始化数据 执行main

内存使用示例:

cpp 复制代码
int main() {
    int x = 10;        // 栈区
    int* p = new int;  // 堆区
    static int y = 20; // 数据段
    return 0;
}

2.8 执行过程

取指令 解码 执行 存储结果

复制代码
这就是C++程序从源代码到运行的主要阶段。每个阶段都有其特定的任务和输出,共同协作完成程序的构建和执行。
看不懂也没关系,我们后面将一一来介绍。

3. 预处理

3.1 移除注释

cpp 复制代码
// 输入代码
int main() {
    // 这是一个注释
    int x = 1;  /* 这也是注释 */
    return x;
}

// 预处理后
int main() {
    
    int x = 1;  
    return x;
}

3.2 头文件处理

找到include 没有include 源文件 检查#include 打开头文件 递归处理 替换内容 完成

示例:

cpp 复制代码
// 输入代码
#include <iostream>
#include "myheader.h"

// 预处理后(展开的头文件内容)
namespace std { ... }  // iostream的内容
class MyClass { ... } // myheader.h的内容

3.3 宏定义处理

源代码 宏定义收集 宏展开 替换结果

示例:

cpp 复制代码
// 输入代码
#define MAX 100
#define SQUARE(x) ((x) * (x))
int value = SQUARE(MAX);

// 预处理后
int value = ((100) * (100));

3.4 条件编译

条件为真 条件为假 源代码 检查条件 保留代码 移除代码 结果代码

示例:

cpp 复制代码
// 输入代码
#ifdef DEBUG
    void log(const char* msg) { printf("%s\n", msg); }
#else
    void log(const char* msg) {}
#endif

// 预处理后(如果DEBUG未定义)
void log(const char* msg) {}

4: 编译阶段

4.1 词法分析(Lexical Analysis)

源代码 分词器 标记流 符号表

源代码 → Source Code

分词器 → Lexer

标记流 → Token Stream

符号表 → Symbol Table

示例1
cpp 复制代码
int main() {
    int x = 42;
    return x;
}

对应的Token Stream:

复制代码
[
Token[0]:  {type: KEYWORD,    lexeme: "int",    line: 1, col: 1}
Token[1]:  {type: IDENTIFIER, lexeme: "main",   line: 1, col: 5}
Token[2]:  {type: SYMBOL,     lexeme: "(",      line: 1, col: 9}
Token[3]:  {type: SYMBOL,     lexeme: ")",      line: 1, col: 10}
Token[4]:  {type: SYMBOL,     lexeme: "{",      line: 1, col: 12}
Token[5]:  {type: KEYWORD,    lexeme: "int",    line: 2, col: 5}
Token[6]:  {type: IDENTIFIER, lexeme: "x",      line: 2, col: 9}
Token[7]:  {type: OPERATOR,   lexeme: "=",      line: 2, col: 11}
Token[8]:  {type: NUMBER,     lexeme: "42",     line: 2, col: 13}
Token[9]:  {type: SYMBOL,     lexeme: ";",      line: 2, col: 15}
Token[10]: {type: KEYWORD,    lexeme: "return", line: 3, col: 5}
Token[11]: {type: IDENTIFIER, lexeme: "x",      line: 3, col: 12}
Token[12]: {type: SYMBOL,     lexeme: ";",      line: 3, col: 13}
Token[13]: {type: SYMBOL,     lexeme: "}",      line: 4, col: 1}
]

词法分析器(Lexer)会把源代码切分成一连串 Token(记号)。

每个 Token 一般包含:

type:类型(比如关键字、标识符、运算符等)

lexeme:原始文本(代码里具体的字符串)

line/col:出现在源代码里的行号和列号

4.2 语法分析(Syntax Analysis)

标记流 语法分析器 语法树 错误处理

语法树示例

Program FunctionDeclaration:main ReturnType:int Body Declaration Return Type:int Variable:x Value:42 Variable:x

  1. 函数声明识别

    • Token[0-4]:识别出函数声明的开始
    • 通过Token类型和词素构建FunctionDeclaration节点
  2. 变量声明识别

    • Token[5-9]:识别出变量声明
    • 构建VariableDeclaration节点及其子节点
  3. 返回语句识别

    • Token[10-12]:识别出return语句
    • 构建ReturnStatement节点
  4. 作用域结束识别

    • Token[13]:识别出函数体结束

5. 语义分析(Semantic Analysis)

语法树 类型检查 作用域分析 符号解析 语义树

5.1 检查项目

  1. 类型检查
cpp 复制代码
int x = "hello";  // 错误:类型不匹配
int y = 42;       // 正确:类型匹配
  1. 作用域检查
cpp 复制代码
void func() {
    int x = 1;
    {
        int x = 2;  // 合法:新作用域
        int y = x;  // 使用内层x
    }
    int y = x;      // 使用外层x
}
  1. 符号解析
cpp 复制代码
int func() {
    return x;  // 错误:x未定义
}

6. 中间代码生成(IR Generation)

工作流程

语义树 IR生成器 优化器 中间代码

示例

cpp 复制代码
// 源代码
int x = a + b * c;

// 中间代码
t1 = b * c    // 临时变量t1存储b*c的结果
t2 = a + t1   // 临时变量t2存储最终结果
x = t2        // 将结果赋值给x

7. 目标代码生成(Code Generation)

优化后的IR 指令选择 寄存器分配 指令调度 目标代码

示例

cpp 复制代码
// 中间代码
t1 = b * c
t2 = a + t1
x = t2

// x86汇编代码
mov eax, [b]    ; 加载b到eax
imul eax, [c]   ; 乘以c
add eax, [a]    ; 加上a
mov [x], eax    ; 存储结果到x
相关推荐
我在人间贩卖青春5 天前
汇编之伪指令
汇编·伪指令
我在人间贩卖青春5 天前
汇编之伪操作
汇编·伪操作
济6175 天前
FreeRTOS基础--堆栈概念与汇编指令实战解析
汇编·嵌入式·freertos
myloveasuka5 天前
汇编TEST指令
汇编
我在人间贩卖青春5 天前
汇编编程驱动LED
汇编·点亮led
我在人间贩卖青春5 天前
汇编和C编程相互调用
汇编·混合编程
myloveasuka6 天前
寻址方式笔记
汇编·笔记·计算机组成原理
请输入蚊子6 天前
《操作系统真象还原》 第六章 完善内核
linux·汇编·操作系统·bochs·操作系统真像还原
myloveasuka6 天前
指令格式举例
汇编·笔记·计算机组成原理
我在人间贩卖青春7 天前
汇编之分支跳转指令
汇编·arm·分支跳转