从一次illegal instruction问题排查,总结下LoongCollector/iLogtail在环境兼容性方面的工作

太业

背景

LoongCollector/iLogtail 是一款专为端上可观测数据采集而设计的工具,它在开发过程中采用了C++与Go语言的混合编程策略,以充分发挥两者的优势。这款采集器能够在多种机器环境中部署,能够服务于不同的应用场景。由于C++部分涉及到对底层系统的直接操作,因此其兼容性问题便成为了一个极具挑战性的课题。

以Linux环境为例,有众多因素可能会影响C++的兼容性,包括但不限于内核版本、核心依赖库的版本、CPU型号以及CPU指令集等。不同的机器配置和系统环境可以导致程序在某些特性上的表现不一致,从而引发潜在的运行时错误或性能问题。

在我们推出的iLogtail 2.0版本中,就面临了一个兼容性问题。在这个过程中,我们不仅进行了详细的排查与定位,还深入探讨了相关的技术背景与解决方案。本篇文章将系统记录该问题的排查流程、所采用的技术手段及我们在这一过程中获得的宝贵经验与思考,希望能够为类似问题的解决提供一些参考与借鉴。通过这次经验,我们也意识到持续的兼容性测试与多环境验证是产品迭代中不可或缺的一部分。

问题表现

在iLogtail 2.0开源版本发布后,社区用户对此进行了积极的反馈,迅速提出了一些问题案例。这些问题引起了开发团队的高度重视,特别是用户在GitHub上提到的几个重要链接,例如issue #1370discussion #1389。在这些反馈中,用户普遍遇到的核心错误为“Illegal instruction”,这表明程序在执行过程中尝试进行一个无效的指令操作。

问题排查流程

如何复现问题

Illegal instruction,顾名思义,就是指令集不兼容。但是第一次拿到这个报错,也是一脸懵逼,不知如何下手。大家都知道,对于程序员来说,能够复现的问题不叫问题。问题的关键,就是如何复现出来。既然判断是跟机器环境相关,我们从团队的机器里面找了几台比较老的机器,最终在一台相对最老旧的机器上,把问题复现了出来。

机器环境信息:

CPU

$lscpu
Architecture: x86_64
Model name: Intel(R) Xeon(R) CPU E5-2650 v2 @ 2.60GHz
Flags: fpu vme de pse tsc msr pae mce cx8 apic sep mtrr pge mca cmov pat pse36 clflush dts acpi mmx fxsr sse sse2 ss ht tm pbe syscall nx pdpe1gb rdtscp lm constant_tsc arch_perfmon pebs bts rep_good nopl xtopology nonstop_tsc aperfmperf eagerfpu pni pclmulqdq dtes64 ds_cpl vmx smx est tm2 ssse3 cx16 xtpr pdcm pcid dca sse4_1 sse4_2 x2apic popcnt tsc_deadline_timer aes xsave avx f16c rdrand lahf_lm arat epb pln pts dtherm tpr_shadow vnmiflexpriority ept vpid fsgsbase smep erms xsaveopt

操作系统

Linux version 3.10.0-327.ali2014.alios7.x86_64 (admin@23ae0643ac79) (gcc version 4.8.5 20150623 (Red Hat 4.8.5-4) (GCC) ) #1 SMP Fri Jan 12 12:33:55 CST 2018

执行 iLogtail2.0 开源版本,出现如下报错:

通过 gdb 调试得知,程序最后是挂在了vextracti128 指令(vextracti128 指令属于 Intel AVX2 (高级向量扩展 2)指令集,用于从 256 位整数向量中提取一个 128 位整数向量)上,最后的堆栈信息是folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::SingletonHolder

