LLVM Introduction – Exception Handling 2/2

Copyright (c) 2011 陳韋任 (Chen Wei-Ren)

我們繼續接著看 LLVM 針對 nested try、exception specification 和 destructor 的處理。這邊需要對 C++ 的語法和例外處理有點認識,請見 [1][2]。首先來看 nested try。

C++ nested try 的例子如下 (請見 [3] 第 29 頁):

try {
    ...
    try {
        ...
        MayThrowSomething();
        ...
    } catch (int i) {
        DoSomethingWithInt(i);
    } catch (class A a) {
        DoSomethingWithA(a);
    }
    ...
} catch (class B b) {
    DoSomethingWithB(b);
} catch (...) {
    DoSomethingElse();
}

會丟出例外的 MayThrowSomething 函式其 landingpad 區塊的 LLVM IR 如下:

lpad:
  %info = landingpad { i8*, i32 }
    personality @__gxx_personality_v0
    catch @_ZTIi    // catch (int)
    catch @_ZTI1A   // catch (class A)
    catch @_ZTI1B   // catch (class B)
    catch null      // catch (...)

這裡我們可以看到 landingpad 會列出所有可能會接住 MayThrowSomething 例外的 catch clause。

C++ 允許程序員規範一個函式所能丟出例外其型別 (exception specification),例如 ([3] 第 30 頁):

// foo 不允許丟出任何例外
int foo() throw () {
    bar();
    return 0;
}

LLVM 中用 filter 來實現 exception specification,其對映的 LLVM IR 如下:

  invoke void @_Z3barv() to label %cont unwind label %lpad

cont:
  ret i32 0

lpad:
  %info = landingpad { i8*, i32 }
          personality @__gxx_personality_v0
          filter [0 x i8*] zeroinitializer
  %except = extractvalue { i8*, i32 } %info, 0
  tail call void @__cxa_call_unexpected(%except)
  unreachable

我們可以看到針對 bar 的呼叫是用 invoke,

  invoke void @_Z3barv() to label %cont unwind label %lpad

這裡的重點在 %lpad 區塊。landingpad 後面有一個 filter,

  filter [0 x i8*] zeroinitializer

它後面是一個 typeinfo 的數組 (array) [4]。如果丟出的例外不匹配該數組中的任一個 typeinfo,landingpad 會傳回負值。這種情況下,__cxa_call_unexpected 或是 _Unwind_Resume 應該被呼叫 [5]。這裡我們可以看到,針對不會丟出例外的函式,filter 帶的數組是 [0 x i8*],這是一個沒有元素的數組。

  tail call void @__cxa_call_unexpected(%except)

call 指令之前 tail 標記代表此處的函式呼叫可以做 tail call optimization 或是 sibling call optimization。詳細請見 [6]。其後的 unreachable 是告知優化器執行流程
不會到達此處。以此例而言,__cxa_call_unexpected 具有 noreturn 的屬性,因此其後會加上 unreachable,代表 __cxa_call_unexpected 函式呼叫不會返回。

最後,我們來看 LLVM IR 如何處理 destructor。在做 stack unwinding 的時候,當前 stack 上的物件 (object) 的解構子 (destructor) 會被執行,這是 C++ 的情況。其它 語言或是語言擴展也有提供類似機制。這邊以 GCC cleanup attribute 為例 [7] ([3] 第 31 頁):

void oof(void *);
void bar(void) {
    int x __attribute__((cleanup(oof)));
    foo();
    ...
}

當變數 x 離開目前的範圍,oof 會被呼叫。有兩種情況會導致變數 x 離開目前的範圍,

  • bar 函式正常執行完畢。
  • foo 函式丟出例外,或是其它會導致 stack unwinding 的事情發生。

我們只考慮後者,並看 LLVM 怎麼實現 destructor/cleanup,其對映的 LLVM IR 如下:

define void @bar() {
entry:
  %x = alloca i32
  invoke void @foo() to label %cont unwind label %lpad

cont:
  ...

lpad:
  %info = landingpad { i8*, i32 }
          personality @__gcc_personality_v0
          cleanup
  %var_ptr = bitcast i32* %x to i8*
  call void @oof(i8* %var_ptr)
  resume { i8*, i32 } %info
}

landingpad 後面帶的 cleanup 指明當前的區塊 (%lpad) 有 cleanup 需要執行,這裡是指 oof 函式。因為當前函式沒有合適的 catch clause 處理該例外,最後執行 resume。這裡要注意到,如果 foo 正常執行,走到 %cont 區塊,oof 最終還是會被執行。上述 LLVM IR 只看 foo 會丟出例外的情況。

