[转]printf 函数实现的深入剖析

2023-05-16

研究printf的实现,首先来看看printf函数的函数体
int printf(const char *fmt, ...)
{
int i;
char buf[256];
   
     va_list arg = (va_list)((char*)(&fmt) + 4);
     i = vsprintf(buf, fmt, arg);
     write(buf, i);
   
     return i;
    }
    代码位置:D:/~/funny/kernel/printf.c
   
    在形参列表里有这么一个token:...
    这个是可变形参的一种写法。
    当传递参数的个数不确定时,就可以用这种方式来表示。
    很显然,我们需要一种方法,来让函数体可以知道具体调用时参数的个数。
   
    先来看printf函数的内容:
   
    这句:
   
    va_list arg = (va_list)((char*)(&fmt) + 4);
   
    va_list的定义:
    typedef char *va_list
    这说明它是一个字符指针。
    其中的: (char*)(&fmt) + 4) 表示的是...中的第一个参数。
    如果不懂,我再慢慢的解释:
    C语言中,参数压栈的方向是从右往左。
    也就是说,当调用printf函数的适合,先是最右边的参数入栈。
    fmt是一个指针,这个指针指向第一个const参数(const char *fmt)中的第一个元素。
    fmt也是个变量,它的位置,是在栈上分配的,它也有地址。
    对于一个char *类型的变量,它入栈的是指针,而不是这个char *型变量。
    换句话说:
    你sizeof(p) (p是一个指针,假设p=&i,i为任何类型的变量都可以)
    得到的都是一个固定的值。(我的计算机中都是得到的4)
    当然,我还要补充的一点是:栈是从高地址向低地址方向增长的。
    ok!
    现在我想你该明白了:为什么说(char*)(&fmt) + 4) 表示的是...中的第一个参数的地址。
   
    下面我们来看看下一句:
     i = vsprintf(buf, fmt, arg);
   
    让我们来看看vsprintf(buf, fmt, arg)是什么函数。 
    
   


