← Documents Documentation/arch/x86/exception-tables.rst GitHub 원문 ↗

Linux 6.18.37 · Architecture

Kernel level exception handling

x86 user-access fault가 .fixup과 __ex_table을 통해 -EFAULT로 복구되는 과정을 설명합니다.

Source pathDocumentation/arch/x86/exception-tables.rst
Source versionLinux v6.18.37
TranslationDUJINLABS 전문 번역 + 해설

요약·해설과 원문, 전문 번역을 서로 분리했습니다. API 이름, symbol, source path는 원문 표기를 사용합니다.

1. 요약·해설

원문의 핵심 논리와 kernel programming 관점의 보충 설명입니다. 아래의 전문 번역과는 별도로 작성했습니다.

요약과 해설

exception-tables.rst:1-357

x86 kernel은 user pointer를 미리 VMA lookup하는 대신 MMU가 page fault를 내게 하고, faulting instruction address로 `__ex_table`을 찾아 `.fixup` handler로 이동합니다. 이 방식은 정상 access의 검증 비용을 없애고 실패 path만 별도 ELF section으로 분리합니다.

entry는 faulting instruction과 fixup의 relative offset, 선택적 handler를 담습니다. build-time sorting과 binary search로 빠르게 찾으며, table order를 지키기 위해 exception annotation은 `.text` code에만 사용해야 합니다.

2. 영어 원문 전체

번역 기준이 된 Linux v6.18.37 원문입니다. 줄 번호는 이 버전의 파일 좌표입니다.

