1. 项目概述:内核异常处理的“红绿灯”

在Linux内核开发这片充满挑战的领域里,调试和异常处理是每个驱动工程师或内核贡献者的必修课。想象一下,你正在编写一个设备驱动,代码运行在拥有最高权限的内核空间,一个微小的逻辑错误就可能导致整个系统崩溃(内核恐慌,Kernel Panic),数据丢失。这时,内核提供了一套强大的“断言”机制,就像交通信号灯, BUG_ON() 是刺眼的红灯,意味着必须立即停车(崩溃并收集现场);而 WARN_ON() 则是闪烁的黄灯,提醒你这里有潜在危险,但系统还能继续运行,让你有机会把错误信息记录下来,事后分析。理解并正确使用这两个宏,是写出健壮、可调试内核代码的关键技能,直接关系到你代码的稳定性和问题排查效率。

很多刚接触内核开发的朋友,容易混淆两者的使用场景,要么过度使用 BUG_ON() 导致不必要的崩溃,用户体验极差;要么该用 BUG_ON() 时却用了 WARN_ON() ,让一个本应立刻暴露的严重错误潜伏下来,成为更难排查的“幽灵问题”。今天,我们就深入内核源码,彻底搞懂 BUG_ON() 和 WARN_ON() 的设计哲学、实现原理、使用场景以及背后的调试技巧。无论你是正在学习《Linux设备驱动开发》,还是在实际项目中遇到了棘手的内核Oops,这篇文章都能给你提供清晰的路径和实用的“避坑指南”。

2. 核心设计:为何需要两种级别的“断言”?

内核开发与用户态编程一个本质区别在于运行环境。用户态程序崩溃,通常只影响自身,操作系统可以清理现场。而内核态的一个非法访问或死锁,可能直接破坏整个系统的完整性。因此,内核的异常处理机制必须更加精细和有力。 BUG_ON() 和 WARN_ON() 正是这种精细化设计的产物,它们服务于不同严重级别的“不可接受”条件。

2.1 BUG_ON():不可恢复错误的“终结者”

BUG_ON(condition) 的设计理念非常明确:当 condition 为真时,意味着内核检测到了一个 致命且不可恢复 的逻辑错误。继续执行下去,极有可能导致数据损坏、硬件状态异常或安全漏洞。此时,最负责任的做法就是立即“死给你看”。

它的核心行为包括:

  1. 触发内核恐慌(Kernel Panic) :这是它的主要效果。系统会停止一切活动,打印出Oops信息(包括寄存器状态、调用栈等),然后停止运行或重启(取决于配置)。
  2. 生成崩溃转储 :如果配置了kdump等机制,会保存崩溃时刻的内存映像,供后续深度分析。
  3. 明确标记BUG位置 :在Oops信息中,会清晰指出触发 BUG_ON() 的源代码文件和行号。

使用场景举例:

  • 指针解引用前的空指针检查 : BUG_ON(!ptr); 。在内核中,一个必然非空的指针如果为空,说明前置逻辑有严重缺陷。
  • 锁状态验证 :在某个函数中,假设调用者必须已经持有了某把锁(锁由调用者管理),可以使用 BUG_ON(!spin_is_locked(&my_lock)); 来验证。如果锁未被持有,说明同步逻辑混乱,继续执行可能导致数据竞争。
  • 关键数据结构完整性校验 :例如,在遍历一个双向链表时,发现 prev->next != next->prev ,链表已被破坏,此时 BUG_ON() 是合适的。

注意 : BUG_ON() 绝不能用于处理 可能发生 的运行时错误,比如内存分配失败( kmalloc 返回NULL)、硬件暂时不可用等。这些情况应该通过返回错误码( -ENOMEM , -EIO )给上层来处理。 BUG_ON() 用于保卫那些“理论上绝不应该发生”的条件。

2.2 WARN_ON():可容忍异常的“记录员”

WARN_ON(condition) 则温和得多。当 condition 为真时,它认为这是一个 异常、出乎意料但并非致命 的情况。系统不应该崩溃,但开发者必须知道这件事发生了。