int vsprintf(char *buf, const char *fmt, va_list args)   

   {   

    char* p;   

    char tmp[256];   

    va_list p_next_arg = args;   

     

    for (p=buf;*fmt;fmt++) {   

    if (*fmt != '%') {   

    *p++ = *fmt;   

    continue;   

    }   

     

    fmt++;   

     

    switch (*fmt) {   

    case 'x':   

    itoa(tmp, *((int*)p_next_arg));   

    strcpy(p, tmp);   

    p_next_arg += 4;   

    p += strlen(tmp);   

    break;   

    case 's':   

    break;   

    default:   

    break;   

    }   

    }   

     

    return (p - buf);   

   }   
      
    我们还是先不看看它的具体内容。
    想想printf要左什么吧
    它接受一个格式化的命令,并把指定的匹配的参数格式化输出。
   
    ok,看看i = vsprintf(buf, fmt, arg);
     vsprintf返回的是一个长度,我想你已经猜到了:是的,返回的是要打印出来的字符串的长度
    其实看看printf中后面的一句:write(buf, i);你也该猜出来了。
    write,顾名思义:写操作,把buf中的i个元素的值写到终端。
   
    所以说:vsprintf的作用就是格式化。它接受确定输出格式的格式字符串fmt。用格式字符串对个数变化的参数进行格式化,产生格式化输出。
    我代码中的vsprintf只实现了对16进制的格式化。
   
    你只要明白vsprintf的功能是什么,就会很容易弄懂上面的代码。
   
    下面的write(buf, i);的实现就有点复杂了
   
    如果你是os,一个用户程序需要你打印一些数据。很显然:打印的最底层操作肯定和硬件有关。
    所以你就必须得对程序的权限进行一些限制:
   
    让我们假设个情景:
    一个应用程序对你说:os先生,我需要把存在buf中的i个数据打印出来,可以帮我么?
    os说:好的,咱俩谁跟谁,没问题啦!把buf给我吧。
   
    然后,os就把buf拿过来。交给自己的小弟(和硬件操作的函数)来完成。
    只好通知这个应用程序:兄弟,你的事我办的妥妥当当!(os果然大大的狡猾 ^_^)
    这样 应用程序就不会取得一些超级权限,防止它做一些违法的事。(安全啊安全)
   
    让我们追踪下write吧:
   
    write:
     mov eax, _NR_write
     mov ebx, [esp + 4]
     mov ecx, [esp + 8]
     int INT_VECTOR_SYS_CALL
   
    位置:d:~/kernel/syscall.asm
   
    这里是给几个寄存器传递了几个参数,然后一个int结束
   
    想想我们汇编里面学的,比如返回到dos状态:
    我们这样用的
   
    mov ax,4c00h
    int 21h
   
    为什么用后面的int 21h呢?
    这是为了告诉编译器:号外,号外,我要按照给你的方式(传递的各个寄存器的值)变形了。
    编译器一查表:哦,你是要变成这个样子啊。no problem!
   
    其实这么说并不严紧,如果你看了一些关于保护模式编程的书,你就会知道,这样的int表示要调用中断门了。通过中断门,来实现特定的系统服务。
   
    我们可以找到INT_VECTOR_SYS_CALL的实现:
    init_idt_desc(INT_VECTOR_SYS_CALL, DA_386IGate, sys_call, PRIVILEGE_USER);
   
    位置:d:~/kernel/protect.c
   
    如果你不懂,没关系,你只需要知道一个int INT_VECTOR_SYS_CALL表示要通过系统来调用sys_call这个函数。(从上面的参数列表中也该能够猜出大概)
   
    好了,再来看看sys_call的实现:
    sys_call:
     call save
   
     push dword [p_proc_ready]
   
     sti
   
     push ecx
     push ebx
     call [sys_call_table + eax * 4]
     add esp, 4 * 3
   
     mov [esi + EAXREG - P_STACKBASE], eax
   
     cli
   
     ret
   
   
    位置:~/kernel/kernel.asm
   
    一个call save,是为了保存中断前进程的状态。
    靠!
    太复杂了,如果详细的讲,设计到的东西实在太多了。
    我只在乎我所在乎的东西。sys_call实现很麻烦,我们不妨不分析funny os这个操作系统了
    先假设这个sys_call就一单纯的小女孩。她只有实现一个功能:显示格式化了的字符串。
   
    这样,如果只是理解printf的实现的话,我们完全可以这样写sys_call:
    sys_call:
    
     ;ecx中是要打印出的元素个数
     ;ebx中的是要打印的buf字符数组中的第一个元素
     ;这个函数的功能就是不断的打印出字符,直到遇到:'\0'
     ;[gs:edi]对应的是0x80000h:0采用直接写显存的方法显示字符串
     xor si,si
     mov ah,0Fh
     mov al,[ebx+si]
     cmp al,'\0'
     je .end
     mov [gs:edi],ax
     inc si
    loop:
     sys_call
   
    .end:
     ret
    
   
    ok!就这么简单!
    恭喜你,重要弄明白了printf的最最底层的实现!
   
   
    如果你有机会看linux的源代码的话,你会发现,其实它的实现也是这种思路。
    freedos的实现也是这样
    比如在linux里,printf是这样表示的:
   
    static int printf(const char *fmt, ...)
    {
     va_list args;
     int i;
   
     va_start(args, fmt);
     write(1,printbuf,i=vsprintf(printbuf, fmt, args));
     va_end(args);
     return i;
    }
   
     va_start
     va_end 这两个函数在我的blog里有解释,这里就不多说了
   
    它里面的vsprintf和我们的vsprintf是一样的功能。
    不过它的write和我们的不同,它还有个参数:1
    这里我可以告诉你:1表示的是tty所对应的一个文件句柄。
    在linux里,所有设备都是被当作文件来看待的。你只需要知道这个1就是表示往当前显示器里写入数据
   
    在freedos里面,printf是这样的:
   
     int VA_CDECL printf(const char *fmt, ...)
    {
     va_list arg;
     va_start(arg, fmt);
     charp = 0;
     do_printf(fmt, arg);
     return 0;
    }
   
    看起来似乎是do_printf实现了格式化和输出。
    我们来看看do_printf的实现:
    STATIC void do_printf(CONST BYTE * fmt, va_list arg)
    {
     int base;
     BYTE s[11], FAR * p;
     int size;
     unsigned char flags;
   
     for (;*fmt != '\0'; fmt++)
     {
     if (*fmt != '%')
     {
     handle_char(*fmt);
     continue;
     }
   
     fmt++;
     flags = RIGHT;
   
     if (*fmt == '-')
     {
     flags = LEFT;
     fmt++;
     }
   
     if (*fmt == '0')
     {
     flags |= ZEROSFILL;
     fmt++;
     }
   
     size = 0;
     while (1)
     {
     unsigned c = (unsigned char)(*fmt - '0');
     if (c > 9)
     break;
     fmt++;
     size = size * 10 + c;
     }
   
     if (*fmt == 'l')
     {
     flags |= LONGARG;
     fmt++;
     }
   
     switch (*fmt)
     {
     case '\0':
     va_end(arg);
     return;
   
     case 'c':
     handle_char(va_arg(arg, int));
     continue;
   
     case 'p':
     {
     UWORD w0 = va_arg(arg, unsigned);
     char *tmp = charp;
     sprintf(s, "%04x:%04x", va_arg(arg, unsigned), w0);
     p = s;
     charp = tmp;
     break;
     }
   
     case 's':
     p = va_arg(arg, char *);
     break;
   
     case 'F':
     fmt++;
     /* we assume %Fs here */
     case 'S':
     p = va_arg(arg, char FAR *);
     break;
   
     case 'i':
     case 'd':
     base = -10;
     goto lprt;
   
     case 'o':
     base = 8;
     goto lprt;
   
     case 'u':
     base = 10;
     goto lprt;
   
     case 'X':
     case 'x':
     base = 16;
   
     lprt:
     {
     long currentArg;
     if (flags & LONGARG)
     currentArg = va_arg(arg, long);
     else
     {
     currentArg = va_arg(arg, int);
     if (base >= 0)
     currentArg = (long)(unsigned)currentArg;
     }
     ltob(currentArg, s, base);
     p = s;
     }
     break;
   
     default:
     handle_char('?');
   
     handle_char(*fmt);
     continue;
   
     }
     {
     size_t i = 0;
     while(p[i]) i++;
     size -= i;
     }
   
     if (flags & RIGHT)
     {
     int ch = ' ';
     if (flags & ZEROSFILL) ch = '0';
     for (; size > 0; size--)
     handle_char(ch);
     }
     for (; *p != '\0'; p++)
     handle_char(*p);
   
     for (; size > 0; size--)
     handle_char(' ');
     }
     va_end(arg);
    }
   
   
    这个就是比较完整的格式化函数
    里面多次调用一个函数:handle_char
    来看看它的定义:
    STATIC VOID handle_char(COUNT c)
    {
     if (charp == 0)
     put_console(c);
     else
     *charp++ = c;
    }
   
    里面又调用了put_console
    显然,从函数名就可以看出来:它是用来显示的
    void put_console(int c)
    {
     if (buff_offset >= MAX_BUFSIZE)
     {
     buff_offset = 0;
     printf("Printf buffer overflow!\n");
     }
     if (c == '\n')
     {
     buff[buff_offset] = 0;
     buff_offset = 0;
    #ifdef __TURBOC__
     _ES = FP_SEG(buff);
     _DX = FP_OFF(buff);
     _AX = 0x13;
     __int__(0xe6);
    #elif defined(I86)
     asm
     {
     push ds;
     pop es;
     mov dx, offset buff;
     mov ax, 0x13;
     int 0xe6;
     }
    #endif
     }
     else
     {
     buff[buff_offset] = c;
     buff_offset++;
     }
    }
   
   
    注意:这里用递规调用了printf,不过这次没有格式化,所以不会出现死循环。
   
    好了,现在你该更清楚的知道:printf的实现了
   
    现在再说另一个问题:
    无论如何printf()函数都不能确定参数...究竟在什么地方结束,也就是说,它不知
    道参数的个数。它只会根据format中的打印格式的数目依次打印堆栈中参数format后面地址
    的内容。
   
    这样就存在一个可能的缓冲区溢出问题。。。

