微软在 VS2015 里引入了基于开源 Clang/LLVM 技术的新编译器后端,用来提升代码生成质量,同时支持公司内部代码及应用商店的普遍应用,体现了微软向开源和跨平台技术靠拢的趋势。
编译器不仅是程序正确运行的基础,其性能和生成代码的质量直接影响软件的效率和用户体验,甚至涉及巨大经济价值。高质量的编译器设计需要平衡正确性、速度、代码大小和质量等多个方面。
<html>
<head>
<script>
var e1;
function f1(evt){
e1 = document.createEventObject(evt); // 复制事件对象 evt,但未对内部指针做引用计数增加(AddRef)
document.getElementById("sp").innerHTML = ""; // 清空 span 内部内容,导致 img 标签被销毁(释放内存)
window.setInterval(f2, 50); // 定时调用 f2,异步访问事件对象 e1
}
function f2(){
var t = e1.srcElement; // 访问事件对象的成员,但 e1 已经指向被释放的内存 -> 悬挂指针
}
script>
head>
<body>
<span id="sp">
<img src="any.gif" onload="f1(evt)">
span>
body>
html>
步骤 | 描述 | 详细解释 |
---|---|---|
1 | 传入 onload 事件对象 evt 给 f1 |
JavaScript 触发 img 加载事件,evt 包含事件相关信息 |
2 | f1 中复制事件对象(createEventObject(evt) ),但没有对内部的 C++ 对象 CTreeNode 调用 AddRef |
这里 CTreeNode 是 IE 内部用来管理 DOM 事件树节点的 C++对象,引用计数未增加导致后续可能被释放 |
3 | 通过 innerHTML="" 清空 span,销毁 img 标签,从而释放了事件对象的内部 C++ 资源 CTreeNode |
DOM 树节点被删除,释放内存,导致事件对象内部指针悬空 |
4 | 异步调用 f2 ,访问事件对象 e1 的 srcElement 成员 |
此时 e1 指向已被释放的内存,产生悬挂指针,导致内存错误或劫持控制流(通过虚函数表调用) |
事件触发 ---> 传入 evt
|
V
f1(evt) ---复制evt,e1指向复制对象(未AddRef)
| |
| 内部CTreeNode指针(悬空风险)
|
清空 span 内容 ---> img 标签销毁,释放 CTreeNode
|
V
异步执行 f2() ---> 访问 e1.srcElement (悬挂指针)
|
V
Use-After-Free,攻击发生
mov eax, ecx ; 将 ecx 的值复制到 eax
shr eax, 9 ; eax 右移9位,相当于除以512
mov eax, dword ptr [0x20000000 + eax*4] ; 以 eax 作为索引,访问基地址0x20000000的数组元素(4字节*eax偏移)
shr ecx, 4 ; ecx 右移4位,相当于除以16
bt eax, ecx ; 测试 eax 寄存器中第 ecx 位的值(bit test指令)
jb ok_to_call ; 如果该位是0,则跳转到 ok_to_call(jb: jump if below,表示cf=0)
int 3 ; 否则触发断点异常(int 3是调试中断,通常用于触发调试器)
ecx
作为输入寄存器,经过两次位移:
eax=ecx>>9
)用作索引数组,说明可能是对某个大数据结构的查找或哈希。ecx=ecx>>4
),用于后面bt
指令测试位。[0x20000000 + eax*4]
:访问一个以0x20000000为基址的32位整型数组,索引为ecx>>9
,取得一个32位整数。bt eax, ecx
:检查这个整数的某个位(由ecx>>4
确定)是否被设置。jb ok_to_call
:如果该位为0,跳转继续执行。int 3
:如果该位为1,则触发中断,通常表示这里禁止继续,可能是做权限检查或防止某些操作。这段代码是一个 位图(bitmask)检查 的例子:
ecx
的高位确定应该检查哪个32位块,这段描述讲的是一种通过位图进行安全检查的插装技术,用于保护间接调用:
/Guard
这个新的编译器开关,它的作用和特点:/Guard
是一个新的编译器参数选项(开关),用来开启某种安全防护机制。/Guard
是微软新推出的编译器安全开关,它通过紧密结合操作系统,经过大量测试,能够有效防止当下流行且危险的零日攻击,同时保证程序性能和兼容性。
generator<int> fib(int n)
{
int a = 0;
int b = 1;
while (n-- > 0)
{
yield a; // 生成一个值,挂起执行点
auto next = a + b;
a = b;
b = next;
}
}
int main() {
for (auto v : fib(35)) {
printf("%d\n", v);
if (v > 10)
break; // 中途可以退出协程遍历
}
}
yield
用来从协程生成一个值并挂起,等待下一次恢复。for
循环遍历生成的序列,且可中途打断。微软新引入的 C++ 协程模型:
_yield
和 _await
关键字实现。std::future<void> tcp_reader(int total) {
char buf[64 * 1024];
auto conn = await Tcp::Dial("127.0.0.1", 1337); // 异步建立连接
do {
auto bytesRead = await conn.read(buf, sizeof(buf)); // 异步读取数据
total -= bytesRead;
} while (total > 0);
}
int main() {
tcp_reader(1000 * 1000 * 1000).get(); // 等待协程完成
}
await
关键字挂起协程,等待异步操作完成而不阻塞线程。原始代码是一个常见的条件赋值:
for (…) {
if (cond) {
LHS1 = RHS1;
} else {
LHS2 = RHS2;
}
}
这段代码在循环体中有分支(if-else),在 SIMD(单指令多数据)矢量化时,分支会阻碍指令的并行执行。
改写为矢量化操作,处理4个元素(假设一次处理4个数据):
for (i = 0; i < N; i += 4) {
mask = {cond[i], cond[i+1], cond[i+2], cond[i+3]}; // 4个条件构成的掩码
v1 = RHS1 & mask; // 只在mask为真位置取RHS1的值
v2 = LHS1 & !mask; // 在mask为假位置保留原LHS1的值
LHS1 = v1 | v2; // 合并,更新LHS1
v3 = RHS2 & !mask; // 在mask为假位置取RHS2
v4 = LHS2 & mask; // 在mask为真位置保留原LHS2
LHS2 = v3 | v4; // 合并,更新LHS2
}
&
, |
)模拟条件赋值,避免了分支判断。if
分支,提升性能。以下是详细的理解与分析:for (j = 0; j < NUM_RUNS; j++) {
for (i = start; i < end; i++) {
// ...
if (InputX < 0.0) { // control flow 1
InputX = -InputX;
sign = 1;
} else
sign = 0;
if (sign) { // control flow 2
OutputX = 1.0 - OutputX;
}
// 类似的又出现了control flow 3和4,重复前面两步
if (otype == 0) { // control flow 5
OptionPrice = (sptprice * NofXd1) - (FutureValueX * NofXd2);
} else {
NegNofXd1 = (1.0 - NofXd1);
NegNofXd2 = (1.0 - NofXd2);
OptionPrice = (FutureValueX * NegNofXd2) - (sptprice * NegNofXd1);
}
// ...
}
}
cmpltps xmm1, xmm2 ; xmm1 = (xmm2 < xmm1) ? 0xFFFFFFFF : 0
cmpltps
指令对4个float并行比较,生成对应的掩码(true为全1,false为全0)if (InputX < 0.0)
的判断movaps xmm0, xmm2 ; xmm0 = InputX
subps xmm0, xmm3 ; xmm0 = InputX - 参考值(如0或常量)
andnps xmm4, xmm0 ; xmm4 = (~掩码) & xmm0 (false分支的值)
andps xmm0, xmm2 ; xmm0 = 掩码 & xmm2 (true分支的值)
orps xmm4, xmm0 ; 合并两部分得到结果
andps
, andnps
, orps
实现掩码控制流:
andps
选择掩码为真的分支值andnps
选择掩码为假的分支值orps
合并结果sign
标志和基于otype
的两个分支pcmpeqd xmm1, xmm3 ; 生成掩码 (掩码为 true/false)
pcmpeqd
生成等于比较掩码,用于判断otype == 0
if
语句)导致CPU流水线停顿,分支预测失败,降低性能。标量代码控制流 | 矢量化实现方法 | 使用的SIMD指令 |
---|---|---|
if (InputX < 0) |
cmpltps 生成掩码,掩码控制赋值 |
cmpltps , andps , andnps , orps |
if (sign) |
用掩码控制是否执行操作 | 同上 |
if (otype == 0) |
pcmpeqd 生成掩码选择不同结果 |
pcmpeqd , 位操作合并结果 |
程序/算法 | 矢量化后性能提升 | SIMD指令集 |
---|---|---|
PARSEC\blackscholes | 600%加速 | AVX2 |
388%加速 | SSE2 | |
Numerical Recipe\ks2d1s | 476%加速 | AVX2 |
309%加速 | SSE2 | |
Geekbench_mini\sobel | 231%加速 | SSE2 |
Eigen\quatmul (四元数乘法) | 229%加速 | SSE2 |
这显示了现代CPU SIMD指令对复杂控制流和数值密集计算的强大加速能力。针对具体应用,合理设计矢量化算法,可以带来几倍甚至数十倍的性能提升,是性能优化的关键技术之一。
// 第一次 foo 函数定义
void foo(int * a, int * b, int * c, int count) {
for (int i = 0; i < count; i++) {
c[i] = a[i] * b[i];
}
}
// 大量未改动的代码(100,000 个函数,500个文件)
int bar() {
__declspec(align(16)) int a[128], b[128], c[128];
foo(a, b, c, 128);
// 其他操作
}
// 第二次 foo 函数修改
void foo(int * a, int * b, int * c, int count) {
for (int i = 0; i < count; i++) {
c[i] = c[i] + a[i] * b[i];
}
}
a
, b
, c
不会别名(alias),即它们指向的内存区域不重叠。foo
函数被修改时,只有 foo
这一函数被重新编译,而其它近10万函数和500个文件都不用重新编译。a,b,c
不别名,编译器可以做更积极的优化(比如向量化、内存访问重排),因为可以安全地假设它们不会相互影响。c2.dll
负责编译源代码生成目标文件 .obj
。.obj
文件交给链接器 link.exe
,生成最终的可执行文件或 DLL。IPDB
(Incremental Program Database)来追踪代码的变更和依赖关系。.obj
文件。IPDB
和 c2.dll
配合,使得编译器能快速定位变化,避免全量编译。link.exe
链接,生成新的可执行文件或 DLL。/debug
与 /debug:fastlink
比较:/debug
:生成完整的调试信息,链接时间较长。/debug:fastlink
:微软 Visual Studio 提供的一种快速生成调试信息的方式,极大减少链接时间,但生成的调试信息仍可用。项目 | 链接时间(/debug) | 链接时间(/debug:fastlink) |
---|---|---|
Destiny | 85 | (未列出) |
Chrome | 471 | (未列出) |
Kinect Sports | 338 | (未列出) |
Rival | 11 | (未列出) |
280 | (不清楚具体含义) | |
124 | ||
(看上去这只是部分数据,但主要是说明 /debug:fastlink 链接时间远短于传统 /debug ) |
/debug:fastlink
,链接时间大幅缩短,尤其对大型项目影响明显。PGO 是一种基于实际运行时数据来指导编译器优化的技术,Xbox One 游戏利用它可以获得显著的性能提升,特别是在游戏主线程和渲染线程上。
代码生成在整个软件开发和运行过程中扮演关键角色,负责以下方面:
C# (MSIL) <-- 编译为 --> 中间语言 (MSIL)
C/C++ <-- 通过代码生成器 (C2.dll) --> 目标机器码 (.EXE/.DLL)
Windows Executive Layer (内核层)
|
+-- API + ABI (应用层)
| - Framework-1、Runtime-1(如.NET运行时)
| - STL、BOOST(C++标准库和第三方库)
| - WinRT(Windows Runtime)
| - .NET Native Framework
|
+-- 硬件层(如 ARM 32位4核 CPU)
#include
#include
#include "MyATLObj.h"
extern int bar();
void DoWork() {
std::CComPtr<MyATLObj> myObj;
myObj.CoCreateInstance(CLSID_MyATLOBj);
int value;
myObj->getValue(&value);
std::vector<int> v{ bar(), bar(), value };
}
int main() {
__try {
DoWork();
}
__except(EXCEPTION_EXECUTE_HANDLER) {
__fastfail(1);
}
}
CComPtr
管理 COM 对象。DoWork
创建 COM 对象,调用成员,保存结果到 std::vector
。__try/__except
做结构化异常处理,确保异常安全。#include
#using "MyWinRTNS.winmd"
using namespace MyWinRTNS;
using namespace std;
using namespace std::chrono;
future<int> DoWork() {
MyWinRTObj^ myObj = ref new MyWinRTObj();
return __await async([=]() {
return myObj->baz()->GetResults();
});
}
int bar() {
try {
return DoWork().get() + 5;
} catch (Platform::Exception^ e) {
return -1;
}
}
ref new
和托管指针 ^
。baz()
,使用 future
和 async
处理异步操作。Platform::Exception
异常,保证异常安全。using System;
using System.Threading.Tasks;
using Windows.Web;
using Windows.Web.Http;
using Windows.Foundation;
namespace MyWinRTNS {
public interface IMyWinRTObj {
IAsyncOperation<int> baz();
}
public sealed class MyWinRTObj : IMyWinRTObj {
private async Task<int> DoWork() {
HttpClient client = new HttpClient();
try {
string str = await client.GetStringAsync(new Uri("http://msdn.microsoft.com"));
return str.Length;
} catch (Exception e) {
if (WebError.GetStatus(e.HResult) == WebErrorStatus.BadGateway) {
return -1;
}
throw e;
}
}
public IAsyncOperation<int> baz() {
return DoWork().AsAsyncOperation<int>();
}
}
}
IMyWinRTObj
和实现类 MyWinRTObj
。async/await
获取网页字符串长度。Task
转换为 IAsyncOperation
,以便 WinRT 兼容。fall
(可能指控制流相关的新指令或特性)/O2
完全优化/Od
全调试信息支持driver.cpp
:加载微软后端 c2.dllcc1_main.cpp
:初始化c2后端编译器执行CodeGenAction.cpp
:LLVM IR转微软C2的代码生成调用#ifdef
)来处理不同编译器和平台的实现差异,尤其是微软编译器(C1xx/C2)与Clang/LLVM的兼容性问题,以及逐步摆脱一些历史遗留的兼容性hack。#ifdef
)#ifdef _MSC_VER
// Windows/MSVC专用代码
#endif
#ifdef
区分平台特定代码。#ifdef (conformance)
来开启或关闭某些新标准特性,保证不同编译器的兼容性:struct limits {
template<typename T>
static const T min;
};
template<typename T>
const T limits::min = { };
#ifndef _MSC_VER
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wall"
#endif
或者#ifdef _MSC_VER
#pragma warning(disable : 4127)
#endif
template <typename T> void print(T t) { cout << "Unknown: " << t << endl; }
void print(long long n) { cout << "long long: " << n << endl; }
void print(unsigned long n) { cout << "unsigned long: " << n << endl; }
void print(unsigned long long n) { cout << "unsigned long long: " << n << endl; }
#if defined(_MSC_VER)
print(-(long long)4000000000);
#else
print(-4000000000);
#endif
#ifdef
控制编译,针对平台和编译器差异写不同代码分支。/O2 /fp:fast
(全优化,快速浮点)/O2 -Xclang -ffast-math
(全优化,快速数学运算)void test16(int *x, int *a, int *b) {
while (a < b)
*a++ += 2 + *x++;
}