首发于

https://xz.aliyun.com/t/9275

概述

codeql 是一个静态源码扫描工具,支持 c, python, java 等语言,用户可以使用 ql 语言编写自定义规则识别软件中的漏洞,也可以使用ql自带的规则进行扫描。

环境搭建

codeql的工作方式是首先使用codeql来编译源码,从源码中搜集需要的信息,然后将搜集到的信息保存为代码数据库文件,用户通过编写codeql规则从数据库中搜索出匹配的代码,工作示意图如下:

本节涉及的环境为

Windows 平台: vscode + codeql 用于开发codeql规则并查询
Linux 平台: codeql 用于编译代码创建代码数据库

首先下载codeql的二进制安装包

https://github.com/github/codeql-cli-binaries/releases

二进制包的文件名和对应的类型

codeql-linux64.zip   Linux平台
codeql-osx64.zip macos平台
codeql-win64.zip Windows平台
codeql.zip 全平台

根据自己的平台下载对应的压缩包,然后解压到一个目录即可。

Windows 平台的就下载 codeql-win64.zip 并解压,然后再根据 vscode-codeql-starterreadme 设置 vscode 用于后续编写 codeql 规则和对数据库进行查询.

https://github.com/github/vscode-codeql-starter

下载好vscode-codeql-startervscodecodeql插件 后,使用 vscode 打开vscode-codeql-starter的工作目录( 通过File > Open Workspace),然后进入vscode的设置界面,搜索codeql然后设置 Executable Pathcodeql.exe 的路径

Linux环境主要是使用 codeql 来编译代码,创建代码数据库,所以只要下载 codeql-linux64.zip 解压到一个目录即可。

下面以一个简单的例子来介绍使用方式,代码路径

https://github.com/hac425xxx/sca-workshop/tree/master/hello

首先使用 codeql 编译代码并创建数据库

$ /home/hac425/sca/codeql/codeql database create --language=cpp -c "gcc hello.c -o hello" ./hello_codedb

Initializing database at /home/hac425/sca-workshop/hello_codedb.
Running command [gcc, hello.c, -o, hello] in /home/hac425/sca-workshop.
Finalizing database at /home/hac425/sca-workshop/hello_codedb.
Successfully created database at /home/hac425/sca-workshop/hello_codedb.

其中的命令行选项解释如下

--language=cpp  指定语言是cpp
-c 指定编译代码需要执行的命令命令,比如 make、 gcc等
./hello_codedb 数据库相关文件保存的路径

-c 这里为了简单直接使用了gcc的编译命令,codeql也支持make、cmake等编译系统来创建数据库,比如可以写个Makefile

hello:
gcc hello.c -o hello

然后 -c 指定为 make 编译命令也可以创建出数据库

$ /home/hac425/sca/codeql/codeql database create --language=cpp -c "make -f Makefile_hello" ./hello_codedb

Initializing database at /home/hac425/sca-workshop/hello_codedb.
Running command [make, -f, Makefile_hello] in /home/hac425/sca-workshop.
[2021-02-23 05:09:18] [build] gcc hello.c -o hello
Finalizing database at /home/hac425/sca-workshop/hello_codedb.
Successfully created database at /home/hac425/sca-workshop/hello_codedb.

数据库创建好之后可以直接使用 codeql 插件的 From a folder 选项打开数据库所在目录,即可加载数据库。

由于我是在Linux上创建数据库,然后在Windows平台加载数据库并进行查询,这样的话还需要将数据库打包.

$ /home/hac425/sca/codeql/codeql database bundle -o hello_codedb.zip hello_codedb

Creating bundle metadata for /home/hac425/sca-workshop/hello_codedb...
Creating zip file at /home/hac425/sca-workshop/hello_codedb.zip.

命令行选项解释

database bundle 表示这个命令是要打包数据库
-o 打包后的压缩文件
hello_codedb 数据库所在目录

数据库打包之后就可以拷贝到其他机器上进行分析了。

vscode 加载打包的数据库文件可以使用插件的 From an archive 选项

加载完之后我们就可以编写规则了,这里创建一个简单的codeql查询,用途是找到源码中的所有函数调用并显示调用的的目标函数名和函数调用的位置。

ql 代码如下

import cpp

from FunctionCall fc
select fc.getTarget().getQualifiedName(), fc

