山东大学计算机科学与技术学院软件工程实验日志
---
Author: "Inori_333"
Date: 2025-03-04
---
实验一 团队建立、阅读开源软件
1.队伍创建与分工
队伍最终确定由5人组成,小组成员之间进行了高效的沟通,并确定了各自的负责的部分内容。
2.代码复现与分析
写在前面:由于“小米便签”项目本身过于陈旧,以及伴随指导教程的落后性,对于今天的复现已经没有太大意义,因此从头开始摸索在更新版本的环境中复现方法。
2.1 环境配置与编译运行
2.1.1 Android Studio的下载与安装
访问Android Studio官网,下载最新稳定版本的 Android Studio 安装程序,下载完成后打开,开始安装。安装过程除路径可以自定义以外,其余全部跟随默认选项安装。在安装进行到倒数第二步时,默认会对 Android Studio 需要的SDK组件进行下载,但下载地址指向国际服务器,因此选择使用网络代理跳转IP,或修改DNS配置,或对本地环境变量进行修改,改源到国内镜像站(如阿里云镜像站、清华镜像站)。
安装成功后,打开 Android Studio。
2.2 代码复现与结构分析
访问“小米便签”开源地址,下载zip包或直接通过Git方式将项目源代码拉取至本地。拉取成功后,运行AS(Android Studio ,之后默认缩写为AS),选择“Open”,选择小米便签的根目录,点击打开。打开后,由于版本落后等问题,系统需要一定时间导入项目,之后会提示Gradle同步错误。
首先是无法验证gradle组件下载源的SSL证书,这是由于下载源链接会被重定向到 https://github.com/gradle/gradle-distributions/releases/download/v8.2.0/gradle-7.2-src.zip
,这个地址在 AS 环境下访问过于困难,报错如下:
Cannot connect to host api.soulter.top:443 ssl:True [SSLCertVerificationError: (1, [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: unable to get local issuer certificate (_ssl.c:1010) )]
尝试将./app/wrapper/gradle-wrapper.properties中的下载源更改为国内镜像站,即图中的distributionUrl。
修改后再次尝试对Gradle进行同步,下载成功,但同步仍然失败,这一次返回错误:
Caused by: org.gradle.internal.resolve.ModuleVersionResolveException: Could not resolve gradle:gradle:8.7.
经过查询发现仅更改distributionUrl无法切换下载源指向,此链接已经被写死在Gradle组件本身的源代码中:
private
fun createSourceRepository() = ivy {
val repoName = repositoryNameFor(gradleVersion)
name = "Gradle $repoName"
setUrl("https://services.gradle.org/$repoName")
metadataSources {
artifact()
}
patternLayout {
if (isSnapshot(gradleVersion)) {
ivy("/dummy") // avoids a lookup that interferes with version listing
}
artifact("[module]-[revision](-[classifier])(.[ext])")
}
}
要解决这个问题,可以直接为 Gradle 设置代理进行网络加速。但是这样会导致之前设置的 Maven 镜像链接也会经过代理。
选择另一种暴力的解决方案:绕过系统文件完整性检查。
从Gradle组件源代码中可以看到:
private
fun sourceRootsOf(gradleInstallation: File, sourceDistributionResolver: SourceDistributionProvider): Collection<File> =
gradleInstallationSources(gradleInstallation) ?: downloadedSources(sourceDistributionResolver)
private
fun gradleInstallationSources(gradleInstallation: File) =
File(gradleInstallation, "src").takeIf { it.exists() }?.let { subDirsOf(it) }
当 gradleInstallation
在 src
目录中存在的时候 AS 就不会继续下载 gradle-7.2-src.zip
。这是因为这个值就是 project.gradle.gradleHomeDir。
于是直接把gradle-wrapper.properties
里 distributionUrl
的 bin
改为 all
,再向gradle-wrapper.properties中添加 distributionSha256Sum 属性
,把 distributionSha256Sum
修改为gradle-7.2-bin.zip 对应的值。
修改完成后直接对项目进行 Build ,成功通过哈希校验绕开系统文件完整性检查,成功构筑项目,并打开小米便签。
3.问题思考与总结
3.1 案例分析一
下面这段求两数的平均值的代码在低年级可给满分,请思考它还有什么bug?
unsigned average(unsigned a, unsigned b){ return (a+b)/2; }
首先观察函数类型,类型为 unsigned ,也就是 unsigned int 类型的缩写,所以函数的返回值必须在 unsigned int 能够允许的有效值范围内。再观察传入参数,传入参数的类型为( unsigned , unsigned ),因此两个不同的传入参数也需要传入在 unsigned int 允许的有效值范围内的合法值;再看返回值,返回 (a+b) / 2,因为函数类型为 unsigned ,因此需要保证这个值的范围落在 unsigned 类型的区间内。
可以发现,这个函数存在出现数值溢出情况的bug,当a+b的值超出UINT_MAX常量时,程序无法处理溢出部分,因此在不同架构下可能会出现包括但不限于结果返回0(x86架构,average(0x80000000U, 0x80000000U)=0),结果回绕到较小的值等等。
3.1.1 你还自信自己编过的代码没bug、直接应用于社会没有风险吗?
我一直觉得自己写的代码有很多bug=w=,过去的竞赛、科研和开源社区经验也告诉我,bug这种东西是在所难免的。
3.1.2 你如何信任团队同伴(其他人)编写的代码?
1. 代码审查(Code Review)
-
强制审查流程:通过工具(如 GitHub PR、GitLab MR)要求所有代码必须经过至少一名其他成员的审查才能合并。
2. 编码规范与静态分析
-
统一规范:制定团队代码风格(如命名、注释、错误处理),并通过工具(Clang-Format、ESLint)自动化检查。
-
使用静态分析工具:使用工具(如 SonarQube、Coverity)自动检测潜在问题(内存泄漏、空指针解引用)。
3. 文档与注释
-
自解释代码:优先通过清晰的命名和结构减少注释,但对复杂逻辑需添加必要说明。
-
接口文档:对公共 API 或核心模块,通过文档(如 Swagger、Doxygen)明确输入输出和边界条件。
3.1.3 用什么途径能降低代码风险?
自动化测试
-
单元测试:对每个函数/类编写测试用例,覆盖正常逻辑和异常分支。
-
集成测试:验证模块间的交互,如微服务接口调用、数据库读写。
-
模糊测试(Fuzzing):通过随机输入发现边界问题(如 AFL、libFuzzer)。
2. 持续集成(CI)
-
自动化流水线:每次提交代码时自动运行测试、静态分析和构建。
-
门禁策略:若测试覆盖率不足或静态分析报错,禁止代码合并。
3. 防御性编程
-
输入校验:对函数参数、用户输入、外部数据严格校验(如检查
NULL
、范围限制)。 -
错误处理:明确错误码或异常传递路径,避免静默失败。
-
断言(Assert):在关键逻辑中添加断言,如
assert(b != 0)
。
3.1.4 如何做好软件产品的质量保证(QA,Quality Assurance)
1. 分层测试策略
-
测试金字塔:以大量单元测试为基础,辅以集成测试,当开发软件时,还需要少量端到端(E2E)测试。
-
回归测试:每次迭代后运行历史用例,防止功能回退。
2. 环境隔离与监控
-
多环境部署:通过代码版本管理软件创建多个不同分支,区分开发、测试、预发布和生产环境,避免代码直接部署到生产。
-
监控与日志:通过 Prometheus、ELK 等工具监控运行时异常(如内存泄漏、响应超时)。
3. 用户反馈闭环(开发软件时需要)
-
灰度发布:逐步向小部分用户开放新功能,收集反馈后再全量发布。
-
Bug 跟踪:用 Jira、Redmine 等工具管理问题,确保每个缺陷有根因分析和修复验证。
3.2 案例分析二
double64 转 int16 溢出,这是很古老也是很传统的问题。64 位浮点数的范围远大于 16 位有符号整数。当浮点数值超过 32767 或低于 -32768 时,转换为 16 位整数就会溢出。这种情况下,转换结果会是未定义的行为,或者在某些编程语言中可能产生截断,但显然这里没有正确处理溢出。这个问题和刚才的案例一有异曲同工之处,都是很简单的bug,不同的是,这一次这个bug没有再以单独出现的形式让我们检查,而是被嵌入在了火箭生产与发射这样一个复杂的工业环境中,可以想象的是,箭载计算机的程序代码量会是比较庞大的,因此这样一个短小的bug很容易被忽略,尤其是该程序在先前版本的火箭上已经经过了测试,更加容易被认为是已经稳定的代码而不再进行修改。
从逻辑上来说,这是控制变量法的盲区,因为火箭的迭代无法单次控制一个变量进行测试,因此可能工程师不会想到发射速度会意外激活死代码的问题,但归根结底这还是测试和防御性编程缺失导致的。
-
首先,极端场景覆盖不足
火箭发射过程中的高速状态需要复杂的模拟环境,而测试可能更关注“典型”场景:由于测试数据限制可能仅使用预估值或历史数据,未模拟超范围值(如速度超过32767)。由于环境模拟成本高精度模拟火箭加速阶段的极端条件可能成本高昂,导致测试覆盖不全。 -
其次,代码审查的盲区
可以想象,对于这种古老而典型的问题,静态分析工具正常来说不会遗漏此类问题。审查流程缺陷可能导致代码审查可能更关注功能逻辑而非边界条件,尤其是对“看似无害”的类型转换操作。