#include <stdio.h> void print(char* c) { printf("%s\n", c); //Uses %s to print a string } int main() { char a = 'd'; print(&a); return 0; }
printf怎么知道在打印完'd'后停止打印下一个字符,而结尾没有空的终止字符?当我运行它时,它只打印了'd'并结束了。这是正常的行为吗?
abithluo1#
一个字母字符串至少需要两个字符数组来容纳字母和空终止字符。在你的代码中,你调用了Undefined Behaviour。在你的例子中,你只是幸运内存中的下一个字节是零。如果末尾没有空终止字符,printf如何知道在打印'd'后停止打印下一个字符?它不知道。今天是你的幸运日。会发生什么是不确定的。
#include <stdio.h> void printchar(char c) { printf("%c\n", c); } void printstring(char *s) { printf("%s\n", s); } int main() { char a = 'd'; printchar(a); char b[2] = {'b',0}; printstring(b); return 0; }
ymdaylpp2#
Undefined behevior是未定义的,但在实际环境中,您的特定示例实际上并没有绑定到segfault。只有当printf试图找到终止\0时,没有命中任何终止,并在未Map或访问保护的页面中结束时,才会出现segfault。当你给予它一个栈上字符时,它会搜索栈中当前使用的部分(栈在大多数架构中向下增长)因为你在main中,栈不会很深,只包含main的帧和操作系统和libc放在它前面的内容,但这实际上足以提供大量的零。Unix把argv放在那里(以NULL指针=〉零字节结束),char **environ;的指针数组(也以NULL结束)+所有字符串(每个字符串都以'\0'结束),除此之外,你还可以在调用代码的帧中使用零。即使你不在Unix上(不能依赖environ和arv nul bytes),栈的已用部分上出现nul bytes的可能性也是非常高的。
printf
\0
char **environ;
hrirmatl3#
任何明显错误的代码都是错误的,并调用未定义的行为。他说,您所展示的特定情况在许多系统上都有很大的机会正确打印输出,因为偶然的巧合是,char类型在大多数系统中是可用的最小整数,而首选对齐方式基于本机处理器位数,通常大于char类型。这意味着使用了通常为0值的填充。现在,将一个char变量与0填充放在一起,您将得到一个以空结尾的字符串。密码是错的,成功的可能性很高...
char
vlurs2pr4#
我不能代表您的机器说话,但在我的机器上,您提供的C代码产生了以下机器代码片段(通过编译程序并在其上调用objdump -D -j .text找到):
objdump -D -j .text
117b: c6 45 f7 64 movb $0x64,-0x9(%rbp) 117f: 48 8d 45 f7 lea -0x9(%rbp),%rax 1183: 48 89 c7 mov %rax,%rdi 1186: e8 be ff ff ff call 1149 <print>
(Keep请记住,向编译器传递不同的选项或使用不同的编译器可能会生成不同的机器码)代码存储一个字节(movb),堆栈上的十六进制值为0x 64。0x 64是“d”字符的ASCII值。然后,它将该字节的地址加载到堆栈上(lea)到rax寄存器并将其复制到rdi,rdi是用于将第一个参数传递给Linux上的函数的寄存器,我使用这个寄存器。在下一条指令的print调用中,第一个参数是指向堆栈中字符的指针。使用GDB,可以在执行特定的movb之前和之后检查堆栈内存的内容。之前:
movb
lea
00:0000│ rsp 0x7fffffffe0b0 ◂— 0x0 01:0008│ 0x7fffffffe0b8 ◂— 0x4ef8437da34e1300 02:0010│ rbp 0x7fffffffe0c0 ◂— 0x1
之后:
00:0000│ rsp 0x7fffffffe0b0 ◂— 0x6400000000000000 01:0008│ 0x7fffffffe0b8 ◂— 0x4ef8437da34e1300 02:0010│ rbp 0x7fffffffe0c0 ◂— 0x1
(我安装了一个名为pwndbg的GDB扩展,因此输出可能会有所不同)从本质上讲,“d”被写入的堆栈内存在写入之前似乎是全零的,因此0x 64后面的下一个字节是空字符,这创建了一个“即兴”的可打印字符串。正如其他人所说,这不是一个你可以依赖的行为,相反,它只是一个巧合。当你编写实际上应该工作的程序时,你不应该这样写代码:)但这在其他答案中已经足够强调了。另外,因为这是一个未定义的行为,所以实际上有可能在你的机器上程序出于完全不同的原因工作。为了确定答案,我建议你调试你的代码并检查内存内容在你的情况下是什么样子的。要做到这一点,你应该:1.编译您的程序1.将其拆卸,例如使用objdump -D -j .text <your program name>1.查找负责将“d”传递给打印函数的程序集。提示:它将位于主函数中的某个位置1.如果需要,使用GDB检查内存/寄存器究竟发生了什么
objdump -D -j .text <your program name>
4条答案
按热度按时间abithluo1#
一个字母字符串至少需要两个字符数组来容纳字母和空终止字符。
在你的代码中,你调用了Undefined Behaviour。在你的例子中,你只是幸运内存中的下一个字节是零。
如果末尾没有空终止字符,printf如何知道在打印'd'后停止打印下一个字符?
它不知道。今天是你的幸运日。会发生什么是不确定的。
ymdaylpp2#
Undefined behevior是未定义的,但在实际环境中,您的特定示例实际上并没有绑定到segfault。
只有当
printf
试图找到终止\0
时,没有命中任何终止,并在未Map或访问保护的页面中结束时,才会出现segfault。当你给予它一个栈上字符时,它会搜索栈中当前使用的部分(栈在大多数架构中向下增长)因为你在main中,栈不会很深,只包含main的帧和操作系统和libc放在它前面的内容,但这实际上足以提供大量的零。Unix把argv放在那里(以NULL指针=〉零字节结束),
char **environ;
的指针数组(也以NULL结束)+所有字符串(每个字符串都以'\0'结束),除此之外,你还可以在调用代码的帧中使用零。即使你不在Unix上(不能依赖environ和arv nul bytes),栈的已用部分上出现nul bytes的可能性也是非常高的。
hrirmatl3#
任何明显错误的代码都是错误的,并调用未定义的行为。
他说,您所展示的特定情况在许多系统上都有很大的机会正确打印输出,因为偶然的巧合是,
char
类型在大多数系统中是可用的最小整数,而首选对齐方式基于本机处理器位数,通常大于char
类型。这意味着使用了通常为0值的填充。现在,将一个
char
变量与0填充放在一起,您将得到一个以空结尾的字符串。密码是错的,成功的可能性很高...
vlurs2pr4#
我不能代表您的机器说话,但在我的机器上,您提供的C代码产生了以下机器代码片段(通过编译程序并在其上调用
objdump -D -j .text
找到):(Keep请记住,向编译器传递不同的选项或使用不同的编译器可能会生成不同的机器码)
代码存储一个字节(
movb
),堆栈上的十六进制值为0x 64。0x 64是“d”字符的ASCII值。然后,它将该字节的地址加载到堆栈上(lea
)到rax寄存器并将其复制到rdi,rdi是用于将第一个参数传递给Linux上的函数的寄存器,我使用这个寄存器。在下一条指令的print调用中,第一个参数是指向堆栈中字符的指针。使用GDB,可以在执行特定的
movb
之前和之后检查堆栈内存的内容。之前:
之后:
(我安装了一个名为pwndbg的GDB扩展,因此输出可能会有所不同)
从本质上讲,“d”被写入的堆栈内存在写入之前似乎是全零的,因此0x 64后面的下一个字节是空字符,这创建了一个“即兴”的可打印字符串。
正如其他人所说,这不是一个你可以依赖的行为,相反,它只是一个巧合。当你编写实际上应该工作的程序时,你不应该这样写代码:)但这在其他答案中已经足够强调了。另外,因为这是一个未定义的行为,所以实际上有可能在你的机器上程序出于完全不同的原因工作。为了确定答案,我建议你调试你的代码并检查内存内容在你的情况下是什么样子的。要做到这一点,你应该:
1.编译您的程序
1.将其拆卸,例如使用
objdump -D -j .text <your program name>
1.查找负责将“d”传递给打印函数的程序集。提示:它将位于主函数中的某个位置
1.如果需要,使用GDB检查内存/寄存器究竟发生了什么