高级语言和机器码

编译过程

💡 低优先级
真题练习
考查得很少,大致了解 编译的具体流程每个阶段的具体任务 就行。
预处理器
编译器
汇编器
链接器
源代码
扩展代码
汇编代码
目标文件
可执行程序

一个传统的 C 程序从源代码到可执行二进制程序的过程中,需要经历 预处理(Preprocess)、 编译(Compile)、 汇编(Assemble)、 链接(Link)四个步骤,这四个步骤分别由 预处理器(Preprocessor)、 编译器(Compiler)、 汇编器(Assembler)、 链接器(Linker)完成。

预处理

预处理(Preprocess)阶段负责对 源代码(source code)进行文本的转换和处理,将其转化为 扩展代码(expanded code)。具体而言,预处理阶段包含如下工作:

  • 头文件包含:将 #include 指令包含的头文件内容插入到源文件中。
  • 宏替换:将源代码中定义的宏 #define 进行替换。
  • 删除注释:将代码中的注释删除。

编译

编译(Compile)阶段将 预处理后源代码(extended code)翻译成 汇编代码(assembly code)。编译阶段包含 词法分析、语法分析、语义分析、中间代码优化 和 汇编代码生成 等子过程。

注意

注意 编译(compilation)这个词一般的含义是将高级语言代码转化为二进制程序,但是如果在整个编译流程中谈到这个词,则需要将其与 汇编(assemble)进行区分:

  • 编译是将高级语言代码转化为 汇编代码
  • 汇编是将 汇编代码转化为二进制代码

汇编

汇编(Assemble)阶段负责将 汇编代码(assembly code)转换为 目标文件(object file)。汇编器(assembler)解析 汇编指令,将其翻译为对应的 机器码

链接

源文件
源文件
源文件
源文件
目标文件
目标文件
目标文件
目标文件
链接器
运行库
可执行程序

如上图所示,链接器(linker)的作用是将由编译器生成的一个或多个 目标代码文件(object file,通常是汇编器生成的机器代码)合并为一个单一的 可执行文件。在这个过程中,链接器主要完成如下任务:

  1. 符号解析:查找所有未定义的符号(如函数调用、全局变量)并找到对应的定义。
  2. 重定位:确定 目标文件中的符号地址,并更新相关指令或数据。
  3. 合并代码和数据段:将不同 目标文件的代码和数据合并,形成最终的 可执行文件

链接分为 静态链接动态链接 两种方式。

  • 静态链接:静态链接是在编译时将所有依赖的库代码拷贝到最终的 可执行文件 中,生成一个 完全独立的二进制文件
  • 动态链接:动态链接不会在编译时将库代码合并,而是在运行时加载外部共享库(.so / .dll)。可执行文件 只包含对库的引用,而不包含库的代码。
链接方式对比静态链接编译时:源代码main.c静态库lib.a可执行文件app.exe主程序代码库代码副本运行时:独立运行完全独立的二进制文件动态链接编译时:源代码main.c动态库引用lib.so/.dll可执行文件app.exe主程序代码库引用动态库文件lib.so / lib.dll运行时:可执行文件运行中动态库运行时加载动态加载特点:✓ 文件较大,包含所有代码✓ 运行时无需外部依赖✓ 启动速度快特点:✓ 文件较小,只含引用✓ 可共享库文件✓ 节省内存空间

汇编代码

中优先级
真题练习
偶尔会在大题中进行综合考查,给你一段 C 语言编译成的汇编代码段,然后结合一些指令和存储系统的知识一起考查,所以还是要能理解 常见 C 语言语句和对应汇编代码的关系

高级语言程序对应的 汇编代码 常常与 存储系统 在大题中进行综合考察,这里需要重点掌握 选择循环函数调用 语句对应的汇编代码。

选择结构语句

选择结构 在汇编中通过 条件比较指令(如 cmp)设置标志位,再利用 条件跳转指令(如 jle, jne 等)决定程序流程是否进入某个分支,同时通过 无条件跳转(jmp)跳过不应执行的分支,最终形成“判断 ➝ 跳转 ➝ 执行 ➝ 合流”的控制流图结构。

选择结构 C 语言

if (a > b) {
    max = a;
} else {
    max = b;
}
汇编

; 假设 a, b 的值分别存放在寄存器 eax 和 ebx 中
; 比较 a 和 b
cmp eax, ebx
; 如果 a <= b, 跳转到 else_label
jle else_label       
; a > b 的分支,无需跳转
mov max, eax     ; max = a
jmp endif_label  ; 跳转到 endif_label
; a <= b 的分支
else_label:
mov max, ebx     ; max = b
; 执行结束
endif_label:

上述的汇编代码可以通过以下图示辅助理解:

selection_structurestart开始comparecmp eax, ebx比较 a 和 bstart->compareconditiona > b ?compare->conditiontrue_branchmov max, eaxmax = acondition->true_branchjle不跳转(a > b)false_branchmov max, ebxmax = bcondition->false_branchjle跳转(a <= b)jump_skipjmp endif_label跳过else分支true_branch->jump_skipend_labelendif_label程序继续false_branch->end_labeljump_skip->end_label