它的核心行为包括:

  1. 打印警告信息 :在系统日志(如 dmesg )中输出一条详细的WARNING消息,包含调用栈、条件表达式和位置信息。
  2. 增加警告计数器 :系统全局有一个 warn_count 会递增。你可以通过 /proc/sys/kernel/warn_count 查看。
  3. 继续执行 :这是与 BUG_ON() 最根本的区别。代码会跳过 WARN_ON() ,继续执行后续逻辑。

使用场景举例:

  • 驱动中的罕见路径 :某个硬件状态标志在正常操作下永远为0,但理论上硬件故障可能使其为1。你可以用 WARN_ON(unexpected_hw_flag) 来记录这种罕见事件,而不影响其他正常功能。
  • 性能或优化问题提示 :例如,某个函数本应在中断禁用状态下调用,但检测到中断被启用了。这不一定立即引发错误,但可能导致性能下降或潜在的不确定性。 WARN_ON(!irqs_disabled()) 可以提醒开发者检查调用上下文。
  • 调试期间的临时检查 :在开发阶段,你怀疑某个条件偶尔会成立,但又不想让测试频繁崩溃, WARN_ON() 是完美的工具。它让你能在日志中收集到“案发现场”信息。

两者选择的核心决策流程图:

                   发现一个“错误”条件
                           |
                           v
            [条件成立是否意味着内核状态已不可信/继续运行必然导致灾难?]
                           |
           是(致命,不可恢复)              否(异常,但可继续)
                           |                           |
                           v                           v
                     使用 BUG_ON()               使用 WARN_ON()
                     系统崩溃,保留现场           记录日志,继续运行

3. 源码级实现原理拆解

要真正用好它们,不能只停留在概念。让我们深入到源码(以x86架构的Linux内核为例),看看这两个宏到底做了什么。理解实现,能让你更好地解读它们产生的错误信息。

3.1 BUG_ON() 的实现剖析

在 include/asm-generic/bug.h 中, BUG_ON() 通常最终会展开为对 BUG() 的调用。

#define BUG_ON(condition) do { \
    if (unlikely(condition)) BUG(); \
} while (0)

这里的 unlikely() 是一个给编译器的提示,告诉它这个条件为真的可能性很小,帮助优化指令分支预测。

BUG() 宏则是架构相关的。在x86上( arch/x86/include/asm/bug.h ),它可能最终执行一个非法操作(如未定义指令),或者直接调用 panic() 函数。

一个经典的实现是触发一个 非法操作码 (例如 ud2 指令)或产生一个 调试异常 ( int3 )。CPU执行到这条指令时,会陷入异常处理程序。内核的异常处理程序(例如 do_trap() )会识别出这是由 BUG() 触发的,然后调用 die() 函数。

die() 函数是Oops信息的生产者。它会:

  1. 打印出著名的 Oops: 消息。
  2. 打印出寄存器内容( EIP / RIP 指向触发BUG的指令地址)。
  3. 打印出内核调用栈(stack trace)。
  4. 最后,调用 panic() 函数,使内核停止。

一个典型的 BUG_ON() 触发的Oops信息片段:

BUG: unable to handle kernel NULL pointer dereference at 0000000000000000
IP: [<ffffffff81234567>] my_driver_function+0x123/0x456
PGD 0
Oops: 0002 [#1] SMP PTI
CPU: 0 PID: 1234 Comm: myapp Tainted: G           OE
RIP: 0010:[<ffffffff81234567>]  [<ffffffff81234567>] my_driver_function+0x123/0x456
RSP: 0018:ffffc90001234560  EFLAGS: 00010246
...
Call Trace:
 [<ffffffff810abcde>] ? some_caller+0xfe/0x230
 [<ffffffff81123456>] ? another_caller+0x78/0xa0

关键信息 IP: [<...>] my_driver_function+0x123/0x456 直接告诉你问题发生在 my_driver_function 函数的偏移 0x123 处。结合源码和 addr2line 工具,就能精确定位到 BUG_ON() 所在行。

3.2 WARN_ON() 的实现剖析

WARN_ON() 的定义也在 include/asm-generic/bug.h 中。

#define WARN_ON(condition) ({ \
    int __ret_warn_on = !!(condition); \
    if (unlikely(__ret_warn_on)) \
        __WARN_printf(TAINT_WARN, NULL, "WARNING: at %s:%d\n", __FILE__, __LINE__); \
    unlikely(__ret_warn_on); \
})