#0 folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::SingletonHolder (
vault=..., typeDesc=..., this=0x4bba000) at /usr/local/include/folly/Singleton-inl.h:218
#1 folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::Impl<folly::detail::DefaultTag, folly::detail::DefaultTag>::Impl (this=0x4bba000) at /usr/local/include/folly/Singleton-inl.h:26
#2 folly::detail::thunk::make<folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::Impl<folly::detail::DefaultTag, folly::detail::DefaultTag> > () at /usr/local/include/folly/Singleton-inl.h:24
#3 0x0000000002cc9ecb in folly::detail::(anonymous namespace)::StaticSingletonManagerWithRttiImpl::Entry::get (
make=@0x1d8c060: {void *(void)} 0x1d8c060 <folly::detail::thunk::make<folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::Impl<folly::detail::DefaultTag, folly::detail::DefaultTag> >()>,
this=0x4bb2c90) at /mag/workspace/folly-v2022.11.14.00/folly/detail/StaticSingletonManager.cpp:55
#4 folly::detail::(anonymous namespace)::StaticSingletonManagerWithRttiImpl::create<folly::detail::StaticSingletonManagerWithRtti::Arg> (arg=...) at /mag/workspace/folly-v2022.11.14.00/folly/detail/StaticSingletonManager.cpp:36
#5 0x000000000075c0ea in folly::detail::StaticSingletonManagerWithRtti::create<folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::Impl<folly::detail::DefaultTag, folly::detail::DefaultTag>, void> (
arg=...) at /usr/local/include/folly/Singleton-inl.h:31
#6 folly::detail::StaticSingletonManagerWithRtti::create<folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::Impl<folly::detail::DefaultTag, folly::detail::DefaultTag>, void> ()
at /usr/local/include/folly/detail/StaticSingletonManager.h:148
#7 folly::detail::createGlobal<folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::Impl<folly::detail::DefaultTag, folly::detail::DefaultTag>, void> ()
at /usr/local/include/folly/detail/StaticSingletonManager.h:174
#8 folly::detail::SingletonHolder<facebook::velox::tpch::(anonymous namespace)::DBGenBackend>::singleton<folly::detail::DefaultTag, folly::detail::DefaultTag> () at /usr/local/include/folly/Singleton-inl.h:32
#9 0x000000000075c170 in folly::Singleton<facebook::velox::tpch::(anonymous namespace)::DBGenBackend, folly::detail::DefaultTag, folly::detail::DefaultTag>::getEntry ()
at /apsara/alicpp/built/gcc-9.2.1/gcc-9.2.1/include/c++/9/bits/std_function.h:263
#10 folly::Singleton<facebook::velox::tpch::(anonymous namespace)::DBGenBackend, folly::detail::DefaultTag, folly::detail::DefaultTag>::Singleton (this=<optimized out>, t=..., c=...) at /usr/local/include/folly/Singleton.h:712
#11 folly::Singleton<facebook::velox::tpch::(anonymous namespace)::DBGenBackend, folly::detail::DefaultTag, folly::detail:--Type <RET> for more, q to quit, c to continue without paging--
:DefaultTag>::Singleton (this=<optimized out>, t=...) at /usr/local/include/folly/Singleton.h:698
#12 __static_initialization_and_destruction_0 (__initialize_p=1, __priority=65535)
at /mag/workspace/sls-sql-velox/presto-native-execution/velox/velox/tpch/gen/DBGenIterator.cpp:47
#13 _GLOBAL__sub_I_DBGenIterator.cpp(void) ()
at /mag/workspace/sls-sql-velox/presto-native-execution/velox/velox/tpch/gen/DBGenIterator.cpp:124
#14 0x0000000003367975 in __libc_csu_init (argc=1, argv=0x7fffffffe0a8, envp=0x7fffffffe0b8) at elf-init.c:88
#15 0x00007ffff6beb4e5 in __libc_start_main () from /lib64/libc.so.6
#16 0x00000000007664e7 in _start () at ../sysdeps/x86_64/start.S:122

在 gdb 下,执行 disassemble 可以直接定位到报错的指令

问题准确定位

问题 1: 编译参数问题

iLogtail 编译的时候,并没有指定特殊的指令集,但是 iLogtail2.0 引入了一些新的依赖库,因此首先对依赖库的编译参数做了一遍检查。检查过程中发现了 folly 在编译的时候,会判断一下机器的指令集环境,如果包含 avx 指令集的话,会启用一些指定的指令集,如下图所示

