C语言内幕--printf的运行过程
C语言内幕--printf的运行过程
学习资源:b站up主:底层技术栈
学过C语言都知道,
printf
是输出函数,也是第一个程序“Hello World”
就看到的函数,那是否 了解过printf的执行过程呢?
这篇主要讲解printf
的执行流程,主要分为两步骤:库函数的调用、字符串的寻址
由于涉及到底层技术,如果有错误,请各位大佬指点
printf运行过程
以以下代码为例:
printf(const char* format, ...);
int main()
{
printf("Hello World\n");
}
printf
的使用主要可以分为两个步骤:
- 对
printf
库函数的调用; - 对常量字符串(这里是“Hello World”)的寻址。
以下本文将从这两个方向进行讲解 💬💬💬💬💬
前置知识
📝 回忆:程序执行流程
程序执行流程可以划分为很多阶段,但是总的来说,可以划分为一下四个阶段:
- 预处理:这一阶段主要是简化代码用的,不如说宏定义替换,注释去除等;
- 编译:将高级语言代码转化为汇编代码;
- 汇编:将汇编代码转化为机器代码,*.o;
- 链接:链接程序所需要的库。
🏪 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会被”填满“,那这个过程是怎么处理的呢?,这个应该需要去看汇编代码,但是汇编代码本人没怎么学过,只停留在能看懂一点的层面上,但是这个...
叫做可变参数列表,我感觉这个还是可以学习一下的。
处理可变参数列表的步骤
- 定义 va_list 类型的变量 :
这个变量用于存储和管理可变参数列表的信息。 - 初始化 va_list :
使用va_start
宏初始化va_list
变量。这个宏需要两个参数:va_list
变量和最后一个固定参数的名称。 - 访问参数 :
使用va_arg
宏从参数列表中获取参数。这个宏需要两个参数:va_list
变量和参数的类型。 - 清理 :
使用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
解释
- va_list args :定义一个
va_list
类型的变量args
,用于存储可变参数列表的信息。 - va_start(args, format) :初始化
args
,使其指向第一个可变参数。format
是最后一个固定参数。 - va_arg(args, type) :从参数列表中获取下一个参数,
type
是参数的类型。 - va_end(args) :清理
args
,释放资源。
printf缓冲区简介
输入输出可以分为两个类别:
- 文件IO:主要是直接与磁盘文件交互
- 标准IO:主要是以 流 为核心的输入输出模式,比如C/C++的输出输出流、C++字符串流,C/C++的文件流
printf缓冲大概流程如下:
其他 :