眼尖的人會看到 personality routine 改叫 __gcc_personality_v0,請見 [9]。

LLVM 代碼中有展示例外處理的範例,有興趣的人可以參考 examples/ExceptionDemo/ExceptionDemo.cpp,並遵照底下流程編譯,最後執行檔放在 $INSTALL 目錄。

$ wget http://llvm.org/releases/3.0/llvm-3.0.tar.gz; tar xvf llvm-3.0.tar.gz
$ mkdir build; cd build
$ ../llvm-3.0.src/configure --prefix=$INSTALL
$ make install
$ cd example
$ make install

限於個人才疏學淺,有興趣的人可以參考底下文件。

  • http://www.llvm.org/docs/ExceptionHandling.html
  • http://blog.llvm.org/2011/11/llvm-30-exception-handling-redesign.html
  • http://stackoverflow.com/questions/329059/what-is-gxx-personality-v0-for
  • http://www.airs.com/blog/archives/464
  • http://lists.cs.uiuc.edu/pipermail/llvmdev/2011-July/041748.html
  • http://lists.cs.uiuc.edu/pipermail/llvmdev/2012-January/046700.html

erlv 整理了一下 LLVM 3.0 的新特色,有興趣的人請見 你好,LLVM 3.0。

[1] http://www.cplusplus.com/doc/tutorial/exceptions/
[2] http://en.wikibooks.org/wiki/C++_Programming/Exception_Handling
[3] http://llvm.org/devmtg/2011-09-16/EuroLLVM2011-ExceptionHandling.pdf
[4] http://llvm.org/docs/LangRef.html#t_array
[5] http://libcxxabi.llvm.org/spec.html
[6] http://llvm.org/releases/3.0/docs/LangRef.html#i_call
[7] http://gcc.gnu.org/onlinedocs/gcc/Variable-Attributes.html
[8] http://llvm.org/releases/3.0/docs/LangRef.html#i_invoke
[9] http://people.cs.nctu.edu.tw/~chenwj/log/GCC/richi-2011-12-16.txt

LLVM Introduction – Exception Handling 1/2

Copyright (c) 2011 陳韋任 (Chen Wei-Ren)

LLVM 3.0 其中一個較大的改動是重新設計和改寫對例外處理 (exception handling) 的支援。新的异常处理机制增加數條 LLVM (原生,first-class) 指令,並新增/移除數條 LLVM intrinsic 函式。如果想要擴展 LLVM,通常的建議是先增加 intrinsic 函式。在經過一段時間確認該擴展有必要加入 LLVM 原生支援,才會新增 LLVM IR。這是因為新增 LLVM IR 需要對 LLVM 整套框架做較大的修改。[1] 本篇文章主要以 Duncan Sands 今年在英國舉辦的 2011 European User Group Meeting 投影片 [2] 作為例子說明 LLVM IR 對例外處理的支援。

程式語言中的例外處理主要是針對程式中極少發生的例外狀況提供處理機制。因此理想上,例外處理不應該對程式中正常流程造成任何影響,這是所謂的 zero-cost exception handling。目前所見的例外處理,其實作上基本遵循 Itanium C++ ABI: Exception HandlingThe new LLVM exception handling scheme 這份投影片是展示如何將 C++ 例外處理的語法對映至 LLVM IR。

底下是很典型的 C++ try/catch 述句。

try {
    ...
    MayThrowSomething();
    AnotherFunctionCall();
    ...
} catch (int i) {
    DoSomethingWithInt(i);
} catch (class A a) {
    DoSomethingWithA(a);
}

如果函式 MayThrowSomething 丟出例外,會依序匹配 catch clause 看是否符合該例外的型別。如果都不匹配,則會做 stack unwinding,清空發生例外的函式其堆疊並將該例外傳遞給 caller 繼續處理該例外。針對 try 區塊中的函式呼叫,或是會丟出例外的函式其中的函式呼叫都改用 invoke 而非 call。所以 “MayThrowSomething()” 相對映的 LLVM IR 如下:

  invoke void @_Z17MayThrowSomethingv() to label %cont unwind label %lpad

根據例外是否發生,執行流程會跳到 %cont 區塊或是 %lpad 區塊。這裡的重點在於負責例外處理的 %lpad。在例外處理中,有個名詞叫 “landingpad”,中文譯名為停機坪。之所以稱為 landingpad,是因為當例外發生時,執行流程會跳轉到這裡做一些準備,再轉發到其它地方。我們來看一下 %lpad 的內容。

