记录第一次项目开发

Git, CMake, Doxygen, Unit Test, RDP & more details.

系统: windows10
语言: C11
编译: GCC13.1.0
构建: CMake3.26
IDE: CLion2023.2.1
版本: Git2.41.0
文档: Doxygen1.9.8

OOP

由于C模拟OOP较为复杂, 此处仅简要记录, 实际开发未能使用.

object-oriented programming, 面向对象编程.

对象: 某一类事物的抽象概念.
类: 对象的表现; 具有属性(变量)和方法(函数).
实例: 根据类构建出的具体事物.

封装: 不需要被外界访问的属性和方法私有化(private); 接口与实现分离, 只暴露接口, 无需关心实现; 良好封装可以解耦合.
继承: 从现有类构造出新的类, 属性和方法(protected)被继承; 破坏了封装, 父类实现对子类暴露; 强耦合, 父类改变时子类随之而变.
多态: 具体类型和方法在编程时不确定, 运行时(编译时)可选择多个状态并最终确定; 编译时多态即函数重载/符号重载.

Git

记录快照和hash值.

modified –(add)–> staged –(commit)–> committed

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# 配置
git config
		   --list # 查看配置
		   --global user.name ["用户名"]
		   --global user.email [邮箱]
		   <key> # 查看某项配置

# 基本
git init
git diff # 未暂存的改动
	diff --staged # 或"--cached", 已暂存的改动
git add [文件名] # 暂存, "."通配
git status # 暂存文件
git reset [文件名] # 撤销暂存
git rm --cached [文件名] # 从暂存区移除
git commit -m "提交信息"
           --amend # 补充提交, 新提交覆盖旧提交
git rm -f [文件名] # 不再追踪
git log --patch # 提交记录, 补丁形式
        --stat # 文件总结
		--grep [字符串] # 提交说明中包含指定内容
		-S [字符串] # 添加或删除内容中包含指定内容
		-[n] # 最近的n条提交
		--after [时间] # 指定时间后的提交
		--before [时间] # 指定时间前的提交
		--pretty=format:"%h - %cd, %s" --graph # 图形显示提交hash简写, 日期, 说明
git reset # 默认"--mixed", 回滚, 差异放在工作区
		  --soft # 回滚, 差异放在暂存区
		  --hard # 回滚, 不保存差异
# .gitignore 忽略文件, glob模式匹配, !表示取反

# 远程仓库
git remote add [简称] [链接]
		   -v # 远程简称和对应链接
		   rename [旧简称] [新简称]
		   show [简称]
		   remove [简称] # 移除
git clone [链接] [本地路径]
git fetch [简称] # 拉取(不合并)
git pull [简称] # 合并
git push [简称] [分支] # 提交

# 分支
git branch [分支] # 新建
git	branch -a # 查看全部
		   --merged # 查看已合并
		   --no-merged # 查看未合并
		   -d [分支] # 删除
git log --decorate # 各个分支所指对象
git checkout [分支] # 切换
			 -b [分支] # 新建并切换
git merge [合入分支] # 快速合并
          --no-ff [合入分支] # 非快速合并, 合并到当前分支的新节点
		  --squash [合入分支] # 压缩合并, 合并到当前分支的新节点, 但不保留对合入分支的引用
		  --rebase [合入分支] # 变基合并, 合入分支变基到当前节点
git cherry-pick -[n] commit # 拣选n个节点合并

提交信息格式(推荐): “<type>(<scope>): <subject>”
<type>: feat(feature) 新功能; fix/to 修复bug; docs 文档; style 修改格式; refactor 重构; perf 优化; test 增加测试; chore 构建过程或辅助工具变动; revert 回滚; merge 合并分支.
<scope>: 影响范围.

项目分支管理 (非快速合并)

git-branch

项目结构

函数接口 函数实现
头文件(.h) 源文件(.c)/静态链接库(.a/.lib)/动态链接库(.so/.ddl)

