C语言函数之可变参数原理:va_start、va_arg及va_end

2023-10-31

说到C语言函数可变参数,我们最先想到的可能就是printf、scanf、printk了。在Linux-2.6.24.7内核源码里,printk函数原型如下:

asmlinkage int printk(const char *fmt, ...)

 
    asmlinkage表示通过堆栈传递参数。gcc编译器在汇编过程中调用c语言函数时传递参数有两种方法:一种是通过堆栈,另一种是通过寄存器。缺省时采用寄存器,假如你要在你的汇编过程中调用c语言函数,并且想通过堆栈传递参数,你定义的c函数时要在函数前加上宏asmlinkage。
    从printk函数原型可知,printk除了接收一个固定参数fmt外,后面的参数用...表示。在C/C++语言中,...表示可以接收定数量的参数(0或0以上个参数)。那么printk到底是怎么支持可变参数的呢?
    我们先来看几个宏:va_list、va_start、va_arg及va_end(va的意思应该是variable),在Linux-2.6.24.7内核源码里,其定义(内核里的定义与C语言库的定义是类似的)如下
 

/*
  * Use local definitions of C library macros and functions
  * NOTE: The function implementations may not be as efficient
  * as an inline or assembly code implementation provided by a
  * native C library.
  */


#ifndef va_arg

#ifndef _VALIST
#define _VALIST
typedef char *va_list;
#endif                /* _VALIST */

/*
 * Storage alignment properties
 */

#define _AUPBND (sizeof (acpi_native_int) - 1)
#define _ADNBND (sizeof (acpi_native_int) - 1)

/*
 * Variable argument list macro definitions
 */

#define _bnd(X, bnd) (((sizeof (X)) + (bnd)) & (~(bnd)))
#define va_arg(ap, T) (*(*)(((ap) += (_bnd (T, _AUPBND))) - (_bnd (T,_ADNBND))))
#define va_end(ap) (void) 0
#define va_start(ap, A) (void) ((ap) = (((char *) &(A)) + (_bnd (A,_AUPBND))))

#endif                /* va_arg */

1、va_list
    va_list表示可变参数列表类型,实际上就是一个char指针
2、va_start
    va_start用于获取函数参数列表中可变参数的首指针(获取函数可变参数列表)
  * 输出参数ap(类型为va_list): 用于保存函数参数列表中可变参数的首指针(即,可变参数列表)
  * 输入参数A: 为函数参数列表中最后一个固定参数
3、va_arg
    va_arg用于获取当前ap所指的可变参数并将并将ap指针移向下一可变参数
  * 输入参数ap(类型为va_list): 可变参数列表,指向当前正要处理的可变参数
  * 输入参数T: 正要处理的可变参数的类型
  * 返回值: 当前可变参数的值
 
    在C/C++中,默认调用方式_cdecl是由调用者管理参数入栈操作,且入栈顺序为从右至左,入栈方向为从高地址到低地址。因此,第1个到第n个参数被放在地址递增的堆栈里。所以,函数参数列表中最后一个固定参数的地址加上第一个可变参数对其的偏移量就是函数的可变参数列表了(va_start的实现);当前可变参数的地址加上下一可变参数对其的偏移量的就是下一可变参数的地址了(va_arg的实现)。这里提到的偏移量并不一定等于参数所占的字节数,而是为参数所占的字节数再扩展为机器字长(acpi_native_int)倍数后所占的字节数(因为入栈操作针对的是一个机器字),这也就是为什么_bnd那么定义的原因。
 
4、va_end
    va_end用于结束对可变参数的处理。实际上,va_end被定义为空.它只是为实现与va_start配对(实现代码对称和"代码自注释"功能)
 
    对可变参数列表的处理过程一般为:
1、用va_list定义一个可变参数列表
2、用va_start获取函数可变参数列表
3、用va_arg循环处理可变参数列表中的各个可变参数
4、用va_end结束对可变参数列表的处理
 
下面是一个例子:

#include <stdio.h>
#include <stdarg.h>   /* 使用va_list、va_start等必须包含的头文件 */
#include <string.h>

/* linux C没有itoa函数,所以要自己写 */
char *itoa(int i, char *str)
{
    int mod, div = fabs(i), index = 0;
    char *start, *end, temp;

    do
    {
        mod = div % 10;
        str[index++] = '0' + mod;
        div = div / 10;
    }while(div != 0);

    if (< 0)
        str[index++] = '-';

    str[index] = '\0';

    for (start = str, end = str + strlen(str) - 1;
        start < end; start++, end--)
    {
        temp    = *start;
        *start    = *end;
        *end    = temp;
    }
    
    return str;
}

void print(const char *fmt, ...)
{
    char str[100];
    unsigned int len, i, index;
    int iTemp;
    char *strTemp;
    va_list args;

    va_start(args, fmt);
    len = strlen(fmt);
    for (i=0, index=0; i<len; i++)
    {
        if (fmt[i] != '%')    /* 非格式化参数 */
        {
            str[index++] = fmt[i];
        }
        else                /* 格式化参数 */
        {
            switch(fmt[i+1])
            {
            case 'd':        /* 整型 */
            case 'D':
                iTemp = va_arg(args, int);
                strTemp = itoa(iTemp, str+index);
                index += strlen(strTemp);
                i++;
                break;
            case 's':        /* 字符串 */
            case 'S':
                strTemp = va_arg(args, char*);
                strcpy(str + index, strTemp);
                index += strlen(strTemp);
                i++;
                break;
            default:
                str[index++] = fmt[i];
            }
        }
    }
    str[index] = '\0';
    va_end(args);

    printf(str);        
}

int main()
{
    print("Version: %d; Modifier: %s\n", -958, "lingd");
    return 0;
}

    另一个比较实用的例子:printk只能在内核初始化完控制台(console_init)后才能使用。因此,在Linux内核初始化控制台前,只能使用其它函数来打印内核消息。这些函数有:

void printascii(const char *);
void printhex(unsigned long value, int nbytes);
void printhex2(unsigned char value);
void printhex4(unsigned short value);
void printhex8(unsigned long value);

    这些函数都是用汇编实现的,并直接从低层操作s3c2410的串口,并显示信息。因此不需要等到console_init后就可以显示信息。
    为了使用这些函数,需要特性的内核选项支持:make menuconfig
      Kernel hack ->
       [*]Kernel low-level debugging functions
       [*]Kernel low-level debugging messages via S3C UART
       (0)S3C UART to use for low-level debug

    但是,这些函数并没有printk功能那么强大,支持可变参数,支持格式化字符串。为了在Linux内核初始化控制台前,能使用了一个类似于printk的函数,我们将printascii函数封装到新函数debug里:

extern void printascii(const char *);

static void debug(const char *fmt, ...)
{
    va_list va;                
    char buff[256];

    va_start(va, fmt);
    
    /* 函数vsprintf:用于输出格式化字符串到缓冲区
     * 输出参数buff:用于保存结果的缓冲区
     * 输入参数fmt: 格式化字符串
     * 输入参数va:  可变参数列表
     * 返回值:      实际输出到缓冲区的字符数
     */

    vsprintf(buff, fmt, va);
    va_end(va);

    printascii(buff);
}

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

C语言函数之可变参数原理:va_start、va_arg及va_end 的相关文章

  • 中移链Java-SDK实战使用

    id BSN 2021 公众号 BSN研习社 作者 中移信息 中移链是基于EOS区块链框架改造 满足BSN开放联盟链要求 符合国内监管政策 BSN开放联盟链 BSN Open Permissioned Blockchain 简称OPB 包括