__WARN_printf 是核心,它会:

  1. 增加 warn_count 。
  2. 调用 warn_slowpath_fmt() 函数。
  3. warn_slowpath_fmt() 会打印出格式化的警告信息,包括文件、行号、模块名(如果是以模块形式加载)、以及完整的调用栈。
  4. 它还会给内核“打上”一个 TAINT_WARN 标记。你可以通过 cat /proc/sys/kernel/tainted 查看内核是否被“污染”,以及污染原因。 WARN_ON() 是污染原因之一。

一个典型的 WARN_ON() 输出:

[  123.456789] WARNING: CPU: 1 PID: 5678 at drivers/mydriver/myfile.c:89 my_function+0x45/0xa0 [mydriver]
[  123.456791] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996)
[  123.456792] Modules linked in: mydriver(OE+) ...
[  123.456795] RIP: 0010:my_function+0x45/0xa0 [mydriver]
[  123.456796] RSP: 0018:ffffc90009876e10 EFLAGS: 00010286
[  123.456797] ...
[  123.456799] Call Trace:
[  123.456800]  [<ffffffffc0123456>] ? other_func+0x34/0x50 [mydriver]
[  123.456801]  [<ffffffff81098765>] ? worker_thread+0x105/0x470

注意开头的 WARNING: 标识,以及非常友好的 at drivers/mydriver/myfile.c:89 ,直接指向了源码文件和行号。

3.3 变体:WARN_ON_ONCE() 和 BUG_ON_ONCE()

在实际使用中,你可能会遇到一个循环或高频路径中触发的警告或BUG。如果每次触发都打印日志或崩溃,日志会被刷屏,或者系统在崩溃边缘反复重启,不利于定位根本原因。

内核提供了“仅一次”的变体:

  • WARN_ON_ONCE(condition) :仅在第一次条件为真时打印警告信息,后续再触发则静默。它内部使用一个静态的 bool 变量 __warned 来实现。
  • BUG_ON_ONCE(condition) :同理,仅在第一次条件为真时触发崩溃。这在处理一些“只要发生一次就足以证明系统状态异常”的场景中非常有用,避免了因同一错误在崩溃恢复过程中反复触发而导致的“崩溃风暴”。

4. 实战应用与调试技巧

理解了原理,我们来看看如何在真实的内核模块或驱动开发中使用它们,以及当它们被触发时,如何高效地调试。

4.1 在代码中的正确放置

BUG_ON() 放置原则:前置检查与不变式守卫

它应该放在函数中 你认为绝对不可能发生的条件 检查上,通常用于验证函数执行的 前置条件 或数据结构的 不变式 。

static int my_critical_function(struct my_data *data)
{
    // 前置条件:调用者必须传入有效的data指针
    BUG_ON(!data);
    // 前置条件:本函数必须在持有data->lock的情况下调用
    BUG_ON(!spin_is_locked(&data->lock));

    // 不变式:data->magic 必须等于初始化的值
    BUG_ON(data->magic != MY_DATA_MAGIC);

    // ... 核心业务逻辑 ...

    // 不变式:操作完成后,某个内部链表应该仍然满足特定条件
    BUG_ON(list_empty(&data->active_list) && data->in_use);

    return 0;
}

WARN_ON() 放置原则:异常路径记录与调试辅助

它适合放在那些 理论上可能发生,但代表某种错误或非预期状态 的地方。常用于调试、性能问题追踪或记录硬件异常。

irqreturn_t my_interrupt_handler(int irq, void *dev_id)
{
    struct my_device *dev = dev_id;
    u32 status = ioread32(dev->reg_base + STATUS_REG);

    // 正常情况只有一位被置位,如果多位被置位,可能是硬件毛刺或驱动bug,记录但继续处理
    if (hweight32(status) > 1) {
        WARN_ON(1, "Multiple status bits set: 0x%08x\n", status);
    }

    // 处理中断...
    if (status & DEVICE_ERROR_BIT) {
        // 设备报告错误,这不是内核bug,是硬件问题,记录并尝试恢复
        dev->error_count++;
        WARN_ONCE(dev->error_count > 10, "Device persistent error, count=%d\n", dev->error_count);
        schedule_work(&dev->recovery_work);
    }

    return IRQ_HANDLED;
}

