ASP.NET Core Native AOT 到底是什么?从 C#、IL、JIT 一直讲到 Linux ARM64 ELF
前言
前面几篇我们已经讨论了:
- ASP.NET Core 为什么适合 Linux ARM64 设备
- Minimal API 的 HTTP 请求处理链路
- BackgroundService 与设备后台任务设计
这一篇继续往底层走。
我们要讨论一个非常关键的问题:
ASP.NET Core 程序最终到底是怎么变成 ARM64 Linux 可执行文件的?
很多人第一次看到下面的发布命令:
bash
dotnet publish \
-c Release \
-r linux-arm64 \
-p:PublishAot=true
可能会简单理解成:
text
C#
↓
ARM64 可执行程序
但真实过程远比这个复杂。
一个 Native AOT 程序大致经历:
text
C# Source
↓
Roslyn Compiler
↓
IL + Metadata
↓
Native AOT Compiler
↓
Native Object Code
↓
Native Linker
↓
Linux ARM64 ELF
如果还是:
text
Linux x64 构建机
↓
Linux ARM64 目标设备
那么还会涉及:
text
Cross Compilation
Cross Linker
Sysroot
glibc
crt
libgcc / compiler runtime
Linux ABI
ELF
这一篇就把整条链完整拆开。
一、先回答最基本的问题:什么是 Native AOT
AOT 是:
text
Ahead Of Time
也就是:
提前编译。
Native AOT 的核心思想是:
text
程序发布阶段
↓
把 IL 编译成本机机器码
↓
最终生成原生可执行文件
而不是传统 .NET:
text
程序运行阶段
↓
JIT
↓
把 IL 编译成本机机器码
Microsoft 当前的 Native AOT 文档明确说明,Native AOT 会在 publish 阶段把 IL 编译成本机代码,应用运行时不使用 JIT;发布结果是 self-contained,并且可以运行在没有预安装 .NET Runtime 的机器上。
所以最核心区别就是:
text
JIT
=
运行时编译
而:
text
AOT
=
发布时编译
二、传统 .NET 程序并不是直接编译成 CPU 指令
先来看普通 C# 项目。
例如:
csharp
public static int Add(
int a,
int b)
{
return a + b;
}
我们执行:
bash
dotnet build
并不是直接生成:
text
ARM64 Machine Code
或者:
text
x86-64 Machine Code
C# 编译器首先生成:
text
IL
也就是:
text
Intermediate Language
中间语言。
整体链路:
text
C#
↓
Roslyn
↓
IL
三、什么是 IL
IL 可以理解成:
与具体 CPU 架构相对独立的一种中间指令。
例如 C#:
csharp
return a + b;
概念上可能被转换成类似:
text
ldarg.0
ldarg.1
add
ret
这里并不是:
text
ARM64 ADD
也不是:
text
x86 ADD
而是:
text
.NET IL Instruction
因此同一份:
text
MyApplication.dll
理论上可以运行在:
text
Windows x64
Linux x64
Linux ARM64
macOS ARM64
前提是对应平台存在兼容的 .NET Runtime。
四、IL 为什么具有跨平台能力
假设:
text
MyApplication.dll
里面主要包含:
text
IL
+
Metadata
它并不强绑定:
text
x64
ARM64
真正决定 CPU 指令的是:
text
Runtime
例如:
text
Linux x64 Runtime
↓
生成 x86-64 Machine Code
而:
text
Linux ARM64 Runtime
↓
生成 ARM64 Machine Code
所以传统 .NET 的跨平台思想可以理解为:
text
IL
│
┌──────┴──────┐
▼ ▼
x64 Runtime ARM64 Runtime
│ │
▼ ▼
x64 Machine ARM64 Machine
Code Code
五、JIT 是什么时候工作的
JIT:
text
Just-In-Time Compiler
即:
即时编译器。
传统 .NET 应用启动以后,并不是马上把整个程序集全部编译成本机代码。
通常更接近:
text
调用 Method A
↓
JIT Method A
↓
Native Code
↓
Execute
以后再次调用:
text
Method A
通常直接执行已经生成的本机代码。
可以理解为:
text
IL Method
│
│ 第一次执行
▼
JIT Compiler
│
▼
Native Machine Code
│
▼
CPU
六、JIT 的优势
JIT 并不是落后的设计。
相反,它有很多重要优势。
因为 JIT 在程序真正运行时才工作,所以它知道:
text
当前 CPU
当前 Runtime
当前运行环境
当前实际类型
当前执行热点
于是可以进行:
text
Inlining
Devirtualization
Tiered Compilation
PGO
CPU-specific Optimization
也就是说:
JIT 可以根据运行时信息继续优化代码。
七、JIT 的代价
但是 JIT 同样存在成本。
程序启动时:
text
IL
↓
JIT
↓
Native Code
↓
Execute
需要额外:
text
CPU
Memory
Runtime Infrastructure
对于桌面、服务器环境:
text
16 Core CPU
32 GB RAM
可能并不明显。
但在设备环境:
text
ARM CPU
有限 RAM
有限 Flash
这些成本就值得关注。
八、Framework-dependent 部署
普通 .NET 应用经常使用:
text
Framework-dependent
发布。
例如目标设备部署:
text
MyApplication.dll
然后运行:
bash
dotnet MyApplication.dll
这里:
text
dotnet
实际上来自目标机器安装的:
text
.NET Runtime
架构:
text
Application
│
▼
.NET Runtime
│
▼
Operating System
所以目标机器需要:
text
预安装对应版本 .NET Runtime
九、Self-contained 又是什么
另一种方式:
text
Self-contained
把应用运行所需要的 .NET Runtime 一起带过去。
可以理解为:
text
Application
+
.NET Runtime
+
Runtime Libraries
例如:
bash
dotnet publish \
-r linux-arm64 \
--self-contained true
目标设备就不需要另外安装匹配的共享 .NET Runtime。
但要注意:
Self-contained 不等于 Native AOT。
这是非常容易混淆的地方。
十、Self-contained 仍然可以使用 JIT
例如:
text
Self-contained
可以仍然包含:
text
IL
+
CoreCLR
+
JIT
运行过程仍然是:
text
IL
↓
CoreCLR
↓
JIT
↓
Native Code
只不过:
text
Runtime
不是系统预装,而是随着 Application 一起发布。
所以:
text
Self-contained
≠
Native AOT
十一、ReadyToRun 又是什么
还有一个经常遇到的概念:
text
ReadyToRun
可以把它理解为介于:
text
纯 JIT
和:
text
Native AOT
之间的一种方案。
大致:
text
IL
+
预编译 Native Code
+
JIT Runtime
部分代码提前编译,
但运行时仍然保留:
text
JIT
能力。
因此:
text
ReadyToRun
并不是:
text
完全没有 JIT
而 Native AOT 才是:
text
运行时不使用 JIT
十二、三个模式放一起比较
可以先建立一个简单模型:
| 模式 | IL | JIT | 目标机 Runtime |
|---|---|---|---|
| Framework-dependent | 有 | 有 | 需要 |
| Self-contained | 有 | 有 | 不需要单独安装 |
| ReadyToRun | 有 | 有 | 随发布模式决定 |
| Native AOT | 发布阶段编译 | 无运行时 JIT | 不需要单独安装 |
Native AOT 更接近传统:
text
C++
↓
Native Compiler
↓
Native Executable
的部署模型。
十三、Native AOT 的完整链路
Native AOT 可以抽象成:
text
C#
│
▼
Roslyn
│
▼
IL + Metadata
│
▼
ILCompiler
│
▼
Native Object Code
│
▼
Native Linker
│
▼
Executable
对于 Linux ARM64:
text
C#
↓
IL
↓
Native AOT
↓
AArch64 Native Code
↓
ELF
最终:
bash
file MyApplication
可能看到类似:
text
ELF 64-bit LSB pie executable,
ARM aarch64
十四、这里有一个很重要的误区
Native AOT:
text
不是
简单的:
text
C#
↓
clang
↓
ARM64
clang 并不理解:
text
C#
主要的 managed code 编译工作由:
text
Native AOT Compiler
完成。
clang / linker 更多参与:
text
Native Link
阶段。
也就是:
text
Managed World
↓
Native AOT Compiler
↓
Native Object
↓
Native Toolchain
↓
ELF
十五、为什么 Native AOT 还需要 clang
很多人第一次做 Native AOT 会问:
明明是 C#,为什么 Linux 上还要安装 clang?
Microsoft 的 Native AOT 发布要求中明确要求 Linux 构建环境具备 Native Compiler Toolchain;Ubuntu 等 Linux 环境通常需要安装 clang 以及相关原生开发库。
原因就在最后阶段。
Native AOT 编译以后,需要进行:
text
Native Linking
最终把:
text
Application Native Code
Runtime Native Libraries
System Libraries
Startup Objects
链接成:
text
ELF Executable
十六、什么是 Object File
AOT 编译器不是一步直接"吐出最终应用"。
中间通常会形成 Native Object。
可以粗略理解:
text
application.o
里面已经包含:
text
ARM64 Machine Code
但是:
text
还不能直接运行
因为还有各种外部依赖:
text
libc
pthread
dl
m
zlib
OpenSSL
Runtime Native Libraries
所以需要:
text
Linker
把这些东西组织起来。
十七、Linker 到底干什么
可以把 linker 想象成:
text
多个已经编译好的零件
↓
Linker
↓
完整可执行程序
例如:
text
Application Object
+
Runtime Objects
+
libc
+
libm
+
libpthread
+
Startup Objects
↓
Link
↓
ELF
因此:
text
Native AOT
最终一定会进入传统 Native Toolchain 世界。
十八、什么是 ELF
Windows 原生程序通常是:
text
PE
Linux 原生程序通常是:
text
ELF
ELF:
text
Executable and Linkable Format
例如:
bash
file app
可能输出:
text
ELF 64-bit LSB pie executable,
ARM aarch64,
dynamically linked
这里已经可以得到大量信息。
十九、ARM64 和 AArch64 是什么关系
在很多地方你会看到:
text
ARM64
但 Linux Toolchain 又经常写:
text
aarch64
基本可以理解为:
text
ARM64
≈
AArch64
例如 .NET RID:
text
linux-arm64
而工具链可能叫:
text
aarch64-linux-gnu-gcc
或者 Target Triple:
text
aarch64-linux-gnu
Linux file 输出:
text
ARM aarch64
它们描述的都是 64 位 ARM 架构。
二十、RID 是什么
.NET 发布中经常看到:
text
linux-arm64
这叫:
text
Runtime Identifier
简称:
text
RID
它告诉发布系统:
text
目标 OS
+
目标 Architecture
例如:
text
linux-x64
linux-arm64
win-x64
win-arm64
osx-arm64
所以:
bash
dotnet publish -r linux-arm64
意味着:
我要为 Linux ARM64 目标环境发布。
二十一、Native AOT 为什么必须明确目标平台
传统 IL:
text
CPU 相对无关
但 Native AOT 以后已经变成:
text
ARM64 Machine Code
所以:
text
Linux ARM64 Native AOT
不能直接拿到:
text
Windows x64
执行。
也不能拿到:
text
Linux x64
执行。
Native AOT 发布本身就是平台相关的;当前 .NET Native AOT 支持 Linux x64/Arm64 等目标架构。
二十二、为什么 Windows 不能直接 Native AOT 出 Linux 程序
这是另一个非常重要的问题。
很多人想:
text
Windows x64
↓
linux-arm64
直接编译。
但 Native AOT 不只是生成 CPU 指令。
还需要:
text
Linux Linker
Linux System Libraries
Linux Headers
Linux ABI
Linux Native Toolchain
Microsoft 当前的 Native AOT Cross Compilation 文档说明:
Native AOT 不支持直接跨 OS 编译;例如要从 Windows 构建 Linux Native AOT,通常需要进入 Linux 环境,例如 WSL、虚拟机或类似 Linux 构建环境。
所以比较典型的是:
text
Windows
│
▼
WSL Linux x64
│
▼
Linux ARM64 Cross Compilation
而不是:
text
Windows Native Toolchain
↓
Linux ARM64 ELF
二十三、为什么 Linux x64 可以编译 Linux ARM64
因为这里:
text
OS 相同
Architecture 不同
Host:
text
Linux x64
Target:
text
Linux ARM64
这叫:
text
Cross-Architecture Compilation
Native AOT 支持 x64 和 Arm64 之间一定范围的跨架构编译,但前提是正确安装目标平台所需要的 Native Toolchain。
所以:
text
Linux x64
↓
ARM64 Toolchain
↓
Linux ARM64
是可行的。
二十四、什么叫 Cross Compiler
正常编译:
text
Host Architecture
=
Target Architecture
例如:
text
x64 Linux
↓
x64 Linux
叫 Native Compilation。
而:
text
x64 Linux
↓
ARM64 Linux
叫:
text
Cross Compilation
因为编译器运行在:
text
x64
但生成:
text
ARM64
机器码。
二十五、仅有 Cross Compiler 还不够
假设已经有:
text
aarch64-linux-gnu-gcc
或者:
text
clang --target=aarch64-linux-gnu
是不是就够了?
还不够。
因为程序最终还需要目标平台的:
text
Headers
Libraries
Startup Objects
Dynamic Loader Information
比如:
text
libc.so
libm.so
crt1.o
crti.o
crtn.o
这些都属于:
text
Target Environment
这就引出了:
text
Sysroot
二十六、Sysroot 到底是什么
Sysroot 是交叉编译中非常关键的概念。
可以简单理解成:
一个"伪装成目标 Linux 根文件系统"的编译环境。
例如:
text
/sysroot
│
├── lib
├── lib64
├── usr
│ ├── include
│ ├── lib
│ └── lib64
└── ...
其中放的是:
text
ARM64 Headers
ARM64 Libraries
ARM64 Startup Objects
而不是构建机本身的:
text
x64 Libraries
.NET Runtime 自身的 Linux Cross-Build 文档也采用这种思路:sysroot 保存目标平台的头文件和库,并保持与目标 Linux 根文件系统相似的目录结构,从而让编译器通过一个 sysroot 路径解析目标依赖。
二十七、为什么一定需要 Sysroot
假设构建机:
text
Ubuntu x64
目标:
text
Linux ARM64
构建机上的:
text
/usr/lib
里面是:
text
x86-64 Library
而目标程序需要:
text
ARM64 Library
如果 Linker 错误使用宿主:
text
x64 libc
去链接:
text
ARM64 Object
架构根本不匹配。
所以必须告诉 Toolchain:
text
不要使用 Host RootFS
请使用 Target Sysroot
二十八、Sysroot 本质上解决的是 Host 和 Target 分离
有:
text
Host
和:
text
Target
两个世界。
Host:
text
Linux x64
│
├── /usr/include
└── /usr/lib
Target:
text
Linux ARM64
│
├── /usr/include
└── /usr/lib
Cross Compile 时:
text
Compiler Running On Host
│
▼
Read Target Sysroot
│
▼
Generate Target Binary
所以:
text
Host Tool
+
Target RootFS
=
Cross Compilation Environment
二十九、什么是 glibc
Linux 程序经常看到:
text
glibc
它是 GNU C Library。
大量 Linux 用户态程序最终都会依赖它提供的系统接口封装,例如:
text
malloc
free
open
read
write
socket
pthread
clock
虽然我们写的是:
text
C#
但 Native AOT 最终已经进入:
text
Native Linux Program
世界。
因此仍然可能依赖:
text
glibc
和其他 Native Library。
三十、所以 Native AOT 并不等于"完全无依赖"
很多人看到:
text
Self-contained
就容易理解成:
什么都不依赖。
并不是。
正确理解是:
不要求目标机器额外安装对应的 .NET Runtime。
但 Linux Native 程序仍然需要满足目标系统的:
text
OS ABI
C Library
Native Dependencies
Kernel Interface
例如:
text
glibc
OpenSSL
zlib
是否动态依赖,取决于应用与发布配置。
三十一、为什么构建环境版本会影响目标兼容性
这是 Linux Native AOT 很重要的一点。
Microsoft 当前文档特别指出:
在某个 Linux 版本上生成的 Native AOT 可执行程序通常面向该环境的 Native ABI;例如在较新的 Linux 环境构建的程序,不保证能运行在更老的 Linux 环境。
原因之一就是:
text
glibc symbol version
例如程序构建时链接到了:
text
GLIBC_x.xx
而目标设备 glibc 太旧:
text
没有这个 symbol version
运行时就可能失败。
所以工业设备部署一定不能只问:
text
都是 Linux ARM64 吗?
还要问:
text
glibc 是什么版本?
目标 RootFS 是什么?
Native Library 版本是什么?
三十二、为什么设备自己的 Sysroot 更可靠
对于通用服务器:
text
Ubuntu ARM64
可以使用标准 Linux distribution toolchain。
但工业设备通常是:
text
Yocto
Buildroot
OpenEmbedded
Vendor Linux SDK
系统库版本可能非常特定。
这时更可靠的方案往往是:
text
目标设备 SDK / RootFS
↓
构造 Sysroot
↓
Cross Compile
这样:
text
Build Target ABI
更接近实际设备。
三十三、Native AOT 发布时到底发生了什么
可以把:
bash
dotnet publish
大致拆成多个阶段。
第一阶段:
text
Restore
解析:
text
NuGet Packages
Runtime Packs
Native AOT Compiler
第二阶段:
text
C# Compile
生成:
text
IL
+
Metadata
第三阶段:
text
Trimming Analysis
AOT Compatibility Analysis
分析:
text
哪些代码被使用
哪些代码可以裁剪
哪些动态行为可能不兼容
第四阶段:
text
Native AOT Compilation
把 IL 转换成:
text
Native Object Code
第五阶段:
text
Native Linking
把:
text
Application
Runtime
Libraries
链接成:
text
Linux ELF
三十四、为什么 Native AOT 和 Trimming 关系这么紧
Native AOT 的目标不是:
text
把整个 .NET 世界全部编译进 EXE
它更希望:
text
分析真正需要的代码
↓
去掉没有使用的代码
↓
只编译需要的部分
这就是:
text
Trimming
Native AOT 本身要求 trimming,因此那些依赖"运行时突然找到某个类型"的动态模式就容易出现问题。Microsoft 也把 trimming 兼容性列为 Native AOT 的核心限制之一。
三十五、举一个最简单的例子
假设:
csharp
class A
{
public void Run()
{
}
}
class B
{
public void Run()
{
}
}
程序只使用:
csharp
new A().Run();
从静态分析角度:
text
A
=
Reachable
而:
text
B
=
Unreachable
那么 B 有机会被:
text
Trim
掉。
最终:
text
Binary
不需要包含 B。
三十六、问题来了:Reflection 怎么办
假设程序:
csharp
var type =
Type.GetType(typeName);
var instance =
Activator.CreateInstance(type);
问题是:
text
typeName
可能来自:
text
配置文件
网络
数据库
用户输入
编译阶段无法明确知道:
text
运行时到底会创建哪个 Type
于是 AOT Compiler 面临问题:
text
这个 Type 要不要保留?
构造函数要不要生成?
Metadata 要不要保留?
这就是 Native AOT 与无限制 Reflection 天然冲突的原因。
三十七、Native AOT 并不是"完全没有 Reflection"
这里需要精确一点。
不能简单说:
text
Native AOT 不支持 Reflection
更准确地说:
Native AOT 对 Reflection 的支持受到静态分析和 trimming 的约束。
当前 Native AOT 会分析反射使用,只保留被确定可能通过反射访问的程序元素;不受约束的动态反射模式可能无法正常工作。
所以:
text
已知类型
已知成员
可静态分析
通常更容易兼容。
而:
text
运行时任意加载
任意生成
任意发现类型
问题就很大。
三十八、为什么动态加载很麻烦
例如:
csharp
Assembly.LoadFile(path);
然后:
text
运行时加载一个 publish 时完全不知道的 DLL
Native AOT 无法提前:
text
AOT Compile
这个 DLL 中可能执行的方法。
所以当前 Native AOT 的已知限制包括:
text
Assembly.LoadFile 等动态程序集加载
System.Reflection.Emit 等运行时代码生成
这类机制并不适用于普通 Native AOT 部署。
三十九、Reflection.Emit 为什么不行
JIT 环境可以:
text
Runtime
↓
Generate IL
↓
JIT
↓
Native Code
↓
Execute
但是 Native AOT 的核心原则是:
text
运行时不再生成新的 Native Code
所以:
text
Reflection.Emit
DynamicMethod
Runtime Code Generation
这种:
text
运行时造代码
模式天然不符合 Native AOT 模型。
四十、Source Generator 为什么突然变得重要
以前很多框架运行时喜欢这样做:
text
启动
↓
Reflection
↓
扫描 Type
↓
读取 Property
↓
动态建立 Metadata
Native AOT 更喜欢:
text
Build Time
↓
Source Generator
↓
生成 C# Code
↓
Compiler
↓
Native Code
也就是说:
把原来运行时做的事情,提前到编译阶段做。
这是 Native AOT 非常重要的一种设计思想。
四十一、System.Text.Json 就是典型例子
传统:
csharp
JsonSerializer.Serialize(obj);
需要知道:
text
这个 Type 有哪些属性?
属性叫什么?
如何读取?
如何写?
默认 Reflection 模型可能运行时扫描。
对于 Native AOT,更推荐:
text
Build Time
↓
Json Source Generator
↓
Generate Metadata / Serialization Code
↓
Native AOT
Microsoft 当前文档明确建议 Native AOT 应用使用 System.Text.Json Source Generation,避免依赖默认反射元数据模型。
四十二、一个简单的 JSON Source Generator 示例
模型:
csharp
public sealed record DeviceStatus(
string State,
double Temperature);
定义:
csharp
using System.Text.Json.Serialization;
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
这样 Source Generator 在编译阶段已经知道:
text
DeviceStatus
│
├── State
└── Temperature
无需运行时再进行无边界类型发现。
四十三、在 Minimal API 中使用
例如:
csharp
var builder =
WebApplication.CreateSlimBuilder(args);
builder.Services.ConfigureHttpJsonOptions(
options =>
{
options.SerializerOptions
.TypeInfoResolverChain
.Insert(
0,
AppJsonContext.Default);
});
var app = builder.Build();
app.MapGet(
"/api/status",
() =>
new DeviceStatus(
"Running",
42.5));
app.Run();
对应:
csharp
[JsonSerializable(typeof(DeviceStatus))]
internal partial class AppJsonContext
: JsonSerializerContext
{
}
ASP.NET Core 当前 Native AOT 模板就是沿着类似思路工作:采用 Minimal APIs,并注册 JsonSerializerContext 来支持 JSON Source Generation。
四十四、为什么 Native AOT 模板使用 Minimal API
当前 ASP.NET Core Native AOT 官方模板:
bash
dotnet new webapiaot
主要采用:
text
Minimal API
而不是传统 MVC Controller。
Microsoft 当前文档说明 Native AOT Web API 模板使用 Minimal APIs;MVC 目前仍不是 Native AOT 兼容路径。
这也是设备 Web 项目采用:
text
Minimal API
+
Static HTML
+
JavaScript
非常自然的原因之一。
四十五、CreateSlimBuilder 又是什么
标准 ASP.NET Core:
csharp
WebApplication.CreateBuilder(args);
Native AOT Template 经常使用:
csharp
WebApplication.CreateSlimBuilder(args);
Slim 的核心思想是:
默认只加入更少、更必要的 ASP.NET Core 基础能力。
这样有利于:
text
Trimming
Binary Size
AOT
Startup
当前官方 Native AOT Web API 模板默认采用 CreateSlimBuilder() 来减少默认启用的框架功能。
四十六、是不是一定要使用 CreateSlimBuilder
不是。
text
Native AOT
≠
必须 CreateSlimBuilder
仍然可以根据应用需求:
csharp
WebApplication.CreateBuilder(args);
但是:
text
引入功能越多
↓
依赖越多
↓
AOT Compatibility Surface 越大
↓
Binary 可能越大
所以对资源敏感设备:
text
CreateSlimBuilder
往往值得考虑。
四十七、PublishAot 最好放在哪里
可以命令行:
bash
dotnet publish \
-r linux-arm64 \
-p:PublishAot=true
也可以在:
text
.csproj
加入:
xml
<PropertyGroup>
<PublishAot>true</PublishAot>
</PropertyGroup>
Microsoft 当前更推荐把 PublishAot 作为项目属性,因为它不仅影响 publish,还会启用相关 AOT 分析能力。
四十八、一个基础 Native AOT 项目
例如:
xml
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>
net10.0
</TargetFramework>
<PublishAot>
true
</PublishAot>
<Nullable>
enable
</Nullable>
</PropertyGroup>
</Project>
然后:
bash
dotnet publish \
-c Release \
-r linux-arm64
目标:
text
Linux ARM64
四十九、AOT Warning 一定不要随便忽略
Native AOT 发布过程中可能看到:
text
IL2026
IL3050
...
这些 Warning 和普通:
text
unused variable
完全不是一回事。
它们可能表示:
text
Reflection 无法分析
Trimming 可能删除代码
Runtime Code Generation
AOT Incompatible API
Microsoft 对 ASP.NET Core Native AOT 的建议同样非常明确:发布时应审查并修复 AOT / trimming warnings,因为带着这些警告的程序可能在 Native AOT 运行时出现行为差异。
五十、为什么 Debug 正常,AOT 却可能失败
这是 Native AOT 最容易让人困惑的地方。
开发环境:
bash
dotnet run
通常运行在:
text
CoreCLR
+
JIT
这里:
text
Reflection
Dynamic Code
支持更加完整。
于是:
text
Debug
=
正常
但是 publish:
text
Trim
+
AOT
以后:
text
需要的 Type 被裁剪
或者:
text
动态代码路径无法工作
于是目标设备:
text
Runtime Failure
所以一定要测试:
text
真正 Publish 后的 AOT Binary
而不是只测试:
text
dotnet run
五十一、Native AOT 并不意味着程序里没有 Runtime
这里再纠正一个常见说法。
有时候会说:
text
Native AOT 没有 Runtime
这不够准确。
更准确应该说:
目标机器不需要预安装完整的共享 .NET Runtime,而且程序运行时不使用 JIT。
Native AOT 发布的可执行文件仍然包含应用运行需要的 Runtime Support。
Microsoft 文档描述为发布结果中包含运行应用所需要的代码,包括针对应用裁剪后的 Runtime 部分。
所以:
text
没有外部 .NET Runtime Requirement
不等于:
text
程序完全没有 Runtime Infrastructure
五十二、GC 还存在吗
存在。
即使:
text
Native AOT
C# 仍然有:
text
Managed Heap
Object
GC
Exception
ThreadPool
Task
async/await
不会因为 AOT 就变成:
text
C malloc/free
所以:
text
Native AOT
主要改变的是:
text
Code Generation / Deployment Model
而不是把整个 .NET Programming Model 消灭。
五十三、async/await 还存在吗
当然存在。
例如:
csharp
await socket.ReceiveAsync(...);
仍然可以正常使用。
Native AOT 会把:
text
Async State Machine
对应逻辑提前编译成本机代码。
因此:
text
async/await
和:
text
Native AOT
并不冲突。
五十四、Dependency Injection 还存在吗
也存在。
例如:
csharp
builder.Services
.AddSingleton<DeviceService>();
仍然正常。
但是如果某个第三方 DI 容器大量依赖:
text
Reflection
Dynamic Assembly
Runtime IL Emit
那可能存在:
text
AOT Compatibility
问题。
所以不是:
text
DI 不支持 AOT
而是要看:
text
DI Container 如何实现
五十五、为什么 Native AOT 特别适合设备软件
对于设备端,Native AOT 有几个非常直接的优势。
第一:
text
目标设备不必单独安装 .NET Runtime
部署模型更简单。
第二:
text
启动路径更直接
没有运行时 JIT。
Native AOT 官方文档将:
text
更快启动
更低内存占用
作为其主要优势之一。
第三:
text
部署结构更加固定
设备软件可以变成类似:
text
/opt/device-web/device-web
由:
text
systemd
直接启动。
第四:
text
Binary 行为更加静态
减少:
text
运行时动态加载
动态代码生成
这类行为对设备软件来说通常也是好事情。
五十六、但 Native AOT 绝对不是"免费性能"
不能简单理解为:
text
AOT
=
一定比 JIT 快
运行性能取决于很多因素:
text
CPU
Workload
GC
JIT Optimization
PGO
AOT Optimization
Code Size
Cache
JIT 可以根据:
text
实际 CPU
实际 Runtime
实际执行热点
进行动态优化。
而 AOT 必须:
text
提前决定
因此:
Native AOT 最大价值往往首先是部署模型、启动时间、内存和静态可预测性,而不是"所有业务运行速度一定更快"。
五十七、Native AOT 的 Binary Size 也不是越小越好
Native AOT 会:
text
Trim
+
Compile
因此通常可以得到比较紧凑的部署结果。
但还存在:
text
Size
vs
Speed
权衡。
当前 Native AOT 支持:
xml
<OptimizationPreference>
Size
</OptimizationPreference>
或者:
xml
<OptimizationPreference>
Speed
</OptimizationPreference>
来偏向体积或执行性能。
对设备端来说:
text
Flash 紧张
和:
text
CPU 性能紧张
可能需要不同选择。
五十八、StripSymbols 是什么
Native AOT 编译以后还会产生:
text
Debug Symbols
Linux 上默认可能生成单独的:
text
.dbg
文件。
Microsoft 当前文档指出,Linux Native AOT 默认会生成单独的 .dbg 调试信息文件;是否保留或嵌入调试信息会影响最终发布体积。
设备正式部署时可以:
text
Program Binary
与:
text
Debug Symbol
分开保存。
例如:
text
Device
↓
stripped executable
开发服务器:
text
symbol archive
方便后续分析 Crash。
五十九、Linux ARM64 Cross Compile 的完整图
现在把所有东西串起来。
构建机器:
text
Linux x64
安装:
text
.NET SDK
Native AOT Compiler
clang
ARM64 Cross Toolchain
准备:
text
ARM64 Sysroot
然后:
text
C# Source
│
▼
Roslyn
│
▼
IL
│
▼
Native AOT Compiler
│
▼
ARM64 Object Code
│
▼
clang / linker
│
├── ARM64 Sysroot
│
├── ARM64 libc
│
├── ARM64 crt
│
└── Runtime Native Libraries
│
▼
Linux ARM64 ELF
这就是:
text
Linux x64
↓
Linux ARM64
Native AOT Cross Compilation 的核心模型。
六十、SysRoot 参数在整个流程中的位置
如果交叉编译需要指定 Sysroot,可以概念上理解:
bash
dotnet publish \
-c Release \
-r linux-arm64 \
-p:SysRoot=/path/to/arm64/sysroot
这里:
text
SysRoot
主要服务于 Native Toolchain / Linking 阶段。
.NET Runtime Native AOT 文档同样说明,Linux 跨架构编译可能需要建立目标 sysroot,并通过 SysRoot 指定。
六十一、如何验证最终真的是 ARM64
第一步:
bash
file MyApplication
应该看到类似:
text
ELF 64-bit LSB
ARM aarch64
第二步:
bash
readelf -h MyApplication
可以看到:
text
Class
Machine
Entry point
其中 Machine 应该对应:
text
AArch64
第三步可以查看:
bash
ldd MyApplication
了解动态 Native Dependencies。
注意:
text
Native AOT
不代表:
text
ldd 一定什么都没有。
六十二、为什么 x64 机器不能运行 ARM64 ELF
如果:
text
file app
显示:
text
ARM aarch64
而你在:
text
Linux x64
直接:
bash
./app
通常会看到类似:
text
Exec format error
这不是应用 Bug。
而是:
text
CPU Instruction Set 不匹配
Microsoft 提供的 Native AOT ARM64 Cross-Build 示例也明确展示了:ARM64 ELF 需要在 ARM64 Linux 上执行,在 x64 Host 上直接运行会因为格式/架构不匹配而失败。
六十三、目标设备第一次验证建议做什么
不要一上来就跑完整系统。
先验证:
bash
uname -m
期望:
text
aarch64
然后:
bash
file MyApplication
再:
bash
ldd MyApplication
最后:
bash
./MyApplication
如果失败:
text
No such file or directory
不一定真的是:
text
文件不存在
还有可能是:
text
ELF Interpreter 不存在
Dynamic Loader 路径不匹配
Native Library 缺失
六十四、Linux Native 程序问题为什么要学 readelf
做 Native AOT 到一定程度以后:
text
C# Debugger
已经不能解决所有问题。
你需要逐渐熟悉:
text
file
readelf
ldd
objdump
nm
strace
gdb
因为最后你面对的已经是:
text
Linux Native Executable
不是单纯:
text
Managed DLL
六十五、这也是 Native AOT 最大的思维变化
传统 .NET 开发:
text
C#
ASP.NET Core
NuGet
Runtime
Native AOT Linux 开发则跨越了:
text
Managed World
+
Native World
你同时需要理解:
text
C#
IL
Runtime
AOT
Trimming
Reflection
Source Generator
以及:
text
ELF
ABI
glibc
Linker
Sysroot
Cross Toolchain
这就是为什么 Native AOT 比:
text
dotnet publish
这条命令本身复杂得多。
六十六、最终架构
对于一个工业 Linux ARM64 Web 应用,可以形成:
text
Development
│
▼
C# Source
│
▼
Roslyn
│
▼
IL Code
│
▼
Trimming Analysis
│
▼
Native AOT Compiler
│
▼
ARM64 Native Code
│
▼
Native Toolchain
│
┌────────┴────────┐
│ │
Sysroot Linker
│ │
└────────┬────────┘
▼
Linux ARM64 ELF
│
▼
Deploy
│
▼
systemd
│
▼
ASP.NET Core App
六十七、几个最重要的结论
如果把整篇压缩下来,最值得记住的是下面这些。
1. C# 默认不是直接编译成 CPU 指令
正常:
text
C#
↓
IL
2. 普通 .NET 使用 JIT
text
IL
↓
运行时 JIT
↓
Machine Code
3. Native AOT 把编译提前到 publish
text
IL
↓
AOT
↓
Machine Code
4. Native AOT 不使用运行时 JIT
但仍然存在:
text
GC
ThreadPool
Task
Exception
Runtime Support
5. Self-contained 不等于 Native AOT
Self-contained 解决:
text
Runtime 部署
Native AOT 解决:
text
Code Generation Model
6. Native AOT 最终会进入 Native Toolchain
所以你必须理解:
text
Compiler
Linker
ELF
glibc
ABI
Sysroot
7. Linux x64 → Linux ARM64 属于 Cross Architecture
所以需要:
text
ARM64 Toolchain
+
ARM64 Sysroot
8. Native AOT 和无限动态行为天然冲突
包括:
text
Reflection
Dynamic Assembly Loading
Runtime Code Generation
所以应该更多考虑:
text
Static Analysis
Source Generator
Explicit Metadata
9. JSON Source Generator 非常重要
因为:
text
运行时发现类型
变成:
text
编译时生成类型信息
非常符合 Native AOT 模型。
10. Native AOT 最大价值不只是"快"
对于工业设备,真正有价值的是:
text
无需目标设备单独安装 .NET Runtime
更快启动
更低的 Runtime Memory Overhead
更加静态的部署方式
更加适合固定功能设备
总结
现在再来看:
bash
dotnet publish \
-c Release \
-r linux-arm64 \
-p:PublishAot=true
就应该知道,这并不是简单的:
text
C# → ARM64
背后实际上经历:
text
C#
↓
Roslyn
↓
IL
↓
Static Analysis
↓
Trimming
↓
Native AOT Compiler
↓
ARM64 Object Code
↓
Native Linker
↓
Sysroot / libc / Native Libraries
↓
Linux ARM64 ELF
而最终在设备上执行的:
text
./MyApplication
已经是一个真正的:
text
Linux ARM64 Native Executable
这也是 Native AOT 最重要的意义:
把 .NET 应用从"依赖运行时即时编译的 Managed Application",进一步转换为面向特定目标平台发布的 Native Application。
但与此同时,你也必须接受:
text
动态性降低
构建复杂度增加
AOT Compatibility 要求提高
Native Toolchain 知识要求提高
这就是 Native AOT 的真正代价。
对于功能固定、资源相对受限、长期运行的 Linux ARM64 工业设备,这种交换通常非常值得考虑。
下一篇
下一篇建议继续沿着这里最关键的一个问题深入:
《Linux ARM64 Native AOT 交叉编译详解:Sysroot、glibc、clang 和 Linker 到底是什么关系?》
下一篇不再泛讲 Native AOT,而是专门拆:
text
Windows
↓
WSL Linux x64
↓
.NET SDK
↓
clang
↓
ARM64 Cross Toolchain
↓
Sysroot
↓
glibc
↓
crt
↓
Linker
↓
ARM64 ELF
重点回答:
text
为什么有了 linux-arm64 RID 还不够?
Sysroot 到底应该包含什么?
为什么会出现找不到 crtbeginS.o?
为什么会出现 incompatible library?
为什么 Host 的 /usr/lib 不能拿来链接 ARM64?
glibc 版本为什么决定目标机兼容性?
ELF Interpreter 是什么?
动态链接器 ld-linux-aarch64.so.1 又是什么?
如何使用 file、readelf、ldd 判断一个 AOT 程序到底哪里有问题?
这篇会真正进入 Linux Toolchain 层面。