库文件: 目标文件(.o)的压缩.
静态链接库: 生成的可执行文件可独立运行; 重复调用的模块在链接时被多次复制, 造成代码冗余.
动态链接库(共享链接库): 链接时记录模块位置; 生成的可执行文件无法独立运行.

目录结构
xxx-build/: 构建和编译目录; 下有子目录debug/和release/;在clion中为自动生成; 一般无需追踪
.git/和.gitignore: 版本控制
dist/: 分发目录, 最终发布版本; 下可按版本号划分子目录
docs/: 文档目录
include/: 公共头文件目录; 下可按模块划分子目录
lib/: 外部依赖库目录
src/: 源文件目录; 与头文件目录同结构同名
samples/: 样例程序目录
tests/: 测试文件目录
tools/: 支撑工具目录
copyright: 版权声明文件
CMakeLists.txt或Makefile: 构建配置文件
README: 说明文件

GCC -> Makefile -> CMake

编译过程: .c –(预处理)–> .i –(编译egcs)–> .s –(汇编as)–> .o –(链接ld)–> 可执行文件.

1
2
ar rcs [libxxx.a] [.o] # 生成静态链接库, 名称必须以"lib"开头, win下为"libxxx.lib"
gcc -fPIC -shared [.c] -o [libxxx.so] # 生成动态链接库, 名称必须以"lib"开头, win下为"libxxx.dll"
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 基本
gcc -E [.c] -o [.i] # 仅预处理, 需重定向输出
	-S [.c/.i] -o [.s] # 预处理和编译, 生成.s
	-c [.c/.i/.s] -o [.o] # 预处理, 编译, 汇编, 生成.o
	[.c/.i/.s/.o] -o [out] # 预处理, 编译, 汇编, 生成可执行文件
	-I [path] [.o] # 指明头文件路径
	-staic [.o] [libxxx.a] # 链接静态库
    	   -L [path] -l[xxx] # 指明静态库路径和静态库
	[.c] [.so] -o [out] # 链接动态库
ldd [out] # 查看运行时所需的动态链接库及位置
# 动态链接库生成的可执行文件无法单独运行
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx # 仅当前Terminal生效
# 运行时链接动态库
# linux: <dlfcn.h> dlopen, dlsym, dlclose, dlerror
# windows: <windows.h> LoadLibraryA, GetProcAddress, FreeLibrary

# 调试和功能
-v # 输出详细编译过程
-Q # 输出编译相关统计信息
-Wall # 输出警告
-Werror # 警告视为错误
-g # "-ggdb"或"-g2", 生成可调式的执行文件, debug模式
-O # "-O1", 基本优化
-O2 # 进一步优化, 会增加文件大小, release模式
-O3 # 较激进优化, 会增加文件大小
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
# makefile (仅linux)
make
make clean

# 版本1
[out]: [.c]
	gcc -o [out] [.c] # tab缩进, 不能使用空格, 下同

# 版本2
C = gcc
TARGET = [out]
OBJ = [.o]

$(TARGET): $(OBJ)
	$(C) -o $(TARGET) $(OBJ)

[.o]: [.c]
	$(C) -c [.c]

# 版本3
C = gcc
TARGET = [out]
OBJ = [.o]

CFLAGS = -c -Wall

$(TARGET): $(OBJ)
	$(C) -o $@ $^

%.o: %.c
	$(C) $(CFLAGS) $< -o $@

.PHONY: clean # 避免文件名为clean造成二重含义
clean:
	rm -f *.o $(TARGET)

# 版本4
C = gcc
TARGET = [out]
SRC = $(wildcar *.c)
OBJ = $(patsubst %.cpp, %.o, $(SRC))

CFLAGS = -c -Wall

$(TARGET): $(OBJ)
	$(C) -o $@ $^

%.o: %.c
	$(C) $(CFLAGS) $< -o $@

.PHONY: clean
clean:
	rm -f *.o $(TARGET)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