4.2 调试:解读Oops和Warning信息

当 BUG_ON() 触发后,你会在控制台或日志中看到Oops信息。调试的关键步骤:

  1. 定位出错位置 :找到 IP: 或 RIP: 行,它给出了触发BUG的指令地址。例如 [<ffffffff81234567>] my_driver_function+0x123/0x456 。
  2. 使用工具解析 :
    • 在内核源码树中 :如果你有编译时产生的 System.map 文件,可以用 grep 查找接近的符号。 81234567 这个地址需要减去内核的加载基址(通常 0xffffffff81000000 ),但更简单的方法是直接使用 addr2line 。
    • 使用 addr2line : addr2line -e vmlinux -f -C -i 0xffffffff81234567 。这里 vmlinux 是带有调试信息的内核镜像。这会直接输出函数名和源代码行号。
    • 查看调用栈 : Call Trace: 部分展示了函数调用链,帮助你理解错误发生时的上下文。从下往上看(或从上往下,取决于格式),是调用顺序。
  3. 分析寄存器状态 : RSP 、 RAX 、 RBX 等寄存器的值有时能提供线索。例如,如果BUG是空指针解引用,那么触发异常的指令所访问的地址很可能就在某个寄存器中,你可以检查它是否为0或一个非法值。

对于 WARN_ON() ,调试更简单。日志中已经给出了明确的文件、行号和调用栈。你只需要:

  1. dmesg | tail -50 查看最新的内核日志。
  2. 找到 WARNING 消息,直接去对应的源码行分析 condition 为何会成立。
  3. 结合调用栈,理解函数是如何被执行到那个状态的。

4.3 高级技巧与配置

  • 控制Oops行为 :通过 /proc/sys/kernel/panic 、 /proc/sys/kernel/panic_on_oops 等参数,可以配置系统在发生Oops后是立即恐慌(panic)、一段时间后恐慌,还是只打印信息而不恐慌(对于某些嵌入式调试场景可能有用)。 注意 :修改这些参数可能使系统不稳定,生产环境慎用。
  • kdump与crash工具 :这是分析内核崩溃(包括 BUG_ON 触发)的终极武器。它能在内核崩溃时,捕获一个内存转储文件(vmcore)。然后你可以用 crash 工具离线分析这个转储文件,像调试活系统一样检查崩溃时刻的所有内存、进程、锁的状态。配置和使用 kdump 是一个相对复杂但极其强大的话题。
  • 动态调试与Tracepoint :对于偶现的、由 WARN_ON() 捕获的问题,可以结合内核的 dynamic_debug 功能,在特定路径上打开更详细的打印。或者,在问题函数周围添加 tracepoint ,使用 ftrace 或 perf 来追踪函数的执行流和参数,这比添加大量 printk 更高效、对性能影响更小。
  • 静态分析工具 :像 sparse 、 Coverity 、 Coccinelle 这样的静态代码分析工具,有时能在编译前就发现可能导致 BUG_ON() 条件成立的逻辑错误。将它们集成到开发流程中,能防患于未然。

5. 常见误区、问题排查与性能考量

即使理解了概念,在实际使用中还是会踩坑。下面是一些常见的误区和问题排查思路。

5.1 使用误区对照表

误区 错误示例 可能导致的问题 正确做法
用BUG_ON处理可恢复错误 ret = kmalloc(...); BUG_ON(!ret); 内存紧张时系统无故崩溃,体验差。 if (!ret) return -ENOMEM;
用WARN_ON处理致命错误 链表核心结构被破坏,用 WARN_ON 。 错误被掩盖,系统在损坏状态下继续运行,可能导致随机崩溃或数据损坏,更难调试。 使用 BUG_ON ,立即暴露问题。
在中断上下文或持有锁时调用可能睡眠的函数 BUG_ON(in_interrupt()); /* 正确 */ 但条件检查函数本身(如某些验证函数)可能睡眠。 直接导致死锁或内核异常。 确保 BUG_ON/WARN_ON 的条件表达式本身是安全的,不会调用 might_sleep() 的函数。
过度使用BUG_ON 在每个函数入口都检查参数非空。 代码冗余,且使内核镜像变大(因为BUG宏展开后代码量不小)。如果调用者很多,会轻微影响性能。 只在 最关键、最核心 的不变式上使用。对于函数参数检查,如果调用者也是内核代码,通常依靠代码审查和约定;对用户态传入的参数,则必须用错误返回码验证。
忽略WARN_ON 代码中写了 WARN_ON ,但从不查看 dmesg 。 失去了其预警价值,问题可能积累并最终以更严重的形式爆发。 建立日志监控,将内核WARNING信息纳入监控告警系统。