원문 전체 펼치기
1 .. SPDX-License-Identifier: GPL-2.0
2
3 ===============================
4 Kernel level exception handling
5 ===============================
6
7 Commentary by Joerg Pommnitz <joerg@raleigh.ibm.com>
8
9 When a process runs in kernel mode, it often has to access user
10 mode memory whose address has been passed by an untrusted program.
11 To protect itself the kernel has to verify this address.
12
13 In older versions of Linux this was done with the
14 int verify_area(int type, const void * addr, unsigned long size)
15 function (which has since been replaced by access_ok()).
16
17 This function verified that the memory area starting at address
18 'addr' and of size 'size' was accessible for the operation specified
19 in type (read or write). To do this, verify_read had to look up the
20 virtual memory area (vma) that contained the address addr. In the
21 normal case (correctly working program), this test was successful.
22 It only failed for a few buggy programs. In some kernel profiling
23 tests, this normally unneeded verification used up a considerable
24 amount of time.
25
26 To overcome this situation, Linus decided to let the virtual memory
27 hardware present in every Linux-capable CPU handle this test.
28
29 How does this work?
30
31 Whenever the kernel tries to access an address that is currently not
32 accessible, the CPU generates a page fault exception and calls the
33 page fault handler::
34
35 void exc_page_fault(struct pt_regs *regs, unsigned long error_code)
36
37 in arch/x86/mm/fault.c. The parameters on the stack are set up by
38 the low level assembly glue in arch/x86/entry/entry_32.S. The parameter
39 regs is a pointer to the saved registers on the stack, error_code
40 contains a reason code for the exception.
41
42 exc_page_fault() first obtains the inaccessible address from the CPU
43 control register CR2. If the address is within the virtual address
44 space of the process, the fault probably occurred, because the page
45 was not swapped in, write protected or something similar. However,
46 we are interested in the other case: the address is not valid, there
47 is no vma that contains this address. In this case, the kernel jumps
48 to the bad_area label.
49
50 There it uses the address of the instruction that caused the exception
51 (i.e. regs->eip) to find an address where the execution can continue
52 (fixup). If this search is successful, the fault handler modifies the
53 return address (again regs->eip) and returns. The execution will
54 continue at the address in fixup.
55
56 Where does fixup point to?
57
58 Since we jump to the contents of fixup, fixup obviously points
59 to executable code. This code is hidden inside the user access macros.
60 I have picked the get_user() macro defined in arch/x86/include/asm/uaccess.h
61 as an example. The definition is somewhat hard to follow, so let's peek at
62 the code generated by the preprocessor and the compiler. I selected
63 the get_user() call in drivers/char/sysrq.c for a detailed examination.
64
65 The original code in sysrq.c line 587::
66
67 get_user(c, buf);
68
69 The preprocessor output (edited to become somewhat readable)::
70
71 (
72 {
73 long __gu_err = - 14 , __gu_val = 0;
74 const __typeof__(*( ( buf ) )) *__gu_addr = ((buf));
75 if (((((0 + current_set[0])->tss.segment) == 0x18 ) ||
76 (((sizeof(*(buf))) <= 0xC0000000UL) &&
77 ((unsigned long)(__gu_addr ) <= 0xC0000000UL - (sizeof(*(buf)))))))
78 do {
79 __gu_err = 0;
80 switch ((sizeof(*(buf)))) {
81 case 1:
82 __asm__ __volatile__(
83 "1: mov" "b" " %2,%" "b" "1\n"
84 "2:\n"
85 ".section .fixup,\"ax\"\n"
86 "3: movl %3,%0\n"
87 " xor" "b" " %" "b" "1,%" "b" "1\n"
88 " jmp 2b\n"
89 ".section __ex_table,\"a\"\n"
90 " .align 4\n"
91 " .long 1b,3b\n"
92 ".text" : "=r"(__gu_err), "=q" (__gu_val): "m"((*(struct __large_struct *)
93 ( __gu_addr )) ), "i"(- 14 ), "0"( __gu_err )) ;
94 break;
95 case 2:
96 __asm__ __volatile__(
97 "1: mov" "w" " %2,%" "w" "1\n"
98 "2:\n"
99 ".section .fixup,\"ax\"\n"
100 "3: movl %3,%0\n"
101 " xor" "w" " %" "w" "1,%" "w" "1\n"
102 " jmp 2b\n"
103 ".section __ex_table,\"a\"\n"
104 " .align 4\n"
105 " .long 1b,3b\n"
106 ".text" : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
107 ( __gu_addr )) ), "i"(- 14 ), "0"( __gu_err ));
108 break;
109 case 4:
110 __asm__ __volatile__(
111 "1: mov" "l" " %2,%" "" "1\n"
112 "2:\n"
113 ".section .fixup,\"ax\"\n"
114 "3: movl %3,%0\n"
115 " xor" "l" " %" "" "1,%" "" "1\n"
116 " jmp 2b\n"
117 ".section __ex_table,\"a\"\n"
118 " .align 4\n" " .long 1b,3b\n"
119 ".text" : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
120 ( __gu_addr )) ), "i"(- 14 ), "0"(__gu_err));
121 break;
122 default:
123 (__gu_val) = __get_user_bad();
124 }
125 } while (0) ;
126 ((c)) = (__typeof__(*((buf))))__gu_val;
127 __gu_err;
128 }
129 );
130
131 WOW! Black GCC/assembly magic. This is impossible to follow, so let's
132 see what code gcc generates::
133
134 > xorl %edx,%edx
135 > movl current_set,%eax
136 > cmpl $24,788(%eax)
137 > je .L1424
138 > cmpl $-1073741825,64(%esp)
139 > ja .L1423
140 > .L1424:
141 > movl %edx,%eax
142 > movl 64(%esp),%ebx
143 > #APP
144 > 1: movb (%ebx),%dl /* this is the actual user access */
145 > 2:
146 > .section .fixup,"ax"
147 > 3: movl $-14,%eax
148 > xorb %dl,%dl
149 > jmp 2b
150 > .section __ex_table,"a"
151 > .align 4
152 > .long 1b,3b
153 > .text
154 > #NO_APP
155 > .L1423:
156 > movzbl %dl,%esi
157
158 The optimizer does a good job and gives us something we can actually
159 understand. Can we? The actual user access is quite obvious. Thanks
160 to the unified address space we can just access the address in user
161 memory. But what does the .section stuff do?????
162
163 To understand this we have to look at the final kernel::
164
165 > objdump --section-headers vmlinux
166 >
167 > vmlinux: file format elf32-i386
168 >
169 > Sections:
170 > Idx Name Size VMA LMA File off Algn
171 > 0 .text 00098f40 c0100000 c0100000 00001000 2**4
172 > CONTENTS, ALLOC, LOAD, READONLY, CODE
173 > 1 .fixup 000016bc c0198f40 c0198f40 00099f40 2**0
174 > CONTENTS, ALLOC, LOAD, READONLY, CODE
175 > 2 .rodata 0000f127 c019a5fc c019a5fc 0009b5fc 2**2
176 > CONTENTS, ALLOC, LOAD, READONLY, DATA
177 > 3 __ex_table 000015c0 c01a9724 c01a9724 000aa724 2**2
178 > CONTENTS, ALLOC, LOAD, READONLY, DATA
179 > 4 .data 0000ea58 c01abcf0 c01abcf0 000abcf0 2**4
180 > CONTENTS, ALLOC, LOAD, DATA
181 > 5 .bss 00018e21 c01ba748 c01ba748 000ba748 2**2
182 > ALLOC
183 > 6 .comment 00000ec4 00000000 00000000 000ba748 2**0
184 > CONTENTS, READONLY
185 > 7 .note 00001068 00000ec4 00000ec4 000bb60c 2**0
186 > CONTENTS, READONLY
187
188 There are obviously 2 non standard ELF sections in the generated object
189 file. But first we want to find out what happened to our code in the
190 final kernel executable::
191
192 > objdump --disassemble --section=.text vmlinux
193 >
194 > c017e785 <do_con_write+c1> xorl %edx,%edx
195 > c017e787 <do_con_write+c3> movl 0xc01c7bec,%eax
196 > c017e78c <do_con_write+c8> cmpl $0x18,0x314(%eax)
197 > c017e793 <do_con_write+cf> je c017e79f <do_con_write+db>
198 > c017e795 <do_con_write+d1> cmpl $0xbfffffff,0x40(%esp,1)
199 > c017e79d <do_con_write+d9> ja c017e7a7 <do_con_write+e3>
200 > c017e79f <do_con_write+db> movl %edx,%eax
201 > c017e7a1 <do_con_write+dd> movl 0x40(%esp,1),%ebx
202 > c017e7a5 <do_con_write+e1> movb (%ebx),%dl
203 > c017e7a7 <do_con_write+e3> movzbl %dl,%esi
204
205 The whole user memory access is reduced to 10 x86 machine instructions.
206 The instructions bracketed in the .section directives are no longer
207 in the normal execution path. They are located in a different section
208 of the executable file::
209
210 > objdump --disassemble --section=.fixup vmlinux
211 >
212 > c0199ff5 <.fixup+10b5> movl $0xfffffff2,%eax
213 > c0199ffa <.fixup+10ba> xorb %dl,%dl
214 > c0199ffc <.fixup+10bc> jmp c017e7a7 <do_con_write+e3>
215
216 And finally::
217
218 > objdump --full-contents --section=__ex_table vmlinux
219 >
220 > c01aa7c4 93c017c0 e09f19c0 97c017c0 99c017c0 ................
221 > c01aa7d4 f6c217c0 e99f19c0 a5e717c0 f59f19c0 ................
222 > c01aa7e4 080a18c0 01a019c0 0a0a18c0 04a019c0 ................
223
224 or in human readable byte order::
225
226 > c01aa7c4 c017c093 c0199fe0 c017c097 c017c099 ................
227 > c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5 ................
228 ^^^^^^^^^^^^^^^^^
229 this is the interesting part!
230 > c01aa7e4 c0180a08 c019a001 c0180a0a c019a004 ................
231
232 What happened? The assembly directives::
233
234 .section .fixup,"ax"
235 .section __ex_table,"a"
236
237 told the assembler to move the following code to the specified
238 sections in the ELF object file. So the instructions::
239
240 3: movl $-14,%eax
241 xorb %dl,%dl
242 jmp 2b
243
244 ended up in the .fixup section of the object file and the addresses::
245
246 .long 1b,3b
247
248 ended up in the __ex_table section of the object file. 1b and 3b
249 are local labels. The local label 1b (1b stands for next label 1
250 backward) is the address of the instruction that might fault, i.e.
251 in our case the address of the label 1 is c017e7a5:
252 the original assembly code: > 1: movb (%ebx),%dl
253 and linked in vmlinux : > c017e7a5 <do_con_write+e1> movb (%ebx),%dl
254
255 The local label 3 (backwards again) is the address of the code to handle
256 the fault, in our case the actual value is c0199ff5:
257 the original assembly code: > 3: movl $-14,%eax
258 and linked in vmlinux : > c0199ff5 <.fixup+10b5> movl $0xfffffff2,%eax
259
260 If the fixup was able to handle the exception, control flow may be returned
261 to the instruction after the one that triggered the fault, ie. local label 2b.
262
263 The assembly code::
264
265 > .section __ex_table,"a"
266 > .align 4
267 > .long 1b,3b
268
269 becomes the value pair::
270
271 > c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5 ................
272 ^this is ^this is
273 1b 3b
274
275 c017e7a5,c0199ff5 in the exception table of the kernel.
276
277 So, what actually happens if a fault from kernel mode with no suitable
278 vma occurs?
279
280 #. access to invalid address::
281
282 > c017e7a5 <do_con_write+e1> movb (%ebx),%dl
283 #. MMU generates exception
284 #. CPU calls exc_page_fault()
285 #. exc_page_fault() calls do_user_addr_fault()
286 #. do_user_addr_fault() calls kernelmode_fixup_or_oops()
287 #. kernelmode_fixup_or_oops() calls fixup_exception() (regs->eip == c017e7a5);
288 #. fixup_exception() calls search_exception_tables()
289 #. search_exception_tables() looks up the address c017e7a5 in the
290 exception table (i.e. the contents of the ELF section __ex_table)
291 and returns the address of the associated fault handle code c0199ff5.
292 #. fixup_exception() modifies its own return address to point to the fault
293 handle code and returns.
294 #. execution continues in the fault handling code.
295 #. a) EAX becomes -EFAULT (== -14)
296 b) DL becomes zero (the value we "read" from user space)
297 c) execution continues at local label 2 (address of the
298 instruction immediately after the faulting user access).
299
300 The steps a to c above in a certain way emulate the faulting instruction.
301
302 That's it, mostly. If you look at our example, you might ask why
303 we set EAX to -EFAULT in the exception handler code. Well, the
304 get_user() macro actually returns a value: 0, if the user access was
305 successful, -EFAULT on failure. Our original code did not test this
306 return value, however the inline assembly code in get_user() tries to
307 return -EFAULT. GCC selected EAX to return this value.
308
309 NOTE:
310 Due to the way that the exception table is built and needs to be ordered,
311 only use exceptions for code in the .text section. Any other section
312 will cause the exception table to not be sorted correctly, and the
313 exceptions will fail.
314
315 Things changed when 64-bit support was added to x86 Linux. Rather than
316 double the size of the exception table by expanding the two entries
317 from 32-bits to 64 bits, a clever trick was used to store addresses
318 as relative offsets from the table itself. The assembly code changed
319 from::
320
321 .long 1b,3b
322 to:
323 .long (from) - .
324 .long (to) - .
325
326 and the C-code that uses these values converts back to absolute addresses
327 like this::
328
329 ex_insn_addr(const struct exception_table_entry *x)
330 {
331 return (unsigned long)&x->insn + x->insn;
332 }
333
334 In v4.6 the exception table entry was expanded with a new field "handler".
335 This is also 32-bits wide and contains a third relative function
336 pointer which points to one of:
337
338 1) ``int ex_handler_default(const struct exception_table_entry *fixup)``
339 This is legacy case that just jumps to the fixup code
340
341 2) ``int ex_handler_fault(const struct exception_table_entry *fixup)``
342 This case provides the fault number of the trap that occurred at
343 entry->insn. It is used to distinguish page faults from machine
344 check.
345
346 More functions can easily be added.
347
348 CONFIG_BUILDTIME_TABLE_SORT allows the __ex_table section to be sorted post
349 link of the kernel image, via a host utility scripts/sorttable. It will set the
350 symbol main_extable_sort_needed to 0, avoiding sorting the __ex_table section
351 at boot time. With the exception table sorted, at runtime when an exception
352 occurs we can quickly lookup the __ex_table entry via binary search.
353
354 This is not just a boot time optimization, some architectures require this
355 table to be sorted in order to handle exceptions relatively early in the boot
356 process. For example, i386 makes use of this form of exception handling before
357 paging support is even enabled!
358

