【问题标题】:Understanding printf in C理解 C 中的 printf
【发布时间】:2019-01-08 11:03:01
【问题描述】:

我正在尝试了解printf 在 C 中的工作原理,用于一个简单的案例。我写了以下程序:

#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"

位于“旧”和“新”间断之间。写入发生后程序退出。

问题: 为什么我们分配了这么多页面,为什么在 write syscall 发生后中断没有返回到前一个页面?是否有任何理由使用brk 而不是mmap 进行这种格式化?

【问题讨论】:

  • rodata 中有常量字符串是正常的。 So formatted print seems to perform additional pattern copying from rodata to somewhere else. print 没有理由复制字符串,它无法知道字符串是否为常量,你为什么这么说?所有初始化等都在程序启动时完成
  • printf 有它自己的缓冲。它通常是行缓冲的。
  • 重新“格式化打印似乎执行从rodata到其他地方的额外模式复制”:是的,到标准输出;-)。
  • @St.Antario:格式字符串永远不会发生变异。 printf 逐字节复制到目标,直到它到达第一个 % 字符,然后它使用那里的任何格式说明符格式化下一个参数并继续。一般来说,在任何语言中几乎不可能通过插入数据来更改字符串。
  • @St.Antario:澄清一下,当您说“复制模式”时,它可能暗示该字符串确实是复制了,然后又进行了更改。但是这个函数的任何实现都必须做的是:1)从常量模式字符串中读取一次一个字符和2)写入这个字符或一些格式化值其他地方我>。这个其他地方是直接到输出还是到中间malloc'ed缓冲区,取决于实现。有many "tiny printf" 实现不分配任何临时缓冲区。

标签: c linux printf


【解决方案1】:

brk()(和它的伴侣sbrk())是某种mmap(),专门用于操纵堆大小。由于历史原因,libc也可以直接使用mmap()mremap()

堆随着额外内存的分配而扩展,例如malloc(),这发生在libc内部,例如有足够的空间从格式字符串和参数或许多其他内部事物创建实际字符串(即使用 f* 函数族的缓冲 io 时的输出缓冲区)。

如果堆的某些部分不再使用,它​​通常不会自动释放,主要有两个原因:堆可能是碎片化的,和/或未使用的堆没有低于某个阈值来证明操作是合理的,因为可能很快就会再次需要它。

附带说明:格式字符串本身肯定不会从 ro-section 复制到堆中,这完全没用。但是结果字符串(通常)是建立在堆上的。

【讨论】:

  • the format string itself is certainly not copied from the ro-section to the heap 不修改ro-section 会导致分段错误吗?我刚试过,它确实如此。
  • 这是导致segv 的代码:char *p = "Test %s\n"; printf(p, argv[1]); printf(p, argv[2]); p[5] = 'r'; p[6] = 'r'; for ./bin rr tt。即使修改string-literal是C中的UB,但我们可以编写一个手写的程序集并且修改.rodata无论如何都会导致segfault。
  • @StAntario 你误会了,result stringbuilt 在堆上的,但是 format string 是在此之前没有复制到堆中。这是一个巨大的差异。
【解决方案2】:

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

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

为什么休息时间没有返回 写系统调用发生后的前一个?

同样,如果您很快会要求更多的可能性很高,为什么要免费?

这种格式有什么理由使用brk 而不是mmap

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

旁白:您的问题更多是关于“内存分配策略”而不是“理解 printf”(即上下文)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2012-12-02
    • 1970-01-01
    • 2012-09-24
    • 1970-01-01
    • 2016-02-19
    • 1970-01-01
    • 2023-03-16
    相关资源
    最近更新 更多