问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

C语言内幕--printf的运行过程

创作时间:
作者:
@小白创作中心

C语言内幕--printf的运行过程

引用
CSDN
1.
https://m.blog.csdn.net/weixin_74085818/article/details/143167722

学习资源:b站up主:底层技术栈

学过C语言都知道,printf 是输出函数,也是第一个程序“Hello World”就看到的函数,那是否 了解过printf的执行过程呢?
这篇主要讲解printf的执行流程,主要分为两步骤:库函数的调用、字符串的寻址
由于涉及到底层技术,如果有错误,请各位大佬指点

printf运行过程

以以下代码为例:

printf(const char* format, ...);

int main()
{
    printf("Hello World\n");
}

printf的使用主要可以分为两个步骤:

  1. printf库函数的调用;
  2. 对常量字符串(这里是“Hello World”)的寻址。

以下本文将从这两个方向进行讲解 💬💬💬💬💬

前置知识

📝 回忆:程序执行流程

程序执行流程可以划分为很多阶段,但是总的来说,可以划分为一下四个阶段:

  1. 预处理:这一阶段主要是简化代码用的,不如说宏定义替换,注释去除等;
  2. 编译:将高级语言代码转化为汇编代码;
  3. 汇编:将汇编代码转化为机器代码,*.o;
  4. 链接:链接程序所需要的库。

🏪 ABI寄存器

ABI寄存器是汇编给C语言提供的接口

🎡 程序分段

💪 程序分段目的:为了更好的权限管理,权限主要为:读R,写W,执行E。

在C语言中,程序主要分为以下几个段:

段名
存储属性
内存分配
代码段.text
存放可执行程序的指令,存储态和运行态都有
静态
数据段.data
存放已初始化(非零初始化的全局变量和静态局部变量)的数据,存储态和运行态都有
静态
bss段.bss
存放未初始化(未初始化或者0初始化的全局变量和静态局部变量)的数据,存储态和运行态都有
静态
堆heap
动态分配内存,需要通过malloc手动申请,free手动释放,适合大块内存。容易造成内存泄漏和内存碎片。运行态才有。
动态
栈stack
存放函数局部变量和参数以及返回值,函数返回后,由操作系统立即回收。栈空间不大,使用不当容易栈溢出。运行态才有
静态

对库函数的调用与字符串寻址

🌇 printf库函数存放的地方

当我们在使用printf的时候,printf是储存在**代码段(.text)**中,当然具体实现还是在链接的时候链接printf的源代码上实现。

🐾 printf函数的权限

拥有读R、执行E的权限,不具备写的权限

🚯 既然程序分段,那字符串“Hello World\n”存放到哪里呢?

常量字符串储存在 常量字符串区 ,权限是.rodata,只允许读,不允许写,.rodata储存const修饰的常量、字符串常量、#define定义的宏等。

🌉 printf与字符串储存在不同区域,那怎么链接呢?

通过偏移量连接来实现字符串寻址,即 使用字符串的代码位置,到储存该字符串的内存位置之间有一个“偏移量”,叫做“offset”,也叫重定位符号。

🏀 所以,到这里,从汇编的角度来看,可以分为两部

leaq offset(%rip), rdi
call printf
  • leaq 指令 :计算相对于当前指令指针的地址。
  • call 指令 :调用函数。
  • offset :偏移量

✍️ 编译器

编译器作用就是将语言,比如说C、C++,也可以比喻成人类的高级语言, 转化为机器语言,0、1这些

🔗 连接器

连接器的作用就是,填写机器代码中的“全局的内存地址”,比如说:printf的填补

可变参数函数的基本概念

printf的函数定义新式:

int printf(const char *format, ...);

这里,const char *format 是格式化字符串,... 表示可变参数列表。

如:

printf("age is: %d\n", 18);

在编译的时候,%d会被”填满“,那这个过程是怎么处理的呢?,这个应该需要去看汇编代码,但是汇编代码本人没怎么学过,只停留在能看懂一点的层面上,但是这个...叫做可变参数列表,我感觉这个还是可以学习一下的。

处理可变参数列表的步骤

  1. 定义 va_list 类型的变量
    这个变量用于存储和管理可变参数列表的信息。
  2. 初始化 va_list
    使用 va_start 宏初始化 va_list 变量。这个宏需要两个参数:va_list 变量和最后一个固定参数的名称。
  3. 访问参数
    使用 va_arg 宏从参数列表中获取参数。这个宏需要两个参数:va_list 变量和参数的类型。
  4. 清理
    使用 va_end 宏清理 va_list 变量,以便释放资源。

示例

下面是一个简单的示例,展示了如何使用这些宏来实现一个类似于 printf 的函数(以下代码是AI生成,因为本人知道有这个知识点,但是从来没有用过,这里就借助ai了):

#include <stdio.h>
#include <stdarg.h>  // 头文件

void my_printf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    // 遍历格式化字符串
    for (const char *p = format; *p != '\0'; p++) {
        if (*p == '%') {
            p++;  // 跳过 '%' 符号
            switch (*p) {
                case 'd':
                    printf("%d", va_arg(args, int));
                    break;
                case 's':
                    printf("%s", va_arg(args, char *));
                    break;
                case 'c':
                    printf("%c", va_arg(args, int));  // 注意:字符以 int 类型传递
                    break;
                case 'f':
                    printf("%f", va_arg(args, double));
                    break;
                default:
                    putchar(*p);  // 如果是其他字符,直接输出
                    break;
            }
        } else {
            putchar(*p);
        }
    }

    va_end(args);
}

int main() {
    my_printf("Hello, %s! Your age is %d and height is %.2f.\n", "Alice", 30, 165.5);
    return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

解释

  1. va_list args :定义一个 va_list 类型的变量 args,用于存储可变参数列表的信息。
  2. va_start(args, format) :初始化 args,使其指向第一个可变参数。format 是最后一个固定参数。
  3. va_arg(args, type) :从参数列表中获取下一个参数,type 是参数的类型。
  4. va_end(args) :清理 args,释放资源。

printf缓冲区简介

输入输出可以分为两个类别:

  • 文件IO:主要是直接与磁盘文件交互
  • 标准IO:主要是以 为核心的输入输出模式,比如C/C++的输出输出流、C++字符串流,C/C++的文件流

printf缓冲大概流程如下:

其他

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号