5.2 性能影响分析

这是一个经常被问到的问题: BUG_ON() 和 WARN_ON() 会影响性能吗?

  • BUG_ON() :在条件为假(这是期望的正常情况)时,它基本上就是一条 unlikely 分支判断指令,对现代CPU的性能影响微乎其微。其开销主要在于它会使内核代码段变大。 因此,不必过于担心其性能开销,而应关注其必要性。
  • WARN_ON() :它的开销稍大一些。因为即使条件为假,它也需要计算条件,并且宏展开会包含一些函数调用框架(尽管 unlikely 会优化分支)。更重要的是, 当条件为真时 ,生成警告信息、打印调用栈是一个相对昂贵的操作,涉及获取锁、格式化字符串、控制台输出等。因此,绝对不要在高频路径(如网络数据包处理循环、内存分配热路径)中使用 WARN_ON ,除非是调试目的。对于高频路径中的检查,考虑使用 WARN_ON_ONCE 或静态的 rate-limited 打印。

一个性能优化案例: 假设你在一个每秒被调用数百万次的快速路径函数中,需要检查一个很少出现的硬件标志。

// 不好:每次调用都检查并可能打印
void fast_path_func(void) {
    if (unlikely(hw_reg & UNEXPECTED_FLAG)) {
        WARN_ON(1);
    }
    // ...
}

// 更好:使用ONCE变体,或者将检查移出热路径
void fast_path_func(void) {
    // 如果标志位一旦被设置就持续有效,可以在中断处理函数中设置一个状态变量,这里只读变量。
    if (unlikely(device_state_has_error)) {
        handle_error_slowly(); // 慢速处理函数
    }
    // ...
}

5.3 问题排查清单

当你的内核模块因为 BUG_ON() 或频繁的 WARN_ON() 而出现问题时,可以按照以下清单进行排查:

  1. 信息收集 :

    • 完整截取控制台Oops信息或 dmesg 中的WARNING信息。
    • 记录内核版本、模块版本、配置。
    • 如果可能,记录操作步骤,看是否能稳定复现。
  2. 初步分析 :

    • 对于BUG_ON :立即用 addr2line 或 gdb/vmlinux 定位到具体代码行。分析触发条件( condition )所依赖的变量,在调用栈的上一层函数中,这些变量是如何被设置或传递的?
    • 对于WARN_ON :查看完整的调用栈。这个警告是在什么上下文中触发的(进程上下文、中断上下文、软中断)?条件表达式中的变量当前值是什么?它是否与某个硬件状态、用户输入或并发事件相关?
  3. 并发与竞态怀疑 :

    • 内核中大部分棘手的BUG都与并发有关。检查触发点附近的代码:
      • 是否有共享数据(全局变量、结构体字段)?
      • 这些数据是否被多个CPU核心、中断处理程序或内核线程访问?
      • 访问时是否使用了正确的锁( spin_lock , mutex , rcu_read_lock )?
      • 是否存在“先检查后执行”(TOCTOU)的竞态条件? BUG_ON/WARN_ON 本身也可能在检查的瞬间条件成立,但执行时状态已变。
  4. 内存损坏怀疑 :

    • 如果BUG是非法指针访问,或者数据结构 magic 字段被篡改,很可能发生了内存越界写入(缓冲区溢出)或释放后使用(Use-After-Free)。
    • 使用 KASAN (内核地址消毒器)工具重新编译内核和模块。 KASAN 能检测出很多内存错误,并在发生错误的第一时间报告,比等到 BUG_ON 触发更容易定位。
    • 检查是否有其他地方写穿了数组边界,或者错误地使用了 kfree / devm_kfree 。
  5. 硬件/驱动交互怀疑 :

    • 对于驱动代码, WARN_ON 可能提示硬件状态异常。检查:
      • 硬件初始化序列是否正确?
      • 中断处理程序是否清除了正确的状态位?
      • 是否存在DMA操作与CPU访问的同步问题(需要内存屏障 barrier() )?
      • 硬件本身是否有缺陷?换一个硬件试试。
  6. 调试复现 :

    • 尝试构造一个最小的、可复现的测试用例。
    • 使用 ftrace 的 function_graph 跟踪相关函数的调用和返回。
    • 在可疑代码区域添加更多的 pr_debug (配合 dynamic_debug 动态开启)或 trace_printk 来追踪数据流。
    • 对于并发问题,可以尝试使用 lockdep (锁依赖检测器)来验证锁的使用是否正确。 lockdep 是内核内置的强力工具,能发现很多潜在的锁顺序死锁问题。