转载于:https://www.cnblogs.com/pianist/p/3315801.html

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

[转]printf 函数实现的深入剖析 的相关文章

  • 机器人ROS系统学习随笔->2 RPLIDAR激光雷达使用

    一 驱动的安装 SLAMTEC官网下载激光雷达资料 xff1a http www slamtec com 本人用的是RPLIDAR A1的激光雷达 在下载界面下载相应的资料 xff0c sdk与固件及其他 二 安装 1 建立工作空间并编译
  • 蓝牙飞控数传套装(适合APM/Pixhaw/Pixhack/Pixhawk2飞控)

    SSC FK BL900是迅瞻电子初创的一款蓝牙 43 射频模块一体的飞控数传套装电台 xff0c 它内嵌入Digi的900HP模块和蓝牙模块 xff0c 对外提供XT60的电池供电接口 xff0c 支持5 28V的宽电压工作 xff0c
  • VC++ 编译过程

    一 前言 一开始编译C 43 43 代码的时候可能会对编译的错误觉得很难理解 xff0c 搞不清楚究竟是哪里错了 了解编译过程 xff0c 能够更好的处理编译错误 二 名词解释 编译单元 xff1a 当一个c或cpp文件在编译时 xff0c
  • PLC的ST编程方式--文本编程,简洁啊

    一 ST语言介绍 发现网上PLC的ST编程资料极少 不过 xff0c 道理也很简单 xff0c 因为做PLC的基本都是电气出身 xff0c 梯形图类似于继电器逻辑 xff0c 比较接近他们的习惯 ST属于文本编程 xff0c 符合程序员的习
  • stm32气压传感器 带探头的_基于STM32的真空度的测量装置的设计

    阮敬华 43 张忠伟 43 徐沛 43 李雪莲 43 阚茹男 摘 要 xff1a 采用USART串口通信 SPI通信作为核心技术 xff0c 通过气压和温湿度传感器对环境的真空值 温湿度参数进行数据采集 xff0c 将采集到的数据通过SD卡
  • Python 迭代器

    一 迭代器 迭代是访问集合元素的一种方式 迭代器是一个可以记住遍历的位置的对象 迭代器对象从集合的第一个元素开始访问 xff0c 直到所有的元素被访问完结束 迭代器只能往前不会后退 1 1 判断一个对象是否可迭代 可以使用 isinstan
  • EasyPlayerPro Windows播放器进行本地对讲喊话音频采集功能实现

    需求 在安防行业应用中 xff0c 除了在本地看到摄像机的视频和进行音频监听外 xff0c 还有一个重要的功能 xff0c 那就是对讲 EasyPlayerPro win为了减轻二次开发者的工作量 xff0c 将本地音频采集也进行了集成 x
  • 大端法、小端法、网络字节序

    关于字节序 大端法 小端法 的定义 UNXI网络编程 定义 xff1a 术语 小端 和 大端 表示多字节值的哪一端 小端或大端 存储在该值的起始地址 小端存在起始地址 xff0c 即是小端字节序 xff1b 大端存在起始地址 xff0c 即
  • 阿里云安全肖力:云原生安全构筑下一代企业安全架构

    34 数字经济的发展驱动越来越多的企业上云 xff0c 每个企业都会基于云原生安全能力构筑下一代企业安全架构 xff0c 完成从扁平到立体式架构的进化 xff0c 届时云原生安全技术红利也将加速释放 xff01 9月27日 xff0c 阿里
  • Vue 自定义按键修饰符

    如点击F2 触发某个事件 lt input type 61 34 button 34 name 61 34 34 id 61 34 34 value 61 34 添加 34 64 keyup f2 61 34 add 34 gt 自定义全局
  • android Studio keytool' 不是内部或外部命令,也不是可运行的程序 或批处理文件

    android Studio keytool 39 不是内部或外部命令 xff0c 也不是可运行的程序 或批处理文件 遇到这个问题好久了 xff0c 一直没解决今天搜集了大量的资料 xff0c 有的说什么Java没配置好 xff0c 不是扯
  • java -jar 运行springboot项目时内存设置

    java Xms64m JVM启动时的初始堆大小 Xmx128m 最大堆大小 Xmn64m 年轻代的大小 xff0c 其余的空间是老年代 XX MaxMetaspaceSize 61 128m XX CompressedClassSpace
  • Jupyter notebook 读取文件的问题

    Jupyter notebook只能打开当前目录下的数据集 xff08 txt CSV等 xff09 xff0c 所以需要把数据集倒导入到当前目录下 xff0c 导入的方法是 1 文件不大时 直接上传文件 pd read csv读取 2 文
  • MIUI目前为止最简单安装谷歌服务框架教程

    安装谷歌服务框架方法有很多 xff0c 比如用第三方 rec卡刷gapps包 用第三方工具安装 然而这些对于新手来说还是比较难的 xff01 我今天说的方法可以说是最简单的 xff1a 1 不需要修改文件 xff1b 2 不需要借助第三方软
  • 用docker安装emby的详细步骤

    搭建个人存储服务器NAS xff0c 媒体播放器少不了 群晖自带的Video Station据说没有Emby Plex好用 Plex是收费的 xff0c Emby是开源的 本文看似很长 实际上有一半篇幅是关于设置 亲测 xff0c 那些设置
  • ubuntu 下通过ftp命令下载文件

    连接 ftp 192 168 180 2 Connected to 192 168 180 2 Name 192 168 180 2 rivsidn admin Password 获取远端文件 ftp gt get test pdf loc
  • pycharm安装到32位操作系统

    在32位操作系统中安装pycharm过程中发现的一些问题 首先是下载了最新版本的pycharm安装后打开 xff0c 弹出未发现可执行的文件 xff0c 然后想到了其他的办法 1 下载最新版本的pycharm不能直接运行 xff0c 因此可
  • 遗忘的初境

    农历八月深夜的黎明朦胧昏黄 xff0c 林立在山雾中的坟地貌似并不太平 xff0c 白露轻风清不走香纸白烛烟气 这层孽障将要带着旧尘怨气破土而出 筹谋着一场突击 这支躲藏在大山幽月中的乡村透着光亮 一双双眼睛凝视着挂在横梁上昏暗的煤灯 泛白
  • 2019-2020-1 20175313 《信息安全系统设计基础》第二周学习总结

    目录 一 教材学习内容总结二 教材学习中的问题和解决过程三 心得体会四 学习进度条五 参考资料 一 教材学习内容总结 第二章内容 基本知识 重点和难点 二 教材学习中的问题和解决过程 问题1 xff1a 对教材49页上的代码进行验证时 xf
  • centos下通过conda安装pytorch

    一 安装anaconda anaconda安装简单 xff0c 只要确定自己的系统即可 xff0c 具体安装请参考这里 二 确定自己的系统版本 我的是centos cat etc redhat release 查看linux系统方法 xff

