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为任何类型的变量都可以)得到的都是一个固定的值。(32位计算机中都是得到的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后面地址 的内容。 这样就存在一个可能的缓冲区溢出问题。。。


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

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

随机推荐

  • [BPU部署教程] 教你搞定YOLOV5部署 (版本: 6.2)

    最近一些人问我怎么在BPU上部署yolov5 xff0c 因为之前的博客 BPU部署教程 一文带你轻松走出模型部署新手村介绍的网络都是基于Caffe的 xff0c 自己的网络都是基于pytorch的 xff0c 所以遇到了很多坑 鉴于这些需
  • 在旭日X3派开发板上使用USB Wifi来提高网络速度

    对于我来说 xff0c 开发板自带的wifi模块速度不是很满意 xff0c 下载或者传文件啥的最多也就2M s 而且 xff0c 在之前测评博客 首发 多方位玩转 地平线新发布AIoT开发板 旭日X3派 Sunrise x3 Pi 插电 x
  • 体验极速——在旭日X3派上使用双频1300M USB无线网卡

    上一篇博客 在旭日X3派开发板上使用USB Wifi来提高网络速度 提供一种低成本 xffe5 20的USB Wifi解决方案 这个模块的传输速度在10M s以内 xff0c 尽管满足正常的开发需求 xff0c 但在项目应用时 xff0c
  • linux深度学习服务器搭建——CUDA与cuDNN的选择与安装

    前言 本文章参考实验室师妹的文章Ubuntu14 04 43 CUDA8 0 43 Opencv3 1 43 Anaconda2 43 Caffe安装 xff0c 最近安装最新版时候遇到不少坑 xff0c 下面就介绍下如何去安装CUDA和c
  • 卷积神经网络处理Cifar-10分类数据集

    Cifar 10分类数据集 Cifar 10分类数据集简介 CIFAR 10数据集由10个类的60000个32x32彩色图像组成 xff0c 每个类有6000个图像 有50000个训练图像和10000个测试图像 数据集分为五个训练批次和一个
  • STM32和ROS串口通信常见问题汇总答疑

    STM32和ROS串口通信常见问题汇总答疑 大家好 我是白茶清欢 最近看了博客文章 stm32和ros的串口通信 有很多问题的评论 这里汇总回复一下 问题1 运行时报错如下 rosrun topic example publish node
  • 无人机导航中常见的坐标系

    无人机导航中常见的坐标系包括 xff1a 地球中心坐标系 ECEF EarthCenteredEarthFixedCoordinateSystem xff0c ECEF WGS 84大地坐标系 WorldGeodeticCoordinate
  • DEVC++(1)单文件实现重载运算符的十六进制数类

    本文运用DEVC 43 43 软件 xff0c 通过C 43 43 类的定义和重载运算符来实现十六进制数类的运算操作 xff0c 代码以单文件的方式来构建 题目描述如下 xff1a 设计1 4位的无符号十六进制数据类class HEX 可以
  • Jetson Nano – UART

    There is a UART on the J41 GPIO Header of the NVIDIA Jetson Nano Developer Kit Useful when you need a little bit of extr
  • 关于thinkbook14+以及16+安装ubuntu22.04 LTS后WIFI问题

    首先 xff0c 介绍一下电脑配置 购买的是2022款Thinkbook14 43 R7 6800H锐龙核显版 Intel的也一样可以用 1 设置bios 点击开机键后疯狂按F1打开BIOS xff0c 将security boot设置为d
  • RTC可调节时钟

    此代码只可显示小时 分钟 xff0c 大家可以参考并写出秒甚至年月日的相关操作代码 rtc h ifndef RTC H define RTC H 时间结构体 typedef struct vu8 hour vu8 min vu8 sec
  • C语言中关于float、double、long double精度及数值范围理解

    转自 xff1a http blog sina com cn s blog 6ebd49350101gdgo html IEEE754 浮点数的表示方法 C 语言里对 float 类型数据的表示范围为 3 4 10 38 xff5e 43
  • 移动机器人系列----->前言

    移动机器人系列 gt 前言 准备开始写移动机器人相关的文章 初步的想法是做一个能够实现室内自主定位导航的移动机器人 xff0c 通过写这一系列的文章来记录和探讨学习过程中的问题 xff08 这是一篇立flag的文章 xff0c 希望不会立马
  • 移动机器人系列----->框架开篇

    移动机器人系列 gt 框架开篇 1 xff1a 框架浅聊 这次项目的重点是实现移动机器人的定位建图以及路径规划算法 xff0c 底盘硬件部分不过多的进行展开 下图是项目简单的硬件框架示意图 xff08 1 xff09 为节约时间 xff0c
  • 移动机器人系列----->树莓派4B测试ORB-SLAM2

    移动机器人系列 gt 树莓派4B运行测试ORB SLAM2 注 测试平台为树莓派4B xff08 8 43 32G xff09 1 xff1a 树莓派安装ubuntu18 04及编译ORB SLAM2 1 1 xff1a 树莓派上安装ubu
  • PX4飞控之自主返航(RTL)控制逻辑

    本文基于PX4飞控1 5 5版本 xff0c 分析导航模块中自护返航模式的控制逻辑和算法 自主返航模式和导航中的其他模式一样 xff0c 在Navigator main函数中一旦触发case vehicle status s NAVIGAT
  • MATLAB的MCC命令

    mcc函数将matlab的m文件转化为c c 43 43 文件 mcc函数命令格式 xff1a mcc option fun fun2 mexfile1 mlifile 函数作用 xff1a 将matlab程序中的fun m转化为fun c
  • STM32 | 分享自定义协议的一些典型例子

    1024G 嵌入式资源大放送 xff01 包括但不限于C C 43 43 单片机 Linux等 关注微信公众号 嵌入式大杂烩 xff0c 回复1024 xff0c 即可免费获取 xff01 上次分享的 分享一个很酷的上位机软件 中 xff0
  • 基于TCP/IP和UDP协议的socket编程结构解析

    1 套接字 xff08 socket xff09 socket起源于Unix xff0c 而Unix Linux基本哲学之一就是 一切皆文件 xff0c 都可以用 打开open gt 读写write read gt 关闭close 模式来操
  • printf 函数实现的深入剖析[转载]

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