Go的简单入门:开始使用模糊测试
开始使用模糊测试
文章目录
- 开始使用模糊测试
- 一、介绍
- 二、准备
- 三、实践
- 3.1 为你的代码创建一个目录
- 3.2 添加代码用于测试
- 写代码
- 运行代码
- 3.3 添加单元测试
- 写代码
- 运行代码
- 3.4 添加模糊测试
- 写代码
- 运行代码
- 3.5 修复无效的字符串错误
- 诊断错误
- 写代码
- 运行代码
- 修复错误
- 写代码
- 运行代码
- 3.6 修复两次反转的错误
- 诊断错误
- 写代码
- 运行代码
- 修复错误
- 写代码
- 运行代码
一、介绍
这篇指导介绍Go中基础的模糊测试。通过模糊测试,随机数据会针对您的测试运行,以尝试找出漏洞或导致崩溃的输入。例如,通过模糊测试能够发现SQL注入的漏洞,缓冲区溢出,拒绝服务或者跨域攻击。
在这篇指导中,你将为一个简单的函数写一个模糊测试,运行Go命令行,调试并修复代码中的问题。
你将通过下面的部分:
- 为你的代码创建一个目录;
- 添加代码用于测试;
- 添加单元测试;
- 添加模糊测试;
- 修复两个bug;
- 探索更多的资源;
二、准备
- 安装Go1.18 或以上版本;
- 一个用于编写代码的工具;
- 命令行工具;
- 一个支持模糊测试的环境。目前仅在 AMD64 和 ARM64 架构上使用覆盖检测进行模糊测试。
三、实践
3.1 为你的代码创建一个目录
打开命令行,进入到工作目录
从为你将要写的代码创建目录开始。
$ cd $
通过命令行,创建一个名称为
fuzz
的目录$ mkdir fuzz $ cd fuzz/ $
为你的代码创建一个模块
运行
go mod init
命令,并给它一个新的代码模块路径$ go mod init example/fuzz go: creating new go.mod: module example/fuzz $
接下来,你将添加一些简单代码去反转字符串,它将用于模糊。
3.2 添加代码用于测试
在这一步,将添加函数用于反转字符串。
写代码
使用文本编辑器,在fuzz目录中,创建一个叫
main.go
的文件。进入
main.go
文件,在文件的顶部,粘贴下面的代码声明。package main
一个独立的程序(和标准库相反),总是在main包中。
在包声明的下面,粘贴下面的函数声明
func Reverse(s string) string {b := []byte(s)for i,j := 0, len(b)-1; i < len(b)/2; i, j = i+1, j-1{b[i], b[j] = b[j], b[i]}return string(b) }
函数接受一个字符串,同时循环byte,最后返回反转的字符串。
注意:这个代码基于
golang.org/x/example
的stringUtils.Reverse
函数。在
main.go
文件的顶部,在包声明的下面,粘贴下面的main函数去初始化一个字符串,反转它,打印输出,并重复这样做
func main() {input := "The quick brown fox jumped over the lazy dog"rev := Reverse(input)doubleRev := Reverse(rev)fmt.Printf("original: %q\n", input)fmt.Printf("reversed: %q\n", rev)fmt.Printf("reversed agained: %q\n", doubleRev)
}
这个函数将运行Reverse
操作,然后打印命令行的输出。这个是有用的,在实践中查看代码,也是有用的对于发现潜在的debug。
main
函数使用了fmt
包,因此,你将需要导入它。第一行代码看起来像这样:
package main import "fmt"
运行代码
在包含main.go
文件目录的命令行下,运行代码:
$ go run .
original: "The quick brown fox jumped over the lazy dog"
reversed: "god yzal eht revo depmuj xof nworb kciuq ehT"
reversed agained: "The quick brown fox jumped over the lazy dog"
$
你能看到原始数据,反转的结果数据,然后对结果再次旋转。它和原始数据相等。
现在代码正在运行,是时间去测试它了。
3.3 添加单元测试
在这一步,你将为Reverse函数写基础的单元测试。
写代码
使用你的文本编辑器,在fuzz目录中,创建一个文件叫做
reverse_test.go
粘贴下面的代码到
reverse_test.go
package mainimport ("testing" )func TestReverse(t *testing.T) {testcases := []struct {in, want string}{{"Hello, world", "dlrow ,olleH"},{" ", " "},{"!12345", "54321!"},}for _, tc := range testcases {rev := Reverse(tc.in)if rev != tc.want {t.Errorf("Reverse: %q, want %q", rev, tc.want)}} }
这个简单的函数将断言,那一列输入的字符串将被正确的反转。
运行代码
使用go test
运行单元测试。
$ go test
PASS
ok example/fuzz 0.325s
$ go test -v
=== RUN TestReverse
--- PASS: TestReverse (0.00s)
PASS
ok example/fuzz 0.253s
$
接下来,你将改变单元测试变成模糊测试。
3.4 添加模糊测试
单元测试拥有局限性,即每次输入必须被开发者添加到测试。一个模糊测试的好处是它能为你的代码创建输入,并且能确认边缘化的用例,那些你没有想象出来的用例。
在这一部分你将转换单元测试变成模糊测试,以便于你能通过少量的工作产生更多输入。
注意,你将保留单元测试、基准测试和模糊测试在相同的*_test.go
文件下面。但是对于这个案例,你将转换单元测试成为模糊测试。
写代码
在你的文本编译器中,使用下面的模糊测试替换单元测试。
func FuzzReverse(f *testing.F) {testcase := []string{"Hello World", " ", "!12345"}for _, tc := range testcase {f.Add(tc)}f.Fuzz(func(t *testing.T, orig string) {rev := Reverse(orig)doubleRev := Reverse(rev)if orig != doubleRev {t.Errorf("Before: %q, after: %q", orig, doubleRev)}if utf8.ValidString(orig) && !utf8.ValidString(rev) {t.Errorf("Reverse produced invalid UTF-8 string %q", rev)}})
}
Fuzzing也有一些局限性。在你的单元测试中,你能预测Reverse
函数期待的输出,并且验证实际的输出满足预期。
例如,在测试用例Reverse("Hello, world")
单元测试明确返回dlrow ,olleH
。
使用模糊查询,你不能预测期待的输出,因为你不能控制输入。
可是,这里有一些Reverse
函数的属性,你能在fuzz测试中进行验证。其中两个能在模糊测试中验证的是:
- 反转字符串两次,保留原始值;
- 反转的字符串将其状态保留在UTF-8;
注意在单元测试和模糊测试中的语法不同。
- 函数以
FuzzXxx
开头代替TestXxx
,并且获取*testing.F
代替*testing.T
; - 在你期待看到
t.Run
执行的地方,你替换成f.Fuzz
,在那儿你获取一个模糊测试的函数,它的参数是*testing.T
和一个用于模糊执行的类型。单元测试的输入使用 f.Add 作为种子语料库输入提供。
确保新的包unicode/utf8
是被导入。
package mainimport ("testing""unicode/utf8"
)
使用模糊测试覆盖单元测试,同时,再次运行测试。
运行代码
运行模糊测试,不带模糊属性,确保种子输入能够通过
$ go test PASS ok example/fuzz 1.089s $
带上模糊,运行
FuzzReverse
,去看是否任意的随机产生的字符串输入将产生错误。这次执行使用go test
带上一个新的标识,-fuzz
。$ go test -fuzz=Fuzz fuzz: elapsed: 0s, gathering baseline coverage: 0/3 completed fuzz: elapsed: 0s, gathering baseline coverage: 3/3 completed, now fuzzing with 8 workers fuzz: elapsed: 0s, execs: 780 (19533/sec), new interesting: 5 (total: 8) --- FAIL: FuzzReverse (0.04s)--- FAIL: FuzzReverse (0.00s)reverse_test.go:20: Reverse produced invalid UTF-8 string "0\x86\xcb"Failing input written to testdata/fuzz/FuzzReverse/51fd8412ffb50aa7caa0c705c8d9b9f44bd34af7973d0f5940d60a7b24e6ff34To re-run:go test -run=FuzzReverse/51fd8412ffb50aa7caa0c705c8d9b9f44bd34af7973d0f5940d60a7b24e6ff34 FAIL exit status 1 FAIL example/fuzz 1.106s $
使用模糊测试的时候一个错误产生了,并且引起问题的输入是被写到了一个种子语料库文件。它将在下次
go test
被调用的时候运行,即便不带-fuzz
标识。为了展示引起错误的输入,在编译器中打开被写到testdata/fuzz/FuzzReverse
目录下的语料库文件。你的语料库文件可能包含不同的字符串,但是格式是相同的。$ cat testdata/fuzz/FuzzReverse/51fd8412ffb50aa7caa0c705c8d9b9f44bd34af7973d0f5940d60a7b24e6ff34 go test fuzz v1 string("ˆ0") $
语料库文件第一行包含了编码版本。以下每一行代表构成语料库条目的每种类型的值。因为模糊测试的目标仅获取一个值,在版本后面仅仅有一个值。
再次不带
-fuzz
运行go test
,将使用新的失败种子语料库条目。$ go test --- FAIL: FuzzReverse (0.00s)--- FAIL: FuzzReverse/51fd8412ffb50aa7caa0c705c8d9b9f44bd34af7973d0f5940d60a7b24e6ff34 (0.00s)reverse_test.go:20: Reverse produced invalid UTF-8 string "0\x86\xcb" FAIL exit status 1 FAIL example/fuzz 1.076s $
因为我们的测试失败了,是时候进行调试了。
3.5 修复无效的字符串错误
在这一部分,你将调试错误,并修复错误。
在继续之前,请随意花一些时间思考这个问题并尝试自己解决问题。
诊断错误
这儿有一些不同的方法用来调试错误。如果你使用VS Code作为你的编译器,你能设置断点进行调试。
在这个教程中,你将打印有用的调试信息到你的控制台。
首先,思考对于utf8.ValidString
的文档。
ValidString 报告 s 是否完全由有效的 UTF-8 编码符文组成。
当前的Reverse
函数,一个字节一个字节的反转字符串,这就是我们的问题。为了保留原始字符串的 UTF-8 编码符文,我们必须逐个符文反转字符串。
为了检查当反转的时候为什么那个输入(在这种情况下,字符串ˆ0
)造成了Reverse
产生了一个无效的字符串,你能检查反转字符串符文的数量。
写代码
在文本编译器中,使用下面的代码替换FuzzReverse
下的模糊目标。
f.Fuzz(func(t *testing.T, orig string){rev := Reverse(orig)doubleRev := Reverse(rev)t.Logf("Number of runes: orig=%d, rev=%d, doubleRev=%d", utf8.RuneCountInString(orig), utf8.RuneCountInString(rev), utf8.RuneCountInString(doubleRev))if orig != doubleRev {t.Errorf("Before: %q, after: %q", orig, doubleRev)}if utf8.ValidString(orig) && !utf8.ValidString(rev) {t.Errorf("Reverse produced invalid UTF-8 string %q", rev)}
})
这个t.Logf
行将打印到命令行,如果一个错误发生。获取如果执行测试带上-v
,它能帮助你调试特定的议题。
运行代码
使用go test
运行测试
$ go test
--- FAIL: FuzzReverse (0.00s)--- FAIL: FuzzReverse/51fd8412ffb50aa7caa0c705c8d9b9f44bd34af7973d0f5940d60a7b24e6ff34 (0.00s)reverse_test.go:16: Number of runes: orig=2, rev=3, doubleRev=2reverse_test.go:21: Reverse produced invalid UTF-8 string "0\x86\xcb"
FAIL
exit status 1
FAIL example/fuzz 1.098s
$
整个种子语料库使用字符串,其中每个字符都是一个字节。可是,字符像^
需要若干个字节。因此,一个字节一个字节的反转字符串是无效的对于多字节字符。
注意,如果你好奇关于Go处理字符串,阅读博客Strings, bytes, runes and characters in Go进行更深度的理解。
为了更好的理解错误,更正反向功能的错误。
修复错误
为了修复Reverse
函数,让我们按照符文遍历字符,代替通过字节。
写代码
在文本编译器中,使用下面的代码替换存在的Reverse()
函数。
func Reverse(s string) string {r := []rune(s)for i, j := 0, len(r)-1; i<len(r)/2; i, j = i+1, j-1 {r[i], r[j] = r[j], r[i]}return string(r)
}
关键的不同是,Reverse
现在是遍历字符串中的每个符文,而不是每一个字符。
运行代码
使用
go test
运行测试$ go test PASS ok example/fuzz 1.207s $
再次使用
go test -fuzz=Fuzz
进行模糊测试,去看现在是否有新的问题。$ go test -fuzz=Fuzz fuzz: elapsed: 0s, gathering baseline coverage: 0/9 completed fuzz: minimizing 31-byte failing input file fuzz: elapsed: 0s, gathering baseline coverage: 4/9 completed --- FAIL: FuzzReverse (0.03s)--- FAIL: FuzzReverse (0.00s)reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1reverse_test.go:18: Before: "\xa0", after: "�"Failing input written to testdata/fuzz/FuzzReverse/1d81da97512e2e81bf08cdb8b161334d7d9639f5c5b18255c920ad25370ff2aaTo re-run:go test -run=FuzzReverse/1d81da97512e2e81bf08cdb8b161334d7d9639f5c5b18255c920ad25370ff2aa FAIL exit status 1 FAIL example/fuzz 1.081s $
我们能够看到,经过两次反转的结果是不同于原始结果。这次输入是一个无效的字符串。如果我们使用字符串进行模糊测试,这怎么可能?
让我们再次调试。
3.6 修复两次反转的错误
在这一部分,我们将调试两次反转的错误,并修复这个错误。
在继续之前,自由的好一些时间去思考这个问题,并修复这个问题。
诊断错误
就像之前,有若干个方法调试错误,在这种场景下,使用断点是一个好的方法。
在这个教程中,我们将在Reverse函数中打印有用的调试信息。
仔细查看反转的字符串以发现错误。在Go中,一个字符串实际上是一个字符切片。并且能够包含非有效的UTF-8字节。原始字符串是一个字节切片,有一个字节,‘\x91’。 当输入字符串设置为 []rune 时,Go 将字节切片编码为 UTF-8,并将字节替换为 UTF-8 字符 �。 当我们将替换的 UTF-8 字符与输入字节切片进行比较时,它们显然不相等。
写代码
在文本编辑器中,使用下面的代码替换
Reverse
函数func Reverse(s string) string {fmt.Printf("input: %q\n", s)r := []rune(s)fmt.Printf("runes: %q\n", r)for i, j := 0, len(r)-1; i<len(r)-1; i, j = i+1, j-1 {r[i], r[j] = r[j], r[i]}return string(r) }
这将帮助我们理解发生了什么错误,当字符串转化成符文切片。
运行代码
这个时候,我们仅仅想运行失败的测试为了检查日志。为了做这个,我们将使用go test -run
。
$ go test -run FuzzReverse/1d81da97512e2e81bf08cdb8b161334d7d9639f5c5b18255c920ad25370ff2aa
input: "\xa0"
runes: ['�']
input: "�"
runes: ['�']
--- FAIL: FuzzReverse (0.00s)--- FAIL: FuzzReverse/1d81da97512e2e81bf08cdb8b161334d7d9639f5c5b18255c920ad25370ff2aa (0.00s)reverse_test.go:16: Number of runes: orig=1, rev=1, doubleRev=1reverse_test.go:18: Before: "\xa0", after: "�"
FAIL
exit status 1
FAIL example/fuzz 0.435s
$
为了运行包含在FuzzXxx/testdata
中特定语料库,你能提供{FuzzTestName}/filename
到-run
。这是有用的在调试的时候。
知道了输入是无效的unicode
,让我们修复在Reverse
函数的错误。
修复错误
为了修复这个问题,如果Reverse
输入不是一个有效的的UTF-8,让我们返回一个错误。
写代码
在你的编译器中,使用下面的函数替换存在的
Reverse
函数func Reverse(s string) (string, error) {if !utf8.ValidString(s) {return s, errors.New("输入是一个无效的UTF-8")}fmt.Printf("input: %q\n", s)r := []rune(s)fmt.Printf("runes: %q\n", r)for i, j := 0, len(r)-1; i < len(r)-1; i, j = i+1, j-1 {r[i], r[j] = r[j], r[i]}return string(r), nil }
这次改变,当输入的字符串是一个无效的UTF-8时,将返回一个错误。
因为
Reverse
函数现在返回了一个错误,修复main函数去丢弃额外的错误值。使用下面的代码替换已经存在的main
函数。func main() {input := "The quick brown fox jumped over the lazy dog"rev, revErr := Reverse(input)doubleRev, doubleErr := Reverse(rev)fmt.Printf("original: %q\n", input)fmt.Printf("reversed: %q, err: %v\n", rev, revErr)fmt.Printf("reversed agained: %q, err: %v\n", doubleRev, doubleErr) }
这些调用将会返回一个
nil
错误,因为输入是有效的UTF-8。你需要导入
errors
和unicode/utf8
包。在main.go
中的导入语句看起来像这样:import ("errors""fmt""unicode/utf8" )
修改
reverse_test.go
文件,检查错误并跳过测试,如果错误是被返回值产生。func FuzzReverse(f *testing.F) {testcase := []string{"Hello World", " ", "!12345"}for _, tc := range testcase {f.Add(tc) // 使用 f.Add 提供种子语料库}f.Fuzz(func(t *testing.T, orig string) {rev, err1 := Reverse(orig)if err1 != nil {return}doubleRev, err2 := Reverse(rev)if err2 != nil {return}if orig != doubleRev {t.Errorf("Before: %q, after: %q", orig, doubleRev)}if utf8.ValidString(orig) && !utf8.ValidString(rev) {t.Errorf("Reverse produced invalid UTF-8 string %q", rev)}}) }
除了返回,您还可以调用 t.Skip() 来停止执行该模糊输入。
运行代码
使用
go test
运行测试$ go test PASS ok example/fuzz 0.491s $
使用带有
go test -fuzz=Fuzz
进行模糊化,然后若干秒后通过,使用ctrl-C
停止模糊测试$ go test -fuzz=Fuzz fuzz: elapsed: 0s, gathering baseline coverage: 0/9 completed fuzz: elapsed: 0s, gathering baseline coverage: 9/9 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 306930 (102280/sec), new interesting: 32 (total: 41) fuzz: elapsed: 6s, execs: 870767 (187915/sec), new interesting: 33 (total: 42) fuzz: elapsed: 9s, execs: 1293839 (141029/sec), new interesting: 34 (total: 43) ...... fuzz: elapsed: 1m18s, execs: 10520829 (142569/sec), new interesting: 36 (total: 45) fuzz: elapsed: 1m21s, execs: 10909615 (129590/sec), new interesting: 37 (total: 46) fuzz: elapsed: 1m24s, execs: 11289776 (126692/sec), new interesting: 37 (total: 46) fuzz: elapsed: 1m27s, execs: 11689172 (133198/sec), new interesting: 37 (total: 46) fuzz: elapsed: 1m30s, execs: 12068776 (126454/sec), new interesting: 37 (total: 46) fuzz: elapsed: 1m33s, execs: 12461830 (131088/sec), new interesting: 37 (total: 46) fuzz: elapsed: 1m36s, execs: 12843006 (127040/sec), new interesting: 37 (total: 46) ^Cfuzz: elapsed: 1m38s, execs: 13067321 (134596/sec), new interesting: 37 (total: 46) PASS ok example/fuzz 98.729s lifeideMacBook-Pro:fuzz lifei$
除非您通过 -fuzztime 标志,否则模糊测试将一直运行,直到遇到失败的输入。如果没有发生故障,默认是永远运行,并且可以使用 ctrl-C 中断该过程。
使用
go test -fuzz=Fuzz -fuzztime 30s
进行模糊化,如果没有发现失败,它将在退出之前模糊 30 秒。$ go test -fuzz=Fuzz -fuzztime 30s fuzz: elapsed: 0s, gathering baseline coverage: 0/46 completed fuzz: elapsed: 0s, gathering baseline coverage: 46/46 completed, now fuzzing with 8 workers fuzz: elapsed: 3s, execs: 432881 (144195/sec), new interesting: 3 (total: 49) fuzz: elapsed: 6s, execs: 881372 (149498/sec), new interesting: 3 (total: 49) fuzz: elapsed: 9s, execs: 1320812 (146554/sec), new interesting: 3 (total: 49) fuzz: elapsed: 12s, execs: 1735749 (138323/sec), new interesting: 3 (total: 49) fuzz: elapsed: 15s, execs: 2140404 (134860/sec), new interesting: 3 (total: 49) fuzz: elapsed: 18s, execs: 2553033 (137558/sec), new interesting: 3 (total: 49) fuzz: elapsed: 21s, execs: 2934209 (127072/sec), new interesting: 3 (total: 49) fuzz: elapsed: 24s, execs: 3289376 (118391/sec), new interesting: 3 (total: 49) fuzz: elapsed: 27s, execs: 3670350 (126988/sec), new interesting: 4 (total: 50) fuzz: elapsed: 30s, execs: 4071006 (133510/sec), new interesting: 4 (total: 50) fuzz: elapsed: 30s, execs: 4071006 (0/sec), new interesting: 4 (total: 50) PASS ok example/fuzz 30.994s $
模糊化测试通过。
除了添加-fuzz
标识,若干个新的标识能够添加到go test
上,在文档中能够看到:custom-settings 。
点击文档,go.dev/doc/fuzz 进一步阅读。
Go的简单入门:开始使用模糊测试相关推荐
- 模糊测试(fuzz testing)介绍(一)
模糊测试(fuzz testing)是一类安全性测试的方法.说起安全性测试,大部分人头脑中浮现出的可能是一个标准的"黑客"场景:某个不修边幅.脸色苍白的年轻人,坐在黑暗的房间中,正 ...
- 模糊测试Fuzzing详细总结
目录 1.简介 2.测试步骤 (1)确定输入向量 (2)生成模糊测试数据 (3)执行模糊测试 (4)监视异常 (5)根据被测系统的状态判断是否存在潜在的安全漏洞 3.举例和方法 (1)方法1 (2)方 ...
- 模糊测试技术简单整理(一)
模糊测试技术 2022-01-09- 文章目录 模糊测试技术 预处理 1. 插桩: 2. 符号执行 3. 污点分析 基于AFL的模糊测试方法 输入构造 评估 结果分析 具体应用场景下的模糊测试 物联网 ...
- 模糊测试入门案例,利用AFL和Honggfuzz模糊测试Tiff
1.AFL模糊测试tiff AFL的安装已经在前文记录过American Fuzzy Lop(AFL)的安装与简单使用,不再赘述.这里主要记录一下在使用AFL时的可以注意的点. 最好选择由C或者C++ ...
- 漏洞挖掘 | 简单高效的模糊测试Fuzzing
文章目录 定义 用途 技术 详细说明 流程图 1.大量的测试用例 2.对测试用例做过滤 3.要用正确的方法 4.花90%时间阅读文档 5.Fuzzing工具 摘要:Fuzzing是一种通过构造输入来发 ...
- 知识普及:关于Fuzzing模糊测试入门原理及实践的讨论
狩猎者网络安全旗下--知柯信息安全团队(知柯信安) 漏洞挖掘是否是真正的安全呢? "The best alternative to defense mechanisms is to find ...
- 渗透测试入门24之渗透测试参考书、课程、工具、认证
白帽子渗透测试入门资源:参考书.课程.工具.认证文章目录 前言 名词解析 Pwk课程与OSCP证书 CTF 工具 参考书 相关文献推荐 资源打包前言 初入渗透测试领域,过程中遇到不少错综复杂的知识,也 ...
- linux 符号执行,[原创]符号执行Symcc与模糊测试AFL结合实践
上个月末无聊的划水时间段内,在推上看到有人发了一篇关于如何结合去年新发布的符号执行Symcc与模糊测试引擎AFL,以提升Fuzz效率的视频贴.打开这个链接后才发现是个卖课的,emmm.... , 看价 ...
- Mybatis简单入门
1 mybatis简单介绍 MyBatis是一个ORM的数据库持久化框架. Mybatis是一个支撑框架,它以映射sql语句orm方式来数据库持久化操作. ORM:对象关系映射(Object Rela ...
最新文章
- jQuery Mobile基础
- 软件作坊模式工件应用论
- 测试的艺术:测试用例的设计
- GraphQL入门2
- Tomcat启动项目没问题,网页一片空白
- 【今日CV 计算机视觉论文速览 第136期】Wed, 26 Jun 2019
- Flutter底部导航栏BottomNavigationBar页面状态保持解决方案
- mysql的order by,group by和distinct优化
- 《中学生可以这样学Python》84节配套微课免费观看地址
- KVM的安装和配置命令详解
- system verilog中的参数传递——ref,input,output
- java 换行分割_java – 如何通过换行分割字符串?
- 用golang生成6位数的唯一id
- 《测绘管理与法律法规》真题易错本
- C语言深度剖析笔记2
- 计算三维空间中直线和三角形的交点
- http网页返回状态码含义
- Linux中DNS服务器地址查询命令nslookup使用教程
- 实现海康监控视频播放(录像回放)(抓拍,录像等功能)
- real——表单样式