6. 总结与最佳实践心得

经过对 BUG_ON() 和 WARN_ON() 从设计到源码,从使用到调试的深入探讨,我们可以提炼出一些核心的最佳实践,这也是我在多年内核开发中积累的经验:

1. 态度决定选择: 把 BUG_ON() 视为 对内核完整性的终极守护 ,用于防御那些“一旦发生,世界(内核)就不应该继续存在”的错误。把 WARN_ON() 视为 给开发者的监控告警 ,用于记录那些“不对劲,但我们可以暂时忍受并调查”的事件。

2. 前置条件与不变式: 在函数的开头,用 BUG_ON() 严格校验调用者必须满足的、且你无法在编译时保证的 强前置条件 (如持有某把锁)。在函数的中间或结尾,用 BUG_ON() 校验关键的 数据结构不变式 。这相当于给你的代码加上了坚固的护栏。

3. 日志是金: 永远不要忽略 WARN_ON() 产生的日志。建议在开发和测试环境中,将内核日志级别调低(如 echo 8 > /proc/sys/kernel/printk ),并确保所有 WARNING 信息能被捕获和存档。可以考虑使用 netconsole 将内核日志通过网络发送到另一台机器,即使系统完全卡死,也能收到最后的“遗言”。

4. 性能敏感路径慎用: 在性能关键的代码路径(调度器、网络/块设备驱动数据面、内存分配器核心路径)中,避免使用任何会在条件为真时产生副作用的检查。如果必须检查,考虑使用 READ_ONCE() / WRITE_ONCE() 安全地读取变量,并结合静态的、基于时间或计数的限速报告机制,而不是每次都调用 WARN_ON 。

5. 利用更强大的工具: BUG_ON 和 WARN_ON 是运行时检查。在它们触发之前,我们可以借助更早阶段的工具来消灭BUG:

  • 编译时 :使用 __attribute__((nonnull)) 标注指针参数,让编译器给出警告。使用静态分析工具(如 Coccinelle 的语义补丁)来查找代码模式中的问题。
  • 运行时早期 :在开发阶段,全力启用内核的 CONFIG_DEBUG_* 系列选项,如 DEBUG_SPINLOCK , DEBUG_MUTEXES , DEBUG_LIST ,它们会在操作不当(如未初始化就使用、重复释放)时提前触发警告或BUG。
  • 内存错误 :务必使用 KASAN 。它在发现内存错误方面的能力是革命性的,能精准定位到越界或释放后使用的代码行。

6. 最后一道防线: 即使代码中布满了 BUG_ON ,它也只是“最后一道防线”。最根本的,还是严谨的设计、清晰的代码逻辑、全面的单元测试(内核有KUnit)和集成测试。 BUG_ON 和 WARN_ON 是你自信的体现——你确信某些条件不会成立,如果成立了,那一定是底层出了问题,需要立刻拉响警报。

我个人在编写内核代码时,会像条件反射一样思考:这个地方如果出问题,是“世界末日”级别的,还是“需要调查”级别的?前者用 BUG_ON ,后者用 WARN_ON 。并且,在提交代码前,我会问自己:如果这个 BUG_ON 被触发了,产生的Oops信息是否足以让另一个工程师(或者三个月后的我自己)快速定位到根本原因?如果不够,那就需要添加更详细的错误上下文信息到数据结构中,或者考虑使用 BUG() 的变体 BUG_ON_MSG(condition, fmt, ...) (如果内核版本支持)来打印更多信息。记住,好的防御性编程,不仅在于发现问题,更在于高效地解决问题。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