3. 한국어 전문 번역

영어 원문의 문단 순서와 의미를 유지한 전체 번역입니다. 코드, 함수명, symbol과 URL은 원문 표기를 유지합니다.

user address 검증 비용과 hardware fault

1-28

이 `GPL-2.0` 문서는 kernel-level exception handling을 설명하며 Joerg Pommnitz `<joerg@raleigh.ibm.com>`가 해설했습니다. kernel mode process는 untrusted program이 넘긴 user-mode memory address에 자주 접근하므로 kernel 자신을 보호하기 위해 address를 검증해야 합니다.

오래된 Linux는 이후 `access_ok()`로 대체된 `int verify_area(int type, const void * addr, unsigned long size)`를 사용했습니다. `addr`에서 시작해 `size`만큼인 area가 `type`의 read/write operation에 접근 가능한지 확인했습니다.

`verify_read`는 address를 포함하는 virtual memory area(VMA)를 lookup해야 했습니다. 정상 program에서는 거의 항상 성공하고 일부 buggy program에서만 실패했지만, profiling에서는 보통 불필요한 이 검증이 상당한 시간을 소비했습니다.

이를 해결하기 위해 Linus는 Linux-capable CPU마다 있는 virtual-memory hardware가 검사를 담당하게 했습니다.

exc_page_fault와 fixup address