执行后就可以显示所有的函数调用信息

对于图中的fc列,可以点击进入对于的源码行进行查看。

QL语言简介和简单示例

codeql 自己实现了 ql 语言,用户通过ql语言从数据库中查询需要的代码片段。QL语言是一种逻辑语言,QL中的所有语句基本都是逻辑语句,虽然有些情况下ql的使用和普通的编程语言(比如python)类似,但是其中的一些理念是完全不一样的,这个下面会进行一些讲解。本节将基于一些简单的例子介绍ql常用语法的使用,完整的语法建议查看官方文档。

示例代码简介

代码路径

https://github.com/hac425xxx/sca-workshop/blob/master/ql-example/example.c

我们知道漏洞都是由于程序在处理外部不可信数据时产生的,因此这个示例代码的实现思路就是模拟一些获取外部数据的函数,然后预设一些漏洞和不存在漏洞的场景,最后我们使用codeql把其中的漏洞查询出来

其中模拟获取外部数据的函数如下

// fake read byte from taint data
char read_byte()
{
return 1;
} // fake read int from taint data
int read_int()
{
} // fake get user input function
char *get_user_input_str()
{
return (char *)malloc(12);
}

system命令执行

本节所使用的示例代码路径

https://github.com/hac425xxx/sca-workshop/tree/master/ql-example
https://github.com/hac425xxx/sca-workshop/tree/master/ql-example/system_query

代码漏洞

int call_system_example()
{ char *user = get_user_input_str(); char *xx = user; system(xx);
return 1;
}

漏洞在于函数首先使用 get_user_input_str 获取外部输入的字符串, 然后会将其传给 system ,可以导致命令执行。

本节通过查询system命令执行漏洞来学习一下ql规则的编写,首先通过一个简单的 ql 查询示例来看看ql查询的组成元素

import cpp

from FunctionCall fc
where fc.getTarget().getName().matches("system")
select fc.getEnclosingFunction(), fc

这个查询的作用是找到所有调用 system 函数的位置,然后显示调用点所在的函数和函数调用的位置,各个语句的作用如下:

  • import 语句可以导入需要的库,库里面会封装一些函数、类供我们使用
  • from 语句用于定义查询中需要使用的变量,比如这里就定义了一个 fc ,类型为 FunctionCall 表示一个函数调用
  • where 语句用于设置变量需要满足的条件,比如这里的条件就是函数调用的目标的名称为 system
  • select 语句则用于将结果显示,可以选择结果中需要输出的东西.

查询结果如下

查询结果中列的数目和列中的数据由 select 语句指定,每一行代表一个结果,这个结果的呈现和sql语句的类似。

浏览查询的结果可以发现有一个 system 调用的参数是一个固定字符串

int call_system_const_example()
{
system("cat /etc/xxx");
return 1;
}

这个不会导致命令注入,我们在查询的where语句中可以增加一个条件过滤掉这个调用。

import cpp

from FunctionCall fc
where fc.getTarget().getName().matches("system") and not fc.getArgument(0).isConstant()
select fc.getEnclosingFunction(), fc, fc.getArgument(0)

where 语句通过 and 增加条件,通过fc.getArgument(0).isConstant()可以判断fc的第一个参数是不是一个常量,这样就可以过滤掉 system 的参数为常量字符串的函数调用。

通过这两个例子可以大概理解一下codeql的语法规则,首先用户会在 from 里面定义需要的语法元素(比如FunctionCall),然后会在where语句里面定义若干个逻辑表达式,然后在执行查询时codeql会根据from语句搜集所有的语法元素(这里是所有的函数调用),然后使用where里面的逻辑表达式对这些元素进行校验,where的结果为真就会进入select语句进行结果的展示。

或者可以这样理解 from 语句中声明的变量类型只是代表某一类语法元素,取值空间很大,比如 FunctionCall 可以表示任意一个函数调用,然后 fc 经过 where 语句里面的各个逻辑表达式的约束,使得 fc 取值空间缩小,然后 select 语句就将所有的取值以表格的形式展现出来。

最开始学习codeql的时候在这一块困扰了一段时间,大概理解ql语言的工作机理后对规则的编写、调试都有很大的帮助。