问题模拟复现-测试代码

为了模拟 folly 库中的特殊指令,我写了一个测试程序,指定使用了AVX2 指令集中指令,并在编译的时候,加上了-mavx2 参数

avx2_example.cpp:

#include <immintrin.h>
#include <iostream>
int main() {
alignas(32) int arr1[8] = {1, 2, 3, 4, 5, 6, 7, 8};
alignas(32) int arr2[8] = {10, 20, 30, 40, 50, 60, 70, 80};
alignas(32) int result[8];
// AVX指令集
__m256i vec1 = _mm256_load_si256(reinterpret_cast<const __m256i*>(arr1));
// AVX指令集
__m256i vec2 = _mm256_load_si256(reinterpret_cast<const __m256i*>(arr2));
// AVX2指令集
__m256i vec3 = _mm256_add_epi32(vec1, vec2);
// AVX指令集
_mm256_store_si256(reinterpret_cast<__m256i*>(result), vec3);
for (int i = 0; i < 8; ++i) {
std::cout << result[i] << " ";
}
std::cout << std::endl;
return 0;
}

CMakeLists.txt:

cmake_minimum_required(VERSION 3.1)
project(AVX2Example)
set(CMAKE_CXX_STANDARD 11)
# 设置编译标志,开启AVX2支持
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -mavx2")
add_executable(avx2_example avx2_example.cpp)

在环境上直接执行编译后的二进制文件,可以看到有报错

通过 gdb 调试可以看到收到了一个 SIGILL,然后报错Illegal instruction

执行 disassemble 可以看到的确是 vpaddd 指令报错

由此可以初步推断出,由于依赖库编译参数带上了 AVX2 指令集,因此在对依赖库的函数调用上,确实引入了 AVX2 的指令。但是,线上环境报错的位置比较奇怪,我们的代码是没有调用的,因此引出了下面的问题。

问题 2: 全局静态对象导致非预期代码引用

对于堆栈的最后几行做一个详细解读

#12 __static_initialization_and_destruction_0
这是一个由 C++ 编译器生成的特殊函数,用于管理某个编译单元(可能是一个 cpp 文件)中非局部对象的静态初始化和析构。__initialize_p=1 指示正在进行初始化(如果是 0 则意味着正在进行析构),__priority=65535 指示初始化的优先级(通常这个值表明是普通优先级)。
在这个例子中,这表示文件 DBGenIterator.cpp 第 47 行的相关静态初始化正在被执行。
#13 _GLOBAL__sub_I_DBGenIterator.cpp
这是另一个由编译器生成的特殊函数,用于处理同一编译单元中静态对象的初始化。名字中的 "sub_I" 表示 "subscription of initialization",即初始化订阅。这通常是在所有静态初始化完成后调用的。
在这个例子中,它指向文件 DBGenIterator.cpp 第 124 行。
#14 __libc_csu_init
这是在程序的启动代码中调用的函数,它负责执行一系列的初始化函数。这些函数是由编译器和链接器安排在程序启动时执行的。
#15 __libc_start_main
这是 C 语言运行时库的一部分,正式的程序入口点。__libc_start_main 准备了程序执行的环境,并最终调用程序的 main 函数。这个函数通常会在调用 main 之前设置一些环境(例如处理命令行参数和环境变量)并在 main 函数返回后进行清理工作。
#16 _start
这是操作系统加载程序后调用的最初的入口点,它设置了运行时堆栈,然后调用 __libc_start_main。对于使用 glibc 的 Linux 系统,_start 函数定义在 start.S 汇编文件中,位于 glibc 库的 sysdeps 目录下。

查阅 velox 源码得知,DBGenIterator.cpp 第 47 行确实是有静态变量初始化

我们代码中并没有实际引用这个对象,但是在编译iLogtail 可执行文件的时候,这个依然被编译进来了。整个引用链路大概是这样的:

如果静态库中包含全局静态对象,那么:

  1. 如果主程序或其他链接的对象文件/库引用了静态库中的任何符号,那么这个静态库中包含全局静态对象的编译单元将被整体链接到主程序中。这意味着全局静态对象的构造函数和析构函数将会被包含,并在程序启动时和退出时被调用。
  2. 如果主程序没有引用静态库中的任何符号,那么默认情况下,整个静态库或含有该全局静态对象的具体编译单元可能不会被链接到主程序中,因此全局静态对象也不会被构造。

为了验证这个结论,我也做了一定的模拟测试

问题模拟复现-测试代码

MyClass.h

#include <iostream>
class MyClass {
public:
MyClass();
~MyClass();
void doSomething();
};

MyClass.cpp

#include "MyClass.h"
MyClass::MyClass() {
std::cout << "MyClass constructed" << std::endl;
}
MyClass::~MyClass() {
std::cout << "MyClass destroyed" << std::endl;
}
void MyClass::doSomething() {
std::cout << "Doing something" << std::endl;
}
static MyClass myGlobalInstance;

CMakeLists.txt

make_minimum_required(VERSION 3.10)
project(MyStaticLib)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
# 创建一个静态库 target,包含 MyClass.cpp
add_library(my_static_lib STATIC MyClass.cpp)
# 设置静态库的输出路径
set_target_properties(my_static_lib PROPERTIES
ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/lib"
)

由此编译得到libmy_static_lib.a

然后主程序如下:

main.cpp

#include <iostream>
#include "MyClass.h"
int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}

CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(MyProject)
# 添加子目录,这将处理静态库的构建
add_subdirectory(myclass)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 11)
# 创建主程序的可执行文件 target
add_executable(main_program main.cpp)
# 链接静态库到主程序
target_link_libraries(main_program my_static_lib)

如果只是单纯的把 libmy_static_lib.a 链接到主程序上,但是主程序中没有任何符号调用的话,编译后的可执行文件里是没有myGlobalInstance 的符号的

但是如果我们引用一下MyClass 中的doSomething()

#include <iostream>
#include "MyClass.h"
int main() {
MyClass obj;
obj.doSomething();
std::cout << "Hello, World!" << std::endl;
return 0;
}

再编译,可执行文件中就会有myGlobalInstance 的符号了

iLogtail 实际的引用链路比这个样例复杂的多。这也提醒我们,在做依赖库引用的时候,还是需要注意剥离无关的依赖,否则无用符号的引入不仅会增加可执行文件的大小,同时还会带来意想不到的问题。

LoongCollector/iLogtail 在兼容性方面的工作与思考

编译时优化

依赖库编译参数梳理

LoongCollector/iLogtail 的依赖库是单独编译的。因为上面发现的问题,所以我们对依赖库的编译参数做了一次详细的梳理,确保了依赖库的编译参数和主程序代码编译的一致性,避免了参数不一致带来的预期之外的问题。

自身编译参数优化

# To be compatible with low version Linux.
if (ENABLE_COMPATIBLE_MODE)
message(STATUS "Enable compatible mode.")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c90")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wl,--wrap=memcpy")
add_definitions(-DENABLE_COMPATIBLE_MODE)
endif ()

这行代码通过将-std=c90参数添加到CMAKE_C_FLAGS中,设置C编译器使用C90标准进行编译。这个标准是C语言的一种较老版本,确保了代码在较旧的编译器上能够被正确编译。这对于需要支持旧版本Linux的项目尤其重要。

除此之外,代码中也针对ENABLE_COMPATIBLE_MODE 的变量,进行了 memcy 函数的替换

版本发布测试强化

Glibc 版本检查流水线集成

检测脚本

脚本中主要使用的是 objdump -T 命令,该命令的解释如下:

-T, --dynamic-syms Display the contents of the dynamic symbol table

就是显示编译后的二进制文件的动态符号表,会显示如下信息:

0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 pthread_rwlock_unlock
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 chdir
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.12 pthread_setname_np

在此,我们只需要把 GLIBC 最高版本找到,就能确认当前二进制文件依赖的最高GLIBC 版本信息。

操作系统暴力遍历测试