lpad:
  %info = landingpad { i8*, i32 } personality @__gxx_personality_v0 catch @_ZTIi %catch @_ZTl1A
  %except = extractvalue { i8*, i32 } %info, 0
  %selector = extractvalue { i8*, i32 } %info, 1
  %typeid = call i32 @llvm.eh.typeid.for(@_ZTIi)
  %match = icmp eq i32 %selector, %typeid
  br i1 %match, label %run_catch, label %try_next

我們先看第一道指令:

%info = landingpad { i8*, i32 } personality @__gxx_personality_v0 catch @_ZTIi %catch @_ZTl1A

在 Itanium C++ ABI: Exception Handling 中定義所謂的 personality routine。它是語言執行時期函示庫中的函式,用來作為 system unwind library 和 language-specific
exception handling semantics 的介面,這是因為不同的程式語言對於例外處理有不同的行為。landingpad 這條 LLVM IR 負責透過 personality routine 取得例外型別和其它資訊,在 C++ 中,該 routine 稱為 __gxx_personality_v0。

{ i8*, i32 } 是 %info 的型別,分別是指向 exception structure 的指針和 selector value。這在 LLVM 型別系統中是一個 struct type,LLVM 3.0 另一項重大修改就是它的型別系統。landingpad 最後列出所有的 catch clause。目前 LLVM 的設計會列出 nest try 中所有的 catch clause,請見 [2] 第 29 頁。

接著用 extractvalue 從 %info 中分別取出指向 exception structure 的指針和 selector value。

%except = extractvalue { i8*, i32 } %info, 0
%selector = extractvalue { i8*, i32 } %info, 1

llvm.eh.typeid.for 是負責例外處理的 intrinsic 函式,它是用來將 catch clause 內的型別 (@_ZTIi) 對映成 selector value (%typeid),

%typeid = call i32 @llvm.eh.typeid.for(@_ZTIi)

接著比較例外和 catch clause 的 selector value 是否一致。

%match = icmp eq i32 %selector, %typeid

如果匹配成功,表示由該 catch clause 處理該例外,跳至 %run_catch 區塊; 否則跳至 %try_next 繼續下一次的匹配。

br i1 %match, label %run_catch, label %try_next

這邊提一下 personality routine,在 C++ 稱為 __gxx_personality_v0。它知道如何進行 catch clause 的匹配,因為這需要比較 C++ 的型別。@_ZTIi 和 @_ZTI1A 分別是語言特定的全域變數,在此是 C++,分別代表 int 和 class A 兩個型別的型別資訊 (typeinfo)。

我們先來看 %run_catch 區塊。

run_catch:
%thrown = call i8* @__cxa_begin_catch(%except)
%tmp = bitcast i8* %thrown to i32*
%i = load i32* %tmp
call void @_Z18DoSomethingWithInti(i32 %i)
call void @__cxa_end_catch()
br label %finished

它對映的是 catch (int i) 裡面的代碼。其中,

%thrown = call i8* @__cxa_begin_catch(%except)
call void @__cxa_end_catch()

分別是語言特定的函式呼叫。還記得之前我們從 %info 取出 exception structure 的指針並賦值給 %except 嗎? __cxa_begin_catch 透過該指針取回 exception structure (仍然是指針),此外它還額外做了一些事。__cxa_end_catch 則是做一些清除的工作。這部分詳細內容請見 [5],這是 LLVM 實現 C++ runtime 的子計畫。

以下三條指令是將 i8* 轉型成 i32*,把 i32* 所指內存的內容讀出,最後傳給 catch(int i) 中的 DoSomethingWithInt 函式 (_Z18DoSomethingWithInti)。

%tmp = bitcast i8* %thrown to i32*
%i = load i32* %tmp
call void @_Z18DoSomethingWithInti(i32 %i)

最後跳至 %finished 區塊,完成例外處理。

br label %finished

如果該例外不匹配 catch (int i),則會跳至 %try_next 進行下一次匹配。

try_next:
%typeid2 = call i32 @llvm.eh.typeid.for(@_ZTI1A)
%match2 = icmp eq i32 %selector, %typeid2
br i1 %match2, label %run_catch2, label %end

這裡的重點在於,如果當前丟出例外的函式其 catch clause 無法處理該例外,就會跳至 %end 做 stack unwinding。

end:
resume { i8*, i32 } %info

