/Users/buildslave/jenkins/workspace/coverage/llvm-project/lldb/source/Symbol/LocateSymbolFile.cpp
Line | Count | Source (jump to first uncovered line) |
1 | | //===-- LocateSymbolFile.cpp ----------------------------------------------===// |
2 | | // |
3 | | // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. |
4 | | // See https://llvm.org/LICENSE.txt for license information. |
5 | | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception |
6 | | // |
7 | | //===----------------------------------------------------------------------===// |
8 | | |
9 | | #include "lldb/Symbol/LocateSymbolFile.h" |
10 | | |
11 | | #include "lldb/Core/Debugger.h" |
12 | | #include "lldb/Core/Module.h" |
13 | | #include "lldb/Core/ModuleList.h" |
14 | | #include "lldb/Core/ModuleSpec.h" |
15 | | #include "lldb/Core/Progress.h" |
16 | | #include "lldb/Host/FileSystem.h" |
17 | | #include "lldb/Symbol/ObjectFile.h" |
18 | | #include "lldb/Utility/ArchSpec.h" |
19 | | #include "lldb/Utility/DataBuffer.h" |
20 | | #include "lldb/Utility/DataExtractor.h" |
21 | | #include "lldb/Utility/LLDBLog.h" |
22 | | #include "lldb/Utility/Log.h" |
23 | | #include "lldb/Utility/StreamString.h" |
24 | | #include "lldb/Utility/Timer.h" |
25 | | #include "lldb/Utility/UUID.h" |
26 | | |
27 | | #include "llvm/ADT/SmallSet.h" |
28 | | #include "llvm/Support/FileSystem.h" |
29 | | #include "llvm/Support/ThreadPool.h" |
30 | | |
31 | | // From MacOSX system header "mach/machine.h" |
32 | | typedef int cpu_type_t; |
33 | | typedef int cpu_subtype_t; |
34 | | |
35 | | using namespace lldb; |
36 | | using namespace lldb_private; |
37 | | |
38 | | #if defined(__APPLE__) |
39 | | |
40 | | // Forward declaration of method defined in source/Host/macosx/Symbols.cpp |
41 | | int LocateMacOSXFilesUsingDebugSymbols(const ModuleSpec &module_spec, |
42 | | ModuleSpec &return_module_spec); |
43 | | |
44 | | #else |
45 | | |
46 | | int LocateMacOSXFilesUsingDebugSymbols(const ModuleSpec &module_spec, |
47 | | ModuleSpec &return_module_spec) { |
48 | | // Cannot find MacOSX files using debug symbols on non MacOSX. |
49 | | return 0; |
50 | | } |
51 | | |
52 | | #endif |
53 | | |
54 | | static bool FileAtPathContainsArchAndUUID(const FileSpec &file_fspec, |
55 | | const ArchSpec *arch, |
56 | 1.44k | const lldb_private::UUID *uuid) { |
57 | 1.44k | ModuleSpecList module_specs; |
58 | 1.44k | if (ObjectFile::GetModuleSpecifications(file_fspec, 0, 0, module_specs)) { |
59 | 1.44k | ModuleSpec spec; |
60 | 1.44k | for (size_t i = 0; i < module_specs.GetSize(); ++i2 ) { |
61 | 1.44k | bool got_spec = module_specs.GetModuleSpecAtIndex(i, spec); |
62 | 1.44k | UNUSED_IF_ASSERT_DISABLED(got_spec); |
63 | 1.44k | assert(got_spec); |
64 | 1.44k | if ((uuid == nullptr || (spec.GetUUIDPtr() && spec.GetUUID() == *uuid)) && |
65 | 1.44k | (1.44k arch == nullptr1.44k || |
66 | 1.44k | (spec.GetArchitecturePtr() && |
67 | 1.44k | spec.GetArchitecture().IsCompatibleMatch(*arch)))) { |
68 | 1.44k | return true; |
69 | 1.44k | } |
70 | 1.44k | } |
71 | 1.44k | } |
72 | 2 | return false; |
73 | 1.44k | } |
74 | | |
75 | | // Given a binary exec_fspec, and a ModuleSpec with an architecture/uuid, |
76 | | // return true if there is a matching dSYM bundle next to the exec_fspec, |
77 | | // and return that value in dsym_fspec. |
78 | | // If there is a .dSYM.yaa compressed archive next to the exec_fspec, |
79 | | // call through Symbols::DownloadObjectAndSymbolFile to download the |
80 | | // expanded/uncompressed dSYM and return that filepath in dsym_fspec. |
81 | | |
82 | | static bool LookForDsymNextToExecutablePath(const ModuleSpec &mod_spec, |
83 | | const FileSpec &exec_fspec, |
84 | 128k | FileSpec &dsym_fspec) { |
85 | 128k | ConstString filename = exec_fspec.GetFilename(); |
86 | 128k | FileSpec dsym_directory = exec_fspec; |
87 | 128k | dsym_directory.RemoveLastPathComponent(); |
88 | | |
89 | 128k | std::string dsym_filename = filename.AsCString(); |
90 | 128k | dsym_filename += ".dSYM"; |
91 | 128k | dsym_directory.AppendPathComponent(dsym_filename); |
92 | 128k | dsym_directory.AppendPathComponent("Contents"); |
93 | 128k | dsym_directory.AppendPathComponent("Resources"); |
94 | 128k | dsym_directory.AppendPathComponent("DWARF"); |
95 | | |
96 | 128k | if (FileSystem::Instance().Exists(dsym_directory)) { |
97 | | |
98 | | // See if the binary name exists in the dSYM DWARF |
99 | | // subdir. |
100 | 1.44k | dsym_fspec = dsym_directory; |
101 | 1.44k | dsym_fspec.AppendPathComponent(filename.AsCString()); |
102 | 1.44k | if (FileSystem::Instance().Exists(dsym_fspec) && |
103 | 1.44k | FileAtPathContainsArchAndUUID(dsym_fspec, mod_spec.GetArchitecturePtr(), |
104 | 1.44k | mod_spec.GetUUIDPtr())) { |
105 | 1.43k | return true; |
106 | 1.43k | } |
107 | | |
108 | | // See if we have "../CF.framework" - so we'll look for |
109 | | // CF.framework.dSYM/Contents/Resources/DWARF/CF |
110 | | // We need to drop the last suffix after '.' to match |
111 | | // 'CF' in the DWARF subdir. |
112 | 5 | std::string binary_name(filename.AsCString()); |
113 | 5 | auto last_dot = binary_name.find_last_of('.'); |
114 | 5 | if (last_dot != std::string::npos) { |
115 | 5 | binary_name.erase(last_dot); |
116 | 5 | dsym_fspec = dsym_directory; |
117 | 5 | dsym_fspec.AppendPathComponent(binary_name); |
118 | 5 | if (FileSystem::Instance().Exists(dsym_fspec) && |
119 | 5 | FileAtPathContainsArchAndUUID(dsym_fspec, |
120 | 3 | mod_spec.GetArchitecturePtr(), |
121 | 3 | mod_spec.GetUUIDPtr())) { |
122 | 3 | return true; |
123 | 3 | } |
124 | 5 | } |
125 | 5 | } |
126 | | |
127 | | // See if we have a .dSYM.yaa next to this executable path. |
128 | 126k | FileSpec dsym_yaa_fspec = exec_fspec; |
129 | 126k | dsym_yaa_fspec.RemoveLastPathComponent(); |
130 | 126k | std::string dsym_yaa_filename = filename.AsCString(); |
131 | 126k | dsym_yaa_filename += ".dSYM.yaa"; |
132 | 126k | dsym_yaa_fspec.AppendPathComponent(dsym_yaa_filename); |
133 | | |
134 | 126k | if (FileSystem::Instance().Exists(dsym_yaa_fspec)) { |
135 | 0 | ModuleSpec mutable_mod_spec = mod_spec; |
136 | 0 | Status error; |
137 | 0 | if (Symbols::DownloadObjectAndSymbolFile(mutable_mod_spec, error, true) && |
138 | 0 | FileSystem::Instance().Exists(mutable_mod_spec.GetSymbolFileSpec())) { |
139 | 0 | dsym_fspec = mutable_mod_spec.GetSymbolFileSpec(); |
140 | 0 | return true; |
141 | 0 | } |
142 | 0 | } |
143 | | |
144 | 126k | return false; |
145 | 126k | } |
146 | | |
147 | | // Given a ModuleSpec with a FileSpec and optionally uuid/architecture |
148 | | // filled in, look for a .dSYM bundle next to that binary. Returns true |
149 | | // if a .dSYM bundle is found, and that path is returned in the dsym_fspec |
150 | | // FileSpec. |
151 | | // |
152 | | // This routine looks a few directory layers above the given exec_path - |
153 | | // exec_path might be /System/Library/Frameworks/CF.framework/CF and the |
154 | | // dSYM might be /System/Library/Frameworks/CF.framework.dSYM. |
155 | | // |
156 | | // If there is a .dSYM.yaa compressed archive found next to the binary, |
157 | | // we'll call DownloadObjectAndSymbolFile to expand it into a plain .dSYM |
158 | | |
159 | | static bool LocateDSYMInVincinityOfExecutable(const ModuleSpec &module_spec, |
160 | 117k | FileSpec &dsym_fspec) { |
161 | 117k | Log *log = GetLog(LLDBLog::Host); |
162 | 117k | const FileSpec &exec_fspec = module_spec.GetFileSpec(); |
163 | 117k | if (exec_fspec) { |
164 | 113k | if (::LookForDsymNextToExecutablePath(module_spec, exec_fspec, |
165 | 113k | dsym_fspec)) { |
166 | 1.43k | if (log) { |
167 | 3 | LLDB_LOGF(log, "dSYM with matching UUID & arch found at %s", |
168 | 3 | dsym_fspec.GetPath().c_str()); |
169 | 3 | } |
170 | 1.43k | return true; |
171 | 111k | } else { |
172 | 111k | FileSpec parent_dirs = exec_fspec; |
173 | | |
174 | | // Remove the binary name from the FileSpec |
175 | 111k | parent_dirs.RemoveLastPathComponent(); |
176 | | |
177 | | // Add a ".dSYM" name to each directory component of the path, |
178 | | // stripping off components. e.g. we may have a binary like |
179 | | // /S/L/F/Foundation.framework/Versions/A/Foundation and |
180 | | // /S/L/F/Foundation.framework.dSYM |
181 | | // |
182 | | // so we'll need to start with |
183 | | // /S/L/F/Foundation.framework/Versions/A, add the .dSYM part to the |
184 | | // "A", and if that doesn't exist, strip off the "A" and try it again |
185 | | // with "Versions", etc., until we find a dSYM bundle or we've |
186 | | // stripped off enough path components that there's no need to |
187 | | // continue. |
188 | | |
189 | 558k | for (int i = 0; i < 4; i++446k ) { |
190 | | // Does this part of the path have a "." character - could it be a |
191 | | // bundle's top level directory? |
192 | 446k | const char *fn = parent_dirs.GetFilename().AsCString(); |
193 | 446k | if (fn == nullptr) |
194 | 0 | break; |
195 | 446k | if (::strchr(fn, '.') != nullptr) { |
196 | 14.9k | if (::LookForDsymNextToExecutablePath(module_spec, parent_dirs, |
197 | 14.9k | dsym_fspec)) { |
198 | 3 | if (log) { |
199 | 0 | LLDB_LOGF(log, "dSYM with matching UUID & arch found at %s", |
200 | 0 | dsym_fspec.GetPath().c_str()); |
201 | 0 | } |
202 | 3 | return true; |
203 | 3 | } |
204 | 14.9k | } |
205 | 446k | parent_dirs.RemoveLastPathComponent(); |
206 | 446k | } |
207 | 111k | } |
208 | 113k | } |
209 | 115k | dsym_fspec.Clear(); |
210 | 115k | return false; |
211 | 117k | } |
212 | | |
213 | 117k | static FileSpec LocateExecutableSymbolFileDsym(const ModuleSpec &module_spec) { |
214 | 117k | const FileSpec *exec_fspec = module_spec.GetFileSpecPtr(); |
215 | 117k | const ArchSpec *arch = module_spec.GetArchitecturePtr(); |
216 | 117k | const UUID *uuid = module_spec.GetUUIDPtr(); |
217 | | |
218 | 117k | LLDB_SCOPED_TIMERF( |
219 | 117k | "LocateExecutableSymbolFileDsym (file = %s, arch = %s, uuid = %p)", |
220 | 117k | exec_fspec ? exec_fspec->GetFilename().AsCString("<NULL>") : "<NULL>", |
221 | 117k | arch ? arch->GetArchitectureName() : "<NULL>", (const void *)uuid); |
222 | | |
223 | 117k | FileSpec symbol_fspec; |
224 | 117k | ModuleSpec dsym_module_spec; |
225 | | // First try and find the dSYM in the same directory as the executable or in |
226 | | // an appropriate parent directory |
227 | 117k | if (!LocateDSYMInVincinityOfExecutable(module_spec, symbol_fspec)) { |
228 | | // We failed to easily find the dSYM above, so use DebugSymbols |
229 | 115k | LocateMacOSXFilesUsingDebugSymbols(module_spec, dsym_module_spec); |
230 | 115k | } else { |
231 | 1.44k | dsym_module_spec.GetSymbolFileSpec() = symbol_fspec; |
232 | 1.44k | } |
233 | | |
234 | 117k | return dsym_module_spec.GetSymbolFileSpec(); |
235 | 117k | } |
236 | | |
237 | 1.00k | ModuleSpec Symbols::LocateExecutableObjectFile(const ModuleSpec &module_spec) { |
238 | 1.00k | ModuleSpec result; |
239 | 1.00k | const FileSpec &exec_fspec = module_spec.GetFileSpec(); |
240 | 1.00k | const ArchSpec *arch = module_spec.GetArchitecturePtr(); |
241 | 1.00k | const UUID *uuid = module_spec.GetUUIDPtr(); |
242 | 1.00k | LLDB_SCOPED_TIMERF( |
243 | 1.00k | "LocateExecutableObjectFile (file = %s, arch = %s, uuid = %p)", |
244 | 1.00k | exec_fspec ? exec_fspec.GetFilename().AsCString("<NULL>") : "<NULL>", |
245 | 1.00k | arch ? arch->GetArchitectureName() : "<NULL>", (const void *)uuid); |
246 | | |
247 | 1.00k | ModuleSpecList module_specs; |
248 | 1.00k | ModuleSpec matched_module_spec; |
249 | 1.00k | if (exec_fspec && |
250 | 1.00k | ObjectFile::GetModuleSpecifications(exec_fspec, 0, 0, module_specs)981 && |
251 | 1.00k | module_specs.FindMatchingModuleSpec(module_spec, matched_module_spec)14 ) { |
252 | 0 | result.GetFileSpec() = exec_fspec; |
253 | 1.00k | } else { |
254 | 1.00k | LocateMacOSXFilesUsingDebugSymbols(module_spec, result); |
255 | 1.00k | } |
256 | | |
257 | 1.00k | return result; |
258 | 1.00k | } |
259 | | |
260 | | // Keep "symbols.enable-external-lookup" description in sync with this function. |
261 | | |
262 | | FileSpec |
263 | | Symbols::LocateExecutableSymbolFile(const ModuleSpec &module_spec, |
264 | 117k | const FileSpecList &default_search_paths) { |
265 | 117k | FileSpec symbol_file_spec = module_spec.GetSymbolFileSpec(); |
266 | 117k | if (symbol_file_spec.IsAbsolute() && |
267 | 117k | FileSystem::Instance().Exists(symbol_file_spec)167 ) |
268 | 24 | return symbol_file_spec; |
269 | | |
270 | 117k | Progress progress(llvm::formatv( |
271 | 117k | "Locating external symbol file for {0}", |
272 | 117k | module_spec.GetFileSpec().GetFilename().AsCString("<Unknown>"))); |
273 | | |
274 | 117k | FileSpecList debug_file_search_paths = default_search_paths; |
275 | | |
276 | | // Add module directory. |
277 | 117k | FileSpec module_file_spec = module_spec.GetFileSpec(); |
278 | | // We keep the unresolved pathname if it fails. |
279 | 117k | FileSystem::Instance().ResolveSymbolicLink(module_file_spec, |
280 | 117k | module_file_spec); |
281 | | |
282 | 117k | ConstString file_dir = module_file_spec.GetDirectory(); |
283 | 117k | { |
284 | 117k | FileSpec file_spec(file_dir.AsCString(".")); |
285 | 117k | FileSystem::Instance().Resolve(file_spec); |
286 | 117k | debug_file_search_paths.AppendIfUnique(file_spec); |
287 | 117k | } |
288 | | |
289 | 117k | if (ModuleList::GetGlobalModuleListProperties().GetEnableExternalLookup()) { |
290 | | |
291 | | // Add current working directory. |
292 | 418 | { |
293 | 418 | FileSpec file_spec("."); |
294 | 418 | FileSystem::Instance().Resolve(file_spec); |
295 | 418 | debug_file_search_paths.AppendIfUnique(file_spec); |
296 | 418 | } |
297 | | |
298 | 418 | #ifndef _WIN32 |
299 | | #if defined(__NetBSD__) |
300 | | // Add /usr/libdata/debug directory. |
301 | | { |
302 | | FileSpec file_spec("/usr/libdata/debug"); |
303 | | FileSystem::Instance().Resolve(file_spec); |
304 | | debug_file_search_paths.AppendIfUnique(file_spec); |
305 | | } |
306 | | #else |
307 | | // Add /usr/lib/debug directory. |
308 | 418 | { |
309 | 418 | FileSpec file_spec("/usr/lib/debug"); |
310 | 418 | FileSystem::Instance().Resolve(file_spec); |
311 | 418 | debug_file_search_paths.AppendIfUnique(file_spec); |
312 | 418 | } |
313 | 418 | #endif |
314 | 418 | #endif // _WIN32 |
315 | 418 | } |
316 | | |
317 | 117k | std::string uuid_str; |
318 | 117k | const UUID &module_uuid = module_spec.GetUUID(); |
319 | 117k | if (module_uuid.IsValid()) { |
320 | | // Some debug files are stored in the .build-id directory like this: |
321 | | // /usr/lib/debug/.build-id/ff/e7fe727889ad82bb153de2ad065b2189693315.debug |
322 | 111k | uuid_str = module_uuid.GetAsString(""); |
323 | 111k | std::transform(uuid_str.begin(), uuid_str.end(), uuid_str.begin(), |
324 | 111k | ::tolower); |
325 | 111k | uuid_str.insert(2, 1, '/'); |
326 | 111k | uuid_str = uuid_str + ".debug"; |
327 | 111k | } |
328 | | |
329 | 117k | size_t num_directories = debug_file_search_paths.GetSize(); |
330 | 235k | for (size_t idx = 0; idx < num_directories; ++idx118k ) { |
331 | 118k | FileSpec dirspec = debug_file_search_paths.GetFileSpecAtIndex(idx); |
332 | 118k | FileSystem::Instance().Resolve(dirspec); |
333 | 118k | if (!FileSystem::Instance().IsDirectory(dirspec)) |
334 | 418 | continue; |
335 | | |
336 | 117k | std::vector<std::string> files; |
337 | 117k | std::string dirname = dirspec.GetPath(); |
338 | | |
339 | 117k | if (!uuid_str.empty()) |
340 | 112k | files.push_back(dirname + "/.build-id/" + uuid_str); |
341 | 117k | if (symbol_file_spec.GetFilename()) { |
342 | 4.31k | files.push_back(dirname + "/" + |
343 | 4.31k | symbol_file_spec.GetFilename().GetCString()); |
344 | 4.31k | files.push_back(dirname + "/.debug/" + |
345 | 4.31k | symbol_file_spec.GetFilename().GetCString()); |
346 | | |
347 | | // Some debug files may stored in the module directory like this: |
348 | | // /usr/lib/debug/usr/lib/library.so.debug |
349 | 4.31k | if (!file_dir.IsEmpty()) |
350 | 154 | files.push_back(dirname + file_dir.AsCString() + "/" + |
351 | 154 | symbol_file_spec.GetFilename().GetCString()); |
352 | 4.31k | } |
353 | | |
354 | 117k | const uint32_t num_files = files.size(); |
355 | 238k | for (size_t idx_file = 0; idx_file < num_files; ++idx_file120k ) { |
356 | 120k | const std::string &filename = files[idx_file]; |
357 | 120k | FileSpec file_spec(filename); |
358 | 120k | FileSystem::Instance().Resolve(file_spec); |
359 | | |
360 | 120k | if (llvm::sys::fs::equivalent(file_spec.GetPath(), |
361 | 120k | module_file_spec.GetPath())) |
362 | 0 | continue; |
363 | | |
364 | 120k | if (FileSystem::Instance().Exists(file_spec)) { |
365 | 9 | lldb_private::ModuleSpecList specs; |
366 | 9 | const size_t num_specs = |
367 | 9 | ObjectFile::GetModuleSpecifications(file_spec, 0, 0, specs); |
368 | 9 | ModuleSpec mspec; |
369 | 9 | bool valid_mspec = false; |
370 | 9 | if (num_specs == 2) { |
371 | | // Special case to handle both i386 and i686 from ObjectFilePECOFF |
372 | 0 | ModuleSpec mspec2; |
373 | 0 | if (specs.GetModuleSpecAtIndex(0, mspec) && |
374 | 0 | specs.GetModuleSpecAtIndex(1, mspec2) && |
375 | 0 | mspec.GetArchitecture().GetTriple().isCompatibleWith( |
376 | 0 | mspec2.GetArchitecture().GetTriple())) { |
377 | 0 | valid_mspec = true; |
378 | 0 | } |
379 | 0 | } |
380 | 9 | if (!valid_mspec) { |
381 | 9 | assert(num_specs <= 1 && |
382 | 9 | "Symbol Vendor supports only a single architecture"); |
383 | 9 | if (num_specs == 1) { |
384 | 9 | if (specs.GetModuleSpecAtIndex(0, mspec)) { |
385 | 9 | valid_mspec = true; |
386 | 9 | } |
387 | 9 | } |
388 | 9 | } |
389 | 9 | if (valid_mspec) { |
390 | | // Skip the uuids check if module_uuid is invalid. For example, |
391 | | // this happens for *.dwp files since at the moment llvm-dwp |
392 | | // doesn't output build ids, nor does binutils dwp. |
393 | 9 | if (!module_uuid.IsValid() || module_uuid == mspec.GetUUID()7 ) |
394 | 8 | return file_spec; |
395 | 9 | } |
396 | 9 | } |
397 | 120k | } |
398 | 117k | } |
399 | | |
400 | 117k | return LocateExecutableSymbolFileDsym(module_spec); |
401 | 117k | } |
402 | | |
403 | 5.44k | void Symbols::DownloadSymbolFileAsync(const UUID &uuid) { |
404 | 5.44k | if (!ModuleList::GetGlobalModuleListProperties().GetEnableBackgroundLookup()) |
405 | 5.44k | return; |
406 | | |
407 | 0 | static llvm::SmallSet<UUID, 8> g_seen_uuids; |
408 | 0 | static std::mutex g_mutex; |
409 | 0 | Debugger::GetThreadPool().async([=]() { |
410 | 0 | { |
411 | 0 | std::lock_guard<std::mutex> guard(g_mutex); |
412 | 0 | if (g_seen_uuids.count(uuid)) |
413 | 0 | return; |
414 | 0 | g_seen_uuids.insert(uuid); |
415 | 0 | } |
416 | | |
417 | 0 | Status error; |
418 | 0 | ModuleSpec module_spec; |
419 | 0 | module_spec.GetUUID() = uuid; |
420 | 0 | if (!Symbols::DownloadObjectAndSymbolFile(module_spec, error, |
421 | 0 | /*force_lookup=*/true, |
422 | 0 | /*copy_executable=*/false)) |
423 | 0 | return; |
424 | | |
425 | 0 | if (error.Fail()) |
426 | 0 | return; |
427 | | |
428 | 0 | Debugger::ReportSymbolChange(module_spec); |
429 | 0 | }); |
430 | 0 | } |
431 | | |
432 | | #if !defined(__APPLE__) |
433 | | |
434 | | FileSpec Symbols::FindSymbolFileInBundle(const FileSpec &symfile_bundle, |
435 | | const lldb_private::UUID *uuid, |
436 | | const ArchSpec *arch) { |
437 | | // FIXME |
438 | | return FileSpec(); |
439 | | } |
440 | | |
441 | | bool Symbols::DownloadObjectAndSymbolFile(ModuleSpec &module_spec, |
442 | | Status &error, bool force_lookup, |
443 | | bool copy_executable) { |
444 | | // Fill in the module_spec.GetFileSpec() for the object file and/or the |
445 | | // module_spec.GetSymbolFileSpec() for the debug symbols file. |
446 | | return false; |
447 | | } |
448 | | |
449 | | #endif |