字符串文字和 strcat
string literals and strcat
我不确定为什么 strcat 在这种情况下对我有用:
char* foo="foo";
printf(strcat(foo,"bar"));
它成功地为我打印了 "foobar"。
但是,根据此处在 Whosebug 上讨论的先前主题:I just can't figure out strcat
它说,上面的内容不应该工作,因为 foo 被声明为字符串文字。相反,它需要被声明为一个缓冲区(一个预定大小的数组,以便它可以容纳我们试图连接的另一个字符串)。
既然如此,为什么上面的程序对我成功了?
此代码调用未定义行为 (UB),这意味着您无法保证会发生什么(此处失败)。
原因是字符串文字是不可变的。这意味着它们不是可变的,任何这样做的尝试都会调用 UB。
请注意 UB 可能会出现什么困难的逻辑错误,因为它可能会工作(今天和在你的系统中),但它仍然是错误的,这使得你很可能会错过错误,并得到一路顺风。
PS: 在这个Live Demo中,我很幸运得到了Segmentation fault。我说幸运,因为这个段错误会让我调查和调试代码。
值得注意的是,GCC 没有发出警告,来自 Clang 的警告也无关紧要:
p
rog.c:7:8: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
printf(strcat(foo,"bar"));
^~~~~~~~~~~~~~~~~
prog.c:7:8: note: treat the string as an argument to avoid this
printf(strcat(foo,"bar"));
^
"%s",
1 warning generated.
字符串字面值是 immutable 的意思是 编译器将在您不会改变它们的假设下运行,而不是您一定会得到如果您尝试修改它们,则会出错。在法律术语中,这是 "undefined behavior",所以任何事情都可能发生,而且,就标准而言,这很好。
现在,在现代平台和现代编译器上,您确实有额外的保护:在具有内存保护的平台上,字符串 table 通常被放置在只读内存区域,因此修改它会你是运行时错误。
不过,您可能有一个不提供任何运行时强制检查的编译器,要么是因为您正在为没有内存保护的平台进行编译(例如 pre-80386 x86,因此几乎所有 C 编译器都适用于DOS,例如 Turbo C,大多数微控制器在 RAM 而不是闪存上运行时,...),或者使用默认情况下不利用此硬件功能以保持与旧版本兼容的旧编译器(旧 VC ++ 很长一段时间),或者使用显式启用了这样一个选项的现代编译器,同样是为了与旧代码兼容(例如 gcc with -fwritable-strings
)。在所有这些情况下,您不会收到任何运行时错误是正常的。
最后,还有一个额外的曲折的极端情况:current-day optimizers actively exploit undefined behavior - 即他们假设它永远不会发生,并相应地修改代码。一个特别聪明的编译器可以生成只是丢弃这种写入的代码并非不可能,因为法律允许它在这种情况下做任何它最喜欢做的事情。
这个可以看一些简单的代码,比如:
int foo() {
char *bar = "bar";
*bar = 'a';
if(*bar=='b') return 1;
return 0;
}
这里,启用了优化:
- VC++ 看到 write 只用于紧随其后的条件,因此它将整个事情简化为
return 0
;没有内存写入,没有段错误,它 "appears to work" (https://godbolt.org/g/cKqYU1);
- gcc 4.1.2 "knows" 文字不变;写是多余的,它被优化掉了(所以,没有段错误),整个事情变成了
return 1
(https://godbolt.org/g/ejbqDm);
- 任何更现代的 gcc 选择更精神分裂的路线:写入不会被省略(所以你会得到默认链接器选项的段错误),但如果它成功(例如,如果你手动 fiddle 内存保护) 你会得到一个
return 1
(https://godbolt.org/g/rnUDYr) - so, memory modified but the code that follows thinks it hasn't been modified; this is particularly egregious on AVR,其中没有内存保护并且写入成功。
- 叮当声does pretty much the same as gcc.
长话短说:不要碰运气,小心行事。始终将字符串文字分配给 const char *
( 而不是 普通 char *
)并让类型系统帮助您避免此类问题。
我不确定为什么 strcat 在这种情况下对我有用:
char* foo="foo";
printf(strcat(foo,"bar"));
它成功地为我打印了 "foobar"。
但是,根据此处在 Whosebug 上讨论的先前主题:I just can't figure out strcat
它说,上面的内容不应该工作,因为 foo 被声明为字符串文字。相反,它需要被声明为一个缓冲区(一个预定大小的数组,以便它可以容纳我们试图连接的另一个字符串)。
既然如此,为什么上面的程序对我成功了?
此代码调用未定义行为 (UB),这意味着您无法保证会发生什么(此处失败)。
原因是字符串文字是不可变的。这意味着它们不是可变的,任何这样做的尝试都会调用 UB。
请注意 UB 可能会出现什么困难的逻辑错误,因为它可能会工作(今天和在你的系统中),但它仍然是错误的,这使得你很可能会错过错误,并得到一路顺风。
PS: 在这个Live Demo中,我很幸运得到了Segmentation fault。我说幸运,因为这个段错误会让我调查和调试代码。
值得注意的是,GCC 没有发出警告,来自 Clang 的警告也无关紧要:
p
rog.c:7:8: warning: format string is not a string literal (potentially insecure) [-Wformat-security]
printf(strcat(foo,"bar"));
^~~~~~~~~~~~~~~~~
prog.c:7:8: note: treat the string as an argument to avoid this
printf(strcat(foo,"bar"));
^
"%s",
1 warning generated.
字符串字面值是 immutable 的意思是 编译器将在您不会改变它们的假设下运行,而不是您一定会得到如果您尝试修改它们,则会出错。在法律术语中,这是 "undefined behavior",所以任何事情都可能发生,而且,就标准而言,这很好。
现在,在现代平台和现代编译器上,您确实有额外的保护:在具有内存保护的平台上,字符串 table 通常被放置在只读内存区域,因此修改它会你是运行时错误。
不过,您可能有一个不提供任何运行时强制检查的编译器,要么是因为您正在为没有内存保护的平台进行编译(例如 pre-80386 x86,因此几乎所有 C 编译器都适用于DOS,例如 Turbo C,大多数微控制器在 RAM 而不是闪存上运行时,...),或者使用默认情况下不利用此硬件功能以保持与旧版本兼容的旧编译器(旧 VC ++ 很长一段时间),或者使用显式启用了这样一个选项的现代编译器,同样是为了与旧代码兼容(例如 gcc with -fwritable-strings
)。在所有这些情况下,您不会收到任何运行时错误是正常的。
最后,还有一个额外的曲折的极端情况:current-day optimizers actively exploit undefined behavior - 即他们假设它永远不会发生,并相应地修改代码。一个特别聪明的编译器可以生成只是丢弃这种写入的代码并非不可能,因为法律允许它在这种情况下做任何它最喜欢做的事情。
这个可以看一些简单的代码,比如:
int foo() {
char *bar = "bar";
*bar = 'a';
if(*bar=='b') return 1;
return 0;
}
这里,启用了优化:
- VC++ 看到 write 只用于紧随其后的条件,因此它将整个事情简化为
return 0
;没有内存写入,没有段错误,它 "appears to work" (https://godbolt.org/g/cKqYU1); - gcc 4.1.2 "knows" 文字不变;写是多余的,它被优化掉了(所以,没有段错误),整个事情变成了
return 1
(https://godbolt.org/g/ejbqDm); - 任何更现代的 gcc 选择更精神分裂的路线:写入不会被省略(所以你会得到默认链接器选项的段错误),但如果它成功(例如,如果你手动 fiddle 内存保护) 你会得到一个
return 1
(https://godbolt.org/g/rnUDYr) - so, memory modified but the code that follows thinks it hasn't been modified; this is particularly egregious on AVR,其中没有内存保护并且写入成功。 - 叮当声does pretty much the same as gcc.
长话短说:不要碰运气,小心行事。始终将字符串文字分配给 const char *
( 而不是 普通 char *
)并让类型系统帮助您避免此类问题。