继续回调示例,此时我们的结果还剩下两个,其中 call_system_safe_example 中会调用函数 clean_data 对用户的输入进行校验,仅仅是为了教学我们假设 clean_data 可以确保用户输入是干净的,否则就返回0,那么我们需要将 call_system_safe_example 过滤掉。

对于我们这个简单的例子,我们可以加一些表达式,过滤掉在函数中既调用了system 有调用的 clean_data 函数的结果。

import cpp

from FunctionCall fc, FunctionCall clean_fc
where
fc.getTarget().getName().matches("system") and
not fc.getArgument(0).isConstant() and
clean_fc.getTarget().getName().matches("clean_data") and
not clean_fc.getEnclosingFunction() = fc.getEnclosingFunction()
select fc.getEnclosingFunction(), fc, fc.getArgument(0)

当然这样去过滤会产生漏报和误报,比如clean_data检查的数据和实际传入system的数据不是一个。

	clean_data(data_1)
................
................
system(data_2)

还有就是这样做搜索无法判断system的入参是否为外部可控。

这时候就需要使用 codeql 的污点跟踪功能,示例代码如下

import cpp
import semmle.code.cpp.dataflow.TaintTracking from FunctionCall system_call, FunctionCall user_input, DataFlow::Node source, DataFlow::Node sink
where
system_call.getTarget().getName().matches("system") and
user_input.getTarget().getName().matches("get_user_input_str") and
sink.asExpr() = system_call.getArgument(0) and
source.asExpr() = user_input and
TaintTracking::localTaint(source, sink)
select user_input, user_input.getEnclosingFunction()

污点跟踪由 TaintTracking 模块提供,codeql 支持 localglobal 两种污点追踪模块,区别在于 local 的污点追踪只能追踪函数内的代码,函数外部的不追踪,global 则会在整个源码工程中对数据进行追踪。

回到上面的 codeql 代码,首先我们要明确我们的目标和已知的信息。

  • get_user_input_str 函数模拟程序从外部获取数据,其返回值里面的数据是外部数据,即污点源 (source)
  • systemsink 点,数据从 get_user_input_str 流向 system 函数的就很大概率是有漏洞

查询的解释如下:

  1. 首先定义了两个函数调用 system_calluser_input ,分别表示调用 systemget_user_input_str 的函数调用表达式
  2. 然后定义 sourcesink 作为污点跟踪的 sourcesink
  3. 然后利用 sink.asExpr() = system_call.getArgument(0) 设置 sink 点为 system 函数调用的第一个参数
  4. 然后利用 source.asExpr() 设置 sink 点为 system 函数调用的第一个参数
  5. 最后使用 TaintTracking::localTaint 查找从 sourcesink 的查询

这个查询的作用就是查询 system 第一个参数由 get_user_input_str 返回值控制的调用点,比如

但是由于这里采用的是 localTaint 所以下面这种情况会漏报,如果要查询下面这个情况有两种方式

  1. our_wrapper_system 函数加到 sink 里面
  2. 使用 global taint 进行跟踪
void our_wrapper_system(char* cmd)
{
system(cmd);
} int call_our_wrapper_system_example()
{ char* user = get_user_input_str(); char* xx = user; our_wrapper_system(xx);
return 1;
}

第一种方案的查询如下,其实就是把 our_wrapper_system 也考虑进 sink

import cpp
import semmle.code.cpp.dataflow.TaintTracking predicate setSystemSink(FunctionCall fc, Expr e) {
fc.getTarget().getName().matches("system") and
fc.getArgument(0) = e
} predicate setWrapperSystemSink(FunctionCall fc, Expr e) {
fc.getTarget().getName().matches("our_wrapper_system") and
fc.getArgument(0) = e
} from FunctionCall fc, FunctionCall user_input, DataFlow::Node source, DataFlow::Node sink
where
(
setWrapperSystemSink(fc, sink.asExpr()) or
setSystemSink(fc, sink.asExpr())
) and
user_input.getTarget().getName().matches("get_user_input_str") and
sink.asExpr() = fc.getArgument(0) and
source.asExpr() = user_input and
TaintTracking::localTaint(source, sink)
select user_input, user_input.getEnclosingFunction()

使用global taint 的代码如下

