原文地址:https://monoinfinito.wordpress.com/2013/04/11/c-exceptions-under-the-hood-11-reading-a-cfi-table/
作者:nicolasbrailo
要从我们已经为我们的ABI实现的personality函数里正确处理异常,我们需要阅读LSDA(语言特定数据区)来了解哪个调用帧(即哪个函数)可以处理哪个异常,以及了解哪里可以找到着陆垫(catch块))。LSDA是CFI格式的,我们将在本章里学习如何读它。
读CFI数据是相当直截了当的,但有一些我们需要首先考虑的陷阱。实际上,两个:
- 关于.gcc_except_table格式的文档非常少(实际上,我仅找到关于它的几封邮件),因此我们将需要读大量的源代码,并反汇编来理解它。
- 虽然格式本身不是特别复杂,它使用一个使得读这个表不那么直截了当的LEB编码。
就我所知,大多数DWARF代价像这样编码,使用LEB格式,对一头雾水的程序员看起来不错,在编码任意长的整数时节省代码空间。幸运地,在这里我们可以耍个小花招:大多数时间里,可以uint8_t来读LEB编码的数字,因为我们不准备处理大的异常表或任何类似的东西。
备注:你可以从我的github repo下载完整的源代码。
让我们直接从汇编分析CFI数据,然后看我们是否能构建某些在我们的personality函数上读它的东西。我将重命名这些标记,使它们对我们更友好些。LSDA有三部分,试着在下面找出它们:
1 2 3 4 |
.local_frame_entry: .globl __gxx_personality_v0 .section .gcc_except_table,"a",@progbits .align 4 |
这个非常简单:它只是声明我们将使用__gxx_personality_v0作为全局对象,并让链接器知道我们准备为.gcc_except_table节声明内容的头部。继续:
1 2 3 4 5 6 7 8 9 10 11 |
.local_lsda_1: # This declares the encoding type. We don't care. .byte 0xff # This specifies the landing pads start; if zero, the func's ptr is # assumed (_Unwind_GetRegionStart) .byte 0 # Length of the LSDA area: check that LLSDATT1 and LLSDATTD1 point to the # end and the beginning of the LSDA, respectively .uleb128 .local_lsda_end - .local_lsda_call_site_table_header |
现在这有更多一些信息。这些标签相当模糊,但确实遵循一个模式。LSDA表示语言特定数据区,前面的L表示本地,因此这是本地(对于编译单元,.o文件)语言特定数据区编号1。其他标记遵循类似的模式,但我还没有时间把它们算出来。不过,我们确实不需要。
1 2 3 4 5 6 |
.local_lsda_call_site_table_header: # Encoding of items in the landing pad table. Again, we don't care. .byte 0x1. # The length of the call site table (ie the landing pads) .uleb128 .local_lsda_call_site_table_end - .local_lsda_call_site_table |
另一个单调的头。继续:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
.local_lsda_call_site_table: .uleb128 .LEHB0-.LFB1 .uleb128 .LEHE0-.LEHB0 .uleb128 .L8-.LFB1 .uleb128 0x1 .uleb128 .LEHB1-.LFB1 .uleb128 .LEHE1-.LEHB1 .uleb128 0 .uleb128 0 .uleb128 .LEHB2-.LFB1 .uleb128 .LEHE2-.LEHB2 .uleb128 .L9-.LFB1 .uleb128 0 .local_lsda_call_site_table_end: |
这有趣得多,现在我们看到调用表本身。不管怎样,在所有这些项里,我们应该能够找到我们的着陆垫。根据一些网页,每个调用项的格式应该是:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
struct lsda_call_site_entry { // Start of the IP range size_t cs_start; // Length of the IP range size_t cs_len; // Landing pad address size_t cs_lp; // Offset into action table size_t cs_action; }; |
看起来了我们在正轨上,虽然我们还不知道为什么在我们仅定义了一个着陆垫时,有3个调用项。在任何情形里,我们可以耍点小花招:通过查看汇编,我们可以推断CFI上的所有值将小于128,这意味着在LEB编码中,它们可以作为uchar来读。这使得我们读CFI的代码大为容易,下次我们将看到如何在personality函数里使用它。