cmake -S . -B cmake-build -D[CACHE]=[value]
# 现代CMake: 基于target和module
cmake_minimum_required()

project()

add_subdirectory()

add_library() # STATIC/SHARED
add_executable()
target_include_directions() # PRIVATE/PUBLIC/INTERFACE
target_include_directories()
target_compile_features()
target_compile_options()

include(CTest)
enable_testing()

# 变量 set(); option(); list()
# 条件 if()/elseif()/else()/endif()
# 循环 foreach()/endforeach(); while()/endwhile(); continue(); break()
# 宏 marco()/endmacro()
# 函数 function()/endfunction()
# 输出 message()
# 文件操作 file()
# 支持glob模式匹配, bash调用方式 ${file_name}; GLOB_RECURSE递归匹配

# pch: 预编译头文件
target_precompile_headers() # REUSE_FROM

# 自定义命令
add_custom_command() # COMMAND

自动生成文档 (Doxygen)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
/** 文件注释
* @file
* @brief
* @details
* @author
* @version
* @date
* @copyright
*/

/// 变量或常量前注释
///< 变量或常量后注释

/** 函数注释
* @brief
* @details
* @param[in]
* @param[out]
* @return
* 	@retval
* @attention
* @warning
* @exception
*/

/** 可选项
* @note
* @remarks
* @example
* @see
* @var
* @enum
* @code @endcode
* @bug
* @todo
* @pre
* @post
* @deprecated
*/

// 特殊标识
// TODO 代办
// FIXME 需修正
// XXX 实现了功能, 但有待改进
// HACK 需根据需求调整

单元测试

对每个单元(函数/方法/类/子模块)的输出和异常进行测试.

以下为goolgetest方法, 由于兼容问题实际并未使用.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// MACRO
TEST (globalConfigurationTest, configurationDataTest)
// Assert: [(non)fatal]_[type]
// (non)Fatal: ASSERT_, fatal; EXPECT_, nonfatal
// Basic: _TRUE(condition); _FALSE(condition)
// Binary: _EQ(var1, var2), equal; _NE(var1, var2), not equal
//         _LT(var1, var2), less than; _LE(var1, var2), less equal
//		   _GT(var1, var2), big than; _GE(var1, var2), big equal
// String: _STREQ(str1, str2), same content; _STRNE(str1, str2), different content
// 		   _STRCASEEQ(str1, str2), same ignoring case; _STRCASENE(str1, str2), different ignoring content
// Float: _FLOAT_EQ(var1, var2), almost equal 4ULP; _DOUBLE_EQ(var1, var2), almost equal 4ULP
//		  _NEAR()_EQ(var1, var2, abs_error), almost equal
// Exception: _THROW(statement, exception_type); _ANY_THROW(statement); _NO_THROW(statement)
// SUCESS(); FAIL(), fatal; ADD_FAIL, nonfatal

远程桌面 (RDP over SSH)

需求为win10客户端远程连接win10服务端.

1
2
3
4
5
Get-WindowsCapability -Online | Where-Object Name -like 'OpenSSH*' # 查询
Add-WindowsCapability -Online -Name OpenSSH.Client~~~~0.0.1.0 # 安装客户端
Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 # 安装服务端(客户端无需安装)
Set-Service -Name sshd -StartupType 'Automatic' # 开机启动
Start-Service sshd # 启动服务

修改"C:\ProgramData\ssh\sshd_config"文件, 若无权限保存则复制替换.
修改 Port 选项为55555.
修改 PermitRootLogin 选项为 no.
修改 PasswordAuthentication 选项为 no.
不注释 “PubkeyAuthentication yes”.
不注释 “AuthorizedKeysFile .ssh/authorized_keys”.
注释 “Match Group administrators”.
注释 “AuthorizedKeysFile PROGRAMDATA/ssh/administrators_authorized_keys”.

