1 /*
2 * Copyright (c) 2024 Huawei Device Co., Ltd.
3 * Licensed under the Apache License, Version 2.0 (the "License");
4 * you may not use this file except in compliance with the License.
5 * You may obtain a copy of the License at
6 *
7 * http://www.apache.org/licenses/LICENSE-2.0
8 *
9 * Unless required by applicable law or agreed to in writing, software
10 * distributed under the License is distributed on an "AS IS" BASIS,
11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 * See the License for the specific language governing permissions and
13 * limitations under the License.
14 */
15
16 #include "ecmascript/compiler/aot_file/gdb_jit.h"
17
18 #include <securec.h>
19 #include <mutex>
20
21 #include "ecmascript/log_wrapper.h"
22 #include "libpandabase/macros.h"
23 #include "llvm/BinaryFormat/ELF.h"
24
25 #ifndef PANDA_TARGET_MACOS
26 // Keep in sync with gdb/gdb/jit.h
27 extern "C" {
28 typedef enum {
29 JIT_NOACTION = 0,
30 JIT_REGISTER_FN,
31 JIT_UNREGISTER_FN
32 } jit_actions_t;
33
34 struct jit_code_entry {
35 struct jit_code_entry *next_entry;
36 struct jit_code_entry *prev_entry;
37 const char *symfile_addr;
38 uint64_t symfile_size;
39 const char *file_addr; // extend the standard
40 };
41
42 struct jit_descriptor {
43 uint32_t version;
44 // This should be jit_actions_t, but we want to be specific about the
45 // bit-width.
46 uint32_t action_flag;
47 struct jit_code_entry *relevant_entry;
48 struct jit_code_entry *first_entry;
49 };
50
51 // We put information about the JITed function in this global, which the
52 // debugger reads. Make sure to specify the version statically, because the
53 // debugger checks the version before we can set it during runtime.
54 struct jit_descriptor __jit_debug_descriptor = {
55 1, JIT_NOACTION, nullptr, nullptr
56 };
57
58 // Debuggers puts a breakpoint in this function.
__jit_debug_register_code()59 void __attribute__((noinline)) __jit_debug_register_code()
60 {
61 LOG_COMPILER(INFO) << "__jit_debug_register_code() is called.";
62 }
63 }
64
65 std::mutex g_descMutex;
66
67 namespace panda::ecmascript {
68 namespace jit_debug {
69 using namespace llvm::ELF;
70
71 static bool RegisterStubAnToDebuggerImpl(const char *fileAddr);
72
RegisterStubAnToDebugger(const char * fileAddr)73 void RegisterStubAnToDebugger(const char *fileAddr)
74 {
75 std::lock_guard<std::mutex> guard(g_descMutex);
76 auto entry = __jit_debug_descriptor.first_entry;
77 while (entry != nullptr && entry->file_addr != fileAddr) {
78 entry = entry->next_entry;
79 }
80 if (entry != nullptr) {
81 return;
82 }
83 if (RegisterStubAnToDebuggerImpl(fileAddr)) {
84 LOG_COMPILER(INFO) << "success to register stub.an to debugger.";
85 } else {
86 LOG_COMPILER(ERROR) << "Can't register stub.an to debugger.";
87 }
88 }
89
UnregisterStubAnFromDebugger(const char * fileAddr)90 void UnregisterStubAnFromDebugger(const char *fileAddr)
91 {
92 std::lock_guard<std::mutex> guard(g_descMutex);
93 auto entry = __jit_debug_descriptor.first_entry;
94 while (entry != nullptr && entry->file_addr != fileAddr) {
95 entry = entry->next_entry;
96 }
97 if (entry == nullptr) {
98 return;
99 }
100 if (entry->prev_entry != nullptr) {
101 entry->prev_entry->next_entry = entry->next_entry;
102 }
103 if (entry->next_entry != nullptr) {
104 entry->next_entry->prev_entry = entry->prev_entry;
105 }
106 __jit_debug_descriptor.action_flag = JIT_UNREGISTER_FN;
107 __jit_debug_descriptor.relevant_entry = entry;
108 __jit_debug_register_code();
109 __jit_debug_descriptor.relevant_entry = nullptr;
110 delete entry->symfile_addr;
111 delete entry;
112 }
113
114 template<typename T, typename U>
OffsetAlignUp(U * addr,uint64_t offset,uint32_t align)115 inline T OffsetAlignUp(U *addr, uint64_t offset, uint32_t align)
116 {
117 auto value = reinterpret_cast<uint64_t>(addr) + offset;
118 auto result = value;
119 if (align != 0 && (value % align != 0)) {
120 result = value + (align - (value % align));
121 }
122 return reinterpret_cast<T>(result);
123 }
124
125 struct StubAnInfo {
126 uintptr_t fileAddr;
127 Elf64_Ehdr *ehdr;
128 Elf64_Shdr *shdrTab;
129 uint32_t shStrIdx;
130 Elf64_Shdr *shStrHdr;
131 Elf64_Shdr *textHdr;
132 Elf64_Shdr *asmstubHdr;
133 Elf64_Shdr *symtabHdr;
134 Elf64_Shdr *strtabHdr;
135 uint64_t bcStubBegin;
136 uint64_t bcStubEnd;
137 uint32_t symCnt;
138 };
139
140 /*
141 * [0] file header
142 * [1] program header
143 * [2] shstrtab
144 * [3] strtab
145 * [4] symtab
146 * [5] .eh_frame
147 * [6] empty header
148 * [7] shstrtab-header
149 * [8] strtab-header
150 * [9] symtab-header
151 * [10] text-header
152 * [11] .eh_frame header
153 */
154 const char SHSTR[] = "\0.shstrtab\0.strtab\0.symtab\0.text\0.eh_frame";
155 const uint32_t SHSTRTAB_NAME = 1;
156 const uint32_t STRTAB_NAME = SHSTRTAB_NAME + strlen(".shstrtab") + 1;
157 const uint32_t SYMTAB_NAME = STRTAB_NAME + strlen(".strtab") + 1;
158 const uint32_t TEXT_NAME = SYMTAB_NAME + strlen(".symtab") + 1;
159 const uint32_t EH_FRAME_NAME = TEXT_NAME + strlen(".text") + 1;
160
161 const uint32_t SHSTRTAB_HDR_IDX = 1;
162 const uint32_t STRTAB_HDR_IDX = 2;
163 const uint32_t SYMTAB_HDR_IDX = 3;
164 const uint32_t TEXT_HDR_IDX = 4;
165
166 const uint32_t HEADER_CNT = 6;
167
InfoGetBind(unsigned char info)168 inline int InfoGetBind(unsigned char info)
169 {
170 const uint32_t shift = 4;
171 return info >> shift;
172 }
173
CollectStubAnInfo(uintptr_t fileAddr)174 StubAnInfo CollectStubAnInfo(uintptr_t fileAddr)
175 {
176 auto *ehdr = reinterpret_cast<Elf64_Ehdr *>(fileAddr);
177 auto *shdrTab = reinterpret_cast<Elf64_Shdr *>(fileAddr + ehdr->e_shoff);
178 uint32_t shStrIdx = ehdr->e_shstrndx;
179 Elf64_Shdr *shStrHdr = &shdrTab[shStrIdx];
180 const char *shstrtab = reinterpret_cast<const char *>(fileAddr + shStrHdr->sh_offset);
181 Elf64_Shdr *textHdr = nullptr;
182 Elf64_Shdr *asmstubHdr = nullptr;
183 Elf64_Shdr *symtabHdr = nullptr;
184 Elf64_Shdr *strtabHdr = nullptr;
185 for (uint32_t i = 0; i < ehdr->e_shnum; i++) {
186 Elf64_Shdr *shdr = &shdrTab[i];
187 const char *name = &shstrtab[shdr->sh_name];
188 if (strcmp(name, ".text") == 0) {
189 textHdr = shdr;
190 } else if (strcmp(name, ".ark_asmstub") == 0) {
191 asmstubHdr = shdr;
192 } else if (strcmp(name, ".symtab") == 0) {
193 symtabHdr = shdr;
194 } else if (strcmp(name, ".strtab") == 0) {
195 strtabHdr = shdr;
196 }
197 }
198 ASSERT(symtabHdr != nullptr);
199 Elf64_Sym *symtab = reinterpret_cast<Elf64_Sym *>(fileAddr + symtabHdr->sh_offset);
200 const char *strtab = reinterpret_cast<const char *>(fileAddr + strtabHdr->sh_offset);
201 uint32_t symCnt = 2;
202 uint64_t bcStubBegin = UINT64_MAX;
203 uint64_t bcStubEnd = 0;
204 for (uint32_t symIdx = 0; symIdx < symtabHdr->sh_size / symtabHdr->sh_entsize; symIdx++) {
205 Elf64_Sym *sym = symtab + symIdx;
206 if (InfoGetBind(sym->st_info) != STB_GLOBAL) {
207 continue;
208 }
209 if (strncmp(&strtab[sym->st_name], "BCStub", strlen("BCStub")) != 0) {
210 symCnt++;
211 } else {
212 if (sym->st_value < bcStubBegin) {
213 bcStubBegin = sym->st_value;
214 }
215 if (sym->st_value + sym->st_size > bcStubEnd) {
216 bcStubEnd = sym->st_value + sym->st_size;
217 }
218 }
219 }
220 return StubAnInfo {
221 fileAddr, ehdr, shdrTab, shStrIdx, shStrHdr, textHdr, asmstubHdr, symtabHdr, strtabHdr,
222 bcStubBegin, bcStubEnd, symCnt
223 };
224 }
225
CopyStrTab(uintptr_t baseAddr,const StubAnInfo & info)226 bool CopyStrTab(uintptr_t baseAddr, const StubAnInfo &info)
227 {
228 Elf64_Ehdr *newEhdr = reinterpret_cast<Elf64_Ehdr *>(baseAddr);
229 Elf64_Phdr *newPhdr = reinterpret_cast<Elf64_Phdr *>(newEhdr + 1);
230 char *shStrBuff = reinterpret_cast<char *>(newPhdr + 1);
231 if (memcpy_s(shStrBuff, sizeof(SHSTR), SHSTR, sizeof(SHSTR)) != EOK) {
232 return false;
233 }
234 char *newStrtab = shStrBuff + sizeof(SHSTR);
235 if (memcpy_s(newStrtab, info.strtabHdr->sh_size,
236 reinterpret_cast<void *>(info.fileAddr + info.strtabHdr->sh_offset), info.strtabHdr->sh_size) != EOK) {
237 return false;
238 }
239 const char bcStubName[] = "BCStubInterpreterRoutine";
240 if (memcpy_s((newStrtab + 1), sizeof(bcStubName), bcStubName, sizeof(bcStubName)) != EOK) {
241 return false;
242 }
243 return true;
244 }
245
ConstructSymTab(Elf64_Sym * newSymtab,const StubAnInfo & info)246 void ConstructSymTab(Elf64_Sym *newSymtab, const StubAnInfo &info)
247 {
248 Elf64_Sym *symtab = reinterpret_cast<Elf64_Sym *>(info.fileAddr + info.symtabHdr->sh_offset);
249 const char *strtab = reinterpret_cast<const char *>(info.fileAddr + info.strtabHdr->sh_offset);
250 memset_s(newSymtab, sizeof(Elf64_Sym), 0, sizeof(Elf64_Sym));
251 uint32_t newSymIdx = 1;
252 for (uint32_t symIdx = 0; symIdx < info.symtabHdr->sh_size / info.symtabHdr->sh_entsize; symIdx++) {
253 Elf64_Sym *src = symtab + symIdx;
254 if (InfoGetBind(src->st_info) == STB_GLOBAL
255 && strncmp(&strtab[src->st_name], "BCStub", strlen("BCStub")) != 0) {
256 auto dst = newSymtab + newSymIdx;
257 newSymIdx++;
258 *dst = *src;
259 dst->st_shndx = TEXT_HDR_IDX;
260 dst->st_value -= info.textHdr->sh_offset;
261 }
262 }
263 auto bcSym = newSymtab + newSymIdx;
264 bcSym->st_name = 1;
265 bcSym->st_info = newSymtab[1].st_info;
266 bcSym->st_other = newSymtab[1].st_other;
267 bcSym->st_shndx = TEXT_HDR_IDX;
268 bcSym->st_value = info.bcStubBegin - info.textHdr->sh_offset;
269 ASSERT(info.bcStubEnd >= info.bcStubBegin);
270 bcSym->st_size = info.bcStubEnd - info.bcStubBegin;
271 }
272
ConstructEhdrAndPhdr(Elf64_Ehdr * newEhdr,Elf64_Shdr * newShdrtab,uintptr_t baseAddr,const StubAnInfo & info)273 void ConstructEhdrAndPhdr(Elf64_Ehdr *newEhdr, Elf64_Shdr *newShdrtab, uintptr_t baseAddr, const StubAnInfo &info)
274 {
275 Elf64_Phdr *newPhdr = reinterpret_cast<Elf64_Phdr *>(newEhdr + 1);
276 {
277 *newEhdr = *info.ehdr;
278 newEhdr->e_flags = info.ehdr->e_flags;
279 newEhdr->e_machine = info.ehdr->e_machine;
280 if (memcpy_s(newEhdr->e_ident, sizeof(info.ehdr->e_ident),
281 info.ehdr->e_ident, sizeof(info.ehdr->e_ident)) != EOK) {
282 return;
283 }
284 newEhdr->e_version = 1;
285 newEhdr->e_phoff = sizeof(Elf64_Ehdr);
286 newEhdr->e_shoff = reinterpret_cast<uintptr_t>(newShdrtab) - baseAddr;
287 newEhdr->e_ehsize = sizeof(Elf64_Ehdr);
288 newEhdr->e_phentsize = sizeof(Elf64_Phdr);
289 newEhdr->e_phnum = 1;
290 newEhdr->e_shentsize = sizeof(Elf64_Shdr);
291 newEhdr->e_shnum = HEADER_CNT;
292 newEhdr->e_shstrndx = SHSTRTAB_HDR_IDX;
293 newEhdr->e_type = ET_REL;
294 newEhdr->e_entry = 0;
295 }
296 uintptr_t textAddr = info.textHdr->sh_offset + info.fileAddr;
297 uint64_t textSize = info.asmstubHdr->sh_offset + info.asmstubHdr->sh_size - info.textHdr->sh_offset;
298 {
299 newPhdr->p_type = PT_LOAD;
300 newPhdr->p_flags = PF_X | PF_R;
301 newPhdr->p_offset = textAddr - info.fileAddr;
302 newPhdr->p_vaddr = textAddr;
303 newPhdr->p_paddr = textAddr;
304 newPhdr->p_filesz = textSize;
305 newPhdr->p_memsz = textSize;
306 newPhdr->p_align = 0x1000;
307 }
308 }
309
ConstructShdrTab(Elf64_Shdr * newShdrTab,Elf64_Sym * newSymtab,uintptr_t baseAddr,void * ehFrame,uint32_t ehFrameSize,const StubAnInfo & info)310 void ConstructShdrTab(Elf64_Shdr *newShdrTab, Elf64_Sym *newSymtab, uintptr_t baseAddr, void *ehFrame,
311 uint32_t ehFrameSize, const StubAnInfo &info)
312 {
313 Elf64_Shdr hdr{};
314 Elf64_Shdr *emptyShdr = newShdrTab;
315 Elf64_Shdr *newShstrHdr = emptyShdr + 1;
316 Elf64_Shdr *newStrtabHdr = newShstrHdr + 1;
317 Elf64_Shdr *newSymHdr = newStrtabHdr + 1;
318 Elf64_Shdr *newTextHdr = newSymHdr + 1;
319 Elf64_Shdr *ehFrameHdr = newTextHdr + 1;
320 *emptyShdr = hdr;
321
322 newShstrHdr->sh_offset = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr);
323 newShstrHdr->sh_size = sizeof(SHSTR);
324 newShstrHdr->sh_name = SHSTRTAB_NAME;
325 newShstrHdr->sh_addr = newStrtabHdr->sh_offset + baseAddr;
326 newShstrHdr->sh_type = SHT_STRTAB;
327 newShstrHdr->sh_flags = SHF_ALLOC;
328
329 newStrtabHdr->sh_offset = newShstrHdr->sh_offset + newShstrHdr->sh_size;
330 newStrtabHdr->sh_size = info.strtabHdr->sh_size;
331 newStrtabHdr->sh_name = STRTAB_NAME;
332 newStrtabHdr->sh_addr = newStrtabHdr->sh_offset + baseAddr;
333 newStrtabHdr->sh_addralign = 1;
334 newStrtabHdr->sh_type = SHT_STRTAB;
335 newStrtabHdr->sh_flags = SHF_ALLOC;
336 newStrtabHdr->sh_link = SHSTRTAB_HDR_IDX;
337
338 *newSymHdr = *info.symtabHdr;
339 newSymHdr->sh_offset = reinterpret_cast<uintptr_t>(newSymtab) - baseAddr;
340 newSymHdr->sh_size = info.symCnt * info.symtabHdr->sh_entsize;
341 newSymHdr->sh_entsize = info.symtabHdr->sh_entsize;
342 newSymHdr->sh_addralign = info.symtabHdr->sh_addralign;
343 newSymHdr->sh_name = SYMTAB_NAME;
344 newSymHdr->sh_addr = reinterpret_cast<uintptr_t>(newSymtab);
345 newSymHdr->sh_link = STRTAB_HDR_IDX;
346
347 newTextHdr->sh_offset = 0;
348 newTextHdr->sh_size = info.asmstubHdr->sh_offset + info.asmstubHdr->sh_size - info.textHdr->sh_offset;
349 newTextHdr->sh_name = TEXT_NAME;
350 newTextHdr->sh_addr = info.fileAddr + info.textHdr->sh_offset;
351 newTextHdr->sh_addralign = sizeof(uint32_t);
352 newTextHdr->sh_type = SHT_NOBITS;
353 newTextHdr->sh_flags = SHF_ALLOC | SHF_EXECINSTR;
354 newTextHdr->sh_link = SYMTAB_HDR_IDX;
355
356 ehFrameHdr->sh_offset = reinterpret_cast<uintptr_t>(ehFrame) - baseAddr;
357 ehFrameHdr->sh_size = ehFrameSize;
358 ehFrameHdr->sh_name = EH_FRAME_NAME;
359 ehFrameHdr->sh_addr = reinterpret_cast<uintptr_t>(ehFrame);
360 ehFrameHdr->sh_addralign = sizeof(uintptr_t);
361 ehFrameHdr->sh_type = SHT_PROGBITS;
362 ehFrameHdr->sh_flags = SHF_ALLOC;
363 ehFrameHdr->sh_link = TEXT_HDR_IDX;
364 }
365
CreateDebuggerElf(uintptr_t fileAddr,void ** result,uint64_t * elfSize)366 bool CreateDebuggerElf(uintptr_t fileAddr, void **result, uint64_t *elfSize)
367 {
368 auto info = CollectStubAnInfo(fileAddr);
369 std::vector<uint8_t> ehFrame {
370 0x10, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x1, 0x0, 0x4, 0x78, 0x1e, 0x0, 0x0, 0x0,
371 0x0, 0x0, 0x0, 0x0, 0x4b, 0x0, 0x0, 0x0, 0x18, 0x0, 0x0, 0x0, 0x0, 0xe0, 0x73, 0xab,
372 0xff, 0xff, 0x0, 0x0, 0x30, 0x2c, 0x12, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x1d, 0x0, 0x10,
373 0x1e, 0x17, 0x8d, 0x0, 0x12, 0x8, 0x18, 0x1c, 0x6, 0x8, 0x0, 0x29, 0x28, 0x7, 0x0, 0x8,
374 0x10, 0x1c, 0x6, 0x2f, 0xee, 0xff, 0x8, 0x8, 0x22, 0x10, 0x1d, 0x17, 0x8d, 0x0, 0x12, 0x8,
375 0x18, 0x1c, 0x6, 0x8, 0x0, 0x29, 0x28, 0x7, 0x0, 0x8, 0x10, 0x1c, 0x6, 0x2f, 0xee, 0xff,
376 0x8, 0x0, 0x22,
377 };
378 const uint32_t addrOff = 28;
379 const uint32_t lenOff = 36;
380 auto writeU64 = [&ehFrame](uint32_t idx, uint64_t data) {
381 for (uint32_t i = 0; i < sizeof(uint64_t); i++) {
382 ehFrame[idx + i] = (data >> (8 * i)) & 0xff;
383 }
384 };
385 writeU64(addrOff, info.bcStubBegin + fileAddr);
386 ASSERT(info.bcStubEnd >= info.bcStubBegin);
387 writeU64(lenOff, info.bcStubEnd - info.bcStubBegin);
388
389 uint32_t totalSize = sizeof(Elf64_Ehdr) + sizeof(Elf64_Phdr) + sizeof(SHSTR) + info.strtabHdr->sh_size;
390 totalSize += info.symtabHdr->sh_entsize * info.symCnt + sizeof(Elf64_Sym); // for align
391 totalSize += ehFrame.size() + sizeof(uintptr_t);
392 totalSize += sizeof(Elf64_Shdr) * HEADER_CNT + sizeof(Elf64_Shdr); // for align
393
394 char *buffer = new char[totalSize];
395 Elf64_Ehdr *newEhdr = reinterpret_cast<Elf64_Ehdr *>(buffer);
396 Elf64_Phdr *newPhdr = reinterpret_cast<Elf64_Phdr *>(newEhdr + 1);
397 const char *shStrBuff = reinterpret_cast<const char *>(newPhdr + 1);
398 const char *newStrtab = shStrBuff + sizeof(SHSTR);
399 if (!CopyStrTab(reinterpret_cast<uintptr_t>(buffer), info)) {
400 delete[] buffer;
401 return false;
402 }
403
404 auto newSymtab = OffsetAlignUp<Elf64_Sym *>(newStrtab, info.strtabHdr->sh_size, info.symtabHdr->sh_addralign);
405 ConstructSymTab(newSymtab, info);
406
407 auto ehFrameBuffer = OffsetAlignUp<char *>(newSymtab, info.symtabHdr->sh_entsize * info.symCnt, sizeof(uintptr_t));
408 if (memcpy_s(ehFrameBuffer, ehFrame.size(), ehFrame.data(), ehFrame.size()) != EOK) {
409 delete[] buffer;
410 return false;
411 }
412
413 auto newShdrtab = OffsetAlignUp<Elf64_Shdr *>(ehFrameBuffer, ehFrame.size(), sizeof(uintptr_t));
414 ConstructEhdrAndPhdr(newEhdr, newShdrtab, reinterpret_cast<uintptr_t>(buffer), info);
415 ConstructShdrTab(newShdrtab, newSymtab, reinterpret_cast<uintptr_t>(buffer), ehFrameBuffer, ehFrame.size(), info);
416
417 *result = reinterpret_cast<void *>(buffer);
418 *elfSize = totalSize;
419 return true;
420 }
421
RegisterStubAnToDebuggerImpl(const char * fileAddr)422 static bool RegisterStubAnToDebuggerImpl(const char *fileAddr)
423 {
424 auto *entry = new jit_code_entry;
425 if (!CreateDebuggerElf(reinterpret_cast<uintptr_t>(fileAddr),
426 reinterpret_cast<void **>(const_cast<char **>(&entry->symfile_addr)), &entry->symfile_size)) {
427 delete entry;
428 return false;
429 }
430 entry->prev_entry = nullptr;
431 entry->file_addr = fileAddr;
432
433 // Insert this entry at the head of the list.
434 jit_code_entry *nextEntry = __jit_debug_descriptor.first_entry;
435 entry->next_entry = nextEntry;
436 if (nextEntry) {
437 nextEntry->prev_entry = entry;
438 }
439
440 __jit_debug_descriptor.first_entry = entry;
441 __jit_debug_descriptor.relevant_entry = entry;
442 __jit_debug_descriptor.action_flag = JIT_REGISTER_FN;
443 __jit_debug_register_code();
444 return true;
445 }
446
447 }
448 }
449 #else
450 namespace panda::ecmascript {
451 namespace jit_debug {
RegisterStubAnToDebugger(const char * fileAddr)452 void RegisterStubAnToDebugger(const char *fileAddr)
453 {
454 LOG_COMPILER(INFO) << "MACOS doesn't support RegisterStubAnToDebugger, fileAddr is" << fileAddr;
455 }
456
UnregisterStubAnFromDebugger(const char * fileAddr)457 void UnregisterStubAnFromDebugger(const char *fileAddr)
458 {
459 LOG_COMPILER(INFO) << "MACOS doesn't support RegisterStubAnToDebugger, fileAddr is" << fileAddr;
460 }
461 }
462 }
463 #endif
464