循环结构语句

循环结构汇编中以一个入口标签开始,通过 cmp 或类似指令判断循环条件,结合 条件跳转(如 jge, jl)决定是否继续执行循环体,然后在循环末尾使用 无条件跳转(jmp)返回判断处,形成“判断 ➝ 执行 ➝ 跳回 ➝ 再判断”的闭环结构,直到条件不满足跳出循环。

C 语言中循环语句有 whiledo-whilefor 三种,三者执行流程稍有差别,但核心都在于都使用 条件跳转 控制循环流程:

while 语句

while 循环在汇编中实现的关键是 “先判断、后执行”,即编译器会先生成一个条件判断的跳转逻辑,如果条件不满足则跳出循环,否则进入循环体执行,再跳回判断位置重复该过程。

for 语句

while (count < 10) {
    count++;
}
汇编

; 假设 count 的值存放在寄存器 ecx 中
start:
cmp ecx, 10   ; 比较 count 和 10
jge end       ; 如果 count >= 10, 跳出循环
inc ecx       ; count 增加
jmp start     ; 无条件跳回循环开始
end:

上述的汇编代码可以通过以下图示辅助理解:

while_loopcluster_iterations循环执行过程program_start程序开始loop_startstart:循环入口program_start->loop_startconditioncmp ecx, 10count < 10 ?loop_start->conditionloop_bodyinc ecxcount++condition->loop_bodyjge不跳转(count < 10)loop_endend:循环结束condition->loop_endjge跳转(count >= 10)jump_backjmp start跳回循环开始loop_body->jump_backjump_back->loop_start循环回跳program_continue程序继续loop_end->program_continueiter1第1次: count=0→1iter2第2次: count=1→2iter1->iter2iter3...iter2->iter3iter4第10次: count=9→10iter3->iter4iter5第11次: count=10≥10跳出循环iter4->iter5
do-while 语句

do-while 循环的核心在于 “先执行一次,再判断”,因此汇编中先直接执行循环体,然后再进行条件判断,根据比较结果决定是否跳回继续执行。这种结构通过将判断逻辑放在循环体之后,确保循环体至少执行一次。

do while 语句

do {
    count++;
} while (count < 10);
汇编

; 假设 count 存放在 ecx 中
start:
inc ecx ; count++
cmp ecx, 10 ; 比较 count 和 10
jl start ; 若 count < 10,继续循环
end:

上述的汇编代码可以通过以下图示辅助理解:

do_while_loopcluster_execution执行序列 (假设初始count=8)cluster_comparison与while循环的区别cluster_assembly汇编指令执行顺序program_start程序开始loop_startstart:循环体入口program_start->loop_startloop_bodyinc ecxcount++loop_start->loop_bodyconditioncmp ecx, 10jl startcount < 10 ?loop_body->conditioncondition->loop_startjl跳转(count < 10)继续循环loop_endend:循环结束condition->loop_endjl不跳转(count >= 10)退出循环program_continue程序继续loop_end->program_continueseq1第1次: count=8→99<10, 继续seq2第2次: count=9→1010>=10, 退出seq1->seq2while_flowwhile: 判断→执行→判断...do_while_flowdo-while: 执行→判断→执行...while_flow->do_while_flowguaranteedo-while保证至少执行一次do_while_flow->guaranteeasm11. inc ecx (执行)asm22. cmp ecx, 10 (比较)asm1->asm2asm33. jl start (条件跳转)asm2->asm3asm3->asm1条件为真时回跳
for 语句

for 循环在汇编中的实现是将 初始化判断更新三个阶段明确拆分:先初始化循环变量,再判断是否进入循环体;执行完循环体后进行变量更新,并跳回判断位置。它的本质是 while 循环的结构化变体,但语义更集中,便于生成高效指令序列。

for C 语言

for (int i = 0; i < 10; i++) {
    sum += i;
}
汇编

; 假设 i 存在 ecx 中,sum 存在 eax 中
mov ecx, 0 ; 初始化 i = 0
mov eax, 0 ; 初始化 sum = 0
loop_start:
cmp ecx, 10 ; 判断 i < 10
jge loop_end ; 如果 i >= 10,跳出循环
add eax, ecx ; sum += i
inc ecx ; i++
jmp loop_start ; 回到判断
loop_end:

上述的汇编代码可以通过以下图示辅助理解:

