printf()函数的执行和分段错误

28
#include<stdio.h>

int main()
{
    char *name = "Vikram";
    printf("%s",name);
    name[1]='s';
    printf("%s",name);
    return 0;
}

在终端上没有任何输出,只会得到段错误。但是当我在GDB中运行它时,我得到以下结果 -

Program received signal SIGSEGV, Segmentation fault.
0x0000000000400525 in main () at seg2.c:7
7       name[1]='s';
(gdb) 

这意味着程序在第7行收到了SEG错误(显然我无法写入常量字符数组)。那么为什么第6行的printf()没有执行?


我不太确定。在我的运行OSX Lion的Mac上(使用LLVM编译,用LLDB调试),它按预期工作。 - Richard J. Ross III
4个回答

56

这是由于stdout的流缓冲造成的。除非您执行fflush(stdout)或打印一个换行符"\n",否则输出可能会被缓冲。

在这种情况下,它在缓冲被刷新和打印之前导致了段错误。

您可以尝试使用以下代码而不是原来的代码:

printf("%s",name);
fflush(stdout);        //  Flush the stream.
name[1]='s';           //  Segfault here (undefined behavior)

或者:

printf("%s\n",name);   //  Flush the stream with '\n'
name[1]='s';           //  Segfault here (undefined behavior)

17
请注意,fflush 是正确的方法 - 换行符并不能保证触发刷新(我之前也因此受过影响)。 - Aaron Dufour
顺便提一下,puts(name) 等同于 printf("%s\n",name);,尽管有些人不喜欢隐式换行。编译器会为您优化 printf 以使用 puts。 - Peter Cordes
仅包含换行符的输出会自动刷新,而非使用 fflush 刷新内存缓冲区中的行缓冲 stdout;如果将 stdout 重定向到文件,则会变成全缓冲。stderr 总是无缓冲的,这也是为什么在段错误调试打印时要优先选择 fprintf(stderr,...) 的原因之一,如果您不想包含换行符,或者可能想将日志重定向到文件,并且不想在每次调用后也使用 fflush 刷新缓冲区。 - Peter Cordes

11

首先,你应该在printf语句的末尾添加"\n"(或者至少在最后一个printf语句中添加)。但这与段错误无关。

当编译器编译您的代码时,它将二进制文件分成几个部分。有些是只读的,而其他部分是可写的。向只读部分写入可能会导致段错误。 字符串字面量通常放置在只读段中(gcc应该将其放在".rodata"中)。 指针名称指向该只读段。因此,您必须使用

const char *name = "Vikram";
在我的回答中,我使用了一些"可能"和"应该"。这种行为取决于您的操作系统、编译器和编译设置(链接脚本定义了部分内容)。
添加:

-Wa,-ahlms=myfile.lst

使用gcc命令行会生成名为myfile.lst的文件,其中包含生成的汇编代码。你可以在顶部看到:

    .section .rodata
.LC0:
    .string "Vikram"

这表明字符串在Vikram中。

同样的代码使用(必须在全局作用域中,否则gcc可能将其存储在堆栈上,请注意它是一个数组而不是指针)

char name[] = "Vikram";

产生

    .data
    .type name, @object
    .size name, 7
name:
    .string "Vikram"

语法有点不同,但看看现在它在`.data`部分,这是可读写的。

顺便说一下,这个例子是有效的。


2
如果你注意到了,OP并不是在问为什么会出现段错误,而是为什么字符串一开始就没有被打印出来。 - Richard J. Ross III
2
虽然这可能并不完全回答问题,但对.rodata和.data的提示和解释是有帮助的。 - vts

5
你收到段错误的原因是C字符串文字根据C标准是只读的,而你试图在“Vikram”字面数组的第二个元素上写入's'。
你没有输出结果的原因是你的程序正在缓冲其输出并在刷新其缓冲区之前崩溃。stdio库的目的不仅是提供友好的格式化函数,如printf(3),而且还通过将数据缓冲在内存中的缓冲区中并仅在必要时刷新输出以及偶尔执行输入而不是一直执行来减少i / o操作的开销。实际输入和输出通常不会在调用stdio函数的时候发生,而是只有当输出缓冲区已满(或输入缓冲区为空)时才发生。
如果已设置FILE对象以使其不断刷新(例如stderr),则情况略有不同,但通常情况下,这就是要点。
如果你正在调试,请最好fprintf到stderr以确保在崩溃之前能够刷新你的调试输出。

1

默认情况下,当 stdout 连接到终端时,流是行缓冲的。实际上,在您的示例中,缺少 '\n'(或显式流刷新)就是您没有打印字符的原因。

但在理论上,未定义的行为是没有界限的(来自标准的"对于此国际标准不强制执行要求的行为[...]"),并且段错误甚至可能发生在未定义的行为之前,例如在第一个 printf 调用之前!


那么...你的意思是这种行为是如此未定义,以至于它可以“倒退时间”? - John Lawrence Aspden
@JohnLawrenceAspden 在这种情况下可能不会,但在其他程序中编译器可能会将赋值移到函数顶部甚至并行执行代码。Itanium机器码可以在一个指令束中执行六个操作。找出哪个操作导致处理器陷阱是很有趣的。 - Zan Lynx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接