代码重用通常是新项目中的主要考虑因素,无论是在利用先前项目的遗留代码方面,还是作为后续项目的基础。静态分析可用于确保遗留代码不会成为项目中问题的根源,并保证在其开发过程中生成的任何代码不会影响任何将其作为代码库的项目。
C 代码特别容易受到移植问题的影响,特别是因为不能期望编译器检测到它们,因为代码将符合语言规范(假设没有使用语言扩展)。因此,开发人员必须使用静态分析工具来确认移植将按计划进行。静态分析工具可以通过多种方式帮助解决此问题。
int 大小引起的可移植性问题
int 中的精度(位数)可能因系统而异。为了解决这个问题,通常定义一组 typedef 来将系统类型映射到应用程序类型。可以为 16 位架构定义以下示例:
typedef 无符号字符 U8;
typedef unsigned int U16;
typedef unsigned long U32;
如果将代码移植到 32 位架构,则此示例可以更改为以下内容:
typedef 无符号字符 U8;
typedef 无符号短 U16;
typedef unsigned int U32;
然而,移植并不是那么简单,因为 int 大小的变化会对代码产生一些不太明显的影响。例如,其结果取决于整数提升效果的任何表达式都可能表现出不同的行为。因此,只有在包含受影响类型的对象的所有表达式中的精度符合目的时,这种更改才合适。静态分析可以用来验证这个假设。
编译器不会报告任何这些问题,因为代码对于目标环境完全有效,即使它的行为可能不符合预期。
编译器实现引起的可移植性问题
与编译器相关的实现定义的、未指定的或未定义的行为的差异可能会导致移植时出现缺陷。
实现定义的行为是编译器之间可能不同但由编译器供应商记录的行为。静态分析工具可以检测调用此类行为的代码,以便将其消除以促进移植。
也可以检测到未指定或未定义的行为;但是,它带来的不仅仅是可移植性问题,因为这种行为可能会在同一编译器的不同版本之间以未记录的方式发生变化,甚至可能在同一编译器内的各种用例之间发生变化。调用这种行为的代码可以工作,但很可能会非常脆弱。值得注意的是,迁移到不同版本的编译器可以被视为移植。
编译器不需要检测实现定义的、未指定的或未定义的行为的使用,因为代码是完全有效的。
编码标准
诸如 MISRA C:2004 (www.misra-c.com) 等公开可用的编码标准,可以通过静态分析工具严格执行,包括防御这些可移植性问题的规则。后面的例子使用了这个标准。
C 中的整数转换
在 C 中对表达式求值期间,管理不同算术类型的隐式转换方式和时间的规则很复杂。为确保移植代码时结果符合预期,在考虑了所有此类隐式转换后,表达式中的所有操作都应以相同的类型进行。
与整数提升相关的隐式转换很容易导致代码的执行方式与开发人员期望的方式大不相同。整数提升基本上要求将任何小于 int 的类型(例如 char、short)转换为 int,然后再将其用作表达式中的操作数。许多嵌入式系统广泛使用这些类型,因为它们通常允许更有效地使用内存资源,这可能会受到限制以节省成本、空间和功率。
整数提升是保值的(意味着保留大小和符号),但对象的符号可能会改变。此外,表达式将以比操作数类型更宽的类型进行计算。考虑以下示例:
U8 u8a = 200U;
U8 u8b = 100U;
U8 u8r = u8a + u8b;
在此示例中,u8a 和 u8b 在加法发生之前都被转换为宽度至少为 16 位的有符号整数。然后将加法的结果隐式转换回 8 位,然后再存储到 u8r 中。在这种情况下,开发人员可能会期待结果 (44),因为可以合理地假设他们知道赋值时发生的模 2 运算。这意味着结果实际上与以 8 位精度进行操作时的结果相同(整数提升不影响结果)。
但是,当整数提升与隐式扩展转换同时发生时,可能会造成混淆。考虑以下:
U16 u16a = 0xffffU;
U16 u16b = 0x0001U;
U32 u32r = u16a + u16b;
在 32 位架构上,u32r 的类型为 unsigned int,而 u16a 和 u16b 的类型为 unsigned short。整数提升将导致操作数在加法之前转换为有符号整数,结果将在赋值时隐式转换为无符号整数,最终值为 0x10000。开发人员可以(也许有理由)依靠发生的整数提升来确保加法不会像使用 16 位算术时那样换行。
如果开发人员决定将代码移植到 16 位架构,则 u32r 将具有 unsigned long 类型,而 u16a 和 u16b 将具有 unsigned int 类型。这一次,在加法发生之前(也在 unsigned int 中),不会对已经是 unsigned int 的操作数应用任何转换,并且 0x0000 的结果将在赋值时隐式转换为 unsigned long,最终值为0x0000。以更广泛的类型执行添加的假设现在不再有效,并且存在发生意外回绕的风险。
这表明当代码从一个平台移植到另一个平台时,它可以多么容易地表现出不同的行为。这里的真正问题与在分配结果时发生的隐式扩大转换有关。这可以通过确保始终使用强制转换以必要的精度评估表达式来消除,例如:
u32r = (u32) u16a + u16b;
( u32 ) 强制转换确保表达式始终以具有适当精度的类型进行评估。在前面的示例中,这意味着表达式是以 unsigned long 而不是 unsigned int 计算的。如图 1 所示,静态分析可以很容易地检测到隐式加宽。
图 1: LDRA 工具套件等静态分析工具可以突出显示有效 C 代码中的问题,这些问题在移植时可能导致功能错误。因为代码是有效的 C,编译器不会检测到这些问题。
整数提升也会影响其他操作。考虑以下:
u16a = 0x1234U;
u16r = ~u16a 》》 8;
在 16 位架构上,这将导致 u16a 的位被反转,顶部字节移入底部字节,将 0x00ED 分配给 u16r。
但是,在 32 位架构上,u16a 将在补码发生之前转换为带符号的 int(32 位),从而将值 0xFFED 分配给 u16r。
再一次,使用强制转换将确保行为符合预期:
u16r = ( U16 )~u16a 》》 8;
评估代码适用性
静态分析工具是代码移植的宝贵帮助。如图 2 所示,这些工具允许开发人员评估遗留代码并确保以允许移植的方式开发新代码。
图 2:静态分析工具报告,例如 LDRA 工具套件中的这个示例,可以有效评估代码移植的适用性。
在项目生命周期中尽早采用静态分析将确保尽早验证遗留代码,并确保任何新代码从一开始就可移植。通过缩短开发时间和显着降低残留缺陷水平,开发人员可以快速收回使用此类工具所涉及的初始支出。
审核编辑:郭婷
-
C++
+关注
关注
22文章
2108浏览量
73651 -
编译器
+关注
关注
1文章
1634浏览量
49130
发布评论请先 登录
相关推荐
评论