随机推荐

  • 论文中表格跨页处理

    https www jianshu com p 96a370384459 上面是跨页表格中带有表头的 我希望的样子是跨页不带表头并且第一张表有下横线 xff0c 第二张表有上横线 xff0c 第二张表头有 表XX XX xff08 续表 x
  • Aria2在Windows上如何安装配置使用

    一 下载所需的软件二 安装与使用三 Aria2的额外内容四 Aria2的使用五 Aria2与其它插件配合使用 一 下载所需的软件 可以从一下地址获取最新版本 GitHub xff1a https github com aria2 aria2
  • 银河麒麟操作系统常用问题及解决方法

    银河麒麟操作系统作为国内安全等级较高的国产操作系统 xff0c 很多用户都想自行安装体验 xff0c 自行安装很有可能遇到一些问题 xff0c 现在奉上可能遇到的问题及解决方案 xff0c 用户可自行查阅处理 xff01 xff01 常用问
  • 几种方式加速网页视频播放速度

    现在有不少视频网站 自带了播放加速功能 例如油管 bilibili 慕课等等 节省了很多看视频的时间 特别是看一些技术教程类的视频 不管是念ppt还是手把手演示 在自己付费的一些网站中 一些是自带播放器不支持视频加速的 因为已经被加速惯坏
  • dataframe指定位置插入行

    1 loc 函数可以定位行后 xff0c 并直接赋值插入 如下可见loc函数对直接改变原来行的值 df 61 pd DataFrame 39 动物 39 39 狗 39 39 猫 39 39 兔 39 39 数量 39 3 4 6 prin
  • Linux系统备份与还原

    Linux系统备份与还原 1 整盘备份与还原 1 1 记住几个这里要经常用到操作1 2 整盘克隆的方法 2 推荐 非整盘克隆的方法 2 1 备份系统2 2 还原系统 1 整盘备份与还原 1 1 记住几个这里要经常用到操作 查看存储设备 xf
  • VS2013如何添加LIb库及头文件的步骤

    在VS工程中 xff0c 添加c c 43 43 工程中外部头文件及库的基本步骤 xff1a 1 添加工程的头文件目录 xff1a 工程 属性 配置属性 c c 43 43 常规 附加包含目录 xff1a 加上头文件存放目录 2 添加文件引
  • composer.json和composer.lock到底是什么以及区别?

    composer方文档 xff1a https docs phpcomposer com 04 schema html 我们在做项目的时候 xff0c 总是要安装一些依赖 composer给我们提供了很多方便 直接运行composer in
  • VUE npm run build的项目出现跨域请求的问题npm run dev没有这个问题

    报错信息 Access to XMLHttpRequest at 39 http platformapi test lih elearning cn api v1 login 39 from origin 39 http www vue c
  • PHP message:filesize(): stat failed for 错误

    PHP message filesize stat failed for 错误 message filesize stat failed for F s2017 SinaImgUpload SinaImgUpload bin Debug T
  • PHP 开发者如何做好密码保护 & Laravel 底层密码存储和验证实现

    随着在线攻击的增多 xff0c 密码安全越来越重要 作为开发者我们要担负起安全管理 计算哈希和存储用户密码的责任 xff0c 不管应用是简单的游戏还是绝密商业文件的仓库 xff0c 都要做到这一点 PHP内置了一些工具 xff0c 让保护密
  • python 列表中的数字转为字符串

    1 list1 61 1 2 3 4 5 list1 61 str x for x in list1 2 list1 61 1 2 3 4 5 list1 61 list map str list1 转载于 https www cnblog
  • 恩尼格码的发明和破解

    恩尼格码是二战德军所采用的电子加密机械 其基本组成可以分为三部分 xff1a 键盘 xff0c 转子和显示器 键盘一共具有26个键 xff0c 类似于今天的计算机键盘 xff08 显示起见 xff0c 省略为6个 xff09 转子实际上有3
  • 得力D991CN Plus计算器评测(全程对比卡西欧fx-991CN X)

    得力在2018年出了一款高仿卡西欧fx 991CN X中文版的计算器 xff0c 型号为D991CN Plus xff0c 在实现同样功能的前提下 xff0c 网销价格是卡西欧的三分之一左右 但是这款计算器与卡西欧正版计算器差距是大是小 x
  • 网络机顶盒固件提取、编辑和打包

    提取的话 xff0c 这边有一篇文章可能有用https www znds com tv 649509 1 1 html 首先下载下载来的固件一般是img格式的 xff0c 可以到hdpfans com这个连接下载一个androidTool的
  • 从微软官网下载VisualStudio离线包

    首先要下载VS安装器 xff0c 有社区版 专业版 企业版 xff0c 此处以社区版为例 不同版本的VS xff0c 只需要将cmd命令中exe名称换掉就行了 仅C VisualStudioSetup exe layout D Layout
  • 基于STM32之UART串口通信协议(一)详解

    一 前言 1 简介 写的这篇博客 xff0c 是为了简单讲解一下UART通信协议 xff0c 以及UART能够实现的一些功能 xff0c 还有有关使用STM32CubeMX来配置芯片的一些操作 xff0c 在后面我会以我使用的STM32F4
  • Java内存分配及值、引用的传递

    关于堆栈的内容网上已经有很多资料了 xff0c 这是我找的加上自己理解的一篇说明文 xff1a 一 内存区域类型 1 寄存器 xff1a 最快的存储区 由编译器根据需求进行分配 我们在程序中无法控制 xff1b 2 栈 xff1a 存放基本
  • NEMA-0183(GPRMC GPGGA)详细解释

    NEMA 0183 GPRMC GPGGA 详细解释 nmea数据如下 xff1a GPGGA 121252 000 3937 3032 N 11611 6046 E 1 05 2 0 45 9 M 5 7 M 0000 77 GPRMC
  • [转]printf 函数实现的深入剖析

    研究printf的实现 xff0c 首先来看看printf函数的函数体 int printf const char fmt int i char buf 256 va list arg 61 va list char amp fmt 43