29-64

kernel이 현재 접근할 수 없는 address를 access하면 CPU는 page-fault exception을 만들고 `arch/x86/mm/fault.c`의 page-fault handler를 호출합니다.

void exc_page_fault(struct pt_regs *regs, unsigned long error_code)

stack parameter는 `arch/x86/entry/entry_32.S`의 low-level assembly glue가 준비합니다. `regs`는 stack에 저장한 register를 가리키고 `error_code`는 exception reason code를 담습니다.

`exc_page_fault()`는 먼저 CPU control register `CR2`에서 inaccessible address를 얻습니다. process virtual-address space 안이라면 page가 swap-in되지 않았거나 write-protected인 상황일 수 있습니다. 이 문서가 다루는 경우는 해당 address를 포함하는 VMA가 없어 invalid인 경우이며 kernel은 `bad_area` label로 갑니다.

`bad_area`에서는 exception을 일으킨 instruction address `regs->eip`로 execution을 계속할 fixup address를 찾습니다. search가 성공하면 fault handler가 return address인 `regs->eip`를 fixup으로 바꾸고 return하므로 execution은 fixup code에서 계속됩니다.

fixup은 executable code를 가리키며 user-access macro 안에 숨겨져 있습니다. 문서는 `arch/x86/include/asm/uaccess.h`의 `get_user()`와 `drivers/char/sysrq.c` line 587의 call을 예로 분석합니다.

