运行库:变长(可变)参数原理

2023-10-12 22:38
文章标签 参数 原理 可变 运行库

本文主要是介绍运行库:变长(可变)参数原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

任何C语言程序要想能够得到运行,都离不开背后的一套庞大的代码来进行支持,这一套语言相关的代码便是C语言运行库CRT(C Runtime Library),某种意义上运行库可以看成是C语言的程序和不同操作系统之间的抽象层。一个C语言运行库大致包含了以下功能:
/1.启动初始化与退出清理:包括入口函数及入口函数所依赖的其他函数集合;
/2.标准函数:由C语言标准规定的C语言标准库所拥有的函数实现,如本文将讨论的printf函数(不同平台上向标准输出设备显示字符串的底层实现略有不同,但是C语言提供了printf()函数,并规定了使用方法,由运行库的平台相关部分负责具体的实现);
/3.I/O:I/O功能的封装和实现(文件句柄的初始化和管理);
/4.堆的初始化;
/5.语言实现:语言中一些特殊功能的实现;
/6.调试:实现调试功能的代码。

初期C语言从AT&T实验室诞生,到得以广泛推广的中间存在着一段混乱时期,那时ANSI并没有介入C语言标准,导致彼时存在诸多操作系统的年代,不同系统上的C语言运行库(主要是基础函数库和一些自定义的特殊实现)千差万别。后来ANSI标准协会于1989年介入,正式推出C89语言标准,由C89规定的C语言基础函数库便是就此被称为ANSI C标准运行库。

ANSI C语言标准库STL含有24个头文件规定的基础函数集合,非常轻量级,仅仅包括数学函数、字符串处理和I/O处理等基础(标准输入输出stdlib.h,文件操作stdio.h、字符操作ctype.h、string.h、math.h、time.h、assert.h等),这其中最有意思的无意是变长参数的实现库stdarg.h。下面通过变长参数最为经典的使用场景int printf(const char* format, …)的实现过程来讲述变长参数的奇妙之处。

变长参数
/*
如此的声明表明,printf函数处理第一参数类型为const char*之外,
其后可以追加任意数量、任意类型的参数。
*/
int  printf(const char* format, ⋯);va_list ap; //va_list类型实际是一指针,因为类型不明,因此以void* 和char*
//初始化为最佳。该变量以后将会依次指向各个可变参数。/*
在函数的实现部分,可以使用stdarg.h里的多个宏来访问各个额外的参数:假设
lastarg是变长参数函数的最后一个具名参数(例如printf里的format).
ap必须用va_start初始化一次,其中lastarg必须是函数的最后一个具名的参数
*/
va_start(ap,  lasrtarg);//将va_list类型指针va指向第一个不定参数/*
此后,可以通过va_arg宏来获得下一个不定参数(假设已知其类型为type):
*/
type  next = va_arg(ap, type);//va_arg获取当前不定参数的类型和值,并将指针移动到下一个参数/*
marco implementation part...
*/#define va_list char*
#define va_start(ap,arg) ( ap = (va_list)&arg + sizeof(arg) )
#define va_arg(ap, type)  ( *(type*)( (ap+=sizeof(type))-sizeof(type)) )//ap被移向下一个可变参数的起点位置,并且va_arg将返回当前
//可变参数的值,当前被遍历到的参数的类型type都是format中规定的%d这类字符串格式传递进来的。
#define va_end(ap)  ( ap=(va_list)0 ) //在函数结束前,还必须调用宏
//va_end来清理现场,将指针ap置为空指针,防止野指针误用

printf.c

