C调用约定:谁在可变参数函数与普通函数中清除堆栈?

问题描述

有一些调用约定(例如pascalstdcall),但就我而言,C确实使用了cdecl(C声明)。这些约定中的每个约定在调用方将参数加载到堆栈的方式上略有不同,分别由(调用方/被调用方)进行清理

谈论清理,这是我的问题。我不明白:有三件事吗?

  1. 清洁堆栈
  2. 将指针移回倒数第二个堆栈框
  3. 堆栈恢复

或者我应该怎么看他们?

此外,此问题的目标基本上是可变参数函数如何在调用约定(如Pascal或stdcall)中工作,在这些约定中被调用方应清除/清理/恢复(我不知道哪个操作)堆栈-但是他不知道它将接收多少参数。

编辑

为什么将参数压入堆栈的顺序如此重要?您仍然拥有第一个参数(不是省略号的稳定参数),该参数为您提供有关-例如-可变参数数量的信息。还有一个“监护人”,可以将其添加到省略号中,并可以独立于调用约定而用作可变部分末端的标记。 In this link如果调用者和被调用者在混乱之前都保存了状态,为什么调用者和被调用者都应该恢复它们的值?在调用函数之前,不只是其中之一(例如调用方)将它们保存在堆栈中吗?另外,在同一链接上

“因此,堆栈指针ESP可能会上升和下降,但是EBP寄存器 保持固定。这很方便,因为这意味着我们可以随时参考 将第一个参数设为[EBP + 8],无论推动和 弹出是在函数中完成的。”

推入变量和局部变量在内存中是连续的。使用EBP推荐他们的优势在哪里?即使堆栈大小发生变化,它们之间也永远不会有任何动态偏移。

我读过的材料之一是this site(只是开始),以更好地了解什么是堆栈框架。 然后我继续进行学习,找到了这些stack overviewcall stack教程,但是它们以某种方式错过了我需要的部分。 What does exactly happends when you call the function(我不理解指令“调用地址”,其后的下一条指令a push到达堆栈的值即返回值)。谁控制寄信人地址是什么?呼叫者,召集者?被呼叫者?当被调用者返回时,该程序通过执行一条指令来继续操作,该指令是从寄存器中读取操作或执行什么操作?

解决方法

就我而言,C确实使用了cdecl

尽管有其名称,但cdecl约定对于C代码而言并不是通用的,甚至在x86体系结构上也不是通用的。它具有定义和实现简单的优点,但是它不使用CPU寄存器进行参数传递,因此效率更高。即使在寄存器匮乏的x86上,这也有所不同,但是在具有更多可用寄存器的体系结构(例如x86_64)上,则有很大的不同。

谈论清理,这是我的问题。我不明白: 有三件事吗?

  1. 清洁堆栈
  2. 将指针移回倒数第二个堆栈框
  3. 堆栈恢复

或者我应该怎么看他们?

我倾向于将(1)和(3)解释为表达同一件事的不同方式,但是可以想象有人会在它们之间进行区分。 (3)和相关的用词是我最常遇到的。 (2)不一定是同一件事,因为可能要还原两个相关的堆栈参数:堆栈框架的底部(请参见下文)和堆栈顶部。如果堆栈框架包含的信息比参数和局部变量值(例如,上一个堆栈框架的基础)更多,则堆栈框架的基础非常重要。

此外,这个问题的目标基本上是如何可变 该函数在Pascal或stdcall等调用约定中起作用,其中 被呼叫者应清除/清理/还原(我不知道该执行哪个操作) 堆栈-但他不知道它将接收多少参数。

堆栈不一定是完整的图片。

如果被调用方不知道如何找到其调用方堆栈的顶部,并且不知道如何找到其调用方堆栈框架的基础,则无法恢复堆栈。但是实际上,这通常是硬件辅助的。

以x86(为其设计cdecl)为例,CPU具有用于堆栈(帧)基址和当前堆栈指针的寄存器。调用方的堆栈库以与被调用方的堆栈库已知的偏移量(0)存储在堆栈上。无论参数有多少,被调用方都可以通过将堆栈顶部移至其自己的堆栈基,然后在其中弹出值以获取调用方的堆栈基来恢复堆栈。

