编译器的工作原理
介绍 #
源文件 编译–> .obj文件 链接–> .exe可执行文件
- 预处理(包括标记) –> 创建一棵抽象语法树(将所有代码转换为常量数据或指令) –> 创建代码(CPU将执行的实际机器码)
- 编译器:为每个C++文件(先经过预处理成为翻译单元)生成目标文件
- 文件只是C++向编译器提供源代码的方式(并不需要像java那样文件名必须和类名等同)
- 将.cpp当做C++文件,把.c当做C文件,把.h当做头文件。(默认,也可以改变这个默认约定)
- C++文件先经过预处理后成为翻译单元,之后编译器将翻译单元生成为一个目标文件,有时将CPP文件包含在其他CPP文件中并创建一个包含大量文件的大CPP文件。这种情况下只需要编译这个大的CPP文件并生成一个翻译单元,从而生成一个目标文件
hash include #
//Math.cpp
int Multiply(int a, int b) {
int result = a * b;
return result;
}
并编译
再添加一个文件EndBrace.h
}
修改Math.cpp,修改前删除结束括号,会编译出错,之后修改为:
//Math.cpp
int Multiply(int a, int b) {
int result = a * b;
return result;
#include "EndBrace.h"
编译成功
告诉编译器输出一个包含所有结果的文件 #
简单例子 #
修改配置
这个选项会导致不会生成obj文件


Math.i
#line 1 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\Math.cpp"
int Multiply(int a, int b) {
int result = a * b;
return result;
#line 1 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\EndBrace.h"
}
#line 5 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\Math.cpp"
例子增强 #
#define INTEGER int
INTEGER Multiply(int a, int b) {
INTEGER result = a * b;
return result;
}
Math.i
#line 1 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\Math.cpp"
int Multiply(int a, int b) {
int result = a * b;
return result;
}
if #
if之后为真则包含后面的语句块
#if 1
int Multiply(int a, int b) {
int result = a * b;
return result;
}
#endif
结果(Math.i)
#line 1 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\Math.cpp"
int Multiply(int a, int b) {
int result = a * b;
return result;
}
#line 7 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\Math.cpp"
如果if后为零,则结果(Math.i):
#line 1 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\Math.cpp"
#line 7 "E:\\cppStudyTemp\\ChernoCpp\\HelloWorld\\HelloWorld\\Math.cpp"
include #
#include <iostream>
int Multiply(int a, int b) {
int result = a * b;
return result;
}

接着恢复生成预处理文件的配置(默认不生成)
查看obj文件 #
查看前先禁用JMC优化
配置属性 → C/C++ → 常规 → 支持"仅我的代码"调试 → 否 (/JMC-)
之后修改项目属性

编译后:

即可读的汇编版本:
;Math.asm------
; Line 3
mov eax, DWORD PTR a$[rbp]
imul eax, DWORD PTR b$[rbp]
mov DWORD PTR result$[rbp], eax
; Line 4
mov eax, DWORD PTR result$[rbp]
; Line 5
lea rsp, QWORD PTR [rbp+80]
pop rbp
ret 0
这些是CPU在运行函数时将执行的实际指令
这里做了一个多余的操作,先将a变量放到%eax结果寄存器中,然后再把相乘后的结果放到结果寄存器中,之后将%eax中的值放到一个名为result的变量中,最后再从result变量中取出值放到 %eax结果寄存器。而不是直接把它放到%eax中
修改代码
int Multiply(int a, int b) {
return a * b;
}
查看Math.asm
?Multiply@@YAHHH@Z PROC ; Multiply, COMDAT
; File E:\cppStudyTemp\ChernoCpp\HelloWorld\HelloWorld\Math.cpp
; Line 1
$LN3:
mov DWORD PTR [rsp+16], edx
mov DWORD PTR [rsp+8], ecx
push rbp
sub rsp, 64 ; 00000040H
mov rbp, rsp
; Line 2
mov eax, DWORD PTR a$[rbp]
imul eax, DWORD PTR b$[rbp] ;这里直接将结果存在了寄存器%eax中
; Line 3
lea rsp, QWORD PTR [rbp+64]
pop rbp
ret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
优化 #
1 #
上述的math.asm之所以没有自动优化,是因为我们是在debug模式下编译的,以确保我们的代码尽可能完整、容易调试
在此之前把代码修改为:
int Multiply(int a, int b) {
int result = a * b;
return result;
}
修改配置:

此时O2和RTC不兼容
O2优化会重组代码:重新排序指令、消除冗余操作、内联函数等 RTC需要插入检查代码:在特定位置插入检查未初始化变量、栈溢出的代码 优化后的代码可能使RTC插入点失效,导致检查不准确或无法插入
关闭基本检查

重编译查看math.asm
?Multiply@@YAHHH@Z PROC ; Multiply, COMDAT
; File E:\cppStudyTemp\ChernoCpp\HelloWorld\HelloWorld\Math.cpp
; Line 3
imul ecx, edx ;直接在寄存器中相乘
; Line 4
mov eax, ecx ;结果直接赋给返回寄存器
; Line 5
ret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
还有其他优化没看懂,反正优化了就是了
2 #
禁用优化后编辑代码:
//Math.cpp
int Multiply( ) {
return 5 * 2;
}
; Math.asm
?Multiply@@YAHXZ PROC ; Multiply, COMDAT
; File E:\cppStudyTemp\ChernoCpp\HelloWorld\HelloWorld\Math.cpp
; Line 2
$LN3:
push rbp
sub rsp, 64 ; 00000040H
mov rbp, rsp
; Line 3
mov eax, 10 ;直接将结果值移动到结果寄存器
; Line 4
lea rsp, QWORD PTR [rbp+64]
pop rbp
ret 0
?Multiply@@YAHXZ ENDP ; Multiply
这叫做常量折叠(constant folding),即任何可以在编译时计算出来的常量
3 #
const char* Log(const char* message) {
return message;
}
int Multiply(int a,int b ) {
Log("Multiply");
return a * b;
}
查看Math.asm
; 乘法函数的定义------
?Multiply@@YAHHH@Z PROC ; Multiply, COMDAT
; File E:\cppStudyTemp\ChernoCpp\HelloWorld\HelloWorld\Math.cpp
; Line 5
$LN3:
mov DWORD PTR [rsp+16], edx
mov DWORD PTR [rsp+8], ecx
push rbp
sub rsp, 96 ; 00000060H
lea rbp, QWORD PTR [rsp+32]
; Line 7
lea rcx, OFFSET FLAT:??_C@_08EOBDLMOI@Multiply@
;这里调用了Log函数------
call ?Log@@YAPEBDPEBD@Z ; Log
npad 1
; Line 8
mov eax, DWORD PTR a$[rbp]
imul eax, DWORD PTR b$[rbp]
; Line 9
lea rsp, QWORD PTR [rbp+64]
pop rbp
ret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
_TEXT ENDS
; Function compile flags: /Odtp /ZI
; COMDAT ?Log@@YAPEBDPEBD@Z
_TEXT SEGMENT
message$ = 80
; Log函数的定义------
?Log@@YAPEBDPEBD@Z PROC ; Log, COMDAT
; File E:\cppStudyTemp\ChernoCpp\HelloWorld\HelloWorld\Math.cpp
; Line 1
$LN3:
mov QWORD PTR [rsp+8], rcx
push rbp
sub rsp, 64 ; 00000040H
mov rbp, rsp
; Line 2
mov rax, QWORD PTR message$[rbp]
; Line 3
lea rsp, QWORD PTR [rbp+64]
pop rbp
ret 0
?Log@@YAPEBDPEBD@Z ENDP ; Log
_TEXT ENDS
END
为何函数要用特殊符号,函数签名需要唯一定义,当有多个obj并且我们的函数在多个obj中定义时,链接器的工作就是将他们全部链接在一起,它执行此操作的方式是查找这个函数签名
使用O2优化 #
Math.asm
?Multiply@@YAHHH@Z PROC ; Multiply, COMDAT
; File E:\cppStudyTemp\ChernoCpp\HelloWorld\HelloWorld\Math.cpp
; Line 8
imul ecx, edx
mov eax, ecx
; Line 9
ret 0
?Multiply@@YAHHH@Z ENDP ; Multiply
视频说这里不会调用Log函数,但是如果开启了JMC的话,还是会调用该函数的。ai的解释是:JMC (Just My Code) 是Microsoft编译器的调试功能:它会插入额外的代码来区分用户代码和系统代码,阻止了编译器执行某些优化