import cpp
import semmle.code.cpp.dataflow.TaintTracking class SystemCfg extends TaintTracking::Configuration {
SystemCfg() { this = "SystemCfg" } override predicate isSource(DataFlow::Node source) {
source.asExpr().(FunctionCall).getTarget().getName() = "get_user_input_str"
} override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall call |
sink.asExpr() = call.getArgument(0) and
call.getTarget().getName() = "system"
)
}
} from DataFlow::PathNode sink, DataFlow::PathNode source, SystemCfg cfg
where cfg.hasFlowPath(source, sink)
select source, sink

ps: exists 的作用类似于局部变量

要使用 global taint 需要定义一个类继承自 TaintTracking::Configuration ,然后重写 isSourceisSink

  1. isSource 用于定义 source 点,指定 get_user_input_str 的函数调用为 source
  2. isSink 定义 sink 点,指定 system 的一个参数为 sink
  3. 然后在 where 语句里面使用 cfg.hasFlowPath(source, sink) 查询到从 sourcesink 的代码

查看查询结果发现 call_system_safe_example 也会出现在结果中,前面提到 clean_data 可以确保数据无法进行命令注入,我们可以通过 isSanitizer 函数来剔除掉污点数据流入 clean_data 函数的结果,关键代码如下:

import cpp
import semmle.code.cpp.dataflow.TaintTracking
import semmle.code.cpp.valuenumbering.GlobalValueNumbering class SystemCfg extends TaintTracking::Configuration {
SystemCfg() { this = "SystemCfg" }
............ override predicate isSanitizer(DataFlow::Node nd) {
exists(FunctionCall fc |
fc.getTarget().getName() = "clean_data" and
globalValueNumber(fc.getArgument(0)) = globalValueNumber(nd.asExpr())
)
}
............
}

ps: 使用 globalValueNumber 才能结果正确,这个应该和编译原理 GVN 理论相关。

数组越界

本节使用涉及的代码

https://github.com/hac425xxx/sca-workshop/tree/master/ql-example/array_oob_query

代码漏洞

int global_array[40] = {0};

void array_oob()
{
int user = read_byte();
global_array[user] = 1;
}

首先函数通过 read_byte 获取外部输入的一个字节,然后将其作为数组索引去访问 global_array , 但是 global_array 的大小只有 40 项,所以可能导致数组越界。

这个漏洞模型很清晰,我们使用污点跟踪来查询这个漏洞,首先 source 点就是 read_byte 的函数调用, sink 点就是 污点数据被用作数组索引。

查询代码如下

import cpp
import semmle.code.cpp.dataflow.TaintTracking class ArrayOOBCfg extends TaintTracking::Configuration {
ArrayOOBCfg() { this = "ArrayOOBCfg" } override predicate isSource(DataFlow::Node source) {
source.asExpr().(FunctionCall).getTarget().getName() = "read_byte"
} override predicate isSink(DataFlow::Node sink) {
exists(ArrayExpr ae | sink.asExpr() = ae.getArrayOffset())
}
} from DataFlow::PathNode sink, DataFlow::PathNode source, ArrayOOBCfg cfg
where cfg.hasFlowPath(source, sink)
select source.getNode().asExpr().(FunctionCall).getEnclosingFunction(), source, sink

首先看定义 source 点的代码

source.asExpr().(FunctionCall).getTarget().getName() = "read_byte"

这里就是让 sourceread_byteFunctionCall 语句,其中 .(FunctionCall) 类似于类型强制转换。

下面介绍sink点的查询, 在 ql 中很多语法结构都有对应的类来表示,比如这里涉及的数组访问就可以通过 ArrayExpr 对象获取

import cpp

from ArrayExpr ae
select ae, ae.getArrayOffset(), ae.getArrayBase()

可以看到 getArrayOffset 获取到的是数组偏移的部分,getArrayBase 获取到的是数组的基地址,所以这个查询的作用就是查询数据从 read_byte 流入数组索引的代码。

查询结果如下

可以看到查询到了所有符合条件的代码,其中有一个误报

void no_array_oob()
{
int user = read_byte(); if (user >= sizeof(global_array))
return; global_array[user] = 1;
}

可以看到这里检查了 user 的值,我们可以通过 isSanitizer 来过滤掉这个结果,这里就简单的认为用户输入进入 if 语句的条件判断中就认为用户输入被正确的校验了。

  override predicate isSanitizer(DataFlow::Node nd) {
exists(IfStmt ifs |
globalValueNumber(ifs.getControllingExpr().getAChild*()) = globalValueNumber(nd.asExpr())
)
}

