简体   繁体   English

RET 之后的指令总是 CALL 之后的指令吗?

[英]Is the instruction after a RET always the one after CALL?

In a well-behaved C program, shall the return statement (RET) always return to the instruction following the CALL statement?在一个行为良好的 C 程序中,返回语句 (RET) 是否总是返回到 CALL 语句之后的指令? I know this is the default, but I would like to check if anyone knows or remembers authentic examples of cases where this standard does not apply (common compiler optimization or other things...).我知道这是默认设置,但我想检查是否有人知道或记得该标准不适用的真实案例(常见的编译器优化或其他事情......)。 Someone told me that it could happen with a function pointer (the function pointer would put the value on the stack, instead of the CALL... I searched for it but I did not see an explanation anywhere).有人告诉我,函数指针可能会发生这种情况(函数指针会将值放在堆栈上,而不是 CALL ......我搜索了它,但我没有在任何地方看到解释)。

Let me try to better explain my question.让我试着更好地解释我的问题。 I know that we can use other structures to change the execution flow (including manipulating the stack)... I understand that if we change the return address written on the stack the execution flow will change to the address that was written on the stack.我知道我们可以使用其他结构来更改执行流程(包括操作堆栈)...我了解如果我们更改写入堆栈的返回地址,执行流程将更改为写入堆栈的地址。 What I need to know is: is there any not unusual execution situation where the next instruction is not the one that follows the CALL?我需要知道的是:是否存在任何不寻常的执行情况,即下一条指令不是 CALL 之后的指令? I mean, I would like to be sure that it doesn't happen, unless something unexpected occurs (like a memory access violation that would lead to a structured exception handler).我的意思是,我想确保它不会发生,除非发生意外情况(例如会导致结构化异常处理程序的内存访问冲突)。

My concern is whether the commercial application programs in general ALWAYS follow the mentioned pattern.我担心的是,一般商业应用程序是否总是遵循上述模式。 Notice that in this case I have a fixation for exceptions (it is important to know whether they exist in this case, for a research project I'm developing into a M. Sc. program's discipline).请注意,在这种情况下,我对异常有一个固定(重要的是要知道它们在这种情况下是否存在,对于我正在开发为理学硕士课程学科的研究项目)。 I know, for example, that a compiler may, sometimes, change a RET to a JMP (tail-call optimization).例如,我知道编译器有时可能会将 RET 更改为 JMP(尾调用优化)。 I would like to know if something like this may change the order of the instruction that is executed after the RET and, mainly, if the CALL will always be just before the instruction executed after the RET.我想知道这样的事情是否会改变在 RET 之后执行的指令的顺序,主要是,如果 CALL 总是在 RET 之后执行的指令之前。

CALL subroutine address is equivalent to CALL 子程序地址等价于
PUSH next instruction address + JMP subroutine address . PUSH 下一条指令地址+ JMP 子程序地址

At the same time, PUSH address is nearly equivalent to同时, PUSH地址几乎等价于
SUB xSP, pointer size + MOV [xSP], address . SUB xSP,指针大小+ MOV [xSP],地址

SUB xSP, pointer size can be replaced by PUSH . SUB xSP,指针大小可以用PUSH代替。

RET is nearly equivalent to RET几乎等于
JMP [xSP] followed by ADD xSP, pointer address at the location where JMP leads. JMP [xSP]后跟ADD xSP,指向 JMP 所在位置的指针地址

And ADD xSP, pointer address can be replaced by POP .并且ADD xSP,指针地址可以用POP代替。

So, you can see what kind of basic freedom the compiler has.因此,您可以看到编译器具有什么样的基本自由度。 Oh, btw, it can optimize your code such that your function is entirely inlined and there's neither a call to it, nor a return from it.哦,顺便说一句,它可以优化你的代码,使你的函数完全内联,既没有调用它,也没有返回它。