1
2
3
ssh-keygen # 生成rsa密钥对
# file 可选项, 默认为"C:\Users\username/.ssh/id_rsa."
# passphrase 可选项, 载入私钥时需要的口令, 主要为保障私钥安全
1
2
3
4
# 私钥拷贝至客户端
# 客户端建立ssh隧道
ssh <username>@<Internet ip> -p 33333 -L 12345:127.0.0.1:55555 -N -o ServerAliveInterval=60 -o ServerAliveCountMax=3 -f
# 公网IP可以通过内网穿透实现

项目细节

命令行参数解释

1
2
3
4
5
6
7
# 一般命令行参数解释
main -s --long -n 100 /src
#  短参数 长参数 带值参数 捕获组
#            "-n 100", "--std=c11", "-lhello"
# option flag: 空格        等号         粘连
# 类型: bool, 数值, 字符串, 捕获组
# 扩展功能(用户层): 错误提示, 自动help, 子命令

内存问题

1
2
3
4
5
6
7
8
9
// 函数外部已经分配好的堆上/栈上字符串
int OpStr(char *dest, char *src, size_t len);

// 函数内部需要分配堆上字符串
int OpStr2(char **dest, char *src, size_t len)
{
    char *dest = (char*) calloc(len + 1, 1);
    return 0;
}

文件读写

文件读入, 路径文件不存在时读入为字符串.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
char* GetStringFromFile(size_t size)
{
	char* begin = NULL, *end = NULL;
	for (end = begin = (char*)calloc(size, 1); (*end = (char)getchar()) != '\n'; ++end);
	if (end == begin) exit(EXIT_FAILURE);
	*end = '\0';

	FILE *fp = fopen(begin, "rb");
	if (fp)
	{
		fseek(fp, 0, SEEK_END);
		size_t fileSize = ftell(fp);
		char *str = (char*) calloc(fileSize, 1);
		if (!str) return NULL;

		rewind(fp);
		if (fread(str.Begin, 1, fileSize, fp) != fileSize) return NULL;
		fclose(fp);
		free(begin);
	}
	else char *str = begin;
	return str;
}

写入文件.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
int PutStringToFile(char *str)
{
	char* begin = NULL, *end = NULL;
	for (end = begin = (char*)calloc(size, 1); (*end = (char)getchar()) != '\n'; ++end);
	if (end == begin) exit(EXIT_FAILURE);
	*end = '\0';

	FILE *fp = fopen(begin, "wb");
	size_t size = strlen(str);
	if (fwrite(str, 1, size, fp) != size) return -1;
	return 0;
}

Base64读写

以ascii读入, 需要进行转换, 每4个base64字符为一组存入3个Byte.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef unsigned char Byte;

const static Byte ascii[80] =
{
	62, -1, -1, -1, 63, 52, 53, 54, 55, 56,
	57, 58, 59, 60, 61, -1, -1, -1, 65, -1,
	-1, -1, 0, 1, 2, 3, 4, 5, 6, 7,
	8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
	18, 19, 20, 21, 22, 23, 24, 25, -1, -1,
	-1, -1, -1, -1, 26, 27,28, 29, 30, 31,
	32, 33, 34, 35, 36, 37, 38, 39, 40, 41,
	42, 43, 44, 45, 46, 47, 48, 49, 50, 51
};

void example(void)
{
    char str[5] = "cat+";
	
    Byte trs[4];
	for (int i = 0; i < 4; ++i) trs[i] = ascii[str[i] - '+'];
	// ascii从"+"开始

	Byte b[3] = {0};
	b[0] = ((trs[0] << 2) & 0B11111100) + ((trs[1] >> 4) & 0B11);
	b[1] = ((trs[1] << 4) & 0B11110000) + ((trs[2] >> 2) & 0B1111);
	b[2] = ((trs[2] << 6) & 0B11000000) + ((trs[3] >> 0) & 0B111111);

	printf("%x %x %x\n", b[0], b[1], b[2]);
}