codeql 使用 IfStmt 来表示一个 if 语句,然后使用 getControllingExpr 可以获取到 if 语句的控制语句部分,然后我们使用 getAChild* 递归的遍历控制语句的所有子节点,只要有 nd 为控制语句中的一部分就返回true

引用计数相关

本节相关代码

https://github.com/hac425xxx/sca-workshop/tree/master/ql-example/ref_query

漏洞代码一

int ref_leak(int *ref, int a, int b)
{ ref_get(ref); if (a == 2)
{
puts("error 2");
return -1;
}
ref_put(ref);
return 0;
}

漏洞是当 a=2 时会直接返回没有调用 ref_put 对引用计数减一,漏洞模型:在某些存在 return 的条件分支中没有调用 ref_put 释放引用计数。

查询的代码如下

import cpp
import semmle.code.cpp.dataflow.TaintTracking class RefGetFunctionCall extends FunctionCall {
RefGetFunctionCall() { this.getTarget().getName() = "ref_get" }
} class RefPutFunctionCall extends FunctionCall {
RefPutFunctionCall() { this.getTarget().getName() = "ref_put" }
} class EvilIfStmt extends IfStmt {
EvilIfStmt() {
exists(ReturnStmt rs |
this.getAChild*() = rs and
not exists(RefPutFunctionCall rpfc | rpfc.getEnclosingBlock() = rs.getEnclosingBlock())
)
}
} from RefGetFunctionCall rgfc, EvilIfStmt eifs
where eifs.getEnclosingFunction() = rgfc.getEnclosingFunction()
select eifs.getEnclosingFunction(), eifs

代码使用类来定义某个特定的函数调用,比如 RefPutFunctionCall 用于表示调用 ref_put 函数的函数调用语句。

然后使用 EvilIfStmt 来表示存在 return 语句但是没有调用 ref_put 的代码

class EvilIfStmt extends IfStmt {
EvilIfStmt() {
exists(ReturnStmt rs |
this.getAChild*() = rs and
not exists(RefPutFunctionCall rpfc | rpfc.getEnclosingBlock() = rs.getEnclosingBlock())
)
}
}

大概的逻辑如下

  1. 首先使用 this.getAChild*() = rs 约束 this 为一个包含 return 语句的 if 结构
  2. 然后在加上一个 exists 语句确保 和 rs 同一个块的语句里面没有 reutrn 语句。

漏洞代码二

int ref_dec_error(int *ref, int a, int b)
{
ref_get(ref); if (a == 2)
{
puts("ref_dec_error 2");
ref_put(ref);
}
ref_put(ref);
return 0;
}

漏洞是当 a=2 时调用 ref_put 对引用计数减一但是没有 return

漏洞模型:在某些条件分支中调用 ref_put 释放引用计数,但是没有 reuturn 返回,可能导致 ref_put 多次。

ql 查询代码的关键代码如下

class EvilIfStmt extends IfStmt {
EvilIfStmt() {
exists(RefPutFunctionCall rpfc |
this.getAChild*() = rpfc and
not exists(ReturnStmt rs | rpfc.getEnclosingBlock() = rs.getEnclosingBlock())
)
}
}

外部函数建模

本节涉及代码

https://github.com/hac425xxx/sca-workshop/tree/master/ql-example/model_function

静态污点分析的常见问题当数据流入外部函数(比如没有源码的库函数)中时污点分析引擎就可能会丢失污点传播信息,比如

int custom_memcpy(char *dst, char *src, int sz);

int call_our_wrapper_system_custom_memcpy_example()
{ char *user = get_user_input_str(); char *tmp = malloc(strlen(user) + 1); custom_memcpy(tmp, user, strlen(user)); our_wrapper_system(tmp);
return 1;
}

这个函数首先使用 get_user_input_str 获取外部输入,然后调用 custom_memcpy 把数据拷贝到 tmp 中,然后将 tmp 传入 system 执行命令, custom_memcpy 实际就是对 memcpy 进行了封装,只不过没有提供函数的源码。

直接使用之前的 ql 代码进行查询会发现查询不到这个代码,因为 custom_memcpy 是一个外部函数, codeql 的污点跟踪引擎无法知道污点的传播规则。