While somewhat perverse, it's not impossible to devise much weirder control transfers using instructions and techniques highly specific to the platform (CPU and OS).虽然有些反常,但使用高度特定于平台(CPU 和操作系统)的指令和技术来设计更奇怪的控制传输并非不可能。

You can use IRET instead of CALL and RET for control transfer, provided you put the appropriate stuff on the stack for the instruction.您可以使用IRET而不是CALLRET进行控制传输,前提是您将适当的内容放在指令的堆栈上。

You can use Windows Structured Exception Handling in a way that an instruction that causes a CPU exception (eg division by 0, page fault, etc) diverts execution to your exception handler and from there control can be transferred either back to that same instruction or to the next or to the next exception handler or to any location.您可以使用 Windows Structured Exception Handling ,即导致 CPU 异常(例如,除以 0、页面错误等)的指令将执行转移到您的异常处理程序,并从那里将控制权转移回同一条指令或转移到下一个或下一个异常处理程序或任何位置。 And most of x86 instructions can cause CPU exceptions.而且大多数 x86 指令都会导致 CPU 异常。

I'm sure there are other unusual ways for control transfer to, from and within subroutines/functions.我确信还有其他不寻常的方式可以在子程序/函数之间进行控制转移。

It's not uncommon to see code something like this either:看到这样的代码也很常见:

...
CALL A
A: JMP B
db "some data", 0
B: CALL C ; effectively call C with a pointer to "some data" as a parameter.
...

C:
; extracts the location of "some data" from the stack and uses it.
...
RET

Here, the first call isn't to a subroutine, it's just a way to put on the stack the address of the data stuck in the middle of the code.在这里,第一次调用不是对子程序,它只是一种将卡在代码中间的数据地址放入堆栈的方法。

This is probably what a programmer would write, not a compiler.这可能是程序员会写的,而不是编译器。 But I may be wrong.但我可能错了。

What I'm trying to say with all this is that you shouldn't expect to have CALL and RET as the only ways to enter and leave subroutines and you shouldn't expect them to be used for that purpose only and balance each other.我想说的是,你不应该期望CALLRET作为进入和离开子例程的唯一方法,你不应该期望它们仅用于此目的并相互平衡。

A "well behaved" C program could be translated by a compiler to a program that does not follow this pattern.一个“表现良好”的 C 程序可以被编译器翻译成一个不遵循这种模式的程序。 For example for obfuscation reasons the code could use a push / ret combination instead of a jmp.例如,出于混淆原因,代码可以使用 push / ret 组合而不是 jmp。

Excluding virtual memory situations (where a RET may cause a page fault, technically meaning that the thing the RET triggers is the fault handler), I think the main thing worth discussing is that setjmp and longjmp may completely subvert the stack — so you can legitimately CALL something, then have it hop back an arbitrary number of stack frames without ever hitting the RETs.排除虚拟内存情况(RET 可能导致页面错误,技术上意味着 RET 触发的是错误处理程序),我认为值得讨论的主要事情是setjmplongjmp可能会完全颠覆堆栈 - 所以你可以合法地调用一些东西,然后让它跳回任意数量的堆栈帧,而不会碰到 RET。

I guess it's quite conceivable that a longjmp implementation may involve a RET with a modified stack — it'd be up to the vendor on how they wanted to implement that.我猜很可能, longjmp实现可能涉及带有修改堆栈的 RET——这取决于供应商他们想要如何实现它。

In a well-behaved C program, shall the return statement (RET) always return to the instruction following the CALL statement?在一个行为良好的 C 程序中,返回语句 (RET) 是否总是返回到 CALL 语句之后的指令?

This is kind of a non sequitur because there's nothing that requires calling a function and returning from it to necessarily map to these instructions, though of course it's quite common.这是一种不合理的做法,因为没有什么需要调用函数并从函数返回来必然映射到这些指令,尽管这当然很常见。 One example of that is when a function gets inlined.一个例子是当一个函数被内联时。

