You need to sign in or sign up before continuing.
未验证 提交 497edceb 编写于 作者: O openharmony_ci 提交者: Gitee

!12771 新增开源构建规范和开源编译规范

Merge pull request !12771 from pjscc/master
# 开源构建规范
## 概述
为指导OpenHarmony的开发者开展构建工作,提升构建系统的可重复性、可维护性,提高产品构建质量,构建规范工作组分析总结了各种典型的构建问题,提炼相应的构建规则和建议,制订了本规范。
## 构建总体原则
**P01 构建过程自动化,从构建启动开始到构建最终结束,中间过程不能手工干预。**
手工操作容易出错,且浪费时间。将所有的构建操作变成自动化的,从而使构建变得高效、可靠。
**P02 构建工程和构建环境代码化。**
使用高阶构建框架CMake/Maven/Gradle等描述构建工程;使用Ansible/Dockerfile等描述构建环境。
使用高阶构建框架的目的是向构建人员隐藏构建系统的复杂性。
**P03 构建过程可重复、可追溯。**
管理构建依赖,始终显式指定固定依赖版本号,确保构建依赖版本一致;将构建环境信息/构建工程作为配置项纳入配置管理,确保构建工程可追溯。
**P04 构建脚本简洁清晰,易于维护。**
构建脚本也是代码,构建脚本首先是为阅读它的人而编写的,好的脚本应当可以像故事一样发声朗诵出来。
**P05 构建标准化**
构建目录结构、构建依赖、构建初始化、构建入口、命名等进行标准化约束,使得公司所有产品、平台和组件的构建风格一致,便于构建管理和维护。
## 构建工程
### 公共规则
#### 一键式构建
##### G.COM.01 采用构建脚本,按照交付单元实现一键式自动化构建。
一键式自动化构建是指同一个构建环境下,从构建启动开始到最终结束(最终交付的包生成),中间过程禁止人工干预。
人工干预活动包括但不限于:构建过程中,使用IDE界面进行手工设置、创建或者删除文件目录、创建文件、复制文件、移动文件、删除或者重命名文件、手工设置文件属性、压缩/解压缩文件等。
交付单元是指可独立编译、加载、部署和运行的产品/平台/组件。
【级别】要求
【描述】一键式构建大幅降低构建人员操作复杂度。
【错误示例】某组件的一键式构建只能通过CI系统触发,没有一键式的本地构建。
【错误示例】某组件需要在Xplorer IDE界面手工设置内存映射地址后,再手工编译。
【错误示例】某组件需要手工创建r6c03_view\r6c03_client_view目录。
【正确示例】使用python脚本自动化创建目录:
```python
dir_src = os.getcwd()
dir_client_view = r"r6c03_client_view"
# 处理路径使用os.path可以屏蔽系统差异
dir_mk = os.path.join(dir_src, dir_client_view)
cmd = "{0} {1}".format("mkdir", dir_mk)
cmd_re = subprocess.run(cmd)
```
#### 构建目录
##### G.COM.02 构建过程中禁止删除或修改源代码文件及其目录结构。
【级别】禁止
【描述】
- 构建过程中删除或修改源代码目录结构,会导致构建过程不可重复。
- 构建过程中,构建输出(包括目标文件、临时文件和构建日志)不能污染源码目录;
- 构建过程中,避免修改源文件,包括但不限于拷贝、移动、执行dos2unix进行了源代码的格式转换等,源文件的修改应该在构建前的代码准备阶段完成;
- 工具自动生成源代码应该在构建前的准备阶段完成,如果构建过程中使用工具自动生成源代码,工具自动生成的源代码必须和已有源代码目录隔离,以便区分高价值的源代码和低价值的可重新生成的代码,降低构建系统的复杂性。
【例外】构建补丁时,可能会新增或调整部分源代码。
##### G.COM.12 构建过程中创建的文件和目录应提供合适的权限。
【级别】要求
【描述】构建过程中可能需要创建目标系统的目录或文件,这些目录和文件应符合权限最小化的设计。
例如,构建过程中应尽量避免在Linux系统中创建“777”权限的目录或文件。
Linux系统中常见的目录文件和权限可以参考《Linux安全配置操作规范》。
#### 构建初始化
##### G.COM.03 每个组件提供clean命令。
- 当clean不带任何参数时,清除该层级构建工程下的所有目标文件、临时文件和构建日志,并递归调用下层构建工程的clean,使该构建工程恢复到初始状态;
- 当clean带参数时,只清除与之对应的构建生成的目标文件、临时文件和构建日志。
【级别】要求
【描述】构建前进行clean是为了避免本次构建受到历史构建残留文件和构建日志的影响,确保构建可重复。必须支持不带任何参数clean;带参数的clean,是为了满足日常交付过程和开发人员本地构建的诉求,不作强制要求。
【正确示例】
```
base_dir
|---build.suffix
|---logs
|---component_depository_1
|---build.suffix
|---logs
|---component_depository_2
|---build.suffix
|---logs
#不带参数
base_dir/build.suffix clean
#....分别调用component_depository_1和component_depository_2的clean
#带参数:组件名
base_dir/build.suffix clean component_depository_1
#....调用component_depository_1的clean
#带参数
component_depository_1/build.suffix clean makebin hert umpt
#....调用component_depository_1的umpt单板链接任务的clean,支持详细参数的clean主要应用于内部开发和构建。
```
##### G.COM.04 每个组件发布构建,必须保证构建环境中没有历史构建遗留件。
【级别】要求
【描述】首次下载代码,构建环境已经初始化,构建环境本身确保没有历史构建遗留件,可以不用执行clean命令;如果执行过构建的,必须使用clean命令清除历史构建遗留件。
#### 全量构建
##### G.COM.05 对于版本发布构建,归档的产品全量交付件(含所依赖的所有平台和组件)必须全部重新编译,禁止使用增量编译,禁止使用手工替换文件等方式修改安装盘。
版本发布构建是指产品(含所依赖的所有平台和组件)对外正式发布版本的构建。
【级别】要求
【描述】修改文件后增量编译,会导致部分二进制文件没有更新,造成新的安全编译选项未集成到版本,编译结果不一致。手工替换文件可能会造成构建不可重复、不一致。
#### 构建配置
构建配置数据和构建脚本分离,避免构建工程架构腐化。源码路径、编译选项、目标文件路径等配置与构建脚本放到不同的文件,降低构建脚本维护成本。
##### G.COM.06 禁止使用与操作系统强绑定的文件(如excel)作为构建配置文件。建议使用跨平台的标准配置文件(如XML)来存放配置选项。
【级别】要求
【描述】使用excel作为配置文件带来的问题:
- 产品和平台编译过程中,使用excel作为配置文件,都将调用微软的OfficeAPI,每次访问excel表格都会在后台打开excel,处理速度慢。
- 大量的excel配置需要手动点界面进行操作,可管理性差。
#### 构建日志
##### G.COM.07 构建输出的日志简洁明晰,构建日志的格式为时间戳+模块名(可选)+日志信息等级+日志内容。
【级别】要求
【描述】建议时间戳格式采取“日期和时间”,如"MM/dd/yyyy HH:mm:ss"。
日志信息等级分为error/warning/informational,级别可以全写,也可以简写;对应的简写为:
| 级别(大小写都可以)| 简写(大小写都可以)|
| :---------: | :--------------------------: |
| error | ERROR |
| warning | WARN |
| information | INFO |
建议使用“[]”作分隔符。
【正确示例】
[05/21/2020 00:12:40] [ERROR] mkdir: cannot create directory Permission denied.
【例外】整个日志由工具自动输出的,可用使用以下方式跳过整个日志文件:在日志的最前方(尽可能靠前)输出"This project is built using "+工具名,如"This project is built using CMake."。
##### G.COM.08 构建日志出现error信息表示构建失败,必须终止构建。
【级别】要求
【描述】出现error信息一般是需要人工干预的构建错误,例如配置的环境变量错误,工具的版本错误,操作系统错误等等;或者软件源代码不对。对于版本发布构建,必须消除构建过程中所有的error消息,不允许屏蔽构建error信息。
【错误示例】某组件构建成功,但构建日志中包含大量的fail、Critical、cannot、not found、missing、no input files等异常信息,令人困惑。
##### G.COM.09 构建日志文件只保留本次构建的日志,避免本次构建的日志与历史构建的日志混淆。
【级别】要求
【描述】构建日志文件保留历史构建日志会导致混淆错误,比如:最新构建是失败的,由于保留有历史成功构建日志,会误认为最新这次构建是成功的。
##### G.COM.10 每条日志建议增加对应的模块名,用于问题的快速定界。
【级别】建议
【描述】在日志量较大时,很难快速锁定问题责任模块,需要在日志上加以区分。
【例外】CMake等工具的原生日志,因为输出带有对应模块路径,可以界定问题边界,不用特殊增加模块名维测信息。
#### 构建用户
##### G.COM.11 禁止使用超级管理员用户root和系统用户执行构建,应该使用普通user账户执行构建。
【级别】要求
【描述】超级管理员用户root和系统用户具有比较高的系统权限,使用此类账户执行构建可能导致构建环境被篡改。
安装态可以使用root用户;执行态使用普通user账户,如果需要使用sudo提升权限的,请遵守《身份和访问管理安全设计规范》。
#### 构建输出文件
##### G.COM.12 构建输出文件命名后缀遵守业界约定。
【级别】要求
【描述】错误的后缀命名令人误解。
对lib库、obj等构建输出文件的文件缀,应遵从构建工具默认的命名规则。
【错误示例】某文本文件命名为XXX.lib。
【错误示例】某object文件命名为XXX.a。
【错误示例】某静态库命名无后缀,命名为libxxx。
【正确示例】业界如下网址可以查询常见的文件后缀命名约定:http://www.fileextension.org/ , https://fileinfo.com/ , https://www.file-extensions.org/, http://file-extension.net/ 。
下面是一些常见的文件后缀的命名约定:
| 文件后缀名 | 类型约定 | 文件后缀名 | 类型约定 |
| ---------- | -------------------- | ---------- | --------------- |
| .a | 静态库 | .so | 动态库 |
| .o | object文件 | .7z | 7zip压缩文件 |
| .tar | tar存档文件 | .gz/.gzip | GNU压缩存档文件 |
| .pack | java pack200压缩文件 | .rar/.rar5 | rar压缩包 |
### C/C++构建工程
#### 构建目录
##### G.C&C++.01 构建目录结构标准化。
构建目录按用途分为源树Source Tree、构建中间件树Build Tree、构建安装树Install Tree三种。
- Source Tree是保存源码和构建脚本的目录。
- Build Tree是保存构建中间件的目录,目录名称一般为"build"。
- Install Tree是保存构建发布件的目录,目录名称固定为"output"。
Source Tree、Build Tree和Install Tree目录隔离,互相不重叠,没有交集,即不允许一个目录同时承担两种及以上的用途,譬如一个目录既作为Source Tree存放源码,又作为Build Tree存放编译中间件,这是不允许的。
Source Tree包含下列文件和目录:
- 构建工具入口文件,如CMakeLists.txt,CMakeLists.txt中通过add_subdirectory()命令添加子目录,CMake将自动迭代调用子目录中的CMakeLists.txt,并逐级向下展开。
- build.suffix脚本文件,该文件是一键式构建入口,仅调用该脚本即可完成构建。".suffix"表示对应的构建脚本语言后缀,譬如".bat",".sh",".py"等。
- config.suffix配置文件,该文件用于存放构建配置项,是唯一的配置文件入口。
- 构建脚本目录,可选,如cmake目录,用于保存CMake脚本文件。CMake脚本文件包括宏、函数、toolchain等, CMakeLists.txt通过include()命令包含CMake脚本文件,并调用其中的宏、函数等。
- 组件代码目录,用于存放各组件的源码及构建脚本。
- 上述文件和目录,只有CMakeLists.txt、build.suffix、config.suffix这三个文件是必需的,其它文件或者目录仅用作示例,不强制要求。
Build Tree包含下列目录:
- build目录,用于存放构建中间件。该目录可能在构建过程中创建,在git库上可能没有该目录。
- 有的工程已经将build目录用于保存构建脚本,可以创建别的目录作为Build Tree。
Install Tree包含下列目录:
- output目录,用于存放交付件。该目录可能在构建过程中创建,在git库上可能没有该目录。
【级别】要求
【描述】
典型目录结构如下:
```
base_dir
|---CMakeLists.txt ---|
|---build.suffix |
|---config.suffix |
|---cmake |--> Source Tree
|---component_1 |
|---component_2 |
|---...... |
|---component_n ---|
|---build ------> Build Tree
|---output ------> Install Tree
```
各组件的目录结构与顶层的目录结构类似,譬如:
```
component_1
|---CMakeLists.txt ---|
|---build.suffix |
|---config.suffix |
|---cmake |--> Source Tree
|---module_1 |
|---module_2 |
|---...... |
|---module_n ---|
|---build ------> Build Tree
|---output ------> Install Tree
```
##### G.C&C++.02 构建过程中禁止以任何形式修改Source Tree。
【级别】建议
【描述】构建过程中修改Source Tree会导致构建过程不可重复。
常见的修改Source Tree的操作有:
1)打补丁
2)打点
3)裁剪
4)自动生成源码
5)先修改源码然后还原
6)增加/修改/删除临时文件或者目录
7)修改文件/目录属性或者格式,譬如修改文件可执行权限、dos2unix等
建议解决方案如下:
1)将代码拷贝到Build Tree,然后打补丁,编译。
2)打点工具修改源码,使得构建过程不可信,因此禁止在构建过程中使用打点工具。应将打点后的代码上传到代码库,使用打点后的代码进行构建。
3)裁剪是独立的源码交付需求,裁剪可以看做是代码准备阶段。裁剪前的版本和裁剪后的版本都必须满足在构建过程中不修改Source Tree。
4)自动生成的源码应放在Build Tree下。
5)先修改源码然后还原是掩耳盗铃,构建过程中源码已经发生了变更。
6)临时文件或者目录都应该放在Build Tree下。
7)必须保证代码库中的文件属性和格式是正确的,而不是构建时修改。
检验Source Tree是否发生变化的方法之一:编译完成后在源码目录下执行git status命令,不能有任何变更。先修改后还原导致的Source Tree变更,通过git status可能检测不出来。
【例外】
1)git status检测到Build Tree和Install Tree这两个目录的变更是允许的。
2)git status检测到由于裁剪导致的变更是允许的。
##### G.C&C++.03 Windows构建根目录建议为D:\交付单元的名称+版本号(可选);Linux构建根目录建议为/usr1/交付单元的名称+版本号(可选)。
【级别】建议
【描述】构建根目录按交付单元的名称+版本号命名,禁止使用build或code等无法区分交付单元的目录名称。
清晰的构建目录结构,便于测试人员配置构建参数、执行一键式构建入口和对比构建结果。
根目录示例如下:
```
D:\Offering [Version,可选]或/usr1/Offering [Version,可选]
```
##### G.C&C++.04 构建过程中生成的所有中间件保存在Build Tree中。
【级别】要求
【描述】构建过程中产生的中间件包括构建工具CMake自动生成的makefile、构建脚本自动生成的源码、构建脚本拷贝的源码及补丁、编译产生的object文件、库文件、可执行程序、构建日志等等。如果中间件放在Build Tree以外的目录,势必污染Source Tree或者Install Tree。因此,所有中间件都要保存在Build Tree中。Build Tree仅用于保存构建中间件,不能将Source Tree下某个放置源码或者构建脚本的目录用作Build Tree。
Build Tree下创建构建日志子目录logs,构建日志后缀文件命名为.log。
##### G.C&C++.05 支持指定Source Tree和Install Tree以外的任意目录作为Build Tree。
【级别】要求
【描述】支持指定Source Tree和Install Tree以外的任意目录作为Build Tree,做到构建过程与目录无关。在哪个目录下执行构建,哪个目录就是Build Tree,编译中间件就保存在哪个目录下。Build Tree的目录名称一般为“build”,也可以使用其它名称。
【正确示例】使用CMake系统变量CMAKE_BINARY_DIR和CMAKE_CURRENT_BINARY_DIR访问Build Tree,避免Build Tree与Source Tree产生耦合。
##### G.C&C++.06 所有发布件保存在Install Tree中。
【级别】要求
【描述】本地编译场景下,发布件直接"install"到HOST Computer上并运行。交叉编译场景下,发布件并不在HOST Computer上运行,而是在TARGET Computer上运行。
发布件包括库文件、可执行程序、包文件、头文件等,是组件对外的二进制接口。所有发布件都保存在Install Tree中,不应将发布件放在Install Tree以外的目录下。
Install Tree只用于保存发布件,不应将编译中间件放在Install Tree中。
##### G.C&C++.07 支持指定Source Tree和Build Tree以外的任意目录作为Install Tree。
【级别】要求
【描述】支持指定Source Tree和Build Tree以外的任意目录作为Install Tree,做到构建过程与目录无关。Install Tree的目录名称固定为“output”。
【正确示例】CMake构建工程应支持通过系统变量CMAKE_INSTALL_PREFIX指定Install Tree的根目录。
#### 构建入口
##### G.C&C++.08 每个交付单元的构建入口单一。构建脚本入口名称统一命名为build.suffix,并且路径要求在构建根目录下。
【级别】要求
【描述】通过使用一致的构建入口点,构建过程可以变得更加高效和可自动执行。每个交付单元只有单一构建入口,便于一键式自动构建。
【错误示例】如下构建有多个入口点,如果没有说明文档,无法确认哪一个入口是正确的,造成选择困难。
build.bat
build_all.sh
build_v6.sh
【正确示例】一键式构建脚本build.sh的典型写法如下:
```bash
#!/bin/bash
if [ -d "build" ]; then
rm -fr build/*
else
mkdir build
fi
if [ -d "output" ]; then
rm -fr output/*
else
mkdir output
fi
cd build
cmake ..
cpu_processor_num=$(grep processor /proc/cpuinfo | wc -l)
job_num=$(expr "$cpu_processor_num" \* 2)
echo Parallel job num is "$job_num"
make -j"$job_num"
```
##### G.C&C++.09 支持指定target进行构建。
【级别】要求
【描述】日常开发场景下,通过指定target编译,开发人员只需要编译修改了的代码,不需要编译全部代码,达到快速验证的目的。编译工程应支持指定target进行构建,从而满足灵活多变的编译调试需求。
【正确示例】典型命令如下:
```
base_dir # cd build
base_dir/build # cmake ..
# 编译全部目标
base_dir/build # make
# 编译特定目标
base_dir/build # make target_name
```
##### G.C&C++.10 支持重复编译。
【级别】要求
【描述】编译成功后,不对源代码做任何修改,不清理上次编译的中间件和发布件,不修改编译环境,再次执行编译,必须能重复编译成功。
##### G.C&C++.11 支持增量编译。
【级别】建议
【描述】日常开发场景下,增量编译可以缩短编译时间,提高开发效率,因此建议支持增量编译。
##### G.C&C++.12 支持并行编译。
【级别】要求
【描述】通过"make -jN"命令进行并行编译,可以提高编译速度。本规则仅适用于使用make工具的工程。
支持jobserver统一调度,使整个工程的负载最优。不能出现下面两个告警:
```
warning: jobserver unavailable: using -j1. Add '+' to parent make rule.
warning: -jN forced in submake: disabling jobserver mode.
```
支持jobserver的方法如下:
1. 通过$(MAKE)直接调用make命令
```cmake
ExternalProject_Add(foo
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/foo
CONFIGURE_COMMAND sh configure_ext.sh
BUILD_COMMAND $(MAKE)
)
```
2. 通过shell脚本调用make命令
```cmake
ExternalProject_Add(foo
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/foo
CONFIGURE_COMMAND sh configure_ext.sh
BUILD_COMMAND sh build_ext.sh $(MAKE)
)
```
build_ext.sh内容如下:
```bash
#!/bin/bash
make
```
注意:build_ext.sh不需要解析和使用参数$(MAKE)。
3. 通过python脚本调用make命令
```cmake
ExternalProject_Add(foo
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/foo
CONFIGURE_COMMAND sh configure_ext.sh
BUILD_COMMAND python build_ext.py $(MAKE)
)
```
build_ext.py内容如下:
```bash
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import subprocess
def main():
child = subprocess.Popen("make", close_fds=False)
ret = child.wait()
return
if __name__ == '__main__':
main()
```
注意:build_ext.py不需要解析和使用参数$(MAKE)。
#### 构建依赖
##### G.C&C++.13 定义一个构建依赖文件dependence.xml,文件中描述构建依赖的所有组件。构建脚本自动读取该依赖文件,用于制作最终的软件包。
【级别】建议
【描述】按照依赖文件进行软件包制作,避免在构建脚本中定义依赖组件,提高构建过程可维护性。
#### 构建配置
##### G.C&C++.14 构建根目录的config.suffix配置文件是整个交付项目唯一的配置入口。
【级别】要求
【描述】顶层的config.suffix中,应暴露最少的配置项,只需要用户配置的构建环境、构建工具相关的信息。
【例外】如果构建配置的内容非常少,采取系统键值对配置项,配置文件可以命名成config.conf。
### GN 编写规范
#### 编译规范
##### 规则1.1 禁止在gn中调用外部编译工具编译软件模块
【级别】禁止
【描述】需要将外部组件移植成gn的编译形式,避免编译过程对环境产生不必要的依赖,而且可获得编译框架提供的公共能力,包括不限于:安全编译选项,ASAN等。
【反例】在gn中使用action调用automake和Make来编译三方组件。
【例外】Linux Kernel 编译框架实际完成的用户态程序编译,内核完全可以在编译框架之外完成独立编译。某些平台实现为了实现一键编译,使用gn将内核编译加在编译过程中,是可以接受的。
##### 规则1.2 禁止在模块的gn文件中,再次添加编译系统已经添加的安全编译选项
【级别】禁止
【描述】对于全局已经添加的默认选项,模块开发者应当知晓,不需要为了满足内外部规则再次添加。
| 编译选项 | 编译参数 | 默认值 |
|---------|------------|------------|
| 栈保护 | -fstack-protector-strong| 开 |
| Fortify Source | -D_FORTIFY_SOURCE=2 -O2 | 开 |
【反例】在模块的编译添加 -fstack-protector-strong
##### 规则1.3 禁止在gn中添加和默认编译选项相反的编译选项
【级别】禁止
【描述】默认的编译选项代表了系统的默认能力,自研模块有特殊情况需要去掉部分能力,必须有足有的理由。
【反例】在自研模块中添加 -wno-unused 以消除编译告警。
【例外】移植三方组件,或者使用因为三方组件时,可根据三方组件的要求覆盖默认的编译选项。
##### 规则 2.1 使用gn format 对添加或者修改的gn文件进行格式化,满足格式和排版的需求
【级别】要求
##### 规则 2.2 编写action时,使用python而不是shell
【级别】建议
【描述】python 环境更容易保持统一,可以比较容易的多重操作系统上运行,并且扩展性可读性可测试更好。
##### 规则 2.3 禁止在gn和ninja执行过程修改源码目录的内容
【级别】禁止
【描述】包括但不限于给源码目录打patch,向源码目录中拷贝,在源码目录中执行编译,在源码目录生成中间文件等。
##### 规则 2.4 编译脚本的编码格式设置为utf-8,换行符设置为unix格式
【级别】要求
【反例】在windows上编写脚本后,使用了中文注释并保存为本地编码。
# 开源编译规范
## 概述
**简介:**
本规范包括C/C++/Java语言编译选项或系统配置,包括语言选项、警告选项、安全选项、总体选项、代码生成选项、架构选项、优化选项、编译宏等。
**范围:**
本规范规定了C/C++/Java语言在编译构建过程中需要添加的编译选项或系统配置,并对这些选项的作用进行了简单说明。此外,规范中对涉及到的例外场景也进行了阐释说明。
无OS(如裸核、BIOS、Bootloader、BSBC等)需遵循的安全编译选项还未制定,本规范暂不做要求,在相关规范发布之前,建议实施栈保护。
对于本规范未描述的例外情况,如果存在争议,可申请仲裁。
**条款组织方式:**
每个条款一般包含标题、级别、描述等组成部分。条款内容中的“正确示例”表示符合该条款要求的例子,“错误示例”表示不符合该条款要求的例子。
**标题:**
描述本条款的内容。
规范条款分为原则和规则两个类别,原则可以评价规则内容制定的好坏并引导规则进行相应的调整;规则是需要遵从或参考的实践。通过标题前的编号标识出条款的类别为原则或者规则。
标题前的编号规则参见《安全工程规范内容总纲》,其中'P'为单词Principle首字母,'G'为单词Guideline的首字母。原则条款的编号规则为P.Number。规则的编号方式为G.Language.Element. Number,其中Language是语言分类,Element为领域知识中关键元素(本规范中对应的一级目录)的英文字母缩略语。Number是从1开始递增的两位阿拉伯数字,不足两位时高位补0。
| Language | Element | 目录 | Language | Element | 目录 |
|----------|---------|----------|----------|---------|--------------|
| C&C++ | LANG | 语言选项 | C&C++ | WARN | 警告选项 |
| C&C++ | SEC | 安全选项 | C&C++ | CDG | 代码生成选项 |
| C&C++ | OPT | 优化选项 | C&C++ | MD | 架构选项 |
| C&C++ | OVA | 总体选项 | C&C++ | LNK | 链接选项 |
| C&C++ | DBG | 调试选项 | C&C++ | PRE | 编译宏 |
| C&C++ | OTH | 其他 | JAVA | JAVAC | JAVAC |
| JAVA | MAVEN | MAVEN | | | |
**级别:**
规则类条款分为两个级别:要求、建议。
- 要求:表示产品原则上应该遵从,但可以按照具体产品版本计划和节奏分期实现。
- 建议:表示该条款属于最佳实践,有助于进一步消解风险,产品可结合业务情况考虑是否纳入。
**描述:**
对条款的进一步描述,描述条款的原理,配合正确和错误的代码例子作为示范。有的条款还包含一些规则不适用的例外场景。
## C/C++语言编译选项
### 语言选项
##### G.C&C++.LANG.01 显式设置编译的语言标准
**【级别】** 要求
**【描述】** 按时间先后顺序,常用的ISO C标准包括:"-std=c90","-std=c99","-std=c11",对应的GNU扩展标准为"-std=gnu90","-std=gnu99","-std=gnu11"。
按时间先后顺序,常用的ISO C++标准包括:"-std=c++98","-std=c++11","-std=c++14","-std=c++1z",对应的GNU扩展标准为"-std=gnu++98","-std=gnu++11","-std=gnu++14","-std=gnu++1z"。
"-ansi"对应ISO C标准"-std=c90"和ISO C++标准"-std=c++98"。
GNU扩展标准完全支持对应的ISO标准,并在对应的ISO标准上做了扩展。
"-Wpedantic","-pedantic","-pedantic-errors"等选项用于检查是否严格符合对应的ISO标准,对不符合标准的语法进行警告,GNU扩展语法也可能产生警告。
##### G.C&C++.LANG.02 采用较新的语言标准
**【级别】** 建议
##### G.C&C++.LANG.03 显式设置char的类型:"-fsigned-char"或"-funsigned-char"
**【级别】** 建议
**【描述】**"-fsigned-char":x86环境默认char是signed类型,但是ARM64下,默认char是unsigned类型;编译器适配不同平台后端的指令集,故为了考虑平台兼容性,使用该选项。
部分产品可能默认char等效于unsignd char,这种情况下建议使用选项"-funsigned-char"显式设置。
##### G.C&C++.LANG.04 对C++语言,禁止使用"-fpermissive"选项
**【级别】** 要求
**【描述】** 使用"-fpermissive"选项将C++代码中不符合标准的语法error降级成warning。不允许使用该选项,应采用符合标准的C++语法。
### 警告选项
#### 选项集
##### G.C&C++.WARN.01 打开"-Wall"选项,检查有用的警告选项集
**【级别】** 要求
**【描述】** "-Wall"是gcc编译器认可的、很有用的警告选项集合,包括"-Wpointer-sign"、"-Wframe-address"、"-Wmaybe-uninitialized"、"-Wint-in-bool-context"等警告。对于这些警告,应该理解其含义,通过修改代码来消除警告。
##### G.C&C++.WARN.02 打开"-Wextra"选项,检查除"-Wall"外附加的选项集;"-Wextra"中误报较多的选项,可以使用"-Wno-XXXX"屏蔽
**【级别】** 要求
**【描述】** "-Wextra"是除"-Wall"外的一些有用的警告选项集合,包括“-Wempty-body”、"Wmissing-field-initializers"、"-Wunused-parameter"等警告。
"-Wextra"中某些警告可能存在较多误报,产品在实测的基础上,可以使用“-Wno-XXXX”屏蔽其中误报较多的警告,如某产品实测“-Wunused-parameter
\-Wmissing-field-initializers”误报较多,可以设置“-Wextra -Wno-unused-parameter
\-Wno-missing-field-initializers”,由产品线软件总工批准。
##### G.C&C++.WARN.03 打开"-Weffc++"选项,检查Scott Meyers’ Effective C++选项
**【级别】** 建议
**【描述】** "-Weffc++":Scott Meyers’ Effective C++对应的警告选项集。
#### 警告屏蔽
##### G.C&C++.WARN.04 禁止使用"-w"选项屏蔽所有警告
**【级别】** 要求
**【描述】** 编译器提示的警告通常对于鉴别低劣的代码和隐晦的bug非常有用,使用-w选项会屏蔽了所有的警告。
##### G.C&C++.WARN.05 禁止使用"-Wno-XXXX"抑制"-Wall"包含的所有警告选项
**【级别】** 要求
**【描述】** "-Wall"是gcc编译器认可的、很有用的警告选项集合,禁止使用比如"-Wno-pointer-sign"、"-Wno-frame-address"、"-Wno-maybe-uninitialized"、"-Wno-int-in-bool-context"抑制"-Wall"包含的"-Wpointer-sign"、"-Wframe-address"、"-Wmaybe-uninitialized"、"-Wint-in-bool-context"选项。
##### G.C&C++.WARN.06 禁止使用"-Wno-error= XXXX"选项将已指定的升级错误的警告再次降级成警告
**【级别】** 要求
**【描述】** "-Werror=XXXX"把指定警告升级错误,"-Wno-error=XXXX":将指定升级成错误的警告再次降级成警告,令人困惑。
##### G.C&C++.WARN.07 避免使用"-Wno-XXXX"抑制编译器缺省打开的编译警告选项
**【级别】** 建议
**【描述】** 编译器缺省打开的编译警告选项,是gcc编译器认可的、很有用的警告选项,如"-Wwrite-strings"、"-Wdelete-incomplete"、"-Wsizeof-array-argument"等。对于这些警告,应该理解其含义,通过修改代码来消除警告。
**【错误示例】** 某组件的构建工程中使用"-Wno-write-strings"抑制"-Wwrite-strings"编译警告7749次。
**例外:** 为了确保构建一致性,可以重定义 \__FILE_\_ 宏,消除绝对路径,可以使用"-Wno-builtin-macro-redefined"抑制"-Wbuiltin-macro-redefined"警告。
#### 警告升级
##### G.C&C++.WARN.08 使用"-Werror"、"-Werror=XXXX"选项把警告当错误处理
**【级别】** 建议
**【描述】** 建议打开"-Werror"、"-Werror=XXXX"选项:把警告当错误处理
"-Werror":把警告当错误处理,一旦出现警告,编译就会失败,有利于在开发过程中清除所有的警告。
"-Werror=XXXX":把指定警告当错误处理。使用"-Werror=XXXX"指定某些警告当错误处理,有利于在开发过程中清除所指定的警告。,如"-Werror=implicit-function-declaration"、"-Werror=format-SEC"。
#### 警告管理
##### G.C&C++.WARN.09 同一构建工程中,统一编译警告选项。
**【级别】** 要求
**【描述】** 统一的编译警告选项,确保各部分代码质量统一。
#### 函数
##### G.C&C++.WARN.10 打开"-Wtrampolines"选项,避免内嵌函数生成trampoline
**【级别】** 建议
**【描述】** 内嵌函数是定义于函数中的函数。当内嵌函数指针生成trampoline时,会触发警告。Trampoline是在运行时创建于栈区的一小段数据或代码,它包含了内嵌函数的地址信息,它被用于内嵌函数的间接调用。某些平台上,Trampoline仅仅由一些特殊处理的数据构成。但是,大多数平台上,它是由代码构成的,因此它需要栈可执行来支持。栈变成可执行栈,CPU读取栈上指令执行,攻击者可能通过缓冲区溢出攻击等手段运行栈内存上自己得代码。
**【错误示例】** 在函数main内部定义并通过函数指针调用内嵌函数fun,在”-Wtrampolines”选项下编译警告。
- 源程序:
```
\#include \<stdio.h\>
int main(){
int ret;
int (\*pfunc)(int a, int b);
int fun(int a, int b){
return a + b;
}
pfunc = fun;
ret = pfunc(10, 20);
printf("test gcc option -Wtrampolines! ret = %d\\n", ret);
return 0;
}
```
- 编译选项:
```
gcc -Wtrampolines trampolines.c -o out
```
- 编译结果:
```
warning:trampoline generated for nested function ‘fun’ [-Wtrampolines]
```
**例外**:"-Wtrampolines" xt-xcc和clang编译器不支持。
##### G.C&C++.WARN.11 打开"-Wformat=2"选项,检查格式化输入/输出函数的安全
**【级别】** 建议
**【描述】** "-Wformat=2"是“-Wformat”、“-Wformat-nonliteral”、“-Wformat-SEC“、”\-Wformat-y2k“的集合。
1. “-Wformat”:格式化函数的参数类型、格式错误时,警告
2. “-Wformat-nonliteral”:当格式化字符串为非字符串常量时,警告
3. “-Wformat-SEC“、” -Wformat-y2k“
对于产品自行封装的格式化输入输出框架函数, 应于 API 声明 format attribute
以利用编译器检查能力, 详细参考
[https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html\#Common-Function-Attributes](https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html#Common-Function-Attributes)
##### G.C&C++.WARN.12 打开"-Wstrict-prototypes "选项,避免函数在声明或定义中没有指定参数类型
**【级别】** 建议
**【描述】** 在函数的声明或定义时,显式指明函数参数类型,编译器检查函数调用和定义之间参数类型的不匹配情况。
**【错误示例】**
- 源程序:
```
\#include \<stdio.h\>
int func(param){
return param;
}
```
- 编译选项:
```
gcc -Wstrict-prototypes strict_prototypes.c -o out
```
- 编译结果:
```
warning: function declaration isn't a prototype [-Wstrict-prototypes] int func(param){
```
相关文档:《SEI CERT C Coding Standard》DCL07-C. Include the appropriate type
information in function declarators
#### 二进制一致性
##### G.C&C++.WARN.13 打开"-Wdate-time"选项,避免使用时间宏,确保二进制一致性
**【级别】** 建议
**【描述】** "-Wdate-time":避免代码使用__DATE__、__TIME__、__TIMESTAMP__,以确保二进制一致性。
**【错误示例】**
- 源程序:
```
\#include \<stdio.h\>
int main() {
printf ("%s %s %s\\n",_DATE_,_TIME_,_TIMESTAMP_);
return 0;
}
```
- 编译选项:
```
gcc -Wdate-time datetime.c -o out
```
- 编译结果:
```
warning:macro "_DATE_" might prevent reproducible builds [-Wdate-time] warning:macro "_TIME_" might prevent reproducible builds [-Wdate-time] warning:macro "_TIMESTAMP_" might prevent reproducible builds [-Wdate-time]
```
#### 语句
##### G.C&C++.WARN.14 打开"-Wfloat-equal"选项,避免浮点数相等比较运算
**【级别】** 要求
**【描述】** 由于浮点数存在精度问题,大多数情况下是近似值,不能精确判断是否相等。浮点数相等或不相等比较是不安全的行为,建议通过判断两数之差的绝对值是否小于可接受误差来判断浮点数是否相等,可以使用C语言标准库函数fabs()求两浮点数之差的绝对值,然后与可接受误差比较,如果在可接受误差范围内,则相等,否则不相等。需要特别注意的是:\>\<\>=、\<=这四种比较运算符用于浮点数比较不会警告。
**【错误示例】**
- 源程序 :
```
\#include \<stdio.h\>
int main() {
double a = 0.3;
double b = 0.6;
ouble c = 0.9;
if ((a+b) == c) {
/\* 看似相等,实际运行时,a+b与c不相等\*/
printf("double equal\\n");
}
return 0;
}
```
- 编译选项:
```
gcc -Wfloat-equal float_equal.c -o out
```
上述示例中,进行双精度浮点数相等比较,编译器警告"warning:comparing floating
point with == or != is unsafe[-Wfloat-equal]",a+b和c看似应该相等,但是程序运行时并不相等,这是因为浮点数是近似表达,a+b和c均为近似值,浮点数的相等性比较不可信,是一种不安全的行为。浮点数正确的比较方式是设定一个可接受的误差精度,如果两个浮点数差值的绝对值在这个误差范围内,就表示相等,正确方式如下:
**【正确示例】** 浮点数相等比较
- 源程序 :
```
\#include \<stdio.h\> \#include \<math.h\> \#define EPSILON 1e-6 /
\* 双精度比较可接受的误差 \*/
int main() {
double a = 0.3;
double b = 0.6;
double c = 0.9;
if (fabs((a+b)-c) \< EPSILON) {
printf("double equal\\n");
}
return 0;
}
```
- 编译选项:
```
gcc -Wfloat-equal float_equal.c -o out
```
##### G.C&C++.WARN.15 打开"-Wswitch-default"选项,确保switch语句有default分支
**【级别】** 建议
**【描述】** 如果switch语句没有default分支,在-Wswitch-default选项下编译警告。
**【错误示例】**
- 源程序:
```
enum TintColor{
RED, DARK_RED, GREEN, LIGHT_GREEN
};
void Colorize(enum TintColor Color) {
switch (Color) {
case RED:
/\* code \*/
break;
case DARK_RED:
break;
}
}
```
- 编译选项:
```
gcc -Wswitch-default switch_default.c -o out
```
- 编译结果:
```
warning: switch missing default case [-Wswitch-default] switch (Color)
```
#### 变量
##### G.C&C++.WARN.16 打开"-Wshadow"选项,检查变量覆盖
**【级别】** 建议
**【描述】** "-Wshadow":局部变量覆盖全局变量、函数参数等产生的警告。C++语言打开该选项警告较多,团队可以根据实际情况评估是否打开该选项。
**【错误示例】**
- 源程序:
```
int num = 0;
int foo(int a, int b){
int num = a + b;
return num;
}
```
- 编译选项:
```
gcc -Wshadow shadow.c -o out
```
- 编译结果:
```
warning: declaration of 'num' shadows a global declaration [-Wshadow] int num = a + b;
```
##### G.C&C++.WARN.17 打开"-Wstack-usage=len"选项,设置栈大小,避免栈溢出
**【级别】** 建议
**【描述】** 如果函数使用的栈内存可能超过len个字节,编译警告。len的值,团队根据实际情况设置。
**【错误示例】**
- 源程序:
```
void foo(void) {
int arr[1000] = {0};
return;
}
```
- 编译选项:
```
gcc -Wstack-usage=1000 stack_usage.c -o out
```
- 编译结果:
```
warning: stack usage is 4012 bytes [-Wstack-usage=] void foo(void) {
```
##### G.C&C++.WARN.18 打开"-Wframe-larger-than=len" 选项,设置栈框架大小,避免栈溢出
**【级别】** 建议
**【描述】** 如果一个函数的栈框架超过len字节,编译警告。len的值,团队根据实际情况设置。
**【错误示例】**
- 源程序:
```
void foo(void) {
int arr[1000] = {0};
return;
}
```
- 编译选项:
```
gcc -Wframe-larger-than=1000 stack_usage.c -o out
```
- 编译结果:
```
warning: the frame size of 4000 bytes is larger than 1000 bytes [-Wframe-larger-than=]
```
##### G.C&C++.WARN.19 不建议打开“-Wno-return-local-addr“选项,检查返回局部变量地址
**【级别】** 建议
**【描述】** 如果函数返回局部变量的地址,编译时默认产生“-Wreturn-local-addr”警告。禁止开启“-Wno-return-local-addr”选项屏蔽这些警告。
**【错误示例】**
- 源程序:
```
int\* foo() {
int a=0;
return \&a;
}
```
- 编译选项:
```
gcc -Wreturn-local-addr return_local_addr.c -o out
```
- 编译结果:
```
warning: function returns address of local variable [-Wreturn-local-addr] return \&a;
```
#### 类型转换
##### G.C&C++.WARN.20 打开"-Wconversion"选项,避免隐式转换改变数值
**【级别】** 建议
**【描述】** 如果代码中的隐式转换会改变数值,在-Wconversion下编译警告。
可能造成数值改变的隐式转换包括:带小数的实数转换成整数,无符号数和有符号数之间的转换,较大类型的数转换成较小类型。需要注意的是,如果代码进行显式强转,在-Wconversion下编译不会警告。
**【错误示例】**
- 源程序:
```
int foo(void) {
double num = 1.2;
return num;
}
```
- 编译选项:
```
gcc-Wconversion conversion.c -o out
```
- 编译结果:
```
warning: conversion from 'double' to 'int' may change value [-Wfloat-conversion] return num;
```
不同类型的对象指针之间不应进行强制转换。
##### G.C&C++.WARN.21 打开"-Wcast-qual"选项,指针类型强制转换时,避免目标类型丢失类型限定词
**【级别】** 建议
对指针类型强制转化,导致目标类型丢失类型限定词
**【描述】** 如将const char\*指针类型强制转换为普通的char\*时会丢失const类型限定词,const修饰指针是期望该指针指向的内存不可修改,如果强制转化丢失了const类型限定词,就可以通过转换后的结果指针修改原本不期望被修改的内存,失去了对对象const约束的意义。
**【错误示例】**
- 源程序:
```
static char buf[8];
void foo(){
const char\* ptr = buf;
char\* q = (char\*)ptr;
}
```
- 编译选项:
```
gcc -Wcast-qual cast_qual.c -o out
```
- 编译结果:
```
warning: cast discards 'const' qualifier from pointer target type [-Wcast-qual] char\* q = (char\*)ptr;
```
##### G.C&C++.WARN.22 打开“-Wcast-align”选项,检查指针类型强制转换,避免目标所需的地址对齐字节数增加
**【级别】** 建议
**【描述】** 当源程序中某个指针类型强制转换导致目标所需的地址对齐字节数增加时,则产生警告。比如在整型只能以两字节或四字节边界进行访问的机器上,将 char \* 转换为 int \* 则给出警告。
#### 数组
##### G.C&C++.WARN.23 打开“-Wvla”选项,避免变长数组
**【级别】** 建议
**【描述】** 定义数组时,如果数据长度是变量而非固定值,在-Wvla选项下编译警告。
**【错误示例】**
- 源程序:
```
void foo(int len) {
int arr[len];
}
```
- 编译选项:
```
gcc -Wvla val.c -o out
```
- 编译结果:
```
warning: ISO C90 forbids variable length array 'arr' [-Wvla] int arr[len];
```
#### 无效代码
##### G.C&C++.WARN.24 打开“-Wunused”选项,避免无效代码
**【级别】** 建议
**【描述】** -Wunused选项检查代码中未使用的变量、函数、参数、别名等问题。-Wunused选项包含了多个针对某种类型对象未使用的子选项:
\-Wunused-but-set-variable
\-Wunused-function
\-Wunused-label
\-Wunused-local-typedefs
\-Wunused-variable
\-Wunused-value
需要注意的是,需要使用-Wextra \-Wunused或者-Wunused-parameter,才能对函数中未使用的形参警告。
**【错误示例】**
- 源程序:
```
void foo(void) {
int a;
}
```
- 编译选项:
```
gcc -Wunused unused.c -o out
```
- 编译结果:
```
warning: unused variable 'a' [-Wunused-variable] int a;
```
#### 预处理
##### G.C&C++.WARN.25 打开“-Wundef ”选项,避免预编译指令\#if语句中出现未定义的标识符
**【级别】** 建议
**【描述】** 当一个没有定义的标识符出现在 \#if 中时,给出警告。
**【错误示例】**
- 源程序:
```
\#if DEFINE_A_VALUE
\#endif
```
- 编译选项:
```
gcc -Wunused unused.c -o out
```
- 编译结果:
```
warning: "DEFINE_A_VALUE" is not defined, evaluates to 0 [-Wundef] \#if DEFINE_A_VALUE
```
#### 类
##### G.C&C++.WARN.26 打开“-Wnon-virtual-dtor”选项,避免基类析构函数没有定义虚函数
**【级别】** 建议
**【描述】** 只有基类析构函数是virtual,通过多态调用的时候才能保证派生类的析构函数被调用。
**【错误示例】**
- 源程序 :
```
class Base {
public: virtual void foo() const = 0;\
~Base() {}
};
class Derived: public Base {
public: virtual void foo() const {}
Derived() {}
};
```
- 编译选项:
```
gcc-Wnon-virtual-dtor non_virtual_destructors.cpp -o out
```
- 编译结果:
```
warning: 'class Base' has virtual functions and accessible non-virtual destructor [-Wnon-virtual-dtor]
```
##### G.C&C++.WARN.27 打开“-Wdelete-non-virtual-dtor”选项,当基类析构函数没有定义虚函数时,避免通过指向基类的指针来执行删除操作
**【级别】 建议**
**【描述】** 当基类没有定义虚析构函数,指向基类的指针来执行删除操作,可导致未定义的行为。禁止开启“-Wno-delete-non-virtual-dtor”选项屏蔽这些警告。
**【错误示例】**
- 源程序:
```
class Base {
public: virtual void f();
};
class Sub: public Base {
public: void f(int);
};
int main() {
Sub\ * sub = new Sub();
Base\ * base = sub;
delete base;
}
```
- 编译选项:
```
gcc--Woverloaded-virtual overloaded_virtual.cpp -o out
```
- 编译结果:
```
warning: deleting object of polymorphic class type 'Base' which has non-virtual destructor might cause undefined behavior [-Wdelete-non-virtual-dtor] delete base;
```
##### G.C&C++.WARN.28 打开"-Woverloaded-virtual"选项,避免隐藏基类虚函数
**【级别】** 建议
**【描述】** 派生类重新定义基类的虚函数,导致基类的虚函数被隐藏。
**【错误示例】**
- 源程序:
```
class Base {
public: virtual void f();
};
class Sub: public Base {
public: void f(int);
};
```
- 编译选项:
```
gcc--Woverloaded-virtual overloaded_virtual.cpp -o out
```
- 编译结果:
```
warning: by 'void Sub::f(int)' [-Woverloaded-virtual] void f(int);
```
### 安全选项
#### 选项集
##### G.C&C++.SEC.01 打开栈保护选项
**【级别】** 要求
**【描述】**
**Linux平台用户态**
作用阶段:编译选项
作用范围:可重定位文件(.o)、动态库、可执行程序
用法:-fstack-protector-all/-fstack-protector-strong
**说明:** 当存在缓冲区溢出攻击漏洞时,攻击者可以覆盖栈上的返回地址来劫持程序控制流。启用栈保护后,在缓冲区和控制信息间插入一个canary word。攻击者在覆盖返回地址的时候,往往也会覆盖canary word。通过检查canary word的值是否被修改,就可以判断是否发生了溢出攻击。
1\. GCC4.9版本及以上落地-fstack-protector-strong;
2\. GCC4.9版本以下落地-fstack-protector-all。
3\. windriver linux 4.3 + MIPS的环境不支持该特性。
**Linux平台内核**
作用阶段:编译选项
作用范围:Linux平台内核态
用法:内核编译前打开配置CONFIG_CC_STACKPROTECTOR/CONFIG_CC_STACKPROTECTOR_STRONG
**说明:**
内核3.14及以上版本可支持CONFIG_CC_STACKPROTECTOR_STRONG,原有CONFIG_CC_STACKPROTECTOR(对应-fstack-protector)修改为CONFIG_CC_STACKPROTECTOR_REGULAR,内核4.18及以上版本CONFIG_CC_STACKPROTECTOR_REGULAR(对应-fstack-protector)修改为CONFIG_STACKPROTECTOR,CONFIG_CC_STACKPROTECTOR_STRONG(对应-fstack-protector-strong)修改为CONFIG_STACKPROTECTOR_STRONG
受限于OS内核不支持本选项,导致驱动程序也无法使能本选项的情况可例外。其中,所使用的OS内核必须是下列情况中的一种或多种:
1.官方发布的最新版本或者公司推荐的OS版本;
2.由于产品配套(如,兼容客户OS)或兼容现网存量产品,而必须选择的OS版本;
3.由于商务原因(如,实体名单)无法与OS厂商联系并获取新版本,只能使用老版本的场景;
**LiteOS平台**
作用阶段:编译选项
作用范围:LiteOS V200R003C00及之后的版本
用法:-fstack-protector-all/-fstack-protector-strong
**说明:** 1.GCC4.9版本及以上落地-fstack-protector-strong;
2.GCC4.9版本以下落地-fstack-protector-all。
受限于编译器版本不支持本选项或硬件提供类似栈保护的情况可例外。如以下两种情况:
1.由于IAR 8.20版本以下版本不支持任务栈保护,不作要求。
2.使用硬件栈保护的不作要求(如ARC架构下部分产品能够提供硬件栈保护机制,栈溢出时能够触发硬件异常)。
##### G.C&C++.SEC.02 打开地址随机化选项
**【级别】** 要求
windows平台HighASLR & ForceASLR选项实施级别为建议
**【描述】**
**Linux(用户态)**
**a. 使用命令 echo 2 \>/proc/sys/kernel/randomize_va_space 打开系统随机化配置**
**作用阶段:** 运行系统配置
**作用范围:** 堆、栈、内存映射区(mmap基址、shared libraries、vdso页)
**用法:** echo 2 \>/proc/sys/kernel/randomize_va_space
**说明**
ASLR是一种针对缓冲区溢出的安全保护技术,通过对堆、栈、共享库映射等线性区布局的随机化,增加攻击者预测目的地址的难度,防止攻击者直接定位攻击代码位置,达到阻止溢出攻击的目的。randomize_va_space等于1时,栈、数据段、VDSO会随机化,randomize_va_space等于2时堆地址也会随机化。
需要ASLR开启的级别为最高级别,即randomize_va_space等于2
**b. 打开PIC选项实现动态库随加载**
**作用阶段**:编译选项
**作用范围**:动态库
**用法:** –fPIC(-fpic)
**说明:**
地址无关选项将发生在代码段的重定位移到数据段实现,so文件加载时代码段不会发生任何变化,做到所有进程共用一个代码段副本。
\-fPIC和-fpic均指示GCC产生地址无关代码,唯一的区别是-fPIC产生代码稍大,-fpic产生代码相对较小。
**c. 打开PIE选项实现可执行文件随机加载**
**作用阶段:** 编译链接选项
**作用范围:** 可执行程序
**用法:** –fPIE(-fpie)-pie
**说明:**
具备PIE的可执行文件,在加载执行时可像共享库一样随机加载。有研究表明:PIE可有效降低固定地址类攻击、缓冲溢出类攻击的成功概率。
(1)关注对应的热补丁版本是否支持PIE选项,不支持的场景下不建议使用该选项
(2)-fPIE编译选项,-pie链接选项。
(3)-fPIE产生代码稍大,-fpie产生代码相对较小。
**LiteOS平台**
**a. 配置代码段、数据段随机加载**
**作用阶段:** 编译、链接选项以及运行系统配置
**作用范围:** LiteOS V200R003C00及之后的版本
**用法:** 先编译成可随机的镜像;然后镜像加载时,对地址进行随机修正
**说明:** 1.依赖支持随机地址加载的bootloader,依赖MMU、DDR空间。
2.开启后性能下降10%左右。
3.方案:-fPIE -pie借助GOT表实现地址随机,gcc和自研HCC编译器均可提供支持;
4.开销较大无法落地时,需要业务提供具体的数据到TMG审核。
受限于产品硬件设计或启动流程不支持的情况可例外。如以下三种情况:
1.XIP场景,即系统直接运行于Flash。
2.rom化场景,即全部或部分代码rom化,无法重新加载的场景。
3.bootloader不支持随机地址加载。
**b.配置动态库随机加载**
**作用阶段:** 编译选项
**作用范围:** LiteOS V200R003C00及之后的版本
**用法:** -fPIC
**说明:** 1.动态库编译阶段采用-fPIC。
##### G.C&C++.SEC.03 打开GOT表重定位只读选项
**【级别】** 要求
**【描述】**
**Linux平台-用户态**
**a.部分重定向只读选项:**
**作用阶段:** 链接选项
**作用范围:** 动态库、可执行程序
**用法:** -Wl,-z,relro
**说明**
动态链接的ELF二进制程序使用称为全局偏移表(GOT)的查找表去动态解析位于共享库中的函数。攻击者通过缓冲区溢出修改GOT表项的函数地址值来达到攻击的目的。通过增加RELRO选项,可以防止GOT表被恶意重写。
**b.全部重定向只读选项:**
**作用阶段:** 链接选项
**作用范围:** 动态库、可执行程序
**用法:** -Wl,-z,now
**说明**
开启部分重定项只读保护后,再开启立即绑定可实现全部重定向只读保护,即:全部重定向只读(GOT表全保护):-Wl,-z,relro,-z,now 可较好对ret2plt的攻击进行防护,而对诸如缓冲区溢出等攻击无法防范。
对于大量使用共享库中的函数代码产品,在一定程会导致程序装载(启动)阶段缓慢,而运行时性能不会有影响。
##### G.C&C++.SEC.04 打开堆栈不可执行/数据执行保护选项实现堆栈不可执行保护
**【级别】** 要求
**【描述】**
**Linux平台-用户态**
**作用阶段:** 链接选项
**作用范围:** 动态库、可执行程序
**用法:** -Wl,-z,noexecstack
**说明**
1.如果有内嵌函数,会导致功能错误,需要先用-Wtrampolines进行检测,GCC4.6.4版本及以上。
2.windriver linux 4.3普通版本不支持该特性。
3.windriver linux 6 + MIPS不支持该特性。
**LiteOS平台**
**作用阶段:** 运行系统配置
**作用范围:** LiteOS V200R003C00及之后的版本
**用法:** 运行时配置堆栈不可执行、数据段(BSS,DATA)不可执行
**说明:** 1.依赖硬件支持MMU/MPU/PMP等内存保护单元。
##### G.C&C++.SEC.05 使用-s选项或者strip工具去除符号表
**【级别】** linux平台-用户态:要求,其它平台:建议。
**【描述】**
**Linux平台-用户态**
**作用阶段:** 链接选项
**作用范围:** 动态库、可执行程序
**用法:** -s(strip工具)
**说明:**
符号在链接过程中,发挥着至关重要的作用,链接过程的本质就是把多个不同的目标文件“粘”到一起,符号可看作链接的粘合剂,整个链接过程正是基于符号才正确完成的。链接完成后,符号表对可执行文件运行已经无任何作用,反而会成为攻击者构造攻击的工具,因此删除符号表可防御黑客攻击。事实上删除符号表除防攻击外,还可对文件减肥,降低文件大小。
1.对于静态库,可重定位文件(.o)不能strip,否则出现编译错误,只涉及ELF可执行文件和动态库交付的产品才可以去除符号表
2.仅交付给产品,并不直接参与公司外部发布的组件和平台,需提供正式机制通知下游产品在发布阶段统一执行删除符号表操作
3.因为strip会影响产品定位网上问题和热补丁,构建流程上需要保证strip前后的版本同步,即需要产品本地保留未strip符号表的版本供补丁制作和网上调测使用。如可采用以下方案:
3.1 执行机在编译的时候,生成未剥离符号表的可执行文件和动态库的版本,版本归档到VMP上(CMC)供产品做热补丁
3.2 使用strip工具对动态库和可执行文件删除符号表
3.3 剥离符号的可执行文件和动态库压缩到启动大包
4.strip工具和-s选项可达到一样的去除符号表的效果,基于-s选项会造成版本两次编译构建,建议发布前直接使用strip工具,strip级别为默认,如stripbin.out。
**LiteOS平台**
**作用阶段:** 链接选项
**作用范围:** LiteOS V200R003C00及之后的版本
**用法:** -s(strip)
**说明:** 1.LiteOS产品最终用于烧录的编译结果为bin文件,本身不存在符号表信息,建议不开启。
##### G.C&C++.SEC.06 禁止使用Run-time Search Path选项
**【级别】** 要求
**【描述】**
**Linux平台-用户态**
**作用阶段:** 链接选项
**作用范围:** 动态库、可执行程序
**用法:** -Wl,--disable-new-dtags,--rpath,/libpath1:/libpath2;-Wl,--enable-new-dtags,--rpath,/libpath1:/libpath2
**说明**
主要用于防护LD_LIBRARY_PATH替换同名动态库的攻击。通过加入此选项可以指定一个运行时动态库搜索的路径,该路径的搜索优先级高于LD_LIBRARY_PATH指定的路径。可执行文件在运行阶段进行动态库搜索时会首先在--rpath指定的路径查找动态库,然后才会到LD_LIBRARY_PATH指定的路径搜索。因此可以有效防御LD_LIBRARY_PATH=[attackpath]来替换同名动态库的攻击。但是该选项有也很多局限性,如指向的路径不安全,若普通用户可以在这些目录中使用恶意程序替换正常程序,造成权限提升,引发不安全路径漏洞。
##### G.C&C++.SEC.07 打开代码段/数据段写保护选项
**【级别】** 建议
**【描述】**
**LiteOS平台**
**a. 配置代码段、只读数据段写保护**
**作用阶段:** 运行系统配置
**作用范围:** LiteOS V200R003C00及之后的版本
**用法:** 运行时配置代码段、ReadOnly Data段不可修改
**说明:** 1.依赖硬件支持MMU/MPU/PMP等内存保护单元。
##### G.C&C++.SEC.10 启用FORTIFY_SOURCE编译宏来打开FS选项
**【级别】** 建议
**【描述】**
**Linux平台-用户态**
**作用阶段:** 编译选项
**用法:** -D_FORTIFY_SOURCE=2 -O2
**说明**
程序中使用到静态的固定大小的缓冲区,增加了该选项之后,编译器或运行时库会对相关函数的调用在编译时或运行时进行检查。
原则上推荐级别为-O2(基于性能优化效果优于O1),若产品基于O2的风险性允许使用-O1。
先在分支版本添加,重点做性能测试,根据测试结果取舍。
**LiteOS平台**
**作用阶段:** 编译选项
**作用范围:** LiteOS V200R003C00及之后的版本
**用法:** -D_FORTIFY_SOURCE=2 -O2
**说明:** 1. 选项收益和lib库实现有关。
2.当前LiteOS使用musl库,如果产品替换支持相关功能的lib库,需按需开启。
受限于lib库不支持情况可例外。如以下情况:
1.musl库配置D_FORTIFY_SOURCE=2没有作用,容易对用户造成误导,可不开启。
##### G.C&C++.SEC.11 打开ftrapv选项来检测整数溢出
**【级别】** 建议
**【描述】**
**Linux平台-用户态、LiteOS平台**
**作用阶段:** 编译选项
**用法**:-ftrapv
使用了-ftrapv选项后,执行带符号的整数间的加、减、乘运算时,不是通过CPU的指令,而是用包含在GCC附属库libgcc.c里的函数来实现。
性能影响较大,建议在Release版本不实施。
##### G.C&C++.SEC.13 打开栈检查选项
**【级别】**
Linux平台-用户态:建议
LiteOS平台:要求(禁用)
**【描述】**
**Linux平台-用户态**
**作用阶段:** 编译选项
**作用范围:** 可重定位文件、动态库、可执行程序
**用法:** -fstack-check
**说明:**
stack-check在编译时检查程序中栈空间,如果超过编译告警阀值则产生告警;然后在程序中生成额外的指令来检查运行时栈不会被溢出,stack-check选项会在每个栈空间最低底部设置一个安全的缓冲区,如果函数中申请的栈空间进入安全缓冲区,则触发一个Storage_Error异常。但它所生成的代码实际上并不处理异常,如果检测到异常则会发出一个消息,通知操作系统处理。它只保证操作系统可以检测到栈扩展。
性能影响较大,建立在Debug版本中实施,Release版本不实施
**实施建议:** 可选
**LiteOS平台**
**作用阶段:** 编译选项
**作用范围:** LiteOS V100R003C00及之后的版本
**用法:** -fstack-check
**说明:** 1.开启后程序会访问非法地址,导致执行异常,因此LiteOS平台下禁止打开栈检查选项。
### 优化选项
#### 选项集
##### P.C&C++.01 在实测的基础上,选择合适优化等级和各种优化选项
**【描述】** 在实测的基础上,尝试各种代码优化选项,以查看它们是否确实为生成程序更快。
##### G.C&C++.OPT.01 优化等级建议选"-O2"、"-Os"、"-O3"
**【级别】** 建议
##### G.C&C++.OPT.02 当代码中存在较多的不同类型指针互转时,使用"-fno-strict-aliasing"选项关闭严格别名优化
**【级别】** 建议
**【描述】** GCC的"-O2"打开"-fstrict-aliasing"严格别名规则优化:编译器假定相同的内存地址绝不会存放不同类型的数据,该优化选项相对激进。为了避免代码中不同类型指针互转导致优化问题,可以使用"-fno-strict-aliasing"关闭优化;最好的方式是修改代码,遵守严格别名规则。
注意使用"-fno-strict-aliasing"选项可能会影响产品性能,如某产品一个性能敏感组件实测,"-O2
\-fno-strict-aliasing"相比"-O2"会有性能下降,测的数据最多下降有9%。
##### G.C&C++.OPT.03 X86/ARM架构下,基于DOPRA平台的产品建议使用"-fno-omit-frame-pointer"选项关闭去SFP(Stack Frame Pointer)优化
**【级别】** 建议
**【描述】** "-fno-omit-frame-pointer":GCC的“-O”
("-O1")会打开"-fomit-frame-pointer"优化选项,也就是去掉函数调用时的frame
pointer,优化会导致代码难以调试,建议通过选项"-fno-omit-frame-pointer"禁止该项优化。
产品需要在性能优化和保留调试信息进行权衡。
### 代码生成选项
#### 选项集
##### G.C&C++.CDG.01 未初始化的全局变量放置在目标文件的数据段:"-fno-common"
**【级别】** 要求
**【描述】** "-fno-common":未初始化的全局变量放置在目标文件的数据段,两个不同的编译单元中声明了同一个全局变量导致警告。多个临时的全局变量定义会增加代码维护难度,降低链接速度和增加空间消耗。
##### G.C&C++.CDG.02 将结构体放在寄存器中直接返回:"-freg-struct-return"
**【级别】** 建议
**【描述】** "-freg-struct-return":采用寄存器返回结构与联合值。
“-fpcc-struct-return”:在返回短的结构和联合值时,与较长的值一样,使用内存而非寄存器。
尽可能在寄存器中返回结构和联合值。对小结构而言,这比“-fpcc-struct-return”效率更高。
如果既未使用“-fpcc-struct-return”, 又未使用相反的“-freg-struct-return ”, GNU
CC缺省使用目标机器指定的标准规则。如果没有标准规则, 除了在GNUCC为主要编译器的机器上,GNU CC缺省采用“-fpcc-struct-return”,在可以选择标准的情况下, 我们选择了更高效的寄存器返 回方式。
注意,此选项影响二进制兼容性,应整个产品统一。
##### G.C&C++.CDG.03 设置默认的ELF镜像中符号的可见性为隐藏:"-fvisibility=hidden"
**【级别】** 建议
**【描述】** "-fvisibility=hidden":可以让动态库中仅API外部可见,有效实现二进制的模块化。使用该选项可以提高动态库链接和加载的速度,防止符号冲突。但该选项加上后,需要考虑对该模块函数打补丁的成本,因为原来的全局符号变成LOCAL属性,对其打补丁时需要重新组名(DOPRA补丁规范有详细的组名规则),构建补丁的成本会增加。是否打开该选项,需要权衡。
##### G.C&C++.CDG.04 启用表达式计算顺序强化规则: “-fstrong-eval-order”
**【级别】** 建议
**【描述】** "-fstrong-eval-order":按C++17的规格确定子表达式之间的计算顺序,比如表达式
T().m_i = A().B() 在未开启时可能生成指令的求值顺序时 A() T() B() ,不符合常规预期;该选项当启用 "-std=c++17" 时自动开启, 但当前 gcc7.3默认"-std=c++14",建议显式开启以降低不可预期行为。
### 总体选项
#### 选项集
##### G.C&C++.OVA.01 打开总体选项:"-pipe"
**【级别】** 建议
**【描述】** "-pipe" :编译过程中多管道并发,节省编译时间
### 架构选项
#### 选项集
##### G.C&C++.MD.01 对于嵌入式软件,显式指明如下架构选项
1. 软硬浮点(按照CPU支持类型进行添加或者不添加)
2. 指令集 (如:march=armv7-a/ march=armv8-a)
**【级别】** 要求
### 链接选项
#### 选项集
##### G.C&C++.LNK.01 打开如下链接选项:"-Wl,-Bsymbolic"、"-rdynamic"、" -Wl,--no-undefined"
**【级别】** 建议
**【描述】** -Wl,-Bsymbolic:同名符号优先使用本so,减少got表调转
"-rdynamic":解决dlopen反向依赖的问题;BIN文件通过地址返回符号名称,需要加,否则backtrace_symbol返回的是地址,不能定位;影响:产品BIN文件增大。
"-Wl,--no-undefined":可以将运行时加载错误,在链接期提前识别出来。打开该选项,导致链接时间会变长,因为链接期要进行依赖关系校验。如果-l指定依赖库不全,会有功能问题,需要产品权衡。
### 调试选项
#### 选项集
##### G.C&C++.DBG.01 对于版本发布构建,禁止携带调试信息
**【级别】** 要求
**【描述】** 调试信息指 符号表 和
详细调试信息表,根据当前安全规定,调试信息,不是运行所必须,要求发布件删除这些信息,包括符号表,以提升攻击难度;热补丁、perf分析、抓堆栈等维测场景受影响。
使用 "-s" 链接选项可完全不生成调试信息,需注意此方法生成的组件与不加 "-s"
后重新构建生成的组件 build-id 是不一致的,不能直接用来 gdb定位问题;也可以链接后使用 objcopy --only-keep-debug \<target\> \<xxx.dbg\> 加上objcopy objcopy --strip-unneeded \<target\> 方式分离调试符号,同样可达成交付件不含有符号表等调试信息。
如果编译阶段启用了 -g 生成详细调试信息表, 会因含有源代码绝对路径信息造成不同目录下构建的二进制差异, 此时可使用 -fdebug-prefix-map=old=new来将绝对路径映射成相对路径,达成 BEP 要求。
### 编译宏
#### 选项集
##### G.C&C++.PRE.01 明确-D编译宏的具体用途,建立-D编译宏的清单
**【级别】** 要求
**【描述】** 每增加一个-D编译宏,就需要对它进行额外的测试。为每一种软件-D编译宏所作的代码修改,必须验证能否适用于其他-D编译宏。首先必须针对所有的-D编译宏,对软件进行构建,以确保没有编译错误;其次必须针对所有的-D编译宏进行完整的测试。
对于未使用的-D编译宏,应该直接删除。
### 其他
#### 选项集
##### G.C&C++.OTH.01 同一构建工程中,避免使用重复的或包含关系的编译选项
**【级别】** 建议
**【描述】** 重复的编译选项是冗余信息,不利于维护。如果编译选项具有不同的参数,可能导致与初始预期不同的方式编译源文件。
编译选项之间存在包含关系时,同时使用会导致冗余。譬如"-Wall"包含40多个子警告选项,"-O"包含40多个子优化选项,当它们与子选项同时使用时就会导致冗余。
**【错误示例】** 某组件对编译优化选项"-O"取值达到7055次,其中同一构建工程中出现多个"-O",如"-O2...-O6"、"-O2...-O3"。
\# "-Wall"包含"-Waddress",同时使用产生冗余
gcc -Wall -Waddress -c test.c -o test.o
\# "-O"包含"-fauto-inc-dec",同时使用产生冗余
gcc -O -fauto-inc-dec -c test.c -o test.o
##### G.C&C++.OTH.02 避免使用相反冲突的选项
**【级别】** 建议
**【描述】** 大多数'-f'和'-W'有两个相反的互相否定的选项:
\-fname/-fno-name和-Wname/-Wno-name,同时引用导致冲突,令人疑惑,不利于维护。
**【错误示例】**
\#同时引用-fomit-frame-pointer和-fno-omit-frame-pointer
```
set(CMAKE_C_FLAGS "-MD -MF -Wall -save-temps -fverbose-asm -fsigned-char
\-fomit-frame-pointer -fno-stack-protector \\
\-fno-delete-null-pointer-checks -fno-common -freg-struct-return -O2
\-fno-omit-frame-pointer -fno-strength-reduce" )
```
##### G.C&C++.OTH.03 编译选项的编写顺序:优化等级(如-O2)+总体选项+警告选项+语言选项+代码生成选项+架构选项(MD-Dependent Options)+优化选项+安全编译选项+自定义宏
**【级别】** 建议
**【描述】** 有选项集的,先写选项集,例如"-Wall"应该写到"-Wformat=2"前。
**【正确示例】**
```
\# Copyright (c) Huawei Technologies Co., Ltd. 2019. All rights reserved.
\# toolchain for ARMA15(without FPU)HI1381/HI1215
\# cpu_family = arm
\# bit_width_in_run = 32
\# cpu_core = a15
\# compile flags
set(CC_OPT_LEVEL "-O2")
set(CC_OVERALL_FLAGS "-pipe")
set(CC_WARN_FLAGS "-Wall -Wextra -Wdate-time -Wtrampolines -Wfloat-equal
\-Wshadow -Wformat=2")
set(CC_LANGUAGE_FLAGS "-fsigned-char")
set(CC_CDG_FLAGS "-fno-common -freg-struct-return")
set(CC_MD_DEPENDENT_FLAGS "-mfloat-abi=soft -march=armv7-a -mtune=cortex-a15")
set(CC_OPT_FLAGS "-fno-strict-aliasing -fno-omit-frame-pointer")
set(CC_SEC_FLAGS "-fPIC -fstack-protector-strong --param=ssp-buffer-size=4")
set(CC_DEFINE_FLAGS "-DXXXXX")
set(CC_ALL_OPTIONS "\${CC_OPT_LEVEL} \${CC_OVERALL_FLAGS} \${CC_WARN_FLAGS}
\${CC_LANNGUAGE_FLAGS} \\
\${CC_CDG_FLAGS} \${CC_MD_DEPENDENT_FLAGS} \${CC_OPT_FLAGS} \${CC_SEC_FLAGS}
\${CC_DEFINE_FLAGS}")
\# public link flags
set(PUBLIC_LNK_FLAGS "-rdynamic -Wl,-z,noexecstack -Wl,-z,relro -Wl,-z,now")
\# link flag for module
set(SHARED_LNK_FLAGS "-shared \${PUBLIC_LNK_FLAGS}")
set(PIE_EXE_LNK_FLAGS "-pie \${PUBLIC_LNK_FLAGS}")
```
## Java语言编译选项
### 语言级别
#### 选项集
##### G.JAVA.LANG.01 每个交付单元使用的Java编译语言级别必须一致,且必须与使用的Java版本对应的编译语言级别一致。
**【级别】** 要求
**【描述】** 不同模块的编译语言级别不一致,这些模块需要配置不同的编译选项,导致构建脚本不一致。
使用与Java版本对应的编译语言级别,可以在编译阶段提示对应Java版本不推荐的编码实践,如Java 8版本将Java 7版本部分可用的API标记为 @Deprecated,推荐用更好的API来替换。当使用编译语言级别8时就会在编译阶段发出警告,代码中使用了将被弃用的API。
### MAVEN
#### 选项集
##### G.JAVA.MAVEN.01 版本发布构建时禁止使用maven 编译选项-X,避免输出大量的debug日志。
**【级别】** 要求
**【描述】** -X是debug选项,会输出大量的debug日志。
### JAVAC
#### 选项集
##### G.JAVA.JAVAC.01 禁止使用的javac编译选项:-nowarn/-Xlint:none/-Xlint:name 选项关闭所有或部分javac编译告警;-g:none/-g:[keyword list]选项关闭全部或指定生成部分调试信息
**【级别】** 要求
**【描述】** 编译告警能够帮助提前发现代码存在的缺陷和风险,关闭编译告警会给代码质量带来隐患;使用-g:none或-g:[keywordlist]会导致生成过少或过多的调试信息,影响可维护性或降低运行效率。
**例外**:-Xlint:all,-processing
运行时处理的注解不需要注解处理器,产生编译告警可以通过-Xlint的参数-processing进行抑制。
##### G.JAVA.JAVAC.02 必须使用的javac编译选项:-source,-target,-Xlint:all。 同时maven-compiler-plugin的showWarnings属性必须设置为true。
**【级别】**
**【描述】**
\-source 指定编译器接受的java源文件版本
\-target 指定编译器生成的class文件版本
\-Xlint:all 使能所有推荐编译告警
showWarnings 属性必须设置为true,不设置或者设置为false时部分编译告警无法检查出来
**【正确示例】**
```
\<plugin\>
\<groupId\>org.apache.maven.plugins\</groupId\>
\<artifactId\>maven-compiler-plugin\</artifactId\>
\<configuration\>
\<source\>1.8\</source\>
\<target\>1.8\</target\>
\<showWarnings\>true\</showWarnings\>
\<compilerArgs\>
\<arg\>-Xlint:all\</arg\>
\</compilerArgs\>
\</configuration\>
\</plugin\>
```
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册