仅作为本人学习《深入 C 语言和程序运行原理》的学习笔记,原课程链接:极客时间《深入 C 语言和程序运行原理》——于航
输入输出(I/O)是应用程序不可或缺的一种基本能力。C 语言采用库函数的方式来实现 IO 功能(stdio.h),通过 IO 库函数,我们可以快捷地读取用户键盘输入、输出内容到控制台,乃至读写文件等一系列常规的 IO 功能。
快捷回顾 IO 接口的使用方法
下面是于航老师给出的一个简单的操作文件 IO 的例子:
#include <stdio.h>
int main(void) {
printf("Enter some characters:\n");
FILE* fp = fopen("./temp.txt", "w+");
if (fp) {
char ch;
while (scanf("%c", &ch)) {
if (ch == 'z') break;
putc(ch, fp);
}
} else {
perror("File open failed.");
}
fclose(fp);
return 0;
}
运行时,在终端输入一串字符串,以 z 作为结束符,输入的字符串就会被写入到对应的文件中,
IO 接口的不同级别
IO 接口可以被分成不同层次,C 语言提供的 IO 接口属于“标准 IO”的范畴。与之相对的叫做“低级 IO”,低级 IO 操作的是更加底层的 IO,它会使用与具体操作系统相关的一系列底层接口来提供相应的 IO 能力。
使用“低级 IO”来重写上面的例子:
#include <unistd.h>
#include <fcntl.h>
int main(void) {
const char str[] = "Enter some characters:\n";
write(STDOUT_FILENO, str, sizeof(str));
const int fd = open("./temp.txt", O_RDWR | O_CREAT);
if (fd > 0) {
char ch;
while (read(STDIN_FILENO, &ch, 1)) {
if (ch == 'z') break;
write(fd, &ch, sizeof(ch));
}
} else {
const char errMsg[] = "File open failed.";
write(STDERR_FILENO, errMsg, sizeof(errMsg));
}
close(fd);
return 0;
}
运行效果是相同的
可以看出,“低级 IO”操作时参数更多,需要指定设备文件等,而标准 IO 屏蔽了底层不同系统的实现细节,所以标准 IO 可以做到“一次编写,到处编译”,即跨平台。
带缓冲的标准 IO 模型
虽然不同级别的 IO 接口实现的功能相同,但程序运行时背后的逻辑却有着较大的差异。
与低级 IO 相比,标准 IO 会为我们提供带缓冲的输入与输出操作,这些操作如何实现还因平台差异而不同。而低级 IO 接口会直接通过系统调用来完成响应的 IO 操作。
系统调用的过程涉及到进程在用户模式与内核模式之间的转换,其成本较高。标准 IO 接口通过添加缓冲区的方式,可以减少低级 IO 接口的调用次数。
以上面的代码为例,如果用低级 IO 接口,每次调用 write() 函数,都会将数据写入到文件中,
如果使用标准 IO 接口,每次调用 putc() 函数,并不会直接将数据写入到文件,而是写到了缓冲区。
危险的 gets 函数
C 语言提供的标准 IO 接口并非都是完备的。自 C90 开始,一个名为 gets 的 IO 函数被添加进标准库。该函数主要用于从标准输入流中读取一系列字符,并将它们放到由函数实参指定的字符数组中。 gets 函数在其内部实现中,并没有对用户的输入内容进行边界检查。因此,当用户实际输入的字符数量超过数组所成承受的最大容量时,超出的内容会直接覆盖掉栈帧中位于高地址处的其他数据。而当别有用心的攻击者精心设计输入内容时,甚至可以在某些情况下直接篡改当前函数帧的返回地址,并将其指向另外事先准备好的攻击代码。 正因为如此,gets 函数已经在 C99 标准汇总被弃用,并在 C11 及以后的标准中移除。
|