在一个表现良好的C程序中,return语句(RET)是否总是返回CALL语句后面的指令?我知道这是默认设置,但我想检查是否有人知道或记住这个标准不适用的情况的真实例子(常见的编译器优化或其他东西......)。有人告诉我,它可能发生在一个函数指针(函数指针将值放在堆栈上,而不是CALL ......我搜索它但我没有在任何地方看到解释)。
让我试着更好地解释我的问题。我知道我们可以使用其他结构来改变执行流程(包括操作堆栈)......我知道如果我们更改写在堆栈上的返回地址,执行流程将更改为堆栈上写入的地址。我需要知道的是:是否存在任何不寻常的执行情况,其中下一条指令不是跟随CALL的指令?我的意思是,我希望确保它不会发生,除非出现意外情况(如内存访问违规会导致结构化异常处理程序)。
我担心的是商业应用程序总体上是否总是遵循上述模式。请注意,在这种情况下,我有一个例外的固定(重要的是知道它们是否存在于这种情况下,对于我正在发展成为M. Sc。程序的学科的研究项目)。我知道,例如,编译器有时可以将RET更改为JMP(尾调用优化)。我想知道这样的事情是否会改变RET之后执行的指令的顺序,主要是,如果CALL总是在RET之后执行的指令之前。
答案 0 :(得分:1)
“表现良好”的C程序可以由编译器转换为不遵循此模式的程序。例如,出于混淆的原因,代码可以使用push / ret组合而不是jmp。
答案 1 :(得分:0)
排除虚拟内存情况(RET可能导致页面错误,技术上意味着RET触发的东西是错误处理程序),我认为值得讨论的主要内容是setjmp
和{{1}可能会完全颠覆堆栈 - 所以你可以合法地调用一些东西,然后让它跳回任意数量的堆栈帧而不会碰到RET。
我想可以想象,longjmp
实现可能涉及带有修改堆栈的RET - 供应商应该如何实现它。
答案 2 :(得分:0)
在一个表现良好的C程序中,return语句(RET)是否总是返回CALL语句后面的指令?
这是一种不合理的东西,因为没有什么需要调用函数并从函数返回必须映射到这些指令,尽管当然它很常见。其中一个例子就是函数内联。
我认为对于x86目标编译器来说非常不寻常,因此对应于ret
语句的return
指令除了call
指令之后的地址之外的地方。但我认为这可能偶尔会发生在ARM处理器上。
由于ARM指令不能总是包含完整的32位立即数据,因此常量(数字或字符串)作为代码流中的数据“嵌入”是常见的,因此值或指针可以指向它使用pc
(程序计数器)相对地址加载。通常这些常数位于不需要仅仅因为数据而进行跳转的位置。这些数据的一个更常见的地方是两个函数的代码之间的区域。但是在为函数调用创建分支之后该条件成立的另一个位置,因为在任何情况下都需要采用分支来获取调用站点之后的指令(从函数返回)。因此,在调用之后放置数据并将返回地址设置为跟随数据的地址不会影响执行时间。编译器加载lr
寄存器(由约定用于保存返回地址)和数据后面的地址,然后向函数发出无条件分支。您可能不会经常看到这种情况,但在ARM中常见的是将数据放入代码段的类似技术。
答案 3 :(得分:0)
CALL子程序地址相当于
PUSH下一个指令地址 + JMP子程序地址。
同时, PUSH地址几乎相当于
SUB xSP,指针大小 + MOV [xSP],地址。
SUB xSP,指针大小可以替换为 PUSH 。
RET 几乎相当于
JMP [xSP] ,然后是JMP引导位置的 ADD xSP,指针地址。
ADD xSP,指针地址可以替换为 POP 。
因此,您可以看到编译器具有哪种基本自由度。哦,顺便说一句,它可以优化你的代码,使你的函数完全内联,既没有调用它,也没有从它返回。
虽然有点不正常,但使用高度专用于平台(CPU和OS)的指令和技术来设计更多的更奇怪的控制传输并非不可能。
您可以使用 IRET 代替 CALL 和 RET 进行控制转移,前提是您在指令的堆栈中放入了适当的内容。
您可以使用Windows Structured Exception Handling
,导致CPU异常的指令(例如,除以0,页面错误等)将执行转移到您的异常处理程序,并从那里控制可以转移回相同的指令或下一个或下一个异常处理程序或任何位置。大多数x86指令都可能导致CPU异常。
我确信还有其他不寻常的方法可以将控制权转移到子程序/函数中,从子程序/函数中移出。
看到类似这样的代码并不罕见:
...
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
这里,第一次调用不是一个子程序,它只是一种将数据地址放在代码中间的方法。
这可能是程序员编写的内容,而不是编译器。但我可能错了。
我想说的是,你不应该期望让CALL
和RET
作为进入和离开子程序的唯一方法,你不应该期望它们仅用于此目的并相互平衡。
答案 4 :(得分:0)
理论上,编译器可以给出以下代码:
return f(), g();
按照以下方式生成装配:
push $g
jmp f
答案 5 :(得分:0)
也许。在某些处理器上有一个称为“延迟槽”(有时是两个)的东西,它们是紧跟在分支指令(包括CALL)之后的指令,它们就像它们在分支的目标处一样被执行。这种明显的无意义被添加以提高性能,因为指令预取器在它意识到存在分支时经常在分支指令之前取出。如果存在延迟槽指令,则CALL作为返回地址推送的地址不 CALL之后的地址,返回地址是延迟槽指令之后的地址。
http://en.wikipedia.org/wiki/Delay_slot
这引入了该机器的指令集架构(ISA)的复杂性,例如,如果将分支放在延迟槽中会发生什么,如果延迟槽中的指令导致故障会发生什么?如果有陷阱(如单步陷阱)会发生什么? 您可以看到它变得混乱......但是有大量旧的RISC处理器具有这种功能,如MIPS,SPARC和PA-RISC。