get_user()가 생성한 inline assembly

65-130

원래 `sysrq.c` code는 다음 한 줄입니다.

get_user(c, buf);

preprocessor와 compiler를 거치면 읽기 좋게 편집해도 다음처럼 복잡한 code가 됩니다. `__gu_err`는 기본 `-14`(`-EFAULT`), `__gu_val`은 0이며 address 범위를 확인한 뒤 object size 1, 2, 4 byte에 맞는 inline assembly를 선택합니다.

(
  {
    long __gu_err = - 14 , __gu_val = 0;
    const __typeof__(*( (  buf ) )) *__gu_addr = ((buf));
    if (((((0 + current_set[0])->tss.segment) == 0x18 )  ||
      (((sizeof(*(buf))) <= 0xC0000000UL) &&
      ((unsigned long)(__gu_addr ) <= 0xC0000000UL - (sizeof(*(buf)))))))
      do {
        __gu_err  = 0;
        switch ((sizeof(*(buf)))) {
          case 1:
            __asm__ __volatile__(
              "1:      mov" "b" " %2,%" "b" "1\n"
              "2:\n"
              ".section .fixup,\"ax\"\n"
              "3:      movl %3,%0\n"
              "        xor" "b" " %" "b" "1,%" "b" "1\n"
              "        jmp 2b\n"
              ".section __ex_table,\"a\"\n"
              "        .align 4\n"
              "        .long 1b,3b\n"
              ".text"        : "=r"(__gu_err), "=q" (__gu_val): "m"((*(struct __large_struct *)
                            (   __gu_addr   )) ), "i"(- 14 ), "0"(  __gu_err  )) ;
              break;
          case 2:
            __asm__ __volatile__(
              "1:      mov" "w" " %2,%" "w" "1\n"
              "2:\n"
              ".section .fixup,\"ax\"\n"
              "3:      movl %3,%0\n"
              "        xor" "w" " %" "w" "1,%" "w" "1\n"
              "        jmp 2b\n"
              ".section __ex_table,\"a\"\n"
              "        .align 4\n"
              "        .long 1b,3b\n"
              ".text"        : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
                            (   __gu_addr   )) ), "i"(- 14 ), "0"(  __gu_err  ));
              break;
          case 4:
            __asm__ __volatile__(
              "1:      mov" "l" " %2,%" "" "1\n"
              "2:\n"
              ".section .fixup,\"ax\"\n"
              "3:      movl %3,%0\n"
              "        xor" "l" " %" "" "1,%" "" "1\n"
              "        jmp 2b\n"
              ".section __ex_table,\"a\"\n"
              "        .align 4\n"        "        .long 1b,3b\n"
              ".text"        : "=r"(__gu_err), "=r" (__gu_val) : "m"((*(struct __large_struct *)
                            (   __gu_addr   )) ), "i"(- 14 ), "0"(__gu_err));
              break;
          default:
            (__gu_val) = __get_user_bad();
        }
      } while (0) ;
    ((c)) = (__typeof__(*((buf))))__gu_val;
    __gu_err;
  }
);

