GCC - 修改函数返回后继续执行的位置

Iva*_*anB -25 c gcc return memory-address gcc-extensions

是否可以在GCC中做这样的事情?

void foo() {
    if (something()) returnSomewhereElse;
    else return;
}

void bar() {
    foo();
    return; // something failed, no point of continuing
    somewhereElse:
    // execution resumes here if something succeeds
    // ...
}
Run Code Online (Sandbox Code Playgroud)
  • 这可以通过C和GCC扩展以便携方式实现,而无需使用平台特定组件吗?
  • 堆栈状态在正常返回点和更改的返回点之间不会改变,那么是否可以重用恢复堆栈的代码并从常规返回中注册状态?
  • 考虑到函数可能会或可能不会内联,如果调用它必须改变返回地址,如果内联它只能改变代码路径而不是当前函数返回地址,因为这会破坏代码
  • 备用返回点不需要是标签,但我希望GCC的标签扩展地址在这种情况下可以派上用场

只是为了澄清意图 - 它是关于错误处理.这个例子是一个最小的例子,只是为了说明事情.我打算在更深层次的上下文中使用它,以便在发生错误时停止执行.我还假设状态没有改变,我可能错了,因为在两个返回点之间没有添加额外的局部变量,所以我希望编译器生成的代码在foo的返回时可以重用为此节省了使用longjmp,设置和传递跳转缓冲区的开销.

这个例子"确实有意义",因为它的目的是展示我想要实现的目标,而不是为什么以及如何在实际代码中有意义.

为什么你的想法更简单,只需从foo()返回一个值并让bar()返回或执行somewhereElse:conditionally?

它并不简单,你所建议的不适用于实践,只是在一个简单的例子的背景下,但它更好,因为:

1 - 它不涉及额外返回值

2 - 它不涉及额外检查值

3 - 它不涉及额外的跳跃

我可能错误地认为目标应该在这一点上清楚,并且在所有澄清和解释之后.我们的想法是从深度调用链中提供"转义代码路径",而无需任何额外开销.通过重用编译器生成的代码来恢复前一个调用帧的状态,并简单地修改函数返回后执行恢复的指令.成功跳过"转义代码路径",发生的第一个错误进入它.

if (failure) return; // right into the escape code path
else {
    doMagickHere(); // to skip the escape code path
    return; // skip over the escape code path
}

//...
void bar() {
    some locals;
    foo();
    // enter escape code path here on foo failure so
    destroy(&locals); // cleanup
    return; // and we are done
    skipEscapeCodePath: // resume on foo success
    // escape path was skipped so locals are still valid
}
Run Code Online (Sandbox Code Playgroud)

至于Basile Starynkevitch提出longjmp的"有效"和"甚至十亿longjmp仍然合理"的说法 - sizeof(jmp_buf)给了我大约156个字节,这显然是保存几乎所有寄存器和一堆其他东西所需的空间,所以以后可以恢复.这些都是很多操作,而这十亿次远远超出了我对"有效"和"合理"的个人理解.我的意思是十亿个跳转缓冲区本身就超过了145个GIGABYTES内存,然后还有CPU时间开销.并不是很多系统甚至可以提供那种"合理"的系统.

Bas*_*tch 39

不,这是不可能的,我不确定猜测你想要实现什么.

术语

也许你想要一些非本地的跳跃.仔细阅读有关setjmp.h,协同程序,调用堆栈,异常处理,continuationcontinuation-passing-style的内容.了解什么呼叫/ cc的方案应该是非常有利的.

setjmplongjmp

setjmplongjmp是标准的C99函数(它们非常快,因为保存的状态实际上非常小).使用它们时要特别小心(特别是为了避免任何内存泄漏).longjmp(或POSIX中相关的 siglongjmp)便携式标准C99中逃避某些功能并返回某个调用者唯一方法.

我们的想法是从深度调用链中提供"转义代码路径",而无需任何额外开销

正是中的作用longjmpsetjmp.两者都是快速,恒定时间的操作(特别是longjmp需要花费很短且恒定的时间来展开数千个调用帧的调用栈).内存开销实际上jmp_buf每个捕获点一个本地,不是什么大不了的事.该jmp_buf很少把调用堆栈之外.