I think it would be very unusual for an x86 targeting compiler to rig things so a ret instruction corresponding to a return statement went somewhere other than the address following the call instruction.我认为对于 x86 目标编译器来说,操纵东西是非常不寻常的,因此与return语句相对应的ret指令会出现在call指令后面的地址以外的地方。 But that's something I think might happen occasionally on an ARM processor.但这是我认为在 ARM 处理器上偶尔会发生的事情。

Since an ARM instruction can't always contain a full 32-bits of immediate data, it's common for constants (numeric or string) to be 'embedded' as data in the code stream so the value or a pointer to it can be loaded using a pc (program counter) relative address.由于 ARM 指令不能始终包含完整的 32 位立即数数据,因此常量(数字或字符串)通常作为数据“嵌入”到代码流中,因此可以使用加载值或指向它的指针pc (程序计数器)相对地址。 Usually these constants are located at a spot where a jump doesn't need to be made just because of the data.通常这些常量位于不需要仅仅因为数据而进行跳转的位置。 One of the more common places for such data would be in the area between the code for two functions.此类数据最常见的位置之一是两个函数的代码之间的区域。 But another spot where that condition holds after a branch made for a function call, since a branch needs to be taken in any case to get to the instructions following the call site (the return from the function).但是在为函数调用创建分支之后该条件成立的另一个地方,因为在任何情况下都需要进行分支才能到达调用站点之后的指令(从函数返回)。 So, it doesn't hurt execution time to place the data just after the call and set the return address to be the address that follows the data.因此,在调用之后放置数据并将返回地址设置为数据后面的地址不会影响执行时间。 The compiler loads the lr register (which is used by convention to hold the return address) with the address following the data, then issues an unconditional branch to the function.编译器将数据后面的地址加载到lr寄存器(按惯例用于保存返回地址),然后向函数发出无条件分支。 You might not see this too often, but similar techniques to place data in the code segment are common on the ARM.您可能不会经常看到这种情况,但是将数据放入代码段的类似技术在 ARM 上很常见。

Theoretically, a compiler could, given the following code:理论上,编译器可以,给定以下代码:

return f(), g();

generate assembly along the lines of:按照以下方式生成程序集:

push $g
jmp f

Maybe.或许。 On some processors there is something called a "delay slot" (sometimes two) which are instructions immediately following branch instructions (including CALL) which are executed as if they were at the target of the branch.在某些处理器上,有一种称为“延迟槽”(有时是两个)的东西,它们是紧跟在分支指令(包括 CALL)之后的指令,它们的执行就像它们在分支的目标处一样。 This apparent nonsense was added to increase performance, since the instruction pre-fetcher has quite often fetched ahead of the branch instruction by the time it realizes there is a branch.添加这种明显的废话是为了提高性能,因为指令预取器在意识到存在分支时经常在分支指令之前提取。 The address pushed by a CALL as the return address is not the address following the CALL if there are delay slot instructions, the return address is the address following the delay slot instruction(s).如果有延迟槽指令,则 CALL 作为返回地址推送的地址不是CALL 后面的地址,返回地址是延迟槽指令后面的地址。

http://en.wikipedia.org/wiki/Delay_slot http://en.wikipedia.org/wiki/Delay_slot

This introduced complexity in the Instruction Set Architecture (ISA) for that machine, for example what happens if you place branches in the delay slots, what happens if an instruction in the delay slot causes a fault?这在该机器的指令集架构 (ISA) 中引入了复杂性,例如,如果将分支放在延迟槽中会发生什么,如果延迟槽中的指令导致错误会发生什么? What happens if there is a trap (like a single step trap)?如果有陷阱(如单步陷阱)会发生什么? You can see it gets messy... but a surprising number of older RISC processors have that, like MIPS, SPARC, and PA-RISC.你可以看到它变得一团糟……但数量惊人的旧 RISC 处理器都有这种情况,比如 MIPS、SPARC 和 PA-RISC。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM