-27

有可能在 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
    // ...
}
  • 这可以使用 C 和 GCC 扩展以可移植的方式实现,而不使用特定于平台的程序集吗?
  • 堆栈状态不会在正常返回点和更改后的返回点之间改变,那么是否可以重用从常规返回中恢复堆栈和寄存器状态的代码?
  • 考虑到函数可能被内联也可能不被内联,如果调用它必须改变返回地址,如果内联它只能改变代码路径而不是当前函数返回地址,因为这会破坏代码
  • 备用返回点不需要是标签,但我希望GCC的标签扩展地址可以在这种情况下派上用场

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

该示例“确实有意义”,因为它的目的是展示我想要实现的目标,而不是展示它为什么以及如何在实际代码中有意义。

为什么您的想法比简单地从 foo() 返回一个值并让 bar() 有条件地返回或执行某处Else: 更简单更好?

它并不简单,而且您的建议在实践中并不适用,仅在一个简单示例的背景下,但它更好,因为:

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
}

至于 Basile Starynkevitch 提出longjmp的“高效”和“即使 10 亿个 longjmp 仍然合理”的说法——sizeof(jmp_buf)给了我 156 个字节,这显然是保存几乎所有寄存器和一堆其他东西所需的空间,所以以后可以恢复。这些是很多操作,并且这样做十亿次远远超出了我个人对“高效”和“合理”的理解。我的意思是 10 亿个跳转缓冲区本身就超过 145 GB的内存,然后还有 CPU 时间开销。没有很多系统甚至可以负担得起那种“合理的”。

4

2 回答 2

40

不,这是不可移植的,我不确定你想要达到什么目的。

术语

也许您想要一些非本地跳转。仔细阅读setjmp.hcoroutines调用堆栈异常处理continuationcontinuation-passing-style。了解Scheme中的call/cc应该是非常有益的。

setjmplongjmp

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

这个想法是在没有任何额外开销的情况下从深层调用链中提供“转义代码路径”

正是longjmpwith的作用setjmp。两者都是快速、恒定时间的操作(特别是展开包含数千个调用帧的调用堆栈longjmp需要很短且恒定的时间)。内存开销实际上是每个捕获点一个本地jmp_buf的,没什么大不了的。jmp_buf很少将其放在调用堆栈之外。

有效使用它们的一种常见方法是将setjmp-edjmp_buf放在本地struct(所以在你的调用框架中)并将指向它的指针传递struct给一些内部static函数,这些函数会间接调用longjmp错误。因此setjmp,并且longjmp可以通过明智的编码约定很好和有效地模仿 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);

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

我们现在声明并实现一个错误函数,它将错误消息打印到状态中并以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);
  }

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

  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`
  }

(即使你有几十个像上面这样的内部函数,你也不会jmp_buf在其中任何一个函数中使用 a ;你也可以在它们中进行非常深入的递归。你只需要在所有函数中传递一个-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
  }

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

因此,正确使用十亿次并不会占用大量内存( setjmplongjmp单个local的调用堆栈上只有几百字节,在函数返回时jmp_buf弹出)。my_foo确实,longjmp它比普通的成本略高return(但它会进行没有的转义return),因此您更愿意在错误情况下使用它。

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

setjmp有关C 中的“异常”处理(以及其他的 SFTW)的更好解释,请参阅此内容。FWIW,Chicken Scheme非常有创意地使用longjmpsetjmp(与垃圾收集call/cc!)


备择方案

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

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

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

返回一个 struct

另请注意,在某些ABI上,特别是 Linux 上的 x86-64 ABI,返回一个 struct的(例如两个指针,或一个指针和一个intlongintptr_t数字)非常有效(因为指针或整数都通过寄存器),并且您可以利用这一点:决定您的函数返回一个指向主要结果的指针和一些错误代码,两者都打包在一个 small 中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};
}       

在 Linux/x86-64 上,上面的代码经过优化(使用 编译时gcc -Wall -O)以在两个寄存器中返回(返回的 不消耗任何堆栈struct)。

使用这样的函数非常简单且非常高效(不涉及内存,两个成员 ̀ 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;

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

其他提示

阅读有关语义的更多信息,尤其是操作语义。

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

也许libffi可能是相关的(但我仍然不明白你的目标,只是猜到了)。

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

自修改代码是不受欢迎的(在标准C99中是“不可能的” ),大多数 C 实现将二进制代码放在只读代码段中。另请阅读蹦床功能。考虑一下JIT 编译技术,比如 libjitasmjitGCCJIT

(我坚信,对您的担忧的务实答案是longjmp使用合适的编码约定,或者简单地返回一个小的struct;两者都可以以非常有效的方式便携使用,我无法想象它们效率不够高的情况)

一些语言:Schemecall/cc及其回溯功能,Prolog 及其回溯功能,可能更适应(比 C99)OP 的需求。

于 2015-11-22T11:06:35.813 回答
-9

多加思考后,它并不像最初看起来那么简单。有一件事会阻止它工作 - 函数代码不支持上下文 - 无法知道调用它的框架,这有两个含义:

1 - 如果不能移植,修改指令指针很容易,因为每个实现都为它定义了一个一致的位置,它通常是堆栈上的第一件事,但是修改它的值以跳过转义陷阱也会跳过恢复的代码前一帧状态,因为该代码在那里,而不是在当前帧中 - 它不能执行状态恢复,因为它没有它的信息,如果要省略额外的检查和跳转,解决方法是复制不幸的是,这两个位置的状态恢复代码只能在汇编中完成

2 - 需要跳过的指令数量也是未知的,取决于哪个是前一个堆栈帧,取决于需要销毁的局部数,它会有所不同,它不会是一个统一的值,对此的补救措施将在调用函数时将错误和成功指令指针都压入堆栈,因此它可以根据是否发生错误来恢复其中一个或另一个。不幸的是,这也只能在组装中完成。

似乎这样的方案只能在级别编译器上实现,需要自己的调用约定,即推送两个返回位置并在两者处插入状态恢复代码。而且这种方法的潜在节省几乎不值得编写编译器的努力。

于 2015-11-22T15:13:57.257 回答