saneex.c: try / catch / أخيرًا استنادًا إلى setjmp / longjmp (C99) أسرع من استثناءات C ++ القياسية ¹

بينما كان يكتب هذه المقالة التقنية البحتة ، تمكنت هبر من التحول إلى مكتب محلي لمنظمة الصحة العالمية ، والآن أشعر بالخجل من نشره ... لكن الأمل يسخن في روح أن موظفي تكنولوجيا المعلومات لم يهربوا بعد وستجد قارئها. أم لا؟




لطالما أعجبت بمكتبة C القياسية ، و C نفسها - بكل بساطتها ، لا تزال تهب روح هؤلاء لأول مرة ذو عيون حمراء قراصنة . في مسودة المعيار الرسمي الأول (ANSI C، ويعرف أيضا باسم C89، الملقب ANS X3.159-1989، في وقت لاحق، C90 و IEC 9899: 1990) يحدد 145 وظائف وحدات الماكرو، منها حوالي 25 اختلافات (بسبب عدم وجود في لغة الزائد) ، و 26 رياضيات بحتة. يستشهد K&R في الإصدار الثاني ² بـ 114 وظيفة (بالإضافة إلى الرياضيات) ، معتبرا أن الباقي غريب. يوجد في المسودة C11 بالفعل 348 وظيفة ، ولكن أكثر من مائة منها رياضيات ، و 90 أخرى هي "حمولة زائدة". الآن دعونا نلقي نظرة على Boost ، حيث يوجد 160 مكتبة فقط . تبا لي ...


ومن بين هذه المئات ونصف الوظائف ، كانت هناك دائمًا: معالجة الإشارات والوظائف المتنوعة (التي وصلت إلى تفسير PHP بعد 25 عامًا ، وفي دلفي ، التي كانت تزدهر في وقت واحد ، لا تزال مفقودة) وحوالي 50 وظيفة سلسلة مثل printf () ( mmm ... JavaScript) ، strftime () (...) و scanf () (بديل رخيص للنظامي).



وكان هناك دائمًا setjmp () / longjmp () ، مما يسمح لك بتنفيذ آلية الاستثناء المألوفة بلغات أخرى ، دون تجاوز نطاق المحمولة C. إليك حديث عنهم - عالم الزلازل ، المداخن ، السجلات ، المجمعات وغيرها من العتاد ، والكرز هو إحصاء غريب ( المفسد : Visual Studio ليس ثابتًا ، كأرنب مارس ، ويرمي saneex.c في نصف الوقت على الإطلاق).



نص مخفي

¹ .


² , . 270 , 80 — . , . K&R — , .


³ , ANSI ISO , . .


⁴ , «» TinyURL, URL , і . , -. urlex.org.


جدول المحتويات:



,  — setjmp()/longjmp(), setjmp.h, «SJLJ» ( , ). C89 , -, , (  — ,  — ).


, , , zzeng. , , , ¹, , , ( , ), .


¹ CException  — 60 , , , ANSI C, finally , .


,  — , , , , , « ». (, , - «» , errno .  — , .)


, :


  • , - , ( ,  — , if (error) return -1;)
  • -  — , ,

. , .



setjmp()/longjmp()



, --


, longjmp() — goto, setjmp() — goto run-time. , «goto ». , , , goto,  — , goto . , - , (  — «try»).


, , , , setjmp.h ^W ? , setjmp() , (, , ). , , , . .


, , setjmp() — fork() POSIX . , *nix’ API WinAPI,  — , .  — « , ?»… .



, ,  — , () .  — ( Intel’ — ),  — ( Intel’ — -, ). :


void sub(int s) {
  char buf[256];
  sub(2);
}

int main(int m) {
  sub(1);
}

 — tcc (Tiny C Compiler) - . . tcc . sub() ( Intel, ):


sub     esp, 100h       ;     
mov     eax, 2          ;  
push    eax
call    sub_401000      ;  sub()
add     esp, 4          ;     (= cdecl)

:



 — ( Intel… , ). ESP (RSP x86_64). setjmp() ESP/RSP, , jmp_buf, . longjmp() ( ) — , , setjmp(), (). , «undo» (, ).


setjmp() jmp FEF8h (FDF0h .. — ) , :


void sub(int s) {
  char buf[256];
  jmp_buf jmp;
  setjmp(jmp);
  sub(2);
}

, , ™:




  •  — , , ! — , , , setjmp() , longjmp() , setjmp()


, -, clobbering


. :


#include <stdlib.h>
#include <stdio.h>
#include <setjmp.h>

int main(void) {
  int i;
  jmp_buf jmp;
  i = rand();
  if (setjmp(jmp) == 0) {
    i = rand();
    printf("%d\n", i);
    longjmp(jmp, 1);
  } else {
    printf("%d\n", i);
  }
}

: ?


: . -!


, gcc. -O0, , :


; int main(void) {
  push    ebp             ;  ( stack frame)
  mov     ebp, esp        ; EBP     ESP (  )
  sub     esp, E0h
  ...
  call    _rand           ;    EAX
  mov     [ebp-D4h], eax  ;  i = rand();  i   (EBP-D4h)
  ...
; if (... == 0) {         ;  setjmp()      
  call    _rand
  mov     [ebp-D4h], eax  ;  i = rand();  
; printf("%d\n", i);
  mov     eax, [ebp-D4h]  ;  i    
  mov     esi, eax
  lea     edi, format     ;   "%d\n"
  mov     eax, 0
  call    _printf
  ...
; } else {                ;    setjmp()  
  mov     eax, [ebp-D4h]  ;   i,    
  mov     esi, eax
  lea     edi, format     ; "%d\n"
  mov     eax, 0
  call    _printf

, i ( EBP - D4h). , :


  • 256 char int jmp_buf, 4 200 , 20 - , 224 (E0h) 100h,
  • ESP setjmp() FFF8h - E0h = FF18h ( FEF8h), jmp
    • , ,
  • i, i ( FF18h)
  • longjmp() FF18h, , i , - , (jmp), main() ( )
    • ESP , longjmp() , main()

-O1, :


;   stack frame  ,   ESP 
  sub     esp, E8h
  ...
  call    _rand
  mov     [esp+E8h-DCh], eax  ; i = rand();  ,    -O0
  ...
; -O1 - ,   else  , 
; if (setjmp() == 0) ( - ),  
;  ;       
; if (... == 0) {
  call    _rand
  mov     esi, eax            ; !  i  
; printf("%d\n", i);
  lea     edi, format     ; "%d\n"
  mov     eax, 0
  call    _printf
  ...
; } else {
  mov     esi, [esp+E8h-DCh]  ; !  i  
  lea     edi, format     ; "%d\n"
  mov     eax, 0
  call    _printf

, -O1 gcc :


test.c:6:11: warning: variable ‘i’ might be clobbered by ‘longjmp’ or ‘vfork’ [-Wclobbered]

? i , ( if) gcc, i printf(), ESI, ( ESI printf(), . ABI, . 22 — RDI (format), RSI (i), …). - :


  • ESP + E8h - DCh rand()
  • ESI
  • printf() ( ) ()
  • longjmp() , ,
  • printf() ( else) , , ,
    • ESI, (, printf() longjmp())


, :


stack[i] = rand();          // i = rand();   (1)
if (setjmp(jmp) == 0) {
  ESI = rand();             // i = rand();   (2)
  printf("%d\n", ESI);      //   (2)
  longjmp(jmp, 1);          // 
} else {
  printf("%d\n", stack[i]); //   (1)
  //     :
  printf("%d\n", ESI);      //  ,   -
                            // "" ( printf()  longjmp())
}

, , gcc rand() ESI ( -O3). SO , x86_64 ( ) , EAX. ? , gcc printf() else longjmp(), rand() printf() — , i .


 — .



volatile


« » — volatile ( — «»). , , , :


volatile int i;

-O1 if:


; :
  call    _rand
  mov     esi, eax
; :
  call    _rand
  mov     [rsp+E8h-DCh], eax
  mov     esi, [rsp+E8h-DCh]
;    :
  call    _rand
  mov     esi, eax
  mov     [rsp+E8h-DCh], eax

, ():


if (setjmp(jmp) == 0) {
  ESI = stack[i] = rand();


IRL


,  — -volatile , SJLJ .  — , SJLJ :



, ,  — , « », , , - (). , DrMefistO.


Quake World WinMain(), jmp_buf, , « continue»:


// WinQuake/host.c
jmp_buf         host_abortserver;

void Host_EndGame (char *message, ...)
{
  ...

  if (cls.demonum != -1)
    CL_NextDemo ();
  else
    CL_Disconnect ();

  longjmp (host_abortserver, 1);
}

void Host_Error (char *error, ...)
{
  ...

  if (cls.state == ca_dedicated)
    Sys_Error ("Host_Error: %s\n",string);  // dedicated servers exit

  CL_Disconnect ();
  cls.demonum = -1;

  inerror = false;

  longjmp (host_abortserver, 1);
}

void _Host_Frame (float time)
{
  static double           time1 = 0;
  static double           time2 = 0;
  static double           time3 = 0;
  int                     pass1, pass2, pass3;

  if (setjmp (host_abortserver) )
    return;                 // something bad happened, or the server disconnected

  ...
}

// QW/client/sys_win.c
int WINAPI WinMain (...)
{
  ...

  while (1)
  {
    ...
    newtime = Sys_DoubleTime ();
    time = newtime - oldtime;
    Host_Frame (time);
    oldtime = newtime;
  }

  /* return success of application */
  return TRUE;
}



,  — . , setjmp() glibc , . :


  • , , SJLJ/saneex.c
  • -…- (, ) , -  — ,
  • ,  — zero-cost exceptions (, , zero-cost try), try, () — goto , , , «» , -,

«» zero-cost exceptions , volatile-, , ( longjmp()). , :



, saneex.c zero-cost (  — ), setjmp(), ? , ?  — .




« », main() 100 try/catch throw().


C:


#include <stdio.h>
#include <time.h>
#include "saneex.h"

int main(void) {
  for (int i = 0; i < 100000; i++) {
    try {
      //  ("" = ):
      throw(msgex("A quick fox jumped over a red dog and a nyancat was spawned"));
      //  ("" = ):
      time(NULL);
    } catchall {
      fprintf(stderr, "%s\n", curex().message);
    } endtry
  }
}

++ ( , cerr << fprintf()):


#include <iostream>
#include <vector>
#include <stdexcept>
#include <time.h>

int main() {
  std::vector<int> vec{ 3, 4, 3, 1 };

  for (int i = 0; i < 100000; i++) {
    try {
      //  ("" = ):
      int i{ vec.at(4) };
      //  ("" = ):
      time(NULL);
    }
    catch (std::out_of_range & e) {
      // <<  fprintf()     25-50%
      //std::cerr << "Accessing a non-existent element: " << e.what() << '\n';
      fprintf(stderr, "%s\n", e.what());
    }
    catch (std::exception & e) {
      //std::cerr << "Exception thrown: " << e.what() << '\n';
      fprintf(stderr, "%s\n", e.what());
    }
    catch (...) {
      //std::cerr << "Some fatal error\n";
      fprintf(stderr, "Some fatal error");
    }
  }

  return 0;
}

( 64-):


  • Windows 10 2019 LTSC PowerShell Measure-Command { test.exe 2>$null }
  • Live CD Ubuntu time

Windows __try/__except, :


#include <windows.h>
#include <stdio.h>
#include <vector>

int filterExpression(EXCEPTION_POINTERS* ep) {
  ep->ContextRecord->Eip += 8;
  return EXCEPTION_EXECUTE_HANDLER;
}

int main() {
  static int zero;
  for (int i = 0; i < 100000; i++) {
    __try {
      zero = 1 / zero;
      __asm {
        nop
        nop
        nop
        nop
        nop
        nop
        nop
      }
      printf("Past the exception.\n");
    }
    __except (filterExpression(GetExceptionInformation())) {
      printf("Handler called.\n");
    }
  }
}

 — , :


error C2712: Cannot use __try in functions that require object unwinding

, , . 1100-1300 (Debug Release, x86) — , VS, , g++.




№                     ()¹           saneex 

1.  VS 2019 v16.0.0   Debug     x64   saneex.c        9713  / 8728  = 1.1    1.8 / 1.8
2.  VS 2019 v16.0.0   Debug     x64   saneex.c       95    / 46    = 2      4.5 / 2.3
3.  VS 2019 v16.0.0   Debug     x64   C++             5449  / 4750² = 1.6
4.  VS 2019 v16.0.0   Debug     x64   C++            21    / 20    = 1
5.  VS 2019 v16.0.0   Release   x64   saneex.c        8542³ / 182   = 47     1.8 / 0.4
6.  VS 2019 v16.0.0   Release   x64   saneex.c       80³   / 23    = 3.5    8   / 1.8
7.  VS 2019 v16.0.0   Release   x64   C++             4669³ / 420   = 11
8.  VS 2019 v16.0.0   Release   x64   C++            10³   / 13    = 0.8
9.  gcc 9.2.1         -O0       x64   saneex.c        71    / 351   = 0.2    0.2 / 0.6
10. gcc 9.2.1         -O0       x64   saneex.c       6     / 39    = 0.2    1.5 / 1.1
11. g++ 9.2.1         -O0       x64   C++             378   / 630   = 0.6
12. g++ 9.2.1         -O0       x64   C++            4     / 37    = 0.1
13. gcc 9.2.1         -O3       x64   saneex.c        66    / 360   = 0.2    0.2 / 0.6
14. gcc 9.2.1         -O3       x64   saneex.c       5     / 23    = 0.2    1   / 0.6
15. g++ 9.2.1         -O3       x64   C++             356   / 605   = 0.6
16. g++ 9.2.1         -O3       x64   C++            5     / 38    = 0.1

¹ Windows 7 SP1 x64 VS 2017 v15.9.17 gcc cygwin.


² : fprintf() cerr <<, 3 : 1386/1527 .


³ VS , .


… :


  • / «» VS.  — .
  • cerr << fprintf() VS 3-4 ( 3). ?
  • try throw — (4-28 100 ).
  • «» Debug VS, saneex.c , ( 2.3 VS, 5 gcc/g++), try throw — , . !

… . !


use-case — try throw (« , »), setjmp(), , , , . , , setjmp() OpenBSD (1.45) — Solaris. 2005 . «» — , .


, …



 — saneex.c


, :


  • Visual Studio
  • , throw() , finally catch ( )
  • ( static)
  • - (__thread/_Thread_local)
  • public domain (CC0)


GitHub. , . saneex-demo.c :


01.    #include <stdio.h>
02.    #include "saneex.h"
03.
04.    int main(void) {
05.      sxTag = "SaneC's Exceptions Demo";
06.
07.      try {
08.        printf("Enter a message to fail with: [] [1] [2] [!] ");
09.
10.        char msg[50];
11.        thrif(!fgets(msg, sizeof(msg), stdin), "fgets() error");
12.
13.        int i = strlen(msg) - 1;
14.        while (i >= 0 && msg[i] <= ' ') { msg[i--] = 0; }
15.
16.        if (msg[0]) {
17.          errno = atoi(msg);
18.          struct SxTraceEntry e = newex();
19.          e = sxprintf(e, "Your message: %s", msg);
20.          e.uncatchable = msg[0] == '!';
21.          throw(e);
22.        }
23.
24.        puts("End of try body");
25.
26.      } catch (1) {
27.        puts("Caught in catch (1)");
28.        sxPrintTrace();
29.
30.      } catch (2) {
31.        puts("Caught in catch (2)");
32.        errno = 123;
33.        rethrow(msgex("calling rethrow() with code 123"));
34.
35.      } catchall {
36.        printf("Caught in catchall, message is: %s\n", curex().message);
37.
38.      } finally {
39.        puts("Now in finally");
40.
41.      } endtry
42.
43.      puts("End of main()");
44.    }

, :


  •  — , :

End of try body
Now in finally
End of main()

  • , , (1), catch (1) (26.), :

Caught in catch (1)
Your message: 1 hello, habr!
    ...at saneex-demo.c:18, code 1
Now in finally
End of main()

  • , (30.), ( , ) (33.), :

Caught in catch (2)
Now in finally

Uncaught exception (code 123) - terminating. Tag: SaneC's Exceptions Demo
Your message: 2 TM! kak tam blok4ain?
    ...at saneex-demo.c:18, code 2
calling rethrow() with code 123
    ...at saneex-demo.c:33, code 123
rethrown by ENDTRY
    ...at saneex-demo.c:41, code 123

  • !, «» (uncatchable; 20.) — try , ( catch, finally),  — abort():

Caught in catch (1)
Your message: ! it is a good day to die
    ...UNCATCHABLE at saneex-demo.c:18, code 0
Now in finally

Uncaught exception (code 0) - terminating. Tag: SaneC's Exceptions Demo
Your message: ! it is a good day to die
    ...UNCATCHABLE at saneex-demo.c:18, code 0
UNCATCHABLE rethrown by ENDTRY
    ...at saneex-demo.c:41, code 0

  • , , catchall (35.), :

Caught in catchall, message is: Your message: 3 we need more gold
Now in finally
End of main()


«»



. , ( MSVC¹), C11 (TLS):


#define SX_THREAD_LOCAL _Thread_local

¹ Microsoft - open source, , , 8 , .


sxTag (05.) — , stderr.  — (__DATE__ __TIME__).


SxTraceEntry ( stack trace).  — (struct SxTraceEntry) {...}:


  • newex() — ; __FILE__, __LINE__ = errno ( , fgets(); 11.)
    • 1 1 ( setjmp() 0 ), catch (0)
  • msgex(m) — newex(), ( )
  • exex(m, e) — msgex(), ; free() :

try {
  TimeoutException *e = malloc(sizeof(*e));
  e->elapsed = timeElapsed;
  e->limit = MAX_TIMEOUT;
  errno = 146;
  throw(exex("Connection timed out", e));
} catch (146) {
  printf("%s after %d\n", curex().message,
    //   void *SxTraceEntry.extra:
    ((TimeoutException *) curex().extra)->elapsed);
} endtry

, , designated initializers C99 ( Visual Studio 2013+):


throw( (struct SxTraceEntry) {.message = "kaboom!"} );

:


  • throw(e) — SxTraceEntry
  • rethrow(e) — throw(), stack trace; catch/catchall
  • thrif(x, m) — ; if (x) SxTraceEntry x + m «»
  • thri(x) — thrif(), m

«»  — fgets() (11.), . fgets() ( EOF: ./a.out </dev/null), . :


thri(read(0xBaaD, buf, nbyte));
// errno = 9, "Bad file descriptor"
// Assertion error: read(0xBaaD, buf, nbyte);


… « »


( !):


  • endtry — ( try)
    • , , , try {, endtry
  • return try endtry — , ; PR
    • , goto , - ? </sarcasm>

«», volatile. «»  — (. longjmp()), , try, catch/catchall/finally endtry, volatile. . :


int foo = 1;
try {
  foo = 2;
  //    foo
} catchall {
  //    !
} finally {
  //   !
} endtry
//   !

volatile :


volatile int foo = 1;
try {
  ...


:


- () :


  • struct SxTryContext — try,  — , jmp_buf ; , :

try {
  try {
    //  
  } endtry
} endtry

  • struct SxTraceEntry — stack trace, , ; , try:

try {         //  SxTryContext
  try {       //  SxTryContext
              //  SxTraceEntry
    throw(msgex(" !"));
              //  SxTraceEntry
  } catchall {
              //  SxTraceEntry
    rethrow(msgex("   !"));
              //  SxTraceEntry (*)
  } endtry
} endtry

rethrow() throw(), SxTraceEntry (*) ,  — (stack trace ). , sxAddTraceEntry(e).


حاول والعناصر الهيكلية الأخرى هي وحدات الماكرو (- K.O. ). الأقواس { }بعدها اختيارية. في النهاية ، كل ذلك يتلخص في الرمز الزائف التالي:


try {                             int _sxLastJumpCode = setjmp(add_context()¹);
                                  bool handled = false;
                                  if (_sxLastJumpCode == 0) {
  throw(msgex("Mama mia!"));        clearTrace();
                                    sxAddTraceEntry(msgex(...));
                                    if (count_contexts() == 0) {
                                      fprintf(stderr, "Shurik, vs propalo!");
                                      sxPrintTrace();
                                      exit(curex().code);
                                    } else {
                                      longjmp(top_context());
                                    }
} catch (9000) {                  } else if (_sxLastJumpCode == 9000) {
                                    handled = true;
} catchall {                      } else {
                                    handled = true;
} finally {                       }
                                  //    finally { }
} endtry                          remove_context();
                                  if (!handled) {
                                    //    throw()
                                  }

¹ الأسماء التي تحتوي على _ غير مستخدمة في المكتبة ، إنها مجردة.


أعتقد أنه بعد التوضيحات التفصيلية لكيفية عمل SJLJ ، هناك شيء آخر غير ضروري للتعليق هنا ، لذا دعني أخذ إجازة وأعطيك الكلمة بالفعل.


All Articles