【问题标题】:Is there any operation in C analogous to this assembly code?C 中是否有任何类似于此汇编代码的操作?
【发布时间】:2016-06-21 22:13:44
【问题描述】:

今天,我在汇编代码中使用递增函数指针来创建函数的备用入口点:

.386
.MODEL FLAT, C
.DATA
    INCLUDELIB MSVCRT
    EXTRN puts:PROC
    HLO DB "Hello!", 0
    WLD DB "World!", 0
.CODE
    dentry PROC
        push offset HLO
        call puts           
        add esp, 4
        push offset WLD
        call puts
        add esp, 4
        ret
    dentry ENDP
    main PROC
        lea edx, offset dentry
        call edx
        lea edx, offset dentry
        add edx, 13
        call edx
        ret
    main ENDP
END

(我知道,从技术上讲,这段代码是无效的,因为它在没有初始化 CRT 的情况下调用 puts,但它可以在没有任何程序集或运行时错误的情况下工作,至少在 MSVC 2010 SP1 上是这样。)

请注意,在第二次调用 dentry 时,我像以前一样在 edx 寄存器中获取了函数的地址,但这次我在调用函数之前将其增加了 13 个字节。

因此这个程序的输出是:

C:\Temp>dblentry
Hello!
World!
World!

C:\Temp>

Hello!\nWorld!”的第一个输出是从调用到函数的最开始,而第二个输出是从“push offset WLD”指令开始的调用。

我想知道这种东西是否存在于旨在从 C、Pascal 或 FORTRAN 等汇编程序升级的语言中。我知道 C 不允许你增加函数指针,但是还有其他方法可以实现这种事情吗?

【问题讨论】:

  • 一般是不行的,因为跳过函数序言会导致各种问题。在更高级别的语言中(甚至在 asm 中),您应该将其拆分为两个单独的函数,一个调用另一个函数,让编译器选择最佳实现。
  • 我相信旧版 FORTRAN 支持一个函数的多个入口点,快速谷歌搜索会产生 gcc.gnu.org/onlinedocs/gcc-3.4.4/g77/…docs.oracle.com/cd/E19957-01/805-4939/6j4m0vn99/index.html
  • 实际的问题是为什么你想这样做?这是维护的噩梦。如果达到优化:> 99% 的程序不需要这个。其余部分通常可以由编译器优化,并且结构比这种 70/80ies BASIC hacks 好得多。
  • 您可以使用不添加中断的 switch() 语句来创建一个在函数中模拟这一点的贯穿表。谷歌“达夫的设备”为什么有人可能想做这样愚蠢的事情:)
  • 我刚刚在 FORTRAN 上遇到了一些关于“单进单出”的讨论:programmers.stackexchange.com/a/118793/3601

标签: c assembly function-pointers


【解决方案1】:

可以使用longjmp函数:http://www.cplusplus.com/reference/csetjmp/longjmp/

这是一个相当可怕的功能,但它会做你想要的。

【讨论】:

  • longjmp 只允许您返回到父函数中的某个点。即备份调用树,而不是备份到不是当前运行函数的父级的任意函数。这是进行跨功能跳转的另一种方式,但它与 OP 所要求的完全不同,因为使用要求如此不同。
  • 是的,longjmp 需要预先执行跳转入口。但 OP 示例代码也是如此。
  • 不,它没有! OP 的main()call 变为(dentry+13)()。无论最初的calldentry()(无论如何都在第二个call 之前返回),这都会以同样的方式发生。它甚至不相似。
【解决方案2】:

AFAIK 你只能在 asm 中编写具有多个入口点的函数。

您可以在所有入口点上放置标签,这样您就可以使用正常的直接调用,而不是硬编码从第一个函数名开始的偏移量。

这使得从 C 或任何其他语言正常调用它们变得容易。

如果您担心不允许函数体重叠的工具(或人)混淆,那么早期的入口点的工作方式就像函数进入另一个函数的主体。


如果早期的入口点做一些额外的事情,你可能会这样做,然后进入主函数。它主要是一种节省代码大小的技术(可能会提高 I-cache / uop-cache 命中率)。


编译器倾向于在函数之间复制代码,而不是在稍微不同的函数之间共享大块的通用实现。

但是,您可能只需要一个额外的jmp 就可以完成它,例如:

int foo(int a) { return bigfunc(a + 1); }
int bar(int a) { return bigfunc(a + 2); }

int bigfunc(int x) { /* a lot of code */ }

查看Godbolt compiler explorer上的真实示例

foobar tailcall bigfunc,这比让bar 落入bigfunc 稍差。 (让foo 跳过barbigfunc 仍然很好,尤其是如果bar 不是那么微不足道的话。)


跳转到函数的中间通常并不安全,因为重要的函数通常需要保存/恢复一些 reg。所以序言推动了他们,而尾声则弹出了他们。如果你跳到中间,那么序言中的pops 将使堆栈失衡。 (即将返回地址弹出到寄存器中,并返回垃圾地址)。

另见Does a function with instructions before the entry-point label cause problems for anything (linking)?

【讨论】:

    猜你喜欢
    • 2022-06-16
    • 2011-06-14
    • 2011-03-09
    • 1970-01-01
    • 1970-01-01
    • 2019-09-01
    • 1970-01-01
    • 2011-05-09
    • 1970-01-01
    相关资源
    最近更新 更多