【问题标题】:Why is mmap done during printfs calls?为什么在 printfs 调用期间完成 mmap?
【发布时间】:2015-09-30 03:54:16
【问题描述】:

为什么 printf() 做一个 sys_mmap() 然后将字符串的内容以块(1024)的形式复制到 sys_write() 的新地址空间?

简单的静态“hello”程序的Strace如下所示。

> gcc -o hello -static hello.c
> strace ./hello


execve("./hello", ["./hello"], [/* 71 vars */]) = 0
uname({sys="Linux", node="Kumar", ...})   = 0
brk(0)                                  = 0x1ce8000
brk(0x1ce91c0)                          = 0x1ce91c0
arch_prctl(ARCH_SET_FS, 0x1ce8880)      = 0
readlink("/proc/self/exe", "/home/admin/hello", 4096) = 18
brk(0x1d0a1c0)                          = 0x1d0a1c0
brk(0x1d0b000)                          = 0x1d0b000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 28), ...}) = 0
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7feda2130000
write(1, "Hello", 5Hello)                    = 5
exit_group(0)                           = ?
+++ exited with 0 +++

rodata的Objdump

> objdump -s --start-address=0x4935a0 ./hello | head -5

./hello:     file format elf64-x86-64

Contents of section .rodata:
 4935a0 01000200 48656c6c 6f006c69 62632d73  ....Hello.libc-s

如果我们在内核级别挂钩 sys_write() 系统调用的地址,我们会看到传递给它的地址是 mmap 的地址区域。考虑到字符串已经在二进制文件的第一个可加载段的 .rodata 部分中存在,这不仅仅是浪费新的地址空间。它与没有写权限等有关吗?那么为什么不让编译器首先将字符串放在 .data 部分(也是可写的)?

更新:

Mmap-ed 地址确实适用于 sys_write(),当我们将字符串变大(比如 ~1500 个字符)时,可以更轻松地验证它。 GDB会确认正在打印的数据地址【注意第二个断点】

(gdb) c
Continuing.
Hello World hhhhhhhhhhalhfafeuirafheuhrgiegieguehguergjkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqwwwwwwwwwwwwwwwwwwwwww     pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuiiiiiiiiiiiiiiiiiiiiiiiiiiiiiwqiuwqiuwiquwiqhchasnvjnavjanvjdanvjdanvjdanjfanvjaddijuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuquweuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuuunnnnnnnnnnnnnnnnnnnnnnnnnnnzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz,,,,,,,,,,,,,,,,,,,,,,
Breakpoint 1, _IO_new_file_write (f=0x6b8300 <_IO_2_1_stdout_>, data=0x7ffff7ffc000, n=706) at fileops.c:1257
1257    {

【问题讨论】:

    标签: linux linux-kernel system elf libc


    【解决方案1】:

    您是否尝试过使用调试器?

    $ gdb /tmp/hello
    ...
    (gdb) b __mmap
    Breakpoint 1 at 0x4152e0    
    (gdb) r
    Starting program: /tmp/hello 
    
    Breakpoint 1, 0x00000000004152e0 in mmap64 ()
    (gdb) bt
    #0  0x00000000004152e0 in mmap64 ()
    #1  0x000000000045d73c in _IO_file_doallocate ()
    #2  0x0000000000401fec in _IO_doallocbuf ()
    #3  0x000000000042ca10 in _IO_new_file_overflow ()
    #4  0x000000000042be9d in _IO_new_file_xsputn ()
    #5  0x000000000040111d in puts ()
    #6  0x00000000004004de in main () at hello.c:4
    (gdb) c
    Continuing.
    Hello, w
    [Inferior 1 (process 4294) exited with code 011]
    

    因此它为FILE* 使用的缓冲输入输出分配内存。请注意,仅使用常量字符串的 printf 将导致 puts 调用,因为 GCC 足够聪明。而puts(string) 实际上是一个fputs(string, stdout),其中stdout 是FILE*

    使用原始写入,但是不会产生这样的行为:

    #include <unistd.h>
    
    int main() {
        write(1, "Hello, w\n", sizeof("Hello, w\n"));
    }
    

    【讨论】:

    • 非常感谢!但我有点没有完全得到解释。你的意思是说mmap-ed地址是stdout (FILE*)?
    • 不,mmap 是从 malloc 使用的,从 fopenprintfputs 调用; `malloc` 用于分配stdout 的数据缓冲区。
    • [查看我的更新] 新映射的区域确实是用于存储数据的。注意当字符串变大时会发生什么。 data 参数指向 MMAP 地址。所以我的问题仍然有效
    • @BasileStarynkevitch - 请稍微解释一下数据缓冲区。你的意思是说为打印创建了一个“固定”的缓冲区空间。我的实际数据分块复制到哪里?
    • @SandhyaKumar FILE* 包含一个缓冲区,用于在对其执行读/写操作时存储数据。 (printf 隐式使用名为 stdout 的全局 FILE*)。您可以将 FILE* 操作为行缓冲、无缓冲或完全缓冲。数据存储在该缓冲区中,并且不会写入底层设备(例如您的终端/控制台),直到如果 FILE* 是行缓冲,或者缓冲区已满,或者您显式调用 fflush();跨度>
    猜你喜欢
    • 1970-01-01
    • 2021-07-02
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-05-23
    • 2019-07-10
    相关资源
    最近更新 更多