但是,可以想到的是,某处使用了一种调用约定,除了一次弹出一个元素外,该约定无法将堆栈恢复到选定的先前状态,而不能一次传递一个参数到调用的函数,并且需要被调用方还原调用方的堆栈。这样的调用约定将不支持可变参数功能。

为什么将参数压入堆栈的顺序如此重要?

从任何一般意义上讲,该顺序都不重要,但是对于主叫方和被叫方(它们可以分别编译)达成共识是必不可少的。否则,被调用者无法将传递的值与它们预期的参数进行匹配。因此,无论调用约定在多大程度上依赖堆栈,它都必须精确指定将哪些参数传递给堆栈以及以什么顺序传递。

关于堆栈框架:这是C并未指定的更多材料,至少在一定程度上有所不同。但是,从概念上讲,函数调用的堆栈框架是堆栈中为该调用提供执行上下文的部分。它通常为局部变量提供存储,并且可能包含其他信息,例如返回地址和/或调用者的堆栈框架指针的值。它还可能包含适用于执行环境的其他按功能调用的信息。详细信息是使用中的调用约定的一部分。

,

请注意,实际上,没有主流系统曾经使用过callee-pops-args约定来实现可变参数功能。。它们都使用caller-pops,因此被调用方不需要知道args的数量。进行被叫流行音乐并不是不可能的,但是通常不值得麻烦。

例如,在Windows的32位代码中,我认为stdcall是许多Windows DLL函数的默认功能,但可变参数使用cdecl。 (Linux和MacOS等非Windows x86系统通常默认情况下对所有功能都使用caller-pops调用约定。因此,如果我们谈论的是主流系统,这实际上仅适用于32位Windows。)

因此,printf不必累加格式字符串引用的args的大小(或接收调用方传递的计数),然后模拟ret 12ret 8管他呢。 ret n仅在带有立即操作数的机器代码中可用,因此您无法执行ret ecx等。可以通过多种方式来模拟变量计数ret n,例如最糟糕的情况之一是将返回地址复制到堆栈的较高位置,并在普通ret之前调整ESP。但是与仅使用caller-pops约定相比,这仍然是非常低效的。

此外,这还会使程序变脆:将未使用的arg传递给printf在ISO C中是未定义的行为,但是某些代码依赖于它被静默忽略(偶然或由于类型不匹配)。

Windows还通过“装饰” _foo@12之类的asm符号名称(如int foo(int,int,int))来确保主叫方和被叫方就被弹出的堆栈空间达成一致。 (对于纯的stack-args约定,三个int args = 12字节的堆栈空间)。因此,如果您声明错误(或根本不声明,而隐式声明使用较大的类型),则会得到链接错误,而不是难以调试的错误,而这种错误只有在优化的构建中才会发生。 (如果使用EBP作为框架指针的调试版本在发生任何错误之前碰巧纠正了堆栈不匹配的情况。)

调用约定不匹配和其他asm错误会导致“低于” C / C ++级别的损坏,并且可能很难调试,特别是对于那些仅在调试器中或带有调试打印内容查看C变量的用户。 (与滥用GNU C内联汇编相同。)


正如@johnfound所说,调用约定的关键是调用者和被调用者同意。只要双方同意,任何明确的规则都行之有效。

良好(有效)的调用约定(例如x86-64 System V,并且在较小程度上,Windows x64和32位fastcall / vectorcall)将传递寄存器中的前几个arg,从而避免了将存储/重新加载到堆栈或任何简单功能的堆栈操作。高效的调用约定也很好地结合了call-preserved and call-clobbered registers。简单的调用约定将所有内容传递给堆栈,由调用者或被调用者负责弹出args。甚至更简单的寄存器(如针对asm初学者的Irvine32)也保留所有寄存器。

有关详细信息,请参见Agner Fog's calling conventions guide

相关问答

错误1:Request method ‘DELETE‘ not supported 错误还原:...
错误1:启动docker镜像时报错:Error response from daemon:...
错误1:private field ‘xxx‘ is never assigned 按Alt...
报错如下,通过源不能下载,最后警告pip需升级版本 Requirem...