LoongCollector/iLogtail 为了保证操作系统的兼容性,会定期从 ECS 上拉取所有操作系统镜像,进行部署和验证。

CPU 指令集端到端模拟测试

CPU 指令集,如果想要完整测试,需要在各种 CPU 型号的机器上运行,这显然是很难满足的条件。但是虚拟机技术可以帮助我们在同样的机器上模拟不同的 CPU 型号。

我们采用的是 QEMUQEMU是一款开源的模拟器及虚拟机监管器(Virtual Machine Monitor, VMM)。QEMU主要提供两种功能给用户使用。一是作为用户态模拟器,利用动态代码翻译机制来执行不同于主机架构的代码。二是作为虚拟机监管器,模拟全系统,利用其他VMM(Xen, KVM, etc)来使用硬件提供的虚拟化支持,创建接近于主机性能的虚拟机。

QEMU 的功能有非常多,这里不详细一一介绍,这里只介绍关于 CPU 模拟的功能,通过<font style="color:rgb(36, 41, 46);">-cpu help</font>指令可以看到 qemu 支持的 cpu 型号/架构如下:

$ qemu-system-x86_64 -cpu help
x86 qemu64 QEMU Virtual CPU version 2.0.0
x86 phenom AMD Phenom(tm) 9550 Quad-Core Processor
x86 core2duo Intel(R) Core(TM)2 Duo CPU T7700 @ 2.40GHz
x86 kvm64 Common KVM processor
x86 qemu32 QEMU Virtual CPU version 2.0.0
x86 kvm32 Common 32-bit KVM processor
x86 coreduo Genuine Intel(R) CPU T2600 @ 2.16GHz
x86 486
x86 pentium
x86 pentium2
x86 pentium3
x86 athlon QEMU Virtual CPU version 2.0.0
x86 n270 Intel(R) Atom(TM) CPU N270 @ 1.60GHz
x86 Conroe Intel Celeron_4x0 (Conroe/Merom Class Core 2)
x86 Penryn Intel Core 2 Duo P9xxx (Penryn Class Core 2)
x86 Nehalem Intel Core i7 9xx (Nehalem Class Core i7)
x86 Westmere Westmere E56xx/L56xx/X56xx (Nehalem-C)
x86 SandyBridge Intel Xeon E312xx (Sandy Bridge)
x86 Haswell Intel Core Processor (Haswell)
x86 Opteron_G1 AMD Opteron 240 (Gen 1 Class Opteron)
x86 Opteron_G2 AMD Opteron 22xx (Gen 2 Class Opteron)
x86 Opteron_G3 AMD Opteron 23xx (Gen 3 Class Opteron)
x86 Opteron_G4 AMD Opteron 62xx class CPU
x86 Opteron_G5 AMD Opteron 63xx class CPU
x86 host KVM processor with all supported host features (only available in KVM mode)

启动虚拟机的时候,如果我们指定<font style="color:rgb(36, 41, 46);">-cpu Haswell</font>,那么进去之后,lscpu 可以看到信息如下:

taiye@taiye:~$ lscpu
Architecture: x86_64
CPU op-mode(s): 32-bit, 64-bit
Address sizes: 40 bits physical, 48 bits virtual
Byte Order: Little Endian
CPU(s): 2
On-line CPU(s) list: 0,1
Vendor ID: GenuineIntel
Model name: Intel Core Processor (Haswell)
CPU family: 6
Model: 60
Thread(s) per core: 1
Core(s) per socket: 1
Socket(s): 2
Stepping: 1
BogoMIPS: 4999.99
Flags: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca c
mov pat pse36 clflush mmx fxsr sse sse2 syscall nx rdt
scp lm constant_tsc rep_good nopl cpuid tsc_known_freq
pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2api
c movbe popcnt tsc_deadline_timer aes xsave avx hyperv
isor lahf_lm cpuid_fault invpcid_single pti bmi1 hle a
vx2 smep bmi2 erms invpcid rtm xsaveopt xsavec xgetbv1
xsaves
Virtualization features:
Hypervisor vendor: KVM
Virtualization type: full
Caches (sum of all):
L1d: 64 KiB (2 instances)
L1i: 64 KiB (2 instances)
L2: 8 MiB (2 instances)
NUMA:
NUMA node(s): 1
NUMA node0 CPU(s): 0,1
Vulnerabilities:
Gather data sampling: Not affected
Itlb multihit: KVM: Mitigation: VMX unsupported
L1tf: Mitigation; PTE Inversion
Mds: Vulnerable: Clear CPU buffers attempted, no microcode;
SMT Host state unknown
Meltdown: Mitigation; PTI
Mmio stale data: Unknown: No mitigations
Reg file data sampling: Not affected
Retbleed: Not affected
Spec rstack overflow: Not affected
Spec store bypass: Vulnerable
Spectre v1: Mitigation; usercopy/swapgs barriers and __user pointe
r sanitization
Spectre v2: Mitigation; Retpolines; STIBP disabled; RSB filling; P
BRSB-eIBRS Not affected; BHI Retpoline
Srbds: Not affected
Tsx async abort: Vulnerable: Clear CPU buffers attempted, no microcode;
SMT Host state unknown

如果我们想添加/去掉某个指令集,我们可以在<font style="color:rgb(36, 41, 46);">-cpu Haswell</font>后面加上<font style="color:rgb(36, 41, 46);">+/-具体指令集</font>,比如加上了<font style="color:rgb(36, 41, 46);">-cpu Haswell,-avx</font>,启动后 lscpu 可以看到指令集中已经没有了 avx

Flags: fpu de pse tsc msr pae mce cx8 apic sep mtrr pge mca c
mov pat pse36 clflush mmx fxsr sse sse2 syscall nx rdt
scp lm constant_tsc rep_good nopl cpuid tsc_known_freq
pni pclmulqdq ssse3 fma cx16 pcid sse4_1 sse4_2 x2api
c movbe popcnt tsc_deadline_timer aes xsave hypervisor
lahf_lm cpuid_fault invpcid_single pti bmi1 hle avx2
smep bmi2 erms invpcid rtm xsaveopt xsavec xgetbv1 xsa

通过这种方式,我们可以很方便地在一个机器上模拟多种 CPU 型号。

部署时优化

先进指令集版本 + 兼容版本双轨运行

先进的指令集架构在现代计算中扮演着至关重要的角色,尤其是 SIMD(单指令多数据)指令集,它使得处理器能够在同一时钟周期内同时处理多个数据元素,从而显著提升计算性能和效率。这种并行处理的能力,尤其在处理大量相似数据时,带来了显著的加速效果。例如,在 Json 数据处理领域,SindJson 库相比于 RapidJson 宣称可以实现高达 4 倍的性能提升。这一优势在 LoongCollector/iLogtail 应用中的字符串查找功能等场景中同样体现得淋漓尽致,SIMD 指令集的应用可以显著缩短处理时间。

然而,作为基础设施级别的可观测数据采集器,LoongCollector/iLogtail 还必须考虑到在低指令集环境下的兼容性与稳定性。

这种双重需求要求我们不仅要充分利用先进指令集带来的性能优势,还需确保在不同硬件条件下也能正常运行。因此,我们计划推出两个版本的 LoongCollector/iLogtail ,以满足不同用户的需求。通过灵活的部署脚本,系统能够根据当前的运行环境动态选择适合的版本,从而实现高性能与广泛兼容的双重保障。

安装部署时依据运行环境动态选择版本

安装脚本会动态识别操作系统的 glibc 版本、cpu 指令集等信息:

  • 如果满足更高需求,则选用先进指令集版本;
  • 如果不满足更高需求,则选择兼容环境版本;
  • 如果连最低的环境需求都不满足,则直接报错退出。

参考文章

C++ 全局变量的前世今生

指令集查询

qemu 帮助文档

simdjson


observability.cn Authors 2024 | Documentation Distributed under CC-BY-4.0
Copyright © 2017-2024, Alibaba. All rights reserved. Alibaba has registered trademarks and uses trademarks.
浙ICP备2021005855号-32