末尾有2个"=“时, 最后一组只有1个Byte; 末尾有1个”=“时, 最后一组只有2个Byte.

补尾规则: 1个”=“时, 最后字符补"00"得到, 即”=“前字符须整除4; 2个”=“时, 最后字符补"0000"得到, 即”=“前字符须整除16.

bit Byte base64("=”)
64 8 12(1)
128 16 24(2)
256 32 44(1)
512 64 88(2)
1024 128 172(1)
base64("=") Byte
4n(0) 3n
4n(1) 3n-1
4n(2) 3n-2

每3个Byte为一组输出4个base64字符.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
typedef unsigned char Byte;

const static char base[64] =
{
		'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H',
		'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P',
		'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X',
		'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f',
		'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
		'o', 'p', 'q', 'r', 's', 't', 'u', 'v',
		'w', 'x', 'y', 'z', '0', '1', '2', '3',
		'4', '5', '6', '7', '8', '9', '+', '/'
};

void example(void)
{
	Byte b[3] = {0x71, 0xab, 0x7e};

	Byte trs[4] = {0};
	trs[0] = (b[0] >> 2) & 0B111111;
	trs[1] = ((b[0] << 4) & 0B110000) + ((b[1] >> 4) & 0B1111);
	trs[2] = ((b[1] << 2) & 0B111100) + ((b[2] >> 6) & 0B11);
	trs[3] = b[2] & 0B111111;

	char str[5] = "";
	for (int i = 0; i < 4; ++i) str[i] = base[trs[i]];

	printf("%s\n", str);
}
Byte base64("=")
3n 4n(0)
3n+1 4n+4(2)
3n+2 4n+4(1)

剩余1Byte时, 增加1个空Byte, 末尾补2个"=", 剩余2Byte时, 增加1个空Byte, 末尾补1个"=".

单元转换

当最小操作单元与最小读写单元一致时, 小端序或大端序实现均不会影响结果.

当最小操作单元与最小读写单元不一致时, 需要进行单元转换; 而直接修改指针类型会由于小端序而错误读取.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
typedef unsigned char Byte;
typedef unsigned int Word;

void example(void)
{
// Word to Byte
    Word a = 0x12345678;
    Byte d[4];
    d[0] = (a >> 8 * (4 - 1)) & 0xff;
    d[1] = (a >> 8 * (3 - 1)) & 0xff;
    d[2] = (a >> 8 * (2 - 1)) & 0xff;
    d[3] = a & 0xff;
    printf("%x %x %x %x\n", d[0], d[1], d[2], d[3]);

// Byte to Word
    Word g = 0;
    g += ((Word) d[0] << 8 * (4 - 1));
    g += ((Word) d[1] << 8 * (3 - 1));
    g += ((Word) d[2] << 8 * (2 - 1));
    g += (Word) d[3];
    printf("%x\n", g);
}

字节流

为便于 Bit Padding 和加解密处理, 和出于安全考虑, 对比特流加密前先将字符串型转换为字节流, 结构如下:

1
2
3
4
5
6
7
typedef unsigned char Byte;

typedef struct
{
	Byte* begin;
	Byte* end;
} ByteStream;

对 begin 使用内存分配函数, end 始终指向字节流末尾.

Bit Padding

可分为直接填充和保留长度信息填充, 直接填充即填充到分组的整数倍, 保留长度信息填充也填充到整数倍, 但最后预留一定位置存储长度信息.

而保留长度填充可在直接填充基础上, 比较填充余量和长度预留, 不足时额外增加一个分组即可.

1
2
3
4
5
6
7
8
size_t BitPadding(int strSize, int blockSize, int remains)
{
	int flag = strSize % blockSize ? 1 : 0;
	size_t padSize = (strSize / blockSize + flag) * blockSize - strSize;
	if (remains && (padSize % blockSize) <= remains) padSize += blockSize;
	size_t newSize = strSize + padSize;
	return newSize;
}
Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
主题 StackJimmy 设计