在 GNU/Linux 中用 C语言计算文件的 Hash

在 GNU/Linux 中用 C语言计算文件的 Hash

在今日之前,笔者从未想到使用 C/C++GNU/Linux 计算文件的 Hash (例如:SHA-1MD5SHA-256 等)会这样的麻烦.

笔者以为会有 char * sha256sum(int fd)char * sha256sum(FILE *stream) 类似的函数来轻松的获取文件的 Hash .但事实并非如此,获取文件的 Hash 的方法远比笔者想象中的做法要复杂.

方案1 自行实现Hash函数

这种方案是最为麻烦的,但有着不依赖第三方库和程序的优点.至于如何实现 Hash 函数不是本文重点,笔者对此也不做说明.

方案2 调用Openssl

笔者的 openssl 版本为1.1.1i 8 Dec 2020/usr/include/openssl 中提供的头文件可点击下方的小三角查看.

点此查看更多信息aes.h asn1err.h asn1.h asn1_mac.h asn1t.h asyncerr.h async.h bioerr.h bio.h blowfish.h bnerr.h bn.h buffererr.h buffer.h camellia.h cast.h cmac.h cmserr.h cms.h comperr.h comp.h conf_api.h conferr.h conf.h cryptoerr.h crypto.h cterr.h ct.h des.h dherr.h dh.h dsaerr.h dsa.h dtls1.h ebcdic.h ecdh.h ecdsa.h ecerr.h ec.h engineerr.h engine.h e_os2.h err.h evperr.h evp.h hmac.h idea.h kdferr.h kdf.h lhash.h md2.h md4.h md5.h mdc2.h modes.h objectserr.h objects.h obj_mac.h ocsperr.h ocsp.h opensslconf.h opensslv.h ossl_typ.h pem2.h pemerr.h pem.h pkcs12err.h pkcs12.h pkcs7err.h pkcs7.h rand_drbg.h randerr.h rand.h rc2.h rc4.h rc5.h ripemd.h rsaerr.h rsa.h safestack.h seed.h sha.h srp.h srtp.h ssl2.h ssl3.h sslerr.h ssl.h stack.h storeerr.h store.h symhacks.h tls1.h tserr.h ts.h txt_db.h uierr.h ui.h whrlpool.h x509err.h x509.h x509v3err.h x509v3.h x509_vfy.h
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
#include <openssl/sha.h>
#include <stdio.h>

int main(void)
{
unsigned char result[SHA256_DIGEST_LENGTH];
char *filename = "README.md";

FILE *file = fopen(filename, "rb");
SHA256_CTX hash;

if (file == NULL)
{
perror("fopen");
return 1;
}
SHA256_Init(&hash);

ssize_t size;
unsigned char buf[4096];

while ((size = fread(buf, 1, 4096, file)) != 0)
{
SHA256_Update(&hash, buf, size);
}
SHA256_Final(result, &hash);
for (size_t i = 0; i < SHA256_DIGEST_LENGTH; i++)
{
printf("%02x", result[i]);
}
fclose(file);
return 0;
}

本方案调用了 openssl 提供的 sha.h 比自行实现 Hash函数 能方便一点点.使用本方案的程序在编译时需要使用 -lssl-lcrypto 参数链接相关的库.

方案3 进程间通信调用其他程序

GNU/Linux 中通常含有 sha256sumsha512summd5sum 等程序,并支持以类似 sha256sum <path> 的格式直接调用.那么,就可以使用 popen 函数完成进程间通信,直接获取文件的 Hash

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main(void)
{
FILE *target;
target = popen("sha256sum ~/README.md", "r");
if (target == NULL)
{
perror("popen");
}
char hash[65];
fscanf(target, "%64s", hash);
printf("%s\n", hash);
pclose(target);
return 0;
}

这种方式的好处显而易见「方便」,这种方法是也最容易理解的.

值得多说一句的是:Hash 函数生成的 Hash 是一个由函数决定的常数(例如:SHA-256的结果以字符串输出有 64可打印字符 ),这个特性使得 数组的长度读取的输出长度 是确定的.

测试环境

OS : Arch Linux

Kernel : 5.9.14-arch1-1

openssl : 1.1.1i 8 Dec 2020

gcc : 10.2.0

参考资料

1. W.RichardStevens.Stephen.UNIX环境高级编程[M].第3版.戚正伟,译.北京:人民邮电出版社

命令行参数的误解

命令行参数的误解

前言

我们都知道C语言中允许main函数拥有0个或2个参数,但也存在部分操作系统向程序传入更多的参数,还有部分实现中对标准进行扩展,允许main函数拥有更多的参数
命令行参数作为main函数的两个参数被传递给程序,这两个参数通常被命名为int argc,char **argv,其中argc为参数的数量,argv为一个指向内含 argc + 1char 类型指针指针数组

但仅用这段话进行描述可能难以对命令行参数有一个正确的认识,这种描述可能对命令行参数的理解不利.
我们先来分析一个程序.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* ShowCommandLineArgument.c   */
#include <stdio.h>
int main(int argc, char **argv)
{
printf("argc:%d\n", argc);
for (int i = 0; i < argc; i++)
printf("argv[%d]:%s\n", i, argv[i]);
//argv[i]就是*(argv+i),很明显是一个指向char的指针
//程序并不以%s打印argv[argc],而是退出循环.

//请不要忘记:表达式(argv[argc]==NULL)为真
printf("argv[%d]:%p\n", argc, argv[argc]);
return 0;
}