随机推荐

  • ★教程1:matlab学习教程入门100例目录

    1 订阅本教程用户可以免费获得本博任意2个 包括所有免费专栏和付费专栏 博文对应代码 私信博主给出代码博文的链接和邮箱 2 本MATLAB课程的所有案例 部分理论知识点除外 均由博主编写而成 供有兴趣的朋友们自己订阅学习使用 未经本人允许
  • React性能优化的8种方式了解一下

    react凭借virtual DOM和diff算法拥有高效的性能 除此之外也有很多其他的方法和技巧可以进一步提升react性能 在本文中我将列举出可有效提升react性能的几种方法 帮助我们改进react代码 提升性能 但是我们不必一定要在
  • 记一次Mac挖矿病毒的处置

    title 记一次Mac挖矿病毒的处置 前言 网络安全圈一年一度的叉叉行动的前期准备过程中 在客户现场进行失陷主机监测和处置 通过态势感知发现某台终端主机有每五分钟向矿池 43 249 204 183 8888 发起一次连接请求的情况 态势
  • Python +selenium 实现自动校园网页登录 (Firefox)

    目的 通过分析页面 实现校园网自动输入账号密码 点击登录的操作 环境部署 win10 Firefox 87 64位 python 3 9 安装selenium pip install selenium 安装driver for firefo
  • 小红书评分8.7(从零开始学Python),入门新手必看

    前言 Python 是一种面向对象 解释型计算机程序设计语言 由 Guido van Rossum 于 1989 年底发明 第一个公开 发行版发行于 1991 年 Python 语法简洁而清晰 具有丰富和强大的类库 它常被昵称为胶水语言 能
  • window10基于WSL,通过ppa快速搭建LNMP环境,打造thinkphp6+swoole+docker开发环境(php8.0,对其他版本也适用)

    安装WSL2和UBUNTU 换源 两种方式都行 不过改之前都要记得备份 1 一行语句搞定 sudo sed i s archive ubuntu com mirrors ustc edu cn g etc apt sources list
  • PLSQL环境配置详细步骤

    下载PLSQL和instantclient 配置PLSQL 打开plsql 点击取消按钮 会进入主界面 点击配置 有的版本是选择Tools 首选项 第一个选项 选择Oracle client 安装的主目录D PLsql InstantCli
  • 软件测试与质量 第一章

    目录 一 什么是软件测试 1 1 测试目的 1 2 测试内容 手段和过程 1 2 1 手工动态测试涉及的工作 1 2 2 静态检查 1 2 3 自动化动态测试系统涉及的工作 1 2 4 软件测试的流程 1 3 小结 1 4 软件测试的分类
  • UE4 C++(20) UnrealPak打包和打补丁

    12 27 2020 文章目录 前言 打包 Package 资源类型 执行打包命令 HotPatcher插件 获取资源信息 GetAssetData 路径转换 ConvLongPackageNameToPackagePath 获取资源GUI
  • 最小生成树之普里姆(Prim)算法和克鲁斯卡尔(Kruskal)算法

    作者 STzen 链接 https www jianshu com p 683ffde4f3a3 来源 简书 最小生成树 列子引入 如图假设v0到v8表示9个村庄 现在需要在这9个村庄假设通信网络 村庄之间的数字代表村庄之间的直线距离 求用
  • 力扣刷题-47.全排列Ⅱ、深度优先搜索

    给定一个可包含重复数字的序列 返回所有不重复的全排列 深度优先搜索 DFS 深度优先搜索就是在每一步对每一种可能的选择一条道走到底 然后再回过头尝试另外一种选择 深度优先搜索的关键是要考虑 当前这一步 该如何做 至于 下一步 该怎么做和当前
  • 【Java】Java与C/C++的不同点总结

    Java是在C 的基础上发展而来 但是与C 还是有不同点的 总结如下 不同点 C 支持指针 而 Java 没有指针的概念 Java中起类似作用的是引用 C 支持多继承 而 Java 不支持多重继承 但允许一个类实现多个接口 Java 是完全
  • win10内网穿透实现远程桌面连接

    一 前情提要 在学校的时候曾经看到过同学在实验室用远程桌面控制宿舍自己的电脑 这样不仅能随时使用自己电脑上的文件 还省得把电脑带来带去又麻烦又重 当时也研究过 但没成功 这不新冠肺炎疫情导致开学时间一推再推 最近在家研究起了家庭NAS 无意
  • cesium入门系列(一)

    写在前面 近期开展cesium的入门学习 同时将学习的笔记记录下来 部分地方可能会很粗旷 后续会逐渐完善 一 登录cesium官网注册后获取token 很重要 二 html界面打开cesium的第一个界面 三维地球 cesium项目包准备
  • web前端开发程序员必须知道的HTML常⽤代码汇总

    html css代码 本设置 1 font size 字号 2 font style 字体格式 3 font weight 字体粗细 4 颜 属性color 本颜 注意使 页安全 超链接设置 text decoration 参数 参数取值范
  • 35:16. 最接近的三数之和

    题目 class Solution public int threeSumClosest vector
  • 近来总结

    近来总结 又是好久没写文章了 不能这样 感觉惰性真的会吞噬人的斗志 距离上篇文章又时隔十天了 不过话说回来 这段时间事情还是很多的 有时候就感觉在这么一个很小的外包公司怎么去提升自己的技术积累 小公司会不断的加工作 去看看技术贴 逛一逛Gi
  • QT队列的使用(QQueue)

    头文件 include
  • Linux中使用rm删除文件未释放磁盘

    原因 在Linux系统中 通过rm或者文件管理器删除文件 只是将它会从文件系统的目录结构上解除链接 unlink 也就是说只是删除了文件和系统目录结构的链接 如果文件在删除时是被打开的 有一个进程正在使用该文件 文件被进程锁定或者有进程一直
  • C语言函数之可变参数原理:va_start、va_arg及va_end

    说到C语言函数可变参数 我们最先想到的可能就是printf scanf printk了 在Linux 2 6 24 7内核源码里 printk函数原型如下 asmlinkage int printk const char fmt asmli