import cpp
import semmle.code.cpp.dataflow.TaintTracking class SystemCfg extends TaintTracking::Configuration {
SystemCfg() { this = "SystemCfg" } override predicate isSource(DataFlow::Node source) {
source.asExpr().(FunctionCall).getTarget().getName() = "get_user_input_str"
} override predicate isSink(DataFlow::Node sink) {
exists(FunctionCall call |
sink.asExpr() = call.getArgument(0) and
call.getTarget().getName() = "system"
)
}
} from DataFlow::PathNode sink, DataFlow::PathNode source, SystemCfg cfg
where cfg.hasFlowPath(source, sink)
select source.getNode().asExpr().(FunctionCall).getEnclosingFunction(), source, sink

为了解决这个问题,我们可以选择两种方式:重写isAdditionalTaintStep函数 或者 给ql源码增加模型,下面分别介绍。

重写 isAdditionalTaintStep 函数

使用 TaintTracking::Configuration 时可以通过重写 isAdditionalTaintStep 函数来自定义污点传播规则,代码如下

  override predicate isAdditionalTaintStep(DataFlow::Node pred, DataFlow::Node succ) {
exists(FunctionCall fc |
pred.asExpr() = fc.getArgument(1) and fc.getTarget().getName() = "custom_memcpy"
and succ.asDefiningArgument() = fc.getArgument(0)
)
}

isAdditionalTaintStep 的逻辑是如果函数返回值为 True 就表示污点数据从 pred 流入了 succ.

因此这里指定的就是污点数据从 custom_memcpy 的第1个参数流入了函数的第0个参数。

ql源码增加模型

ql的源码里面内置很多标准库函数的模型,比如strcpymemcpy 等,代码路径为

cpp\ql\src\semmle\code\cpp\models\implementations\Memcpy.qll

我们可以基于这些模型进行改造来快速对需要的函数建模,下面介绍一下步骤

首先在目录下新建一个 .qll 文件,这里就直接拷贝了 Memcpy.qll 然后修改了19行函数名部分,因为本身是对 memcpy 进行的封装。

然后在 Models.qll 里面导入一下即可

这时再去查询就可以了。

相关链接

https://help.semmle.com/QL/ql-handbook/index.html

https://help.semmle.com/QL/learn-ql/

https://securitylab.github.com/ctf/segv

https://github.com/github/codeql-cli-binaries/releases

https://codeql.github.com/docs/codeql-language-guides/analyzing-data-flow-in-cpp

C/C++源码扫描系列- codeql 篇的更多相关文章

  1. 源码学习系列之SpringBoot自动配置(篇一)

    源码学习系列之SpringBoot自动配置源码学习(篇一) ok,本博客尝试跟一下Springboot的自动配置源码,做一下笔记记录,自动配置是Springboot的一个很关键的特性,也容易被忽略的属 ...

  2. Laravel 源码解读系列第四篇-Auth 机制

    前言 Laravel有一个神器: php artisan make:auth 能够快速的帮我们完成一套注册和登录的认证机制,但是这套机制具体的是怎么跑起来的呢?我们不妨来一起看看他的源码.不过在这篇文 ...

  3. 源码学习系列之SpringBoot自动配置(篇二)

    源码学习系列之SpringBoot自动配置(篇二)之HttpEncodingAutoConfiguration 源码分析 继上一篇博客源码学习系列之SpringBoot自动配置(篇一)之后,本博客继续 ...

  4. 鸿蒙源码分析系列(总目录) | 百万汉字注解 百篇博客分析 | 深入挖透OpenHarmony源码 | v8.23

    百篇博客系列篇.本篇为: v08.xx 鸿蒙内核源码分析(总目录) | 百万汉字注解 百篇博客分析 | 51.c.h .o 百篇博客.往期回顾 在给OpenHarmony内核源码加注过程中,整理出以下 ...

  5. MyBatis 源码分析系列文章导读

    1.本文速览 本篇文章是我为接下来的 MyBatis 源码分析系列文章写的一个导读文章.本篇文章从 MyBatis 是什么(what),为什么要使用(why),以及如何使用(how)等三个角度进行了说 ...

  6. Spring源码解析系列汇总

    相信我,你会收藏这篇文章的 本篇文章是这段时间撸出来的Spring源码解析系列文章的汇总,总共包含以下专题.喜欢的同学可以收藏起来以备不时之需 SpringIOC源码解析(上) 本篇文章搭建了IOC源 ...

  7. 鸿蒙内核源码分析(字符设备篇) | 字节为单位读写的设备 | 百篇博客分析OpenHarmony源码 | v67.01

    百篇博客系列篇.本篇为: v67.xx 鸿蒙内核源码分析(字符设备篇) | 字节为单位读写的设备 | 51.c.h.o 文件系统相关篇为: v62.xx 鸿蒙内核源码分析(文件概念篇) | 为什么说一 ...

  8. 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 百篇博客分析OpenHarmony源码 | v25.01

    百篇博客系列篇.本篇为: v25.xx 鸿蒙内核源码分析(并发并行篇) | 听过无数遍的两个概念 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...

  9. 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 百篇博客分析OpenHarmony源码 | v7.07

    百篇博客系列篇.本篇为: v07.xx 鸿蒙内核源码分析(调度机制篇) | 任务是如何被调度执行的 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调 ...

  10. 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 百篇博客分析OpenHarmony源码 | v3.05

    百篇博客系列篇.本篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度谁的贡献最大 | 51.c.h .o 任务管理相关篇为: v03.xx 鸿蒙内核源码分析(时钟任务篇) | 触发调度 ...