int  fputc(int c, FILE* stream)
{if( fwrite(&c, 1, 1, stream) != 1)return EOF;elsereturn c;
}int fputs(const char* str, FILE* stream)
{int len = strlen(str);if (fwrite(str,1,len, stream) != len)return EOF;elsereturn len;
}#ifndef WIN32
#define va_list  char*
#define va_start(ap,arg) ( ap = (va_list)&arg + sizeof(arg))
#define va_arg(ap, t)    ( *(t*) ( (ap+=sizeof(t)) - sizeof(t) ) )
#define va_end(ap)       ( ap = (va_list) 0)
#else
#include "windows.h"
#endif//Mini CRT 中并不支持特殊的格式操作,仅支持%d和%s两种简单的转换
int vfprintf(FILE* stream, const char* format, va_list arglist )
{int translating = 0;int ret = 0; //记录最终输出的字符个数const char* p = 0;//char temp[10];//strcpy(temp, "evilsama");//fputs(temp, stream);//fputs(*(const char**)arglist, stream);//fputs(itoa(123, temp, 10), stream);//fputs("entry the vfprintf\n",stream);for (p = format; *p != '\0'; ++p){switch (*p){case '%'://fputs("\n we truly enter the %-part \n", stream);if (! translating){translating = 1; //translating置为1,代表后面的字符需要解析char temp[10];//itoa( translating, temp, 10);//fputs("translating = ", stream);//fputs(temp, stream);}else{if (fputc('%', stream) < 0)return EOF;++ret;translating = 0;}break;case 'd':if (translating) //%d{char buf[16];translating = 0;itoa( va_arg(arglist, int), buf, 10);if (fputs(buf, stream) < 0)return EOF;ret += strlen(buf);}else if (fputc('d', stream) < 0)return EOF;else++ret;break;case 's':if (translating) //%s{const char* str = va_arg(arglist, const char*);//fputs("\n we truly enter the s-part \n", stream);translating = 0;if (fputs(str, stream) < 0)return EOF;ret += strlen(str);}else if (fputc ('s' , stream) < 0)return EOF;else ++ret;break;default:if (translating)translating = 0;if ( fputc(*p, stream) < 0 )return EOF;else++ret;break;}}return ret;
}int printf(const char* format, ...)
{va_list(arglist);va_start(arglist, format);return vfprintf(stdout, format, arglist);
}int fprintf(FILE* stream, const char* format, ...)
{va_list(arglist);va_start(arglist, format);return vfprintf(stream, format, arglist);
}
函数调用惯例

通过上述宏定义和宏实现的过程,显示了变长参数“顺藤摸瓜”的遍历过程。而谈到这种变长参数“顺藤摸瓜”的效果,则不得不说函数调用规范。正是调用惯例“cdecl”的存在才使得变长参数这种“调用时方能确定详细使用情况”的机制才能得以存在。

调用惯例清理方参数传递顺序函数编译后修饰规则
cdecl函数调用方从右到左的顺序压栈下划线+函数名
stdcall函数本身即被调用方从右到左的顺序压栈下划线+函数名+@+参数占用字节数
fastcall函数本身函数的头两个 DWORD(4Byte)类型或者更少字节的参数被放入寄存器,其他剩下的参数依旧按照从右到左的顺序压栈@+函数名+@+参数的字节数
pacall函数本身从左到右的顺序压栈较为复杂,可单独了解

在进行函数调用之前需要将本次调用的参数放置在栈中,而后才能正式启动本次调用,子函数使用完成后,本轮调用使用的参数显然是需要被弹出栈的,但本着“谁使用,谁处理”的原则,一般的,参数出栈这种清理工作是需要被调用函数主动清理的。被调用方主动清理参数出栈在大多数情况下是合理的,但是如果面对一些事先无法确认调用情况,如参数个数,显然被调用方是无法事先知道弹栈的范围的,这种情况下便是需要cdecl调用惯例,交由调用者来处理出栈。

可借由使用cdecl和stdcall两修饰符的函数的反汇编代码的末端来查看这两种调用惯例的不同之处。
/1.cdecl调用规范:被调用方不负责弹出参数,由主动调用者负责为后栈中参数的弹出清理

mov esp, ebp //恢复到esp此前的栈顶位置,回到调用方的栈顶
pop ebp  //恢复到ebp此前的栈底位置,回到调用方的栈底
ret //将ebp+4处保留的return address装载近eip寄存器

/2.stdcall调用规范:和cdecl调用惯例最大的不同是末端ret变成了ret X

mov esp, ebp //恢复到esp此前的栈顶位置,回到调用方的栈顶
pop ebp  //恢复到ebp此前的栈底位置,回到调用方的栈底
ret X//将ebp+4处保留的return address装载近eip寄存器,//并指示CPU自动弹出栈中X字节的空间,X等于本轮调用时传递的参数占用字节总量

