C语言printf()函数深入分析
 说 起编程语言,C语言大家再熟悉不过。说起最简单的代码,Helloworld更是众所周知。一条简单的printf语句便可以完成这个简单的功能,可是 printf背后到底做了什么事情呢?可能很多人不曾在意,也或许你比我还要好奇!那我们就聊聊printf背后的故事。
一、printf的代码在哪里?
显然,Helloworld的源代码需要经过编译器编译,操作系统的加载才能正确执行。而编译器包含预编译、编译、汇编和链接四个步骤。
#include<stdio.h>
int main()
{
    printf("Hello World !\n");
    return 0;
}
首先,预编译器处理源代码中的宏,比如#include。预编译结束后,我们发现printf函数的声明。
$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -E -quiet main.c -o main.i
# 1 "main.c"
# 1 "<命令行>"
# 1 "main.c"
...
extern int printf (const char *__restrict __format, ...);
...
int main()
{
 printf("Hello World!\n");
 return 0;
}
然后编译器将高级语言程序转化为汇编代码。
$/usr/lib/gcc/i686-linux-gnu/4.7/cc1 -fpreprocessed -quiet main.i -o main.s
    .file      "main.c"
    .section  .rodata
.LC0:
    .string    "Hello World!"
    .text
    .globl    main
    .type      main, @function
main:
    pushl      %ebp
    movl      %esp,  %ebp
    andl      $-16,  %esp
    subl      $16,  %esp
    movl      $.LC0, (%esp)
    call      puts
    movl      $0,    %eax
    leave
    ret
    .size      main, .-main
...
我 们发现printf函数调用被转化为call puts指令,而不是call printf指令,这好像有点出乎意料。不过不用担心,这是编译器对printf的一种优化。实践证明,对于printf的参数如果是以'\n'结束的纯 字符串,printf会被优化为puts函数,而字符串的结尾'\n'符号被消除。除此之外,都会正常生成call printf指令。
如果我们仍希望通过printf调用"Hello World !\n"的话,只需要按照如下方式修改即可。不过这样做就不能在printf调用结束后立即看到打印字符串了,因为puts函数可以立即刷新输出缓冲区。我们仍然使用puts作为例子继续阐述。
    .section  .rodata
.LC0:
    .string    "hello world!\n"
    ...
    call      printf
...
接下来,汇编器开始工作。将汇编文件转化为我们不能直接阅读的二进制格式——可重定位目标文件,这里我们需要gcc工具包的objdump命令查看它的二进制信息。可是我们发现call puts指令里保存了无效的符号地址。
$as -o main.o main.s
$objdump –d main.o
main.o:    文件格式 elf32-i386
Disassembly of section .text:
00000000 <main>:
  0:  55                    push  %ebp
  1:  89 e5                  mov    %esp,%ebp
  3:  83 e4 f0              and    $0xfffffff0,%esp
  6:  83 ec 10              sub    $0x10,%esp
  9:  c7 04 24 00 00 00 00    movl  $0x0,(%esp)
  10:  e8 fc ff ff ff      call  11 <main+0x11>
  15:  b8 00 00 00 00      mov    $0x0,%eax
  1a:  c9                    leave 
  1b:  c3                    ret
而链接器最终会将puts的符号地址修正。由于链接方式分为静态链接和动态链接两种,虽然链接方式不同,但是不影响最终代码对库函数的调用。我们这里关注printf函数背后的原理,因此使用更易说明问题的静态链接的方式阐述。
gnu编译器$/usr/lib/gcc/i686-linux-gnu/4.7/collect2                  \
    -static -o main                                        \
    /usr/lib/i386-linux-gnu/crt1.o                          \
    /usr/lib/i386-linux-gnu/crti.o                          \
    /usr/lib/gcc/i686-linux-gnu/4.7/crtbeginT.o            \
    main.o                                                  \
    --start-group                                          \
    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc.a                \
    /usr/lib/gcc/i686-linux-gnu/4.7/libgcc_eh.a            \
    /usr/lib/i386-linux-gnu/libc.a                          \
    --end-group                                            \
    /usr/lib/gcc/i686-linux-gnu/4.7/crtend.o                \
    /usr/lib/i386-linux-gnu/crtn.o
$objdump –sd main
Disassembly of section .text:
...
08048ea4 <main>:
 8048ea4:  55                    push  %ebp
 8048ea5:  89 e5                  mov    %esp,%ebp
 8048ea7:  83 e4 f0              and    $0xfffffff0,%esp
 8048eaa:  83 ec 10              sub    $0x10,%esp
 8048ead:  c7 04 24 e8 86 0c 08    movl  $0x80c86e8,(%esp)
 8048eb4:  e8 57 0a 00 00      call  8049910 <_IO_puts>
 8048eb9:  b8 00 00 00 00      mov    $0x0,%eax
 8048ebe:  c9                    leave 
 8048ebf:  c3                    ret
...
静 态链接时,链接器将C语言的运行库(CRT)链接到可执行文件,其中crt1.o、crti.o、crtbeginT.o、crtend.o、 crtn.o便是这五个核心的文件,它们按照上述命令显示的顺序分居在用户目标文件和库文件的两侧。由于我们使用了库函数puts,因此需要库文件 libc.a,而
libc.a与libgcc.a和libgcc_eh.a有相互依赖关系,因此需要使用—start-group和—end-group 将它们包含起来。
链 接后,call puts的地址被修正,但是反汇编显示的符号是_IO_puts而不是puts!难道我们的文件不对吗?当然不是,我们使用readelf命令查看一下 main的符号表。竟然发现puts和_IO_puts这两个符号的性质是等价的!objdump命令只是显示了全局的符号_IO_puts而已。
$readelf main –s
Symbol table '.symtab' contains 2307 entries:
  Num:    Value  Size Type    Bind  Vis      Ndx Name
...
  1345: 08049910  352 FUNC    WEAK  DEFAULT    6 puts
...
  1674: 08049910  352 FUNC    GLOBAL DEFAULT    6 _IO_puts
...
那么puts函数的定义真的是在libc.a里吗?我们需要对此确认。我们将libc.a解压缩,然后全局符号_IO_puts所在的二进制文件,输出结果为ioputs.o。然后查看该文件的符号表。发现ioputs.o定义了puts和_IO_puts符号,因此可以确定ioputs.o就是puts函数的代码文件,且在库文件libc.a内。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。