有效使用它们的一种常用方法是将setjmp-ed jmp_buf放在本地struct(所以在你的调用框架中)并将指针传递struct给一些static间接调用longjmp错误的内部函数.因此setjmplongjmp可以与明智编码约定,模仿得很好且有效地C++异常投球和处理(或者Ocaml异常,或Java的例外,这两者都具有不同的语义比C++)的复杂的语义.它们是便携式基本砖,足以满足这一目的.

实际上,代码如下:

  struct my_foo_state_st {
    jmp_buf jb;
    char* rs;
    // some other state, e.g a ? FILE*` or whatever
  };

  /// returns a `malloc? -ed error message on error, and NULL on success
  extern const char* my_foo (struct some_arg_st* arg);
Run Code Online (Sandbox Code Playgroud)

struct my_foo_state_st私人国家.这my_foo公共函数(您将在某些公共标题中声明).您确实记录了(至少在注释中)它在失败时返回堆分配的错误消息,因此调用者负责释放它.成功时,您记录了它返回NULL.当然,您可以拥有其他约定和其他参数和/或结果.

我们现在声明并实现一个错误函数,它将错误消息打印到状态并使用a转义 longjmp

  static void internal_error_printf (struct my_foo_state*sta, 
       int errcode, 
       const char *fmt, ...) 
   __attribute__((noreturn, format(printf(2,3))));

  void internal_error_printf(struct my_foo_state*sta, 
       int errcode, const char *fmt, ...) {
    va_arg args;
    va_start(args, fmt);
    vasprintf(&sta->rs, fmt, args);
    va_end(args);
    longjmp(sta->jb, errcode);
  }
Run Code Online (Sandbox Code Playgroud)

我们现在有几个可能复杂的递归函数来完成大部分工作.我只画草图,你知道你想要他们做什么.当然你可能想给他们一些额外的参数(这通常是有用的,这取决于你).

  static void my_internal_foo1(struct my_foo_state_st*sta) {
    int  x, y;
    // do something complex before that and compute x,y
    if (SomeErrorConditionAbout(sta))
       internal_error_printf(sta, 35 /*error code*/,
                            "errror: bad x=%d y=%d", x, y);
    // otherwise do something complex after that, and mutate sta
  }

  static void my_internal_foo2(struct my_foo_state_st*sta) {
    // do something complex 
    if (SomeConditionAbout(sta))
       my_internal_foo1(sta);
    // do something complex and/or mutate or use `sta`
  }
Run Code Online (Sandbox Code Playgroud)

(即使你有几十个像上述内部功能,你不消耗jmp_buf任何人;而你也可以在他们改乘相当深刻,您只需要通过一个指针 -to struct my_foo_state_st在所有这些,如果你是单线程的,不关心重入,你可以将指针存储在某个static变量中......或者某些线程本地的,甚至没有在一些参数中传递它,我发现它仍然是优选的 - 因为更多的重入和线程友好).

最后,这里是公共功能:它建立状态并做一个 setjmp

  // the public function
  const char* my_foo (struct some_arg_st* arg) {
     struct my_state_st sta;
     memset(&sta, 0, sizeof(sta));
     int err = setjmp(sta->jb);
     if (!err) { // first call
       /// put something in `sta` related to ? arg? 
       /// start the internal processing
       //// later,
       my_internal_foo1(&sta);
       /// and other internal functions, possibly recursive ones
       /// we return NULL to tell the caller that all is ok
       return NULL;
     }
     else { // error recovery
       /// possibly release internal consumed resources
       return sta->rs;
     };
     abort(); // this should never be reached
  }
Run Code Online (Sandbox Code Playgroud)

请注意,您可以调用my_foo十亿次,在没有失败时不会消耗任何堆内存,并且堆栈将增长一百个字节(在返回之前释放my_foo).而且即使你的个人代码失败十亿次调用十亿次internal_error_printf 没有内存泄漏发生(因为你证明my_foo是返回该错误字符串调用者free)如果编码正确.

因此,使用正确 setjmplongjmp十亿倍也不会吃了很多的内存(在调用堆栈上只有几百个字节为一个单一的地方jmp_buf,这是对弹出my_foo函数返回).实际上,longjmp它比普通的稍微贵一点return(但它没有逃脱return),所以你更愿意在错误的情况下使用它.

