鲲鹏社区首页
中文
注册
开发者
C或C++程序ARM上常见迁移适配问题总结

C或C++程序ARM上常见迁移适配问题总结

软件迁移

发表于 2025/10/31

0

一、非void函数缺少返回值引起的未定义行为

问题描述:在 C/C++ 中,当函数声明了非void的返回类型时,所有可能的执行路径都必须包含返回值,否则会导致 “缺少返回值” 的问题,进而引发未定义行为(Undefined Behavior)。该问题在迁移C/C++程序过程中发生最为频繁的问题之一,应当首先排查。

编译阶段:当前多数编译器(如 GCC、Clang)只会发出警告(warning: control reaches end of non-void function),只有部分严格模式下才会直接报错。

编译器行为差异:

编译器默认行为警告选项错误选项
GCC无警告-Wall或-Wreturn-type-Werror=return-type
Clang警告无需选项-Werror=return-type
MSVC警告(C4716)无需选项/WX /W4

运行阶段:属于未定义行为(Undefined Behavior),可能返回随机值(栈中残留的垃圾数据),或导致程序逻辑错误、崩溃等。

X86和ARM架构对于该行为的差异:大部分程序在x86上不会发生明显异常,可以“侥幸”运行,但在ARM上对缺少返回值的行为会更敏感,容易异常。

MyString& operator=(const MyString& other) {
    if (this == &other) {
        std::cout << "自赋值:不做操作\n";
        // return *this;
    }
    
    delete[] data;
    
    len = other.len;
    data = new char[len + 1];
    strcpy(data, other.data);
    
    //return *this; // x86:可以正常运行返回构造的对象;arm运行异常
    
}

原因:x86 和 ARM 架构在函数调用约定上的本质不同导致了未定义行为的不同表现。x86 架构的 EAX 寄存器专门用于返回值,在函数执行过程中可能保留最后一次运算结果,从而产生看似 "正常" 的行为;而 ARM 架构的 R0 寄存器是通用的多功能寄存器,在程序内可自由用于存储临时变量、计算中间结果等,极易被修改,导致返回值完全不可预测。

排查思路:

  1. 启用编译器警告并将其视为错误:如-Werror选项,或者将缺少返回值警告升级为错误:-Werror=return-type
  2. 依次检查main函数,子函数,特别是操作符重载函数,拷贝构造函数等特殊函数。
  3. 使用gdb或者打印日志检查是否存在异常的基础语法逻辑错误,且现象较稳定重复出现:如整型比较结果不符合预期,用int比较时:a(101)<= b(100)返回true。如在int a = 0;或者a = b类似基础的语句操作时出现明显异常。

二、char类型问题

问题说明:char类型在x86架构上默认为signed char, 在arm上默认为unsigned char。x86架构代码移植到鲲鹏平台时,需要强制指定char类型变量默认为signed char,或者手动修改代码逻辑。

处理方式:添加编译参数-fsigned-char,指定char类型变量为有符号类型。

三、多线程竞争问题:

C/C++程序在x86到arm的迁移适配过程中发现的两个多线程竞争的现象:

1、多线程间值读取写入时顺序不同:

//程序简化:
int data  = 0;
bool ready = false;

//线程1
data = 100;
ready = true; // x86通常能保证data的写入在ready写入之前,arm上无法保障data和ready的写入顺序

//线程2
while (!ready); // 等待ready写入完成
printf("%d",data); //在x86上打印结果为100,在arm上打印结果在0和100间变化
if (data == 100) {
    //do something;//在x86上通常执行该分支,程序基本正常
} else {
    //do something;//在arm上可能执行该分支,程序容易出现bug
}

2、omp程序数据竞争行为不同:

int i, j, k; //在循环外定义 - 默认共享
#pragma omp parallel for
for (i = 0; i < N; i++) {
    for (j = 0; j < M; j++) {
        for (k = 0; k < P ; k++) {
            // 所有线程同时读写i,j,k - 产生数据竞争
            // x86执行正常,循环次数正常;arm执行异常,循环次数异常
        }
    }
}

上述两个现象是由于x86和arm架构的内存模型强度缓存一致性的差异造成的:

(1)内存模型强度表示:处理器架构对内存操作重排序限制程度,定义了硬件允许的内存访问乱序程度,以及需要程序员/编译器插入多少同步指令来保证正确性。

强内存模型:x86/x86-64架构,通过硬件保证顺序,简化编程但功耗较高。

弱内存模型:ARM、PowerPC、RISC-V架构,将由软件保证内存访问顺序,硬件更简单、能效更高。

(2)缓存一致性表示:是指在多处理器/多核系统中,确保所有处理器核心的缓存中的数据副本保持一致的机制。

x86:强一致性,采用写失效策略,写操作立即通知其他核心使缓存失效,硬件自动保证强一致性。

arm:弱一致性,采用写更新或写失效策略,写操作可能延迟传播,需要软件显式插入内存屏障。

