理解C语言中的printf函数

8

我正在尝试理解在C语言中printf的工作原理,针对一个简单的例子。我编写了以下程序:

#include "stdio.h"

int main(int argc, char const *argv[])
{
    printf("Test %s\n", argv[1]);
    return 0;
}

在二进制文件上运行objdump时,我注意到Test %s\n位于.rodata中。

objdump -sj .rodata bin

bin:     file format elf64-x86-64

Contents of section .rodata:
 08e0 01000200 54657374 2025730a 00        ....Test %s..

因此,格式化打印似乎会从rodata执行额外的模式复制到其他地方。

编译并使用stare ./bin rr运行后,我注意到在实际写入之前有一个brk系统调用。因此,可以通过以下方式运行:

gdb catch syscall brk
gdb catch syscall write

显示在我的情况下,当前断点等于0x555555756000,但它随后设置为0x555555777000。当进行write操作时,格式化字符串为

x/s $rsi
0x555555756260: "Test rr\n"

这段代码在“旧”和“新”断点之间。写入发生后,程序退出。

问题:为什么我们要分配这么多页面?为什么在写入系统调用之后,断点没有返回到之前的位置?使用brk而不是mmap格式化有什么原因吗?


1
常量字符串在rodata中是很正常的。因此,格式化打印似乎会从rodata复制额外的模式到其他地方。print没有理由复制字符串,它无法知道字符串是否为常量,你为什么这样说?所有的初始化等都是在程序启动时完成的。 - bruno
1
printf有自己的缓冲区,通常是行缓冲。 - KamilCuk
1
关于“格式化打印似乎会从rodata复制额外的模式到其他地方”的问题:是的,输出到标准输出流中。;-) - Peter - Reinstate Monica
2
@St.Antario:格式字符串永远不会被改变。printf 逐字节地复制到目标位置,直到遇到第一个 % 字符,然后使用该处的格式说明符格式化下一个参数并继续。在任何语言中,通过插入数据来就地更改字符串几乎是不可能的。 - vgru
1
@St.Antario:只是为了澄清一下,当你说“复制模式”时,这可能意味着字符串确实被复制,然后再进行更改。但是,此函数的任何实现都必须执行以下操作:1)从const模式字符串逐个字符读取,并且2)将此字符或某些格式化值写入其他地方。无论这个其他地方是直接输出还是中间malloc'ed缓冲区,都取决于实现方式。有许多“微小printf”实现不分配任何临时缓冲区。 - vgru
显示剩余10条评论
2个回答

1

brk()(以及它的伙伴sbrk())是一种专门用于操作堆大小的mmap()。由于历史原因而存在,libc也可以直接使用mmap()mremap()

堆在分配额外内存时会扩展,例如使用malloc()在libc内部进行,例如为了有足够的空间创建来自格式字符串和参数的实际字符串或许多其他内部事物(即使用f*函数族进行缓冲I/O时的输出缓冲区)。

如果堆的某些部分不再使用,则通常不会自动释放,原因有两个主要原因:堆可能被分段,未使用的堆不会降至某个阈值以下,这样做是有必要的,因为它可能很快就会再次需要。

顺便说一下:格式字符串本身肯定不会从只读节复制到堆中,这完全没有用。但结果字符串(通常)是在堆上构建的。


格式字符串本身肯定不会从只读节复制到堆中。修改“ro”节会导致分段错误吗?我刚尝试了一下,的确会导致分段错误。 - St.Antario
1
这是导致 segv 的代码:char *p = "Test %s\n"; printf(p, argv[1]); printf(p, argv[2]); p[5] = 'r'; p[6] = 'r';,对于 ./bin rr tt。即使在 C 中修改字符串字面量是未定义的行为,但我们可以编写手写汇编并修改 .rodata,这仍然会导致段错误。 - St.Antario
1
@StAntario 你误解了我的意思,result string 是在堆上构建的,但是在此之前 format string 并没有被复制到堆上。这是一个很大的区别。 - Ctx

1

为什么我们要分配这么多页面?

使用系统调用的成本很高,因此库会请求比你现在需要更多的页面,因为你很可能很快就需要更多。在用户模式下管理内存的成本较低。这是一个粒度问题。

为什么写入系统调用后break没有返回到以前的位置?

同样,如果很可能很快就会再次请求更多,为什么要释放?

有没有理由使用brk而不是mmap进行这种格式化?

这取决于实现,这是一个选择的问题。

附:你的问题更多地涉及“内存分配策略”而不是“理解printf”(这是上下文)。


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