각 size case는 label `1`에서 실제 user access를 하고 label `2`가 normal continuation입니다. `.fixup` section의 label `3`은 error를 `-14`로 설정하고 read value를 0으로 만든 뒤 `2b`로 jump합니다. `__ex_table`에는 faulting label `1b`와 handler label `3b` pair를 기록합니다. 지원하지 않는 size는 `__get_user_bad()`로 갑니다.

optimizer가 만든 normal path

131-162

GCC optimizer는 복잡한 macro를 다음처럼 이해 가능한 assembly로 줄입니다. 실제 user access는 `movb (%ebx),%dl`입니다.

>         xorl %edx,%edx
>         movl current_set,%eax
>         cmpl $24,788(%eax)
>         je .L1424
>         cmpl $-1073741825,64(%esp)
>         ja .L1423
> .L1424:
>         movl %edx,%eax
>         movl 64(%esp),%ebx
> #APP
> 1:      movb (%ebx),%dl                /* this is the actual user access */
> 2:
> .section .fixup,"ax"
> 3:      movl $-14,%eax
>         xorb %dl,%dl
>         jmp 2b
> .section __ex_table,"a"
>         .align 4
>         .long 1b,3b
> .text
> #NO_APP
> .L1423:
>         movzbl %dl,%esi

unified address space 덕분에 user-memory address를 직접 access할 수 있습니다. normal path는 짧지만 `.section` directive 사이 code가 어떤 역할을 하는지는 final kernel ELF를 살펴봐야 알 수 있습니다.

vmlinux의 .fixup과 __ex_table

163-231

먼저 `vmlinux` section header를 확인합니다.

> objdump --section-headers vmlinux
>
> vmlinux:     file format elf32-i386
>
> Sections:
> Idx Name          Size      VMA       LMA       File off  Algn
>   0 .text         00098f40  c0100000  c0100000  00001000  2**4
>                   CONTENTS, ALLOC, LOAD, READONLY, CODE
>   1 .fixup        000016bc  c0198f40  c0198f40  00099f40  2**0
>                   CONTENTS, ALLOC, LOAD, READONLY, CODE
>   2 .rodata       0000f127  c019a5fc  c019a5fc  0009b5fc  2**2
>                   CONTENTS, ALLOC, LOAD, READONLY, DATA
>   3 __ex_table    000015c0  c01a9724  c01a9724  000aa724  2**2
>                   CONTENTS, ALLOC, LOAD, READONLY, DATA
>   4 .data         0000ea58  c01abcf0  c01abcf0  000abcf0  2**4
>                   CONTENTS, ALLOC, LOAD, DATA
>   5 .bss          00018e21  c01ba748  c01ba748  000ba748  2**2
>                   ALLOC
>   6 .comment      00000ec4  00000000  00000000  000ba748  2**0
>                   CONTENTS, READONLY
>   7 .note         00001068  00000ec4  00000ec4  000bb60c  2**0
>                   CONTENTS, READONLY