在笔者的电脑中,该文件被存储在/home/admin/blog/ShowCommandLineArgument.c,输入命令 gcc ShowCommandLineArgument.c 进行编译,得到a.out,并以cd && ./blog/a.out -f ~/bolg/test1.md >./blog/test2.md /home/admin/blog/test3.md ./blog/test4.md执行该程序.

请思考,该程序会输出什么内容?你是否认为程序的输出为

1
2
3
4
5
6
7
8
9
argc:6
argv[0]:a.out
argv[1]:-f
argv[2]:~/bolg/test1.md
argv[3]:>./blog/test2.md
argv[4]:/home/admin/blog/test3.md
argv[5]:./blog/test4.md
argv[6]:(nil)


什么?你说没看到输出?请认真查看笔者输入的指令,其中包括了 >./blog/test2.md 意味把 a.out标准输出 重定向至文件./blog/test2.md .所以笔者使用 cat >./blog/test2.md 查看输出的内容,该程序在笔者的设备上的输出为:
1
2
3
4
5
6
7
argc:5
argv[0]:./blog/a.out
argv[1]:-f
argv[2]:/home/admin/bolg/test1.md
argv[3]:/home/admin/blog/test3.md
argv[4]:./blog/test4.md
argv[5]:(nil)

是不是和你的预期不尽相同,请听笔者逐一解释.

常见误区

误区1—-「认为 argv[0] 存储文件名」

实际上,argv[0] 会存储调用的指令中的第一个字符串,而不是文件名,strcmp(argv[0],__FILE__)并不总为0

误区2—-「认为命令行参数总是被原样传递」

在上面的例子中可以发现,相对路径 ~/blog/test1.md 作为命令行参数传给程序,程序收到的实际上是文件的绝对路径 /home/admin/blog/test3.md
但同为相对路径./blog/a.out./blog/test4.md却可以正常传递给程序,而不被转换为绝对路径
其他的相对路径写法是否能被正常传递?笔者在此使用由 ShowCommandLineArgument.c 编译得到的 a.out 文件继续测试.使用的指令为 ~/blog/a.out ./test/../blog/test1.md ../test2.md ~admin/blog/test3.md 由这两次测试,笔者大胆猜测只有以 ~ 开头的相对路径会被转换为绝对路径 然后才传递给程序.

1
2
3
4
5
6
argc:4
argv[0]:/home/admin/blog/a.out
argv[1]:./test/../blog/test1.md
argv[2]:../test2.md
argv[3]:/home/admin/blog/test3.md
argv[4]:(nil)

为什么要这么做呢?

请分析笔者的这个程序.

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
#include <stdio.h>
int main(void)
{
char path1[] = "./blog/test2.md";
char path2[] = "~/blog/test2.md";
char path3[] = "~admin/blog/test2.md";
char path4[] = "~/blog/test2.md";
char path5[] = "../blog/test2.md";

if (fopen(path1, "r") == NULL)
perror("path1");
else
printf("1Success\n");
if (fopen(path2, "r") == NULL)
perror("path2");
else
printf("2Success\n");
if (fopen(path3, "r") == NULL)
perror("path3");
else
printf("3Success\n");
if (fopen(path4, "r") == NULL)
perror("path4");
else
printf("4Success\n");
if (fopen(path5, "r") == NULL)
perror("path5");
else
printf("5Success\n");
return 0;
}

笔者用cd && ./blog/a.out调用该程序编译得到的可执行文件,得到的输出为:

1
2
3
4
5
1Success
path2: No such file or directory
path3: No such file or directory
path4: No such file or directory
path5: No such file or directory

我们可以惊讶的发现只有第一次成功的打开了文件,其他4次操作全部报错.当然,其中第五次打开文件的操作失败是理所当然的,因为确实没有这个文件存在.笔者复制该可执行文件至~/test/a.out后重新执行该程序即发现,第5次文件打开操作成功了.

1
2
3
4
5
path1: No such file or directory
path2: No such file or directory
path3: No such file or directory
path4: No such file or directory
5Success

这说明:fopen()无法识别以~开头的相对路径,也体现了命令行参数在传递过程中,转换以~开头的相对路径绝对路径的必要性.

误区3—「认为重定向是命令行参数」

重定向虽然也在命令行参数的位置,但和命令行参数具有本质的区别.

实践说明重定向指令不会被当中命令行参数传递给程序.

在开发中应该小心,防止误认,也需防止命令行参数中出现相关符号被系统当做重定向指令,导致命令行参数传递错误.

测试环境

OS: Arch Linux
Kernel: x86_64 Linux 5.8.14-arch1-1

参考书籍

1. Stephen Prata.C Primer Plus[M].第六版.姜佑,译.北京:人民邮电出版社
2. Kenneth.A.Reek.C和指针[M].徐波,译.北京:人民邮电出版社.2008