处理方式:以上现象都是未定义行为,需规范代码写法,软件保证顺序读写一致性,将数据私有化,减少共享数据。

四、内存越界问题:

在 C/C++ 中,内存越界(访问索引超出数组定义范围的元素)是典型的未定义行为,可能导致程序崩溃、数据损坏、安全漏洞等一系列问题。常见的错误报错信息有“Segmentation Fault, SIGSEGV”、“free(): invalid next size(normal)”等。内存越界的问题难以定位,因为内存越界的后果往往不直接体现在越界发生的位置,而是在后续的代码中爆发。

在迁移过程中发现ARM对内存越界行为更容易触发bug,而x86对内存越界容忍度相对较高,这是因为ARM架构对内存对齐和内存访问异常处理机制比x86更为复杂和严格,但x86的宽松行为可能会带来更大的性能开销和更隐蔽的逻辑错误。

这类问题的排查一般通过:

(1)使用内存检测工具,如Valgrind,ASan,devkit内存诊断。

(2)代码走读,代码优先使用vector容器类,智能指针等。

其中ASan工具是GCC 和 Clang 内置的内存错误检测工具,使用方式为:在编译器和链接器中同时添加“-fsanitize=address -g”,然后直接运行程序,当出现内存越界行为时会自动报错。

五、C/C++库链接问题及工具介绍

问题描述:c/c++程序运行时提示“undefined symbol”问题,该问题应该为c/c++程序最为常见的问题之一了。当链接器找不到某个函数或变量的定义时会出现此错误。

可能的原因包括:函数或类成员变量只声明未定义、函数名拼写错误、缺少必要的库链接或未成功链接外部库等。

排查思路:

1、使用 ldd命令查看是否缺少库和未定义符号:ldd命令是调试动态依赖的核心工具,用于查看分析可执行文件或共享库(.so)所依赖的动态链接库。使用ldd -r lib***.so 查看链接的外部库:-r 查看链接的外部库以及打印未定义的符号。此外可以使用-v参数:查看详细依赖信息(版本、符号);-u参数:查看已链接但未实际使用的动态库,用于优化编译。

2、检查动态库搜索路径:动态链接器的搜索路径遵循严格的优先级顺序,从高到低依次为:1.程序所在目录 > 2.环境变量LD_LIBRARY_PATH > 3. 程序编译时配置的RPATH/RUNPATH路径 >> 4.系统缓存/etc/ld.so.cache(由/etc/ld.so.conf配置)> 5.系统默认路径(/lib64、/usr/lib64等)。然后使用find等命令查找not found对应的库名。

3、具体函数或类型找不到定义:可以使用c++filt命令来查看符号对应的具体代码:c++filt _ZN2cv11namedWindowERKSsi(这里替换为undefined symbol的符号) 显示未定义符号对应的具体代码,便于快速找到未定义的符号。

4、使用nm命令来查看动态库是否有需要的符号:nm 命令是 C/C++ 编译链接调试的关键工具,主要用于查看 ELF 格式文件(目标文件.o、可执行文件、共享库.so、静态库.a)的符号表(Symbol Table)。

除上述工具外,常用的还有objdump、readelf来进行C/C++程序的编译链接链接分析。

两个值得注意的顺序:

(1)链接器按照库在makefile中的顺序从左到右处理,被依赖的库必须放在依赖它的库的后面。如果a.so依赖b.so,此时makefile中的顺序为-la -lb。

(2)若多个库存在同名接口,按照库在makefile中的顺序 “先到先得”:即优先使用第一个出现的库中提供的接口实现,后续库中的同名接口会被忽略。

六、C/C++程序调用fortran接口问题

这个问题和架构没有关系,和不同的编译器以及编译参数有关系,且由于现网存在大量fortran77的代码且在移植适配过程中fortran语言的错误通常不会暴露在编译阶段,甚至运行阶段也不会有core dump,问题较为隐蔽,单独进行总结分享。

1、fortran有严格的行长度限制:fortran77为72个字符,超过72个字符会发生截断,自行将上下行拼接起来。在fortran程序中,tab字符为非标准字符,当存在tab字符时,可能会被不同的编译器填充为不同长度的空格,导致原本在a上正常编译运行的代码,在b上可能会发生异常。
处理方式:添加编译选项-ffree-line-length-none(不限制行长)或-ffixed-line-length-132(拓展为132字符行长)避免该问题;或者升级语言版本到fortran90。

2、C/C++调用fortran接口注意事项1:当fortran中行长少于72字符时,会自动填充空格到末尾,如果有换行的字符串,需要添加编译选项-fno-pad-source解决。

3、C/C++调用fortran接口注意事项2:fortran77中传递字符串给c/c++程序时需要手动添加终止符“\0”,并且需要添加添加“-fbackslash”启动转义字符。在fortran90之后的版本可以直接通过“c_null_char”或者“CHAR(0)”来添加终止符。

本页内容