Python辅助嵌入式代码审查:自动扫描C源码中的空指针与内存泄漏风险
嵌入式C审查靠人眼翻malloc/free容易漏。实用做法是“Python轻筛做全量初检,clang/cppcheck做深度路径分析,报告进CI”:轻筛抓明显坏味道,深分析走AST/CFG查跨分支空指针和未释放路径,误报用白名单和抑制规则压下去。
一、轻量正则筛分配与释放
先全量扫工程,标malloc/calloc/realloc/free不成对、分配后未判空、函数入参直接解引用等模式:
import re, os
ALLOC=re.compile(r'(\w+)\s*=\s*(malloc|calloc|realloc)\s*\(')
FREE=re.compile(r'free\s*\(\s*(\w+)\s*\)')
NULLCK=re.compile(r'if\s*\([^)]*?\b\1\b[^)]*?(==\s*NULL|!=NULL)')
def scan_file(path):
issues=[]
for i,line in enumerate(open(path,encoding='utf-8',errors='ignore'),1):
m=ALLOC.search(line)
if m:
var=m.group(1)
nxt=open(path,encoding='utf-8',errors='ignore').read().splitlines()
# 简化:检查后续20行是否判空
window=' '.join(nxt[i:i+20])
if not re.search(r'\bif\b.*\b'+var+r'\b.*NULL',window):
issues.append((i,var,'malloc未检测NULL'))
if not FREE.search(window):
issues.append((i,var,'疑似未free'))
return issues
正则快但误报多,只当第一道网;自定义my_malloc/my_free、返回给调用者释放、静态池分配都要进白名单。
二、clang AST做路径级检查
装libclang,按函数遍历调用表达式,追踪分配变量是否在每条分支判空、是否有对应free;跨函数可结合call graph只做近似。
from clang.cindex import Index, CursorKind
def analyze(path):
idx=Index.create(); tu=idx.parse(path,args=['-Iinclude','-std=c11'])
for n in tu.cursor.walk_preorder():
if n.kind==CursorKind.CALL_EXPR and n.spelling in ('malloc','calloc'):
line=n.location.line
print('alloc',n.spelling,line)
if n.kind==CursorKind.DECL_REF_EXPR and n.spelling=='free':
print('free at',n.location.line)
if n.kind==CursorKind.UNARY_OPERATOR and n.spelling=='*':
# 解引用前是否缺NULL判断,需要结合父节点控制流细化
print('deref candidate',n.location.line)
路径敏感空指针、use-after-free、泄漏用Clang Static Analyzer更稳:它基于AST/CFG做符号执行,能分叉“分配成功/失败”两条路径。
# 单文件
clang --analyze -Xanalyzer -analyzer-checker=core.NullDereference,unix.Malloc sample.c
# 工程级出HTML/SARIF
scan-build -o report -enable-checker core.NullDereference,unix.Malloc make
scan-build -sarif -o sarif-out make
core.NullDereference抓“条件里可能NULL、后面解引用”;unix.Malloc抓未释放、double free、realloc覆盖原指针。
三、Python串起CI与报告
用subprocess批量跑clang-analyzer/cppcheck,解析结果生成JSON,按文件/函数/等级汇总:
import subprocess, json, glob
def run_clang_analyze(src):
r=subprocess.run(['clang','--analyze',
'-Xanalyzer','-analyzer-checker=core.NullDereference,unix.Malloc',
'-o','clang_plist',src],capture_output=True,text=True)
return r.returncode, r.stderr
def collect(cfiles):
rep=[]
for f in cfiles:
rc,err=run_clang_analyze(f)
for ln in err.splitlines():
if 'warning:' in ln and ('null' in ln.lower() or 'leak' in ln.lower()):
rep.append({'file':f,'msg':ln.strip(),'sev':'high'})
json.dump(rep,open('review.json','w'),ensure_ascii=False,indent=2)
return rep
# cfiles=glob.glob('src/**/*.c',recursive=True)
clang-tidy可用pytest-clang-tidy插件统一跑,clang-analyzer-、bugprone-开起来,和Python单测同流水线出结果。
四、嵌入式降误报规则
自定义分配器建别名表:my_malloc/my_calloc按malloc对待,mem_pool_alloc若返回静态块则不要求free;
RTOS/中断场景:ISR、tick、DMA回调里禁止动态分配,规则改为“发现malloc即告警”而非查泄漏;
多出口函数用goto cleanup统一释放,扫描器识别cleanup标签和资源栈,避免early return漏free;
第三方库、BSP、startup用.cppcheck-suppressions或.clang-tidy头过滤排除,只审应用层;
realloc必须新指针接收再回赋,直接p=realloc(p,n)在NULL时会丢原块,单独规则拦截;
空指针不仅看malloc,还看外部接口返回值、fopen、socket、ioctl、设备句柄;解引用前无判空且无可证明非空来源才报。
按“正则初筛→clang AST/analyzer深查→白名单抑制→JSON/HTML报告→CI门禁”落地,嵌入式C的空指针崩溃和malloc泄漏可在合入前批量拦截,比纯人工review更稳,也便于按版本比对缺陷趋势。





