ASP.NET Core Native AOT 到底是什么?从 C#、IL、JIT 一直讲到 Linux ARM64 ELF

ASP.NET Core Native AOT 到底是什么?从 C#、IL、JIT 一直讲到 Linux ARM64 ELF

前言

前面几篇我们已经讨论了:

  1. ASP.NET Core 为什么适合 Linux ARM64 设备
  2. Minimal API 的 HTTP 请求处理链路
  3. 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 层面。

相关推荐
用户0510122572964 小时前
Linux下有关QT显示环境变量设置
linux·嵌入式
K成长日志5 小时前
BLE链路层-Feature Support
网络·嵌入式·无线通信·蓝牙·iot·ble
捷瑞电子工坊9 小时前
嵌入式蓝桥杯从零点亮第一个LED
stm32·蓝桥杯·cubemx·嵌入式·led·ll·锁存
嵌入式阿蔡1 天前
智能家居互联2026:Matter、Thread与嵌入式开发的新范式
嵌入式
花 满 楼1 天前
HSM自学之路——阶段 1 密码学算法分类与原理
嵌入式·hsm
优信电子1 天前
STM32 驱动 DHT30 测量环境温湿度(兼容SHT30):从时序解析到完整代码实战
stm32·嵌入式·传感器·sht30·温湿度·环境测量·dht30
剑指offer.1 天前
嵌入式硬件-ARM芯片的启动
c语言·嵌入式硬件·嵌入式
晊晌_h1 天前
ARM 学习 |ARM 汇编指令实战笔记
嵌入式·arm·arm汇编
嵌入式阿蔡1 天前
ROS2嵌入式量产2026:从科研原型到工业部署
嵌入式