generated object에는 standard section 외에 executable `.fixup`과 read-only data `__ex_table` 두 section이 있습니다. normal `.text`에서 example user access가 어떻게 link됐는지 disassemble하면 다음과 같습니다.

> objdump --disassemble --section=.text vmlinux
>
> c017e785 <do_con_write+c1> xorl   %edx,%edx
> c017e787 <do_con_write+c3> movl   0xc01c7bec,%eax
> c017e78c <do_con_write+c8> cmpl   $0x18,0x314(%eax)
> c017e793 <do_con_write+cf> je     c017e79f <do_con_write+db>
> c017e795 <do_con_write+d1> cmpl   $0xbfffffff,0x40(%esp,1)
> c017e79d <do_con_write+d9> ja     c017e7a7 <do_con_write+e3>
> c017e79f <do_con_write+db> movl   %edx,%eax
> c017e7a1 <do_con_write+dd> movl   0x40(%esp,1),%ebx
> c017e7a5 <do_con_write+e1> movb   (%ebx),%dl
> c017e7a7 <do_con_write+e3> movzbl %dl,%esi

user-memory access 전체는 x86 instruction 10개로 줄었습니다. `.section` directive 안 instruction은 normal execution path에서 빠져 executable의 별도 `.fixup` section에 놓입니다.

> objdump --disassemble --section=.fixup vmlinux
>
> c0199ff5 <.fixup+10b5> movl   $0xfffffff2,%eax
> c0199ffa <.fixup+10ba> xorb   %dl,%dl
> c0199ffc <.fixup+10bc> jmp    c017e7a7 <do_con_write+e3>

마지막으로 `__ex_table`의 raw content는 다음과 같습니다.

 > objdump --full-contents --section=__ex_table vmlinux
 >
 >  c01aa7c4 93c017c0 e09f19c0 97c017c0 99c017c0  ................
 >  c01aa7d4 f6c217c0 e99f19c0 a5e717c0 f59f19c0  ................
 >  c01aa7e4 080a18c0 01a019c0 0a0a18c0 04a019c0  ................

or in human readable byte order::

 >  c01aa7c4 c017c093 c0199fe0 c017c097 c017c099  ................
 >  c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5  ................
                               ^^^^^^^^^^^^^^^^^
                               this is the interesting part!
 >  c01aa7e4 c0180a08 c019a001 c0180a0a c019a004  ................

human-readable byte order에서 흥미로운 pair는 faulting instruction `c017e7a5`와 fixup `c0199ff5`입니다.

local label과 exception-table pair

232-276

다음 assembly directive가 뒤따르는 code를 지정 ELF section으로 옮깁니다.

.section .fixup,"ax"
.section __ex_table,"a"

그래서 다음 handler instruction은 `.fixup` section에 들어갑니다.

3:      movl $-14,%eax
        xorb %dl,%dl
        jmp 2b

다음 address pair는 `__ex_table` section에 들어갑니다.

.long 1b,3b

`1b`와 `3b`는 local label입니다. `1b`는 뒤쪽(backward)의 다음 label 1, 즉 fault 가능 instruction address `c017e7a5`를 뜻합니다. source와 linked `vmlinux` 표현은 다음과 같습니다.

the original assembly code: > 1:      movb (%ebx),%dl
and linked in vmlinux     : > c017e7a5 <do_con_write+e1> movb   (%ebx),%dl

local label `3b`는 fault handler code address이며 실제 값은 `c0199ff5`입니다.

the original assembly code: > 3:      movl $-14,%eax
and linked in vmlinux     : > c0199ff5 <.fixup+10b5> movl   $0xfffffff2,%eax

