为何char引发的死循环如此顽固,难以破解?
摘要:原来的程序要读的文件内容只有一个字符。(ch = fgetc(fp)) != EOF 却一直是 true。读文件的那段代码是这样的: while ((ch = fgetc(fp)) != EOF) { } 产生了一个死循环 (故意隐藏 ch
原来的程序要读的文件内容只有一个字符。(ch = fgetc(fp)) != EOF 却一直是 true。读文件的那段代码是这样的:
while ((ch = fgetc(fp)) != EOF) {
}
产生了一个死循环 (故意隐藏 ch 的定义)。
TLDR: 点这里折叠了一个简单的解释。
TLDR: 应该用 int 类型的变量接收 fgetc() 的返回值
熟悉 C 语言 getchar() 系列函数的肯定已经猜到,ch 的类型是 char 导致了这个问题。
fgetc(3):
fgetc() reads the next character from stream and returns it as an unsigned char cast to an int, or EOF on end of file or error.
最小复现示例
下面是一个名为 read-null 的程序的代码,这个程序在 x86_64 不会死循环,但是在 aarch64 上会死循环。
#include <stdio.h>
#include <assert.h>
int main()
{
FILE *fp = fopen("/dev/null", "r");
assert(fp != NULL);
char ch;
// Reads from /dev/null always return end of file
while ((ch = fgetc(fp)) != EOF) {
printf("Infinite loop...\n");
}
fclose(fp);
return 0;
}
下面是这个示例在 x86_64 的运行效果,程序直接读到 EOF 退出了:
aarch64 gcc 把比较指令都优化掉了
在 aarch64 机器上,b .L2,无条件跳转,编译器知道 char 和 int 的 -1 比较,(char)-1 != (int)-1 永远都是 true,直接给优化成死循环了,没有对比 (char)fgetc() 返回值是否不等于 EOF这一条指令。
判断 EOF 的操作被编译器优化了。
为什么 x86_64 上是正常的?
C 语言的 EOF一般是常量 -1,来看看如果使用 char 类型的变量来接收 fgetc()的返回值,char 的 -1 和 int 的 -1 到底相等不相等。
#include <stdio.h>
#include <assert.h>
int main()
{
#if defined(__x86_64__) || defined(_M_X64)
printf("=====On x86_64=====\n");
#elif defined(__aarch64__)
printf("=====On ARM64=====\n");
#endif
assert((unsigned char)-1 != -1);
printf("(unsigned char)-1 != -1 is true\n");
assert((char)-1 != -1);
printf("(char)-1 != -1 is true\n");
return 0;
}
x86_64 上,(char)-1 != -1 是 false。aarch64 上,(char)-1 != -1 是 true。
char 和 int 做比较,char 会被隐式转换为 int。unsigned char展开到 int 的时候符号位是不会扩展的,换句话说,(unsigned char)-1 转为 int是 255。(char)-1 转 int 会是多少呢?
x86_64 上是 -1,arm64 上是 255. char 的负数转 int ,或者说 signed char 转 int 这是 UB。