但是使用setjmp并且longjmp棘手有效且可移植的,并且如setjmp所记录的那样使您的代码难以理解.重要的是要非常认真地评论它.使用这些并巧妙而明智地不需要"千兆字节"的RAM,正如在编辑的问题中错误地说的那样(因为你在调用堆栈上只消耗一个,而不是数十亿个).如果你想要更复杂的控制流程,你将在调用堆栈中的每个动态"捕获点" 使用本地(你可能会有几十个,而不是数十亿).在数百万个调用帧递归的假设情况下,你只需要数百万个,每个都是一个捕获点,这是不现实的(即使没有任何例外,你也永远不会有一百万的深度递归处理).setjmplongjmpjmp_bufjmp_bufjmp_buf

对于一个更好的解释setjmp为"异常"用C处理(和SFTW其他的).FWIW,鸡方案有一个非常有创造力的使用longjmpsetjmp(与垃圾收集call/cc!)


备择方案

setcontext(3)可能是POSIX,但现在已经过时了.

GCC有几个有用的扩展(其中一些是Clang/LLVM理解的):语句exprs,本地标签,标签作为值和计算goto,嵌套函数,构造函数调用等.

(我的感觉是你误解了一些概念,特别是调用堆栈的确切作用,所以你的问题很不清楚 ;我给了一些有用的参考资料)

回来一个 struct

还要注意的是一些ABI S,尤其是X86-64 ABI在Linux上,返回一个小的 struct(例如,两个指针,或者一个指针和一个intlongintptr_t数量)是非常有效的(因为这两个指针或整数走通寄存器),和你可以利用这个:决定你的函数返回一个指向主要结果的指针和一些错误代码,两者都打包在一个小的struct:

struct tworesult_st {
 void* ptr;
 int err;
};

struct towresult_st myfunction (int foo) {
  void* res = NULL;
  int errcode = 0;
  /// do something
  if (errcode) 
    return (struct tworesult_st){NULL, errcode};
  else
    return (struct tworesult_st){res, 0};
}       
Run Code Online (Sandbox Code Playgroud)

在Linux/x86-64上,上面的代码经过优化(编译时gcc -Wall -O),返回两个寄存器(没有为返回的代码消耗任何堆栈struct).

使用这样的函数很简单而且效率很高(不涉及内存,两个成员结构将在处理器寄存器中传递)并且可以简单如下:

struct tworesult_st r = myfunction(34);
if (r.err) 
  { fprintf(stderr, "myfunction failed %d\n", r.err); exit(EXIT_FAILURE); }
else return r.ptr;
Run Code Online (Sandbox Code Playgroud)

当然,你可以有一些更好的错误处理(这取决于你).

其他提示

阅读有关语义的更多信息,特别是操作语义.

如果可移植性不是主要问题,请研究系统及其ABI 的调用约定和生成的汇编代码(gcc -O -Wall -fverbose-asm foo.c然后查看内部foo.s),并编写相关asm指令.

也许libffi可能是相关的(但我仍然不理解你的目标,只是猜测它们).

您可以尝试使用标签exprs和计算的gotos,但除非您了解生成的汇编代码,否则结果可能不是您所期望的(因为堆栈指针在函数调用和返回时更改).

自修改代码不受欢迎(并且在标准 C99中"不可能" ),并且大多数C实现将二进制代码放在只读代码段中.另请阅读有关蹦床功能的内容.考虑可能是JIT编译技术,即libjit,asmjit,GCCJIT.

(我坚信实用主义回答您的问题或者是longjmp合适的编码约定,或者简单地返回小struct;既能够方便地在一个非常有效的方式使用,并且他们没有足够有效的,我无法想象的情况下)

有些语言:带有call/ccProlog及其回溯功能的Scheme 可能比OP的需要更适应(比C99更多).

  • 不,我无法轻易解释国家如何变化.我在Univ Paris 6(硕士学位,计算机科学)教授语义学,我花了几个小时的课程来解释这一点.我无法承担花费那么多时间在一个SO答案上.我给了你几个参考.并向你解释这需要我理解你的错误观念(我只是猜到了) (5认同)
  • 帧恢复是特定于实现的,不能在便携式C中完成(但在某种意义上,`longjmp`是*唯一可移植的方式*来恢复调用堆栈).某些C实现不使用任何硬件堆栈(因为IBM serie Z上没有任何一个,例如.eg) (4认同)
  • 为什么这不合理?你有基准吗?我相信你脑子里有几个误解.跳转缓冲区足够小,可以作为参数非常快速地传递 (3认同)
  • 但是在*标准* C99中,***唯一的转义方法*是`longjmp`**(或者`abort`和`exit`,如果你接受转义整个程序) (2认同)