這裡小節一下目前提到有關例外處理的 LLVM IR 和 intrinsics。

  • invoke – 以 C++ 為例,在 try block 中的函式呼叫,或是在會丟出例外的函式中的函式呼叫,一律改用 invoke 而非 call。
  • landingpad – 負責取出例外的相關資訊以供後續處理,並列出所有 catch clause。它取代原本的 llvm.eh.exception 和 llvm.eh.selector intrinsic。
  • llvm.eh.typeid.for – 把 catch clause 內的型別對映成 selector value,以便將來跟例外的 selector value 做比較。
  • resume – 如果例外無法被目前函式處理,做 stack unwinding 並將該例外傳遞至 caller。它取代原本的 llvm.eh.resume intrinsic。

[1] http://llvm.org/docs/ExtendingLLVM.html
[2] http://llvm.org/devmtg/2011-09-16/EuroLLVM2011-ExceptionHandling.pdf
[3] http://www.codesourcery.com/cxx-abi/abi-eh.html
[4] http://en.wikipedia.org/wiki/Call_stack#Unwinding
[5] http://libcxxabi.llvm.org/spec.html

有趣的选项 —— ld –wrap=symbol

工具链的选项非常丰富,有许多功能其实都可以通过选项来实现,比如如下ld的一个选项,我直接把手册的内容贴了下来。里面介绍了如何通过该选项,实现对系统函数进行封装,并且列举了一个例子。

–wrap=symbol
Use a wrapper function for symbol. Any undefined reference to symbol will be resolved to __wrap_symbol. Any undefined reference to __real_symbol will be resolved to symbol.

This can be used to provide a wrapper for a system function. The wrapper function should be called __wrap_symbol. If it wishes to call the system function, it should call __real_symbol.

Here is a trivial example:

              void *
              __wrap_malloc (size_t c)
              {
                printf ("malloc called with %zu\n", c);
                return __real_malloc (c);
              }

If you link other code with this file using –wrap malloc, then all calls to malloc will call the function __wrap_malloc instead. The call to __real_malloc in __wrap_malloc will call the real malloc function.

You may wish to provide a __real_malloc function as well, so that links without the –wrap option will succeed. If you do this, you should not put the definition of __real_malloc in the same file as __wrap_malloc; if you do, the assembler may resolve the call before the linker has a chance to wrap it to malloc.

强制链接一个函数

xmj@hellogcc

1、有时候,我们在程序里定义了一个函数,但是没有显式的调用它,只是用于其它目的比如方便调试。我们不想让编译器将它优化掉。这个时候,可以使用GCC扩展语法,来指定该函数需要保留。这在GCC源代码中也被用到,例如:

#if (GCC_VERSION > 4000)
#define DEBUG_FUNCTION __attribute__ ((__used__))
#define DEBUG_VARIABLE __attribute__ ((__used__))
#else 
#define DEBUG_FUNCTION
#define DEBUG_VARIABLE
#endif
DEBUG_FUNCTION void
debug_bb (basic_block bb)
{
  dump_bb (bb, stderr, 0);
}

2、但是,如果这个函数是在库中,而我们仍然希望将其链接到应用程序中的话,上述方法就不起作用了。这个时候,则可以通过链接器参数来指定。例如,

$ gcc foo.c -Wl,-uprintf -lc

-u的作用是指定该符合,printf,未定义,从而强制将其链接到程序中。

2011 LLVM Developers’ Meeting 4/4

Copyright (c) 2011 陳韋任 (Chen Wei-Ren)

DSC01455
吧檯。別忘了有兩張飲料券可以來這邊兌換。
DSC01456
餐廳內部。
DSC01458
主菜是雞肉,我忘了拍前菜的沙拉。一開始桌上有一籃麵包可以取用。
DSC01459
飯後甜點、咖啡和啤酒。剩下一張兌換券我拿去換礦泉水了。
我和廖世偉老師、MTK 的三個朋友以及上海 ARM 的兩個朋友坐同一桌。廖世偉老師飯後就跑到其它桌串門子了。剩下我們就在聊一些政治敏感的話題。我想兩岸多互相了解是必要的,誤解只會生出仇恨,這是一件很可惜的事。不是每個人都如政客所形容的這麼面目可憎,其實最可憎的是政客,這點要有清楚的認識。MTK 的人問我明天有沒有什麼行程,我說打算在旅館附近逛逛,晚上搭飛機回台灣。他們說廖世偉老師可以帶我到 Google 參觀一下,我當然是樂見其成。跟廖世偉老師確認一下見面地點和聯絡方式,大家見聊的差不多就各自散去。
DSC01460
往旅館的路上,外面飄著雨,真的蠻冷的!