printf背后的故事

来源:互联网 发布:python运行原理 编辑:程序博客网 时间:2024/04/30 08:27

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函数背后的原理,因此使用更易说明问题的静态链接的方式阐述。

$/usr/lib/gcc/i686-linux-gnu/4.7/collect2                   \

    -static -o main                            

0 0
原创粉丝点击