for_loopcluster_for_elementsfor循环三要素cluster_execution执行过程示例program_start程序开始initializationmov ecx, 0mov eax, 0初始化: i=0, sum=0program_start->initializationloop_startloop_start:循环判断入口initialization->loop_startconditioncmp ecx, 10i < 10 ?loop_start->conditionloop_bodyadd eax, ecxsum += icondition->loop_bodyjge不跳转(i < 10)loop_endloop_end:循环结束condition->loop_endjge跳转(i >= 10)incrementinc ecxi++loop_body->incrementjump_backjmp loop_start跳回判断increment->jump_backjump_back->loop_start循环回跳program_continue程序继续loop_end->program_continueinit_box① 初始化int i = 0cond_box② 条件判断i < 10update_box③ 更新操作i++exec1i=0: sum=0+0=0exec2i=1: sum=0+1=1exec1->exec2exec3i=2: sum=1+2=3exec2->exec3exec4...exec3->exec4exec5i=9: sum=36+9=45exec4->exec5exec6i=10: 跳出循环exec5->exec6

函数定义和调用

在 C 语言中,每一个函数调用在底层都会被编译为一套具体的 汇编指令。为了实现 函数的参数传递局部变量管理返回值传递等,汇编层面必须精细地管理 栈帧(stack frame)、寄存器(register)以及 指令流程(control flow)。

函数调用在汇编中的三大阶段

  1. 函数入口
    • 保存寄存器:保存 caller 的寄存器,以确保在函数执行完后,寄存器的值不被改变。
    • 设置栈帧:保存 caller 的栈帧,设置 callee 的栈帧。
  2. 函数体
    • 这部分是函数的执行逻辑,会包含各种操作指令,此时 局部变量会被保存到栈上。
  3. 函数返回
    • 如果函数有返回值,通常会将结果保存在 eax 寄存器中。
    • 恢复栈帧:恢复栈指针,确保栈帧被正确销毁。
    • 恢复寄存器:如果函数入口时保存了寄存器,那么在返回之前,需要将它们恢复。
补充

在理解汇编时,callercallee 是两个非常关键的术语:

  • caller:调用某个函数的一方。
  • callee:被调用的函数本身。
C语言函数调用汇编过程术语说明Caller: 调用函数的一方Callee: 被调用的函数时间轴1. 函数入口Caller栈帧保存寄存器返回地址Callee栈帧寄存器EBP保存ESP调整• 保存Caller寄存器• 设置新栈帧2. 函数体执行Caller栈帧局部变量参数临时数据执行MOVADDSUBCMP...• 局部变量分配• 业务逻辑执行3. 函数返回恢复Caller清理栈帧恢复寄存器EAX返回值• 返回值→EAX• 恢复栈帧和寄存器关键汇编指令入口指令:push %ebpmov %esp, %ebpsub $n, %esp保存栈帧,分配空间执行指令:mov %eax, -4(%ebp)add $10, %eaxcall function业务逻辑处理返回指令:mov %ebp, %esppop %ebpret恢复并返回💡 重点:EBP(栈帧指针) 和 ESP(栈顶指针) 的协调管理是函数调用的核心通过精确的栈帧管理,实现参数传递、局部变量存储和返回值传递

这一节可以结合 函数调用时内存结构 共同理解,下面通过几个简单的例子说明以下 函数定义调用

函数定义

本节以一个非常简单的加法函数 add 进行说明:

add 函数

int add(int x, int y) {
    return x + y;
}
汇编

; 假定 'a' 和 'b' 作为参数通过堆栈传递
.globl _add
_add:
    ; 保存 caller 的 ebp
    push ebp
    ; 设置 callee 的 ebp
    mov ebp, esp
    ; x + y 的汇编表示
    mov eax, [ebp+8]
    add eax, [ebp+12]
    ; 恢复 caller 的 ebp
    pop ebp
    ; 函数返回
    ret

说明

  • add 函数的两个参数 xy 是通过栈传递的。[ebp+8][ebp+12] 分别代表第一个和第二个参数。
  • 函数返回值被保存在 eax 寄存器中。
  • push ebp / mov ebp, esp 是标准做法,用于设置新函数的栈帧,确保不同函数调用之间互不干扰。
函数调用

再看一个稍复杂的例子,func 函数调用了 add 函数。

func 函数

void func(int a, int b) {
    int sum = add(a, b);
    int var = sum * 2;
    // ... 一些使用 var 的代码 ...
}
汇编

.globl _func
_func:
    push ebp
    mov ebp, esp
    ; 调用函数 add
    sub esp, 8 
    push dword [ebp+12]
    push dword [ebp+8]
    call _add
    add esp, 8
    ; 保存返回值
    mov [ebp-4], eax
    mov eax, [ebp-4]
    ; 将eax左移1位,相当于乘以2
    shl eax, 1              
    ; 将结果存储到 'var'
    mov [ebp-8], eax        

    ; ... 更多使用 'var' 的代码 ...

    ; 函数完成,清理堆栈,并恢复ebp
    mov esp, ebp
    pop ebp
    ret

说明:

  • 调用 add(a, b) 之前,参数是从右往左压栈的,这是 C 语言默认的调用约定(cdecl)。
  • call _add 会把当前指令地址压入栈中(以便 ret 时跳回来)。
  • eax 保存了 add 的返回值,存入局部变量 sum
  • 使用 shl eax, 1 是将 sum 乘以 2(左移一位即乘 2)。