我们最近说过,PVS-Studio代码分析器开始与PlatformIO集成。当然,PVS-Studio开发团队与PlatformIO团队进行了交流,出于兴趣的考虑,他们建议检查Zephyr实时操作系统的代码。我们想,为什么不呢?这是一篇有关这种研究的文章。PlatformIO
在开始本文的主要部分之前,我想向嵌入式系统开发人员推荐PlatformIO项目,这可以使他们的生活更轻松。它是一个跨平台的微控制器编程工具。PlatformIO的核心是命令行工具,但建议将其用作Visual Studio Code的插件。支持大量现代芯片和基于它们的主板。它能够自动下载合适的装配系统,并且该站点具有大量用于管理插入式电子组件的库。PVS工作室
PVS-Studio在嵌入式系统领域仍然鲜为人知,因此,以防万一,我将为尚未熟悉此工具的新读者做一个介绍。我们的普通读者可以直接跳到下一部分。PVS-Studio是静态代码分析器,它可以检测用C,C ++,C#和Java编写的程序的代码中的错误和潜在漏洞。如果仅谈论C和C ++,则支持以下编译器:- 视窗 Visual Studio 2010-2019年C,C ++,C ++ / CLI,C ++ / CX(WinRT)
- 视窗 IAR嵌入式工作台,用于ARM C,C ++的C / C ++编译器
- 视窗 QNX Momentics,QCC C,C ++
- Windows / Linux Keil µVision,DS-MDK,ARM编译器5/6 C,C ++
- Windows / Linux 德州仪器Code Composer Studio,ARM代码生成工具C,C ++
- Windows/Linux/macOS. GNU Arm Embedded Toolchain, Arm Embedded GCC compiler, C, C++
- Windows/Linux/macOS. Clang C, C++
- Linux/macOS. GCC C, C++
- Windows. MinGW C, C++
该分析仪具有自己的警告分类系统,但是如果需要,您可以根据编码标准CWE,SEI CERT,MISRA启用警报显示。您甚至可以在大型旧项目中快速开始定期使用PVS-Studio。为此提供了一种特殊的大规模抑制警告机制。当前所有的警告都被视为技术债,并且被隐藏了,这使您可以专注于仅适用于新代码或修改代码的警告。这使团队可以立即在工作中每天开始使用分析仪,并且您可以不时回到技术职责上来改进代码。使用PVS-Studio还有许多其他方案。例如,您可以将其用作SonarQube的插件。可以与Travis CI,CircleCI,GitLab CI / CD等系统集成。对PVS-Studio的更详细描述超出了本文的范围。因此,我建议您熟悉这篇文章,该文章包含许多有用的链接,并且回答了许多问题:“ 将PVS-Studio静态代码分析器引入开发过程的原因。”和风
在将PVS-Studio集成到PlatformIO中的过程中,我们的团队进行了交谈,并被要求从嵌入式世界(即Zephyr)中检出一个项目。我们喜欢这个主意,这就是撰写本文的原因。Zephyr是一种轻量级的实时操作系统,旨在在各种架构的资源有限的设备上运行。该代码根据开源Apache 2.0许可证分发。在以下平台上工作:ARM(Cortex-M0,Cortex-M3,Cortex-M4,Cortex-M23,Cortex-M33,Cortex-R4,Cortex-R5,Cortex-A53),x86,x86-64,ARC,RISC- V,Nios II,Xtensa。一些功能:- 统一地址空间。特定的应用程序代码与自定义内核结合在一起,会创建在设备上执行的整体映像。
- . , .
- . .
- . . .
- : , , , , .
在我们感兴趣的时刻,Synopsys参与了操作系统的开发。2014年,Synopsys收购了Coverity,该公司生产了同名的静态代码分析器。很自然,Zephyr开发人员从一开始就使用Coverity分析器。分析器是市场的领导者,它不仅会更好地影响操作系统代码的质量。Zephyr代码质量
我认为Zephyr操作系统代码就是质量。这就是给我理由的原因:- PVS-Studio 122 High 367 Medium. , , 560 C/C++ . . 7810 C/C++ 10075 . , . , , .
- . «» , .
- 分析了源代码的SourceMonitor实用程序统计出该代码中有48%是注释。这很多,以我的经验,这表明对代码的质量及其对其他开发人员的可理解性表示高度关注。
- 开发项目时,使用Coverity静态代码分析器。由于这个事实,最有可能的是,PVS-Studio分析仪尽管发现了项目中的错误,却无法生动地展示自己,因为在分析其他项目时有时会发生这种情况。
基于此,我相信该项目的作者关心代码的质量和可靠性。现在让我们看一下PVS-Studio分析仪(版本7.06)发出的一些警告。半错误警告
由于项目代码的级别较低,因此它是专门编写的,并带有许多条件编译(#ifdef)。这会产生大量警告,这些警告并不表示真正的错误,但不能仅仅称为错误。用几个例子来澄清这一点将是最容易的。“半错误”响应N1的示例static struct char_framebuffer char_fb;
int cfb_framebuffer_invert(struct device *dev)
{
struct char_framebuffer *fb = &char_fb;
if (!fb || !fb->buf) {
return -1;
}
fb->inverted = !fb->inverted;
return 0;
}
PVS-Studio警告:V560条件表达式的一部分始终为false :!Fb。 cfb.c 188当获取静态变量的地址时,总是获得非零指针。因此,指针fb总是非零,并且其验证没有意义。但是,很明显,这根本不是错误,而只是过度检查,不会造成任何危害。此外,在构建发行版时,编译器会将其丢弃,因此甚至不会导致速度下降。以我的理解,类似的情况属于“半错误”分析仪操作的概念。从形式上讲,分析仪绝对正确。并且最好从代码中删除多余的毫无意义的检查。但是,所有这些都是小问题,在本文的框架中考虑这些警告甚至都没有意思。N2“半切割”致动的示例int hex2char(u8_t x, char *c)
{
if (x <= 9) {
*c = x + '0';
} else if (x >= 10 && x <= 15) {
*c = x - 10 + 'a';
} else {
return -EINVAL;
}
return 0;
}
警告PVS-Studio:V560条件表达式的一部分始终为true:x> =10。hex.c 31分析仪在断言部分条件始终为true时再次形式正确。如果变量x不小于/等于9,那么事实证明它始终大于/等于10。代码可以简化:} else if (x <= 15) {
再一次,很明显,这里没有真正有害的错误,并且进行额外的比较只是为了使代码美观。现在,让我们来看一个更复杂的N3示例:首先,看看如何实现CHECKIF宏。#if defined(CONFIG_ASSERT_ON_ERRORS)
#define CHECKIF(expr) \
__ASSERT_NO_MSG(!(expr)); \
if (0)
#elif defined(CONFIG_NO_RUNTIME_CHECKS)
#define CHECKIF(...) \
if (0)
#else
#define CHECKIF(expr) \
if (expr)
#endif
根据项目的编译模式,可以执行或跳过检查。在我们的例子中,当使用PVS-Studio检查代码时,选择了以下宏实现:#define CHECKIF(expr) \
if (expr)
现在,让我们看看这将导致什么。int k_queue_append_list(struct k_queue *queue, void *head, void *tail)
{
CHECKIF(head == NULL || tail == NULL) {
return -EINVAL;
}
k_spinlock_key_t key = k_spin_lock(&queue->lock);
struct k_thread *thread = NULL;
if (head != NULL) {
thread = z_unpend_first_thread(&queue->wait_q);
}
....
}
PVS-Studio 警告:V547 [CWE-571]表达式'head!= NULL'始终为true。queue.c 244分析器认为检查(head!= NULL)始终为真。确实是这样。如果头指针为NULL,则该函数将由于在函数开头的检查而停止工作:CHECKIF(head == NULL || tail == NULL) {
return -EINVAL;
}
回想一下,此处宏扩展如下:if (head == NULL || tail == NULL) {
return -EINVAL;
}
因此,PVS-Studio分析仪从其角度来看是正确的,并发出正确的警告。但是,此检查无法删除。她是需要的。在另一种情况下,宏将像这样打开:if (0) {
return -EINVAL;
}
然后需要重新检查指针。当然,在此版本的代码编译中,分析器不会生成警告。但是,它会警告编译的调试版本。我希望读者现在可以清楚地知道“半错误”警告的出处。但是,它们没有错。PVS-Studio分析仪提供了多种抑制误报的机制,可以在文档中找到这些机制。案例警告
但是,您是否发现了一些有趣的东西?这是成功的,现在我们将研究各种错误。同时,我要立即注意两点:- . : , , , Coverity. , PVS-Studio - , .
- . , «» . , . GitHub, PVS-Studio.
N1,static void gen_prov_ack(struct prov_rx *rx, struct net_buf_simple *buf)
{
....
if (link.tx.cb && link.tx.cb) {
link.tx.cb(0, link.tx.cb_data);
}
....
}
PVS-Studio警告:V501 [CWE-571]'&&'运算符的左侧和右侧有相同的子表达式:link.tx.cb && link.tx.cb pb_adv.c 377选中了一项相同的变量link.tx.cb。显然,这是一个错字,要检查的第二个变量应该是link.tx.cb_data。片段N2,缓冲区溢出考虑功能net_hostname_get,稍后将使用它。#if defined(CONFIG_NET_HOSTNAME_ENABLE)
const char *net_hostname_get(void);
#else
static inline const char *net_hostname_get(void)
{
return "zephyr";
}
#endif
就我而言,在预处理时,选择了与#else分支相关的选项。也就是说,在预处理文件中,该函数的实现方式如下:static inline const char *net_hostname_get(void)
{
return "zephyr";
}
该函数返回一个指向7个字节的数组的指针(我们考虑到行尾的终端零)。现在考虑导致数组边界退出的代码。static int do_net_init(void)
{
....
(void)memcpy(hostname, net_hostname_get(), MAX_HOSTNAME_LEN);
....
}
PVS-Studio警告:V512 [CWE-119]调用'memcpy'函数将导致'net_hostname_get()'缓冲区超出范围。log_backend_net.c 114预处理后,MAX_HOSTNAME_LEN扩展如下:(void)memcpy(hostname, net_hostname_get(),
sizeof("xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx:xxxx"));
因此,在复制数据时,会出现越界字符串文字。很难预测这将如何影响程序的执行,因为这会导致不确定的行为。片段N3,潜在的缓冲区溢出int do_write_op_json(struct lwm2m_message *msg)
{
u8_t value[TOKEN_BUF_LEN];
u8_t base_name[MAX_RESOURCE_LEN];
u8_t full_name[MAX_RESOURCE_LEN];
....
snprintf(full_name, TOKEN_BUF_LEN, "%s%s", base_name, value);
....
}
PVS-Studio警告:V512 [CWE-119]调用'snprintf'函数将导致缓冲区'full_name'溢出。 lwm2m_rw_json.c 826如果我们替换宏值,则正在发生的情况如下图所示:u8_t value[64];
u8_t base_name[20];
u8_t full_name[20];
....
snprintf(full_name, 64, "%s%s", base_name, value);
在形成字符串
的full_name缓冲区下仅分配了20个字节。在这种情况下,构成字符串的部分将存储在大小为20和64字节的缓冲区中。而且,传递给snprintf函数并设计为防止数组出国的常数64 显然太大了!此代码不一定会导致缓冲区溢出。也许总是幸运的,子字符串总是很小。但是,通常来说,此代码没有任何形式的溢出保护,并且包含经典的安全漏洞CWE-119。片段N4,表达式始终为真static int keys_set(const char *name, size_t len_rd, settings_read_cb read_cb,
void *cb_arg)
{
....
size_t len;
....
len = read_cb(cb_arg, val, sizeof(val));
if (len < 0) {
BT_ERR("Failed to read value (err %zu)", len);
return -EINVAL;
}
....
}
PVS-Studio 警告:V547 [CWE-570]表达式'len <0'始终为false。无符号类型的值永远不会小于0 keys.c 312 len变量具有无符号类型,因此不能小于0。因此,不会以任何方式处理错误状态。在其他地方,类型int或ssize_t用于存储read_cb函数的结果。
例:static inline int mesh_x_set(....)
{
ssize_t len;
len = read_cb(cb_arg, out, read_len);
if (len < 0) {
....
}
注意。read_cb函数似乎一切都不好。事实是它是这样声明的:static u8_t read_cb(const struct bt_gatt_attr *attr, void *user_data)
类型u8_t是无符号字符。该函数始终仅返回unsigned char类型的正数。如果将此值放在类型为int或ssize_t的带符号变量中,则所有这些相同,则该值将始终为正。因此,在其他地方,检查错误状态也不起作用。但是我没有深入研究这个问题。N5片段,很奇怪static char *mntpt_prepare(char *mntpt)
{
char *cpy_mntpt;
cpy_mntpt = k_malloc(strlen(mntpt) + 1);
if (cpy_mntpt) {
((u8_t *)mntpt)[strlen(mntpt)] = '\0';
memcpy(cpy_mntpt, mntpt, strlen(mntpt));
}
return cpy_mntpt;
}
PVS-Studio警告:V575 [CWE-628]'memcpy'函数不会复制整个字符串。使用'strcpy / strcpy_s'函数保留终端null。shell.c 427有人试图模拟strdup函数,但是他没有成功。让我们从分析仪警告开始。它报告memcpy函数复制该行,但不复制终端零,这非常可疑。似乎该终端0复制到这里:((u8_t *)mntpt)[strlen(mntpt)] = '\0';
但不是!这是一个错字,因此终端零将被复制到自身!请注意,写入mntpt数组,而不是cpy_mntpt。如此一来,mntpt_prepare函数将返回一个不包含零的字符串。实际上,程序员想这样写:((u8_t *)cpy_mntpt)[strlen(mntpt)] = '\0';
但是,仍然不清楚为什么它是如此复杂!该代码可以简化为以下选项:static char *mntpt_prepare(char *mntpt)
{
char *cpy_mntpt;
cpy_mntpt = k_malloc(strlen(mntpt) + 1);
if (cpy_mntpt) {
strcpy(cpy_mntpt, mntpt);
}
return cpy_mntpt;
}
片段N6,在验证之前取消对指针的引用int bt_mesh_model_publish(struct bt_mesh_model *model)
{
....
struct bt_mesh_model_pub *pub = model->pub;
....
struct bt_mesh_msg_ctx ctx = {
.send_rel = pub->send_rel,
};
....
if (!pub) {
return -ENOTSUP;
}
....
}
PVS-Studio警告:V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'pub'指针。检查行:708,719。access.c 708一个非常常见的错误模式。首先,将指针取消引用以初始化结构的成员:.send_rel = pub->send_rel,
然后只有检查该指针可以为空。片段N7-N9,在验证之前取消对指针的引用int net_tcp_accept(struct net_context *context, net_tcp_accept_cb_t cb,
void *user_data)
{
....
struct tcp *conn = context->tcp;
....
conn->accept_cb = cb;
if (!conn || conn->state != TCP_LISTEN) {
return -EINVAL;
}
....
}
PVS-Studio警告:V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'conn'指针。检查行:1071,1073。tcp2.c 1071与前面的情况相同。这里不需要解释。在这里可以看到另外两个这样的错误:- V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'context-> tcp'指针。检查行:1512、1518。tcp.c 1512
- V595 [CWE-476]在针对nullptr对其进行验证之前,已使用了'fsm'指针。检查行:365、382。fsm.c 365
N10片段,错误检查static int x509_get_subject_alt_name( unsigned char **p,
const unsigned char *end,
mbedtls_x509_sequence *subject_alt_name)
{
....
while( *p < end )
{
if( ( end - *p ) < 1 )
return( MBEDTLS_ERR_X509_INVALID_EXTENSIONS +
MBEDTLS_ERR_ASN1_OUT_OF_DATA );
....
}
....
}
PVS-Studio 警告:V547 [CWE-570]表达式'(end- * p)<1'始终为假。x509_crt.c 635近距离查看条件:他们彼此矛盾。如果(* p <end),则(end-* p)的值将始终为1或更大。通常,这里有些问题,但是我不知道如何正确拼写。片段N11,无法访问的代码uint32_t lv_disp_get_inactive_time(const lv_disp_t * disp)
{
if(!disp) disp = lv_disp_get_default();
if(!disp) {
LV_LOG_WARN("lv_disp_get_inactive_time: no display registered");
return 0;
}
if(disp) return lv_tick_elaps(disp->last_activity_time);
lv_disp_t * d;
uint32_t t = UINT32_MAX;
d = lv_disp_get_next(NULL);
while(d) {
t = LV_MATH_MIN(t, lv_tick_elaps(d->last_activity_time));
d = lv_disp_get_next(d);
}
return t;
}
PVS-Studio警告:V547 [CWE-571]表达式“ disp”始终为true。 lv_disp.c 148如果disp是空指针,则函数终止。然后,相反,将检查disp指针是否不为null(并且总是如此),然后该函数再次完成其工作。结果,函数中的部分代码将永远无法获得控制权。片段N12,返回值奇怪static size_t put_end_tlv(struct lwm2m_output_context *out, u16_t mark_pos,
u8_t *writer_flags, u8_t writer_flag,
int tlv_type, int tlv_id)
{
struct tlv_out_formatter_data *fd;
struct oma_tlv tlv;
u32_t len = 0U;
fd = engine_get_out_user_data(out);
if (!fd) {
return 0;
}
*writer_flags &= ~writer_flag;
len = out->out_cpkt->offset - mark_pos;
fd->mark_pos = mark_pos;
tlv_setup(&tlv, tlv_type, tlv_id, len);
len = oma_tlv_put(&tlv, out, NULL, true) - tlv.length;
return 0;
}
PVS-Studio警告:V1001已分配'len'变量,但在功能结束时未使用。lwm2m_rw_oma_tlv.c 338该函数包含两个都返回0的return语句。奇怪的是该函数始终返回0。还很奇怪的是,赋值后不再使用len变量。我非常怀疑实际上应该这样写: len = oma_tlv_put(&tlv, out, NULL, true) - tlv.length;
return len;
}
片段N13-N16,同步错误static int nvs_startup(struct nvs_fs *fs)
{
....
k_mutex_lock(&fs->nvs_lock, K_FOREVER);
....
if (fs->ate_wra == fs->data_wra && last_ate.len) {
return -ESPIPE;
}
....
end:
k_mutex_unlock(&fs->nvs_lock);
return rc;
}
PVS-Studio警告:V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:620,549。nvs.c 620在某些情况下,某个功能在未解锁互斥锁的情况下完成了工作。据我了解,这样写是正确的:static int nvs_startup(struct nvs_fs *fs)
{
....
k_mutex_lock(&fs->nvs_lock, K_FOREVER);
....
if (fs->ate_wra == fs->data_wra && last_ate.len) {
rc = -ESPIPE;
goto end;
}
....
end:
k_mutex_unlock(&fs->nvs_lock);
return rc;
}
另外三个这样的错误:- V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:574,549。nvs.c 574
- V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:908、890。net_context.c 908
- V1020该函数退出而未调用'k_mutex_unlock'函数。检查行:1194、1189。shell.c 1194
结论
我希望你喜欢它。请访问我们的博客,以了解有关其他项目和其他有趣出版物的检查。在您的工作中使用静态分析器,即使在编写代码的阶段,也可以减少错误的数量和潜在的漏洞。特别是早期的错误检测与嵌入式系统有关,更新程序通常是一个耗时且昂贵的过程。我还建议不要推迟并尝试使用PVS-Studio分析仪检查项目。请参阅文章:如何快速查看PVS-Studio分析器针对C和C ++代码生成的有趣警告?
如果您想与讲英语的读者分享这篇文章,请使用翻译链接:Andrey Karpov。检查Zephyr操作系统代码。