fixup이 exception을 처리하면 faulting instruction 직후인 local label `2b`로 control flow를 돌려줄 수 있습니다. `__ex_table` directive와 그 결과 pair는 다음과 같습니다.

 > .section __ex_table,"a"
 >         .align 4
 >         .long 1b,3b

becomes the value pair::

 >  c01aa7d4 c017c2f6 c0199fe9 c017e7a5 c0199ff5  ................
                               ^this is ^this is
                               1b       3b

따라서 kernel exception table에는 `c017e7a5,c0199ff5` pair가 저장됩니다.

invalid access에서 -EFAULT 복구까지

277-314

적합한 VMA가 없는 kernel-mode fault의 실제 순서는 다음과 같습니다.

step동작
1invalid address의 `c017e7a5 <do_con_write+e1> movb (%ebx),%dl`에 접근합니다.
2MMU가 exception을 생성합니다.
3CPU가 `exc_page_fault()`를 호출합니다.
4`exc_page_fault()`가 `do_user_addr_fault()`를 호출합니다.
5`do_user_addr_fault()`가 `kernelmode_fixup_or_oops()`를 호출합니다.
6`kernelmode_fixup_or_oops()`가 `regs->eip == c017e7a5` 상태로 `fixup_exception()`을 호출합니다.
7`fixup_exception()`이 `search_exception_tables()`를 호출합니다.
8`search_exception_tables()`가 ELF `__ex_table`에서 `c017e7a5`를 찾아 연관 handler `c0199ff5`를 반환합니다.
9`fixup_exception()`이 자신의 return address를 handler로 바꾸고 return합니다.
10fault-handling code에서 execution을 계속합니다.
11`EAX=-EFAULT`(`-14`), `DL=0`으로 만들고 faulting access 직후 local label 2에서 계속합니다.

마지막 값 변경과 jump는 faulting instruction을 일정한 방식으로 emulate합니다. `get_user()`는 성공하면 0, 실패하면 `-EFAULT`를 반환하므로 handler가 EAX를 `-EFAULT`로 설정합니다. original caller가 return value를 검사하지 않더라도 inline assembly는 값을 반환하며 GCC가 EAX를 선택했습니다.

exception table은 정렬된 형태로 build되어야 하므로 exception은 `.text` section의 code에만 사용하십시오. 다른 section을 사용하면 table 정렬이 깨져 exception handling이 실패합니다.

64-bit relative offset와 handler field

315-347

x86 Linux에 64-bit support를 추가할 때 두 entry를 32 bit에서 64 bit로 늘려 exception-table size를 두 배로 만들지 않았습니다. 대신 table 자신을 기준으로 한 relative offset을 저장합니다.

  .long 1b,3b
to:
        .long (from) - .
        .long (to) - .

C code는 relative value를 다음처럼 absolute address로 되돌립니다.

ex_insn_addr(const struct exception_table_entry *x)
{
        return (unsigned long)&x->insn + x->insn;
}

v4.6에서는 exception-table entry에 32-bit `handler` field를 추가했습니다. 이는 세 번째 relative function pointer이며 현재 다음 handler 중 하나를 가리킵니다.

  • `int ex_handler_default(const struct exception_table_entry *fixup)`: fixup code로 jump하는 legacy case입니다.
  • `int ex_handler_fault(const struct exception_table_entry *fixup)`: `entry->insn`에서 발생한 trap의 fault number를 제공해 page fault와 machine check를 구별합니다.

필요하면 handler function을 더 추가할 수 있습니다.

build-time sort와 binary search

348-357

`CONFIG_BUILDTIME_TABLE_SORT`는 kernel image link 뒤 host utility `scripts/sorttable`로 `__ex_table` section을 정렬합니다. 이 utility는 `main_extable_sort_needed` symbol을 0으로 설정해 boot-time sort를 피합니다.

정렬된 exception table은 runtime exception에서 binary search로 entry를 빠르게 찾게 합니다. 이는 단순한 boot-time optimization만이 아닙니다. 일부 architecture는 boot 초기 exception 처리를 위해 sorted table이 필요합니다. 예를 들어 i386은 paging support를 enable하기 전부터 이 exception handling 형식을 사용합니다.