随机推荐

  1. .NET 7+Angular 4 轻量级新零售进销存系统

    前言 给大家推荐一个专为新零售快消行业打造了一套高效的进销存管理系统. 系统不仅具备强大的库存管理功能,还集成了高性能的轻量级 POS 解决方案,确保页面加载速度极快,提供良好的用户体验. 项目介绍 ...

  2. Web刷题之polarctf靶场(2)

    1.蜜雪冰城吉警店 点开靶场, 发现题目说点到隐藏奶茶(也就是第九杯)就给flag, 但是明显就只有八杯, 猜测大概率考的是前端代码修改 把id=1修改为id=9, 然后回到页面点击原味奶茶即可弹出f ...

  3. Windows Terminal3.1

    其实就是为了把之前写的东西集成起来所以搞了一个终端. 下载 集成功能 Wordle ABCG RandTool cmd 便捷功能 FastFile (快速打开目标文件夹) show 文件说明 Term ...

  4. .NET 工具库高效生成 PDF 文档

    前言 QuestPDF 是一个开源 .NET 库,用于生成 PDF 文档.使用了C# Fluent API方式可简化开发.减少错误并提高工作效率.利用它可以轻松生成 PDF 报告.发票.导出文件等. ...

  5. excel江湖异闻录--◆K

    网名◆K,按照群里同学的说法,K神和老大kluas,以及一个名为KKK的VBA强人,都是K字头家族的高手. 因为函数实力极强,时常碾压难题,被群里同学们冠以了"K神"的称号. 用笔 ...

  6. 『玩转Streamlit』--环境配置

    尽管Streamlit的使用非常直观,但正确的环境配置对于充分发挥其潜力仍然至关重要. 本篇将介绍如何从头开始配置Streamlit环境,以及Streamlit开发过程中常用的几个命令. 最后通过一个 ...

  7. 使用 KubeKey 在 AWS 高可用部署 Kubernetes

    作者:李耀宗 介绍 对于生产环境,我们需要考虑 Kubernetes 集群的高可用性.本文教您部署如何在多台 AWS EC2 实例快速部署一套高可用的生产环境.要满足 Kubernetes 集群服务需 ...

  8. Java 线程池获取池中所有线程列表的方法

    在Java中,获取线程池中所有线程列表并不是一个直接支持的功能,因为线程池的设计通常是为了隐藏和管理底层的线程细节,从而提供更高层次的抽象和并发控制能力.然而,通过一些反射和技巧,我们仍然可以获取到线 ...

  9. 分享一个大模型在请求api接口上的巧用

    前言 自从Chatgpt横空出世以来,各种智能工具层出不穷,聊天.绘画.视频等各种工具帮助很多人高效的工作.作为一个开发者,目前常用应用包括代码自动填充,聊天助手等. 这些是工具层面的使用,有没有将大 ...

  10. PHP的json浮点精度难题

    前言 之前开发的接口需要用到json加签,有一次对接JAVA时,签名怎么都过不了,仔细对比了字符串,发现是PHP进行json_encode时,会将浮点型所有无意义的0给去掉(echo和var_dump ...