可以看到X英爱时在调用前就应该固定下来的,否则也不会被汇编进最后的汇编代码中,调用者也要严格按照API声明的参数个数和类型来进行传递,不能多,不能少,顺序不能乱。看起来呆板不够灵活,但是可靠性强,一旦发生调用不规范可及时示警。这也是Windows API都采用stdcall规范(宏WINAPI的定义)的原因,因为不同的编译器产生栈的方式不尽相同,调用者不一定能正常完成清除工作,如果使用stdcall则函数调用者就可以主动解决参数清理工作,所以涉及跨平台API时,如果能确定函数的行参情况,则应该尽量不使用变长行参,函数使用stdcall调用惯例修饰。但如果遇到可变行参的情况,如printf,则只能使用cdecl。

这篇关于运行库:变长(可变)参数原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/198908

相关文章

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

Andrej Karpathy最新采访:认知核心模型10亿参数就够了,AI会打破教育不公的僵局

夕小瑶科技说 原创  作者 | 海野 AI圈子的红人,AI大神Andrej Karpathy,曾是OpenAI联合创始人之一,特斯拉AI总监。上一次的动态是官宣创办一家名为 Eureka Labs 的人工智能+教育公司 ,宣布将长期致力于AI原生教育。 近日,Andrej Karpathy接受了No Priors(投资博客)的采访,与硅谷知名投资人 Sara Guo 和 Elad G

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

如何在页面调用utility bar并传递参数至lwc组件

1.在app的utility item中添加lwc组件: 2.调用utility bar api的方式有两种: 方法一,通过lwc调用: import {LightningElement,api ,wire } from 'lwc';import { publish, MessageContext } from 'lightning/messageService';import Ca

hdu4407(容斥原理)

题意:给一串数字1,2,......n,两个操作:1、修改第k个数字,2、查询区间[l,r]中与n互质的数之和。 解题思路:咱一看,像线段树,但是如果用线段树做,那么每个区间一定要记录所有的素因子,这样会超内存。然后我就做不来了。后来看了题解,原来是用容斥原理来做的。还记得这道题目吗?求区间[1,r]中与p互质的数的个数,如果不会的话就先去做那题吧。现在这题是求区间[l,r]中与n互质的数的和

4B参数秒杀GPT-3.5:MiniCPM 3.0惊艳登场!

​ 面壁智能 在 AI 的世界里,总有那么几个时刻让人惊叹不已。面壁智能推出的 MiniCPM 3.0,这个仅有4B参数的"小钢炮",正在以惊人的实力挑战着 GPT-3.5 这个曾经的AI巨人。 MiniCPM 3.0 MiniCPM 3.0 MiniCPM 3.0 目前的主要功能有: 长上下文功能:原生支持 32k 上下文长度,性能完美。我们引入了

hdu4407容斥原理

题意: 有一个元素为 1~n 的数列{An},有2种操作(1000次): 1、求某段区间 [a,b] 中与 p 互质的数的和。 2、将数列中某个位置元素的值改变。 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.Inpu

hdu4059容斥原理

求1-n中与n互质的数的4次方之和 import java.io.BufferedInputStream;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.PrintWrit

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出

AI(文生语音)-TTS 技术线路探索学习:从拼接式参数化方法到Tacotron端到端输出 在数字化时代,文本到语音(Text-to-Speech, TTS)技术已成为人机交互的关键桥梁,无论是为视障人士提供辅助阅读,还是为智能助手注入声音的灵魂,TTS 技术都扮演着至关重要的角色。从最初的拼接式方法到参数化技术,再到现今的深度学习解决方案,TTS 技术经历了一段长足的进步。这篇文章将带您穿越时

如何确定 Go 语言中 HTTP 连接池的最佳参数?

确定 Go 语言中 HTTP 连接池的最佳参数可以通过以下几种方式: 一、分析应用场景和需求 并发请求量: 确定应用程序在特定时间段内可能同时发起的 HTTP 请求数量。如果并发请求量很高,需要设置较大的连接池参数以满足需求。例如,对于一个高并发的 Web 服务,可能同时有数百个请求在处理,此时需要较大的连接池大小。可以通过压力测试工具模拟高并发场景,观察系统在不同并发请求下的性能表现,从而