mirror of
https://github.com/chromium/crashpad.git
synced 2024-12-26 23:01:05 +08:00
Enable reading notes from ELF images
Bug: crashpad:30 Change-Id: Ie6c594b05c6d39a869ed30b7a7b49e6a6301cc65 Reviewed-on: https://chromium-review.googlesource.com/792539 Commit-Queue: Joshua Peraza <jperaza@chromium.org> Reviewed-by: Mark Mentovai <mark@chromium.org>
This commit is contained in:
parent
741a84a298
commit
7a0daa6989
@ -17,6 +17,8 @@
|
||||
|
||||
#include_next <elf.h>
|
||||
|
||||
#include <android/api-level.h>
|
||||
|
||||
#if !defined(ELF32_ST_VISIBILITY)
|
||||
#define ELF32_ST_VISIBILITY(other) ((other) & 0x3)
|
||||
#endif
|
||||
@ -35,4 +37,22 @@
|
||||
#define STT_TLS 6
|
||||
#endif
|
||||
|
||||
// ELF note header types are normally provided by <linux/elf.h>. While unified
|
||||
// headers include <linux/elf.h> in <elf.h>, traditional headers do not, prior
|
||||
// to API 21. <elf.h> and <linux/elf.h> can't both be included in the same
|
||||
// translation unit due to collisions, so we provide these types here.
|
||||
#if __ANDROID_API__ < 21 && !defined(__ANDROID_API_N__)
|
||||
typedef struct {
|
||||
Elf32_Word n_namesz;
|
||||
Elf32_Word n_descsz;
|
||||
Elf32_Word n_type;
|
||||
} Elf32_Nhdr;
|
||||
|
||||
typedef struct {
|
||||
Elf64_Word n_namesz;
|
||||
Elf64_Word n_descsz;
|
||||
Elf64_Word n_type;
|
||||
} Elf64_Nhdr;
|
||||
#endif // __ANDROID_API__ < 21 && !defined(NT_PRSTATUS)
|
||||
|
||||
#endif // CRASHPAD_COMPAT_ANDROID_ELF_H_
|
||||
|
@ -16,7 +16,9 @@
|
||||
|
||||
#include <stddef.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <limits>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "base/logging.h"
|
||||
@ -36,6 +38,13 @@ class ElfImageReader::ProgramHeaderTable {
|
||||
virtual bool GetPreferredLoadedMemoryRange(VMAddress* address,
|
||||
VMSize* size) const = 0;
|
||||
|
||||
// Locate the next PT_NOTE segment starting at segment index start_index. If a
|
||||
// PT_NOTE segment is found, start_index is set to the next index after the
|
||||
// found segment.
|
||||
virtual bool GetNoteSegment(size_t* start_index,
|
||||
VMAddress* address,
|
||||
VMSize* size) const = 0;
|
||||
|
||||
protected:
|
||||
ProgramHeaderTable() {}
|
||||
};
|
||||
@ -150,6 +159,21 @@ class ElfImageReader::ProgramHeaderTableSpecific
|
||||
return false;
|
||||
}
|
||||
|
||||
bool GetNoteSegment(size_t* start_index,
|
||||
VMAddress* address,
|
||||
VMSize* size) const override {
|
||||
INITIALIZATION_STATE_DCHECK_VALID(initialized_);
|
||||
for (size_t index = *start_index; index < table_.size(); ++index) {
|
||||
if (table_[index].p_type == PT_NOTE) {
|
||||
*start_index = index + 1;
|
||||
*address = table_[index].p_vaddr;
|
||||
*size = table_[index].p_memsz;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<PhdrType> table_;
|
||||
InitializationStateDcheck initialized_;
|
||||
@ -157,6 +181,150 @@ class ElfImageReader::ProgramHeaderTableSpecific
|
||||
DISALLOW_COPY_AND_ASSIGN(ProgramHeaderTableSpecific<PhdrType>);
|
||||
};
|
||||
|
||||
ElfImageReader::NoteReader::~NoteReader() = default;
|
||||
|
||||
ElfImageReader::NoteReader::Result ElfImageReader::NoteReader::NextNote(
|
||||
std::string* name,
|
||||
NoteType* type,
|
||||
std::string* desc) {
|
||||
if (!is_valid_) {
|
||||
LOG(ERROR) << "invalid note reader";
|
||||
return Result::kError;
|
||||
}
|
||||
|
||||
Result result = Result::kError;
|
||||
do {
|
||||
while (current_address_ == segment_end_address_) {
|
||||
VMSize segment_size;
|
||||
if (!phdr_table_->GetNoteSegment(
|
||||
&phdr_index_, ¤t_address_, &segment_size)) {
|
||||
return Result::kNoMoreNotes;
|
||||
}
|
||||
current_address_ += elf_reader_->GetLoadBias();
|
||||
segment_end_address_ = current_address_ + segment_size;
|
||||
segment_range_ = std::make_unique<ProcessMemoryRange>();
|
||||
if (!segment_range_->Initialize(*range_) ||
|
||||
!segment_range_->RestrictRange(current_address_, segment_size)) {
|
||||
return Result::kError;
|
||||
}
|
||||
}
|
||||
|
||||
retry_ = false;
|
||||
result = range_->Is64Bit() ? ReadNote<Elf64_Nhdr>(name, type, desc)
|
||||
: ReadNote<Elf32_Nhdr>(name, type, desc);
|
||||
} while (retry_);
|
||||
|
||||
if (result == Result::kSuccess) {
|
||||
return Result::kSuccess;
|
||||
}
|
||||
is_valid_ = false;
|
||||
return Result::kError;
|
||||
}
|
||||
|
||||
ElfImageReader::NoteReader::NoteReader(const ElfImageReader* elf_reader,
|
||||
const ProcessMemoryRange* range,
|
||||
const ProgramHeaderTable* phdr_table,
|
||||
ssize_t max_note_size,
|
||||
const std::string& name_filter,
|
||||
NoteType type_filter,
|
||||
bool use_filter)
|
||||
: current_address_(0),
|
||||
segment_end_address_(0),
|
||||
elf_reader_(elf_reader),
|
||||
range_(range),
|
||||
phdr_table_(phdr_table),
|
||||
segment_range_(),
|
||||
phdr_index_(0),
|
||||
max_note_size_(max_note_size),
|
||||
name_filter_(name_filter),
|
||||
type_filter_(type_filter),
|
||||
use_filter_(use_filter),
|
||||
is_valid_(true),
|
||||
retry_(false) {}
|
||||
|
||||
template <typename NhdrType>
|
||||
ElfImageReader::NoteReader::Result ElfImageReader::NoteReader::ReadNote(
|
||||
std::string* name,
|
||||
NoteType* type,
|
||||
std::string* desc) {
|
||||
static_assert(sizeof(*type) >= sizeof(NhdrType::n_namesz),
|
||||
"Note field size mismatch");
|
||||
DCHECK_LT(current_address_, segment_end_address_);
|
||||
|
||||
NhdrType note_info;
|
||||
if (!segment_range_->Read(current_address_, sizeof(note_info), ¬e_info)) {
|
||||
return Result::kError;
|
||||
}
|
||||
current_address_ += sizeof(note_info);
|
||||
|
||||
constexpr size_t align = sizeof(note_info.n_namesz);
|
||||
#define PAD(x) ((x) + align - 1 & ~(align - 1))
|
||||
size_t padded_namesz = PAD(note_info.n_namesz);
|
||||
size_t padded_descsz = PAD(note_info.n_descsz);
|
||||
size_t note_size = padded_namesz + padded_descsz;
|
||||
|
||||
// Notes typically have 4-byte alignment. However, .note.android.ident may
|
||||
// inadvertently use 2-byte alignment.
|
||||
// https://android-review.googlesource.com/c/platform/bionic/+/554986/
|
||||
// We can still find .note.android.ident if it appears first in a note segment
|
||||
// but there may be 4-byte aligned notes following it. If this note was
|
||||
// aligned at less than 4-bytes, expect that the next note will be aligned at
|
||||
// 4-bytes and add extra padding, if necessary.
|
||||
VMAddress end_of_note =
|
||||
std::min(PAD(current_address_ + note_size), segment_end_address_);
|
||||
#undef PAD
|
||||
|
||||
if (max_note_size_ >= 0 && note_size > static_cast<size_t>(max_note_size_)) {
|
||||
current_address_ = end_of_note;
|
||||
retry_ = true;
|
||||
return Result::kError;
|
||||
}
|
||||
|
||||
if (use_filter_ && note_info.n_type != type_filter_) {
|
||||
current_address_ = end_of_note;
|
||||
retry_ = true;
|
||||
return Result::kError;
|
||||
}
|
||||
|
||||
std::string local_name(note_info.n_namesz, '\0');
|
||||
if (!segment_range_->Read(
|
||||
current_address_, note_info.n_namesz, &local_name[0])) {
|
||||
return Result::kError;
|
||||
}
|
||||
if (!local_name.empty()) {
|
||||
if (local_name.back() != '\0') {
|
||||
LOG(ERROR) << "unterminated note name";
|
||||
return Result::kError;
|
||||
}
|
||||
local_name.pop_back();
|
||||
}
|
||||
|
||||
if (use_filter_ && local_name != name_filter_) {
|
||||
current_address_ = end_of_note;
|
||||
retry_ = true;
|
||||
return Result::kError;
|
||||
}
|
||||
|
||||
current_address_ += padded_namesz;
|
||||
|
||||
std::string local_desc(note_info.n_descsz, '\0');
|
||||
if (!segment_range_->Read(
|
||||
current_address_, note_info.n_descsz, &local_desc[0])) {
|
||||
return Result::kError;
|
||||
}
|
||||
|
||||
current_address_ = end_of_note;
|
||||
|
||||
if (name) {
|
||||
name->swap(local_name);
|
||||
}
|
||||
if (type) {
|
||||
*type = note_info.n_type;
|
||||
}
|
||||
desc->swap(local_desc);
|
||||
return Result::kSuccess;
|
||||
}
|
||||
|
||||
ElfImageReader::ElfImageReader()
|
||||
: header_64_(),
|
||||
ehdr_address_(0),
|
||||
@ -467,4 +635,18 @@ bool ElfImageReader::GetAddressFromDynamicArray(uint64_t tag,
|
||||
return true;
|
||||
}
|
||||
|
||||
std::unique_ptr<ElfImageReader::NoteReader> ElfImageReader::Notes(
|
||||
ssize_t max_note_size) {
|
||||
return std::make_unique<NoteReader>(
|
||||
this, &memory_, program_headers_.get(), max_note_size);
|
||||
}
|
||||
|
||||
std::unique_ptr<ElfImageReader::NoteReader>
|
||||
ElfImageReader::NotesWithNameAndType(const std::string& name,
|
||||
NoteReader::NoteType type,
|
||||
ssize_t max_note_size) {
|
||||
return std::make_unique<NoteReader>(
|
||||
this, &memory_, program_headers_.get(), max_note_size, name, type, true);
|
||||
}
|
||||
|
||||
} // namespace crashpad
|
||||
|
@ -17,6 +17,7 @@
|
||||
|
||||
#include <elf.h>
|
||||
#include <stdint.h>
|
||||
#include <sys/types.h>
|
||||
|
||||
#include <memory>
|
||||
#include <string>
|
||||
@ -35,7 +36,77 @@ namespace crashpad {
|
||||
//!
|
||||
//! This class is capable of reading both 32-bit and 64-bit images.
|
||||
class ElfImageReader {
|
||||
private:
|
||||
class ProgramHeaderTable;
|
||||
|
||||
public:
|
||||
//! \brief This class enables reading note segments from an ELF image.
|
||||
//!
|
||||
//! Objects of this class should be created by calling
|
||||
//! ElfImageReader::Notes() or ElfImageReader::NotesWithNameAndType().
|
||||
class NoteReader {
|
||||
public:
|
||||
~NoteReader();
|
||||
|
||||
//! \brief The return value for NextNote().
|
||||
enum class Result {
|
||||
//! \brief An error occurred. The NoteReader is invalidated and message is
|
||||
//! logged.
|
||||
kError,
|
||||
|
||||
//! \brief A note was found.
|
||||
kSuccess,
|
||||
|
||||
//! \brief No more notes were found.
|
||||
kNoMoreNotes,
|
||||
};
|
||||
|
||||
//! \brief A type large enough to hold a note type, potentially across
|
||||
//! bitness.
|
||||
using NoteType = decltype(Elf64_Nhdr::n_type);
|
||||
|
||||
//! \brief Searches for the next note in the image.
|
||||
//!
|
||||
//! \param[out] name The name of the note owner, if not `nullptr`.
|
||||
//! \param[out] type A type for the note, if not `nullptr`.
|
||||
//! \param[out] desc The note descriptor.
|
||||
//! \return a #Result value. \a name, \a type, and \a desc are only valid if
|
||||
//! this method returns Result::kSuccess.
|
||||
Result NextNote(std::string* name, NoteType* type, std::string* desc);
|
||||
|
||||
// private
|
||||
NoteReader(const ElfImageReader* elf_reader_,
|
||||
const ProcessMemoryRange* range,
|
||||
const ProgramHeaderTable* phdr_table,
|
||||
ssize_t max_note_size,
|
||||
const std::string& name_filter = std::string(),
|
||||
NoteType type_filter = 0,
|
||||
bool use_filter = false);
|
||||
|
||||
private:
|
||||
// Reads the next note at the current segment address. Sets retry_ to true
|
||||
// and returns kError if use_filter_ is true and the note's name and type do
|
||||
// not match name_filter_ and type_filter_.
|
||||
template <typename T>
|
||||
Result ReadNote(std::string* name, NoteType* type, std::string* desc);
|
||||
|
||||
VMAddress current_address_;
|
||||
VMAddress segment_end_address_;
|
||||
const ElfImageReader* elf_reader_; // weak
|
||||
const ProcessMemoryRange* range_; // weak
|
||||
const ProgramHeaderTable* phdr_table_; // weak
|
||||
std::unique_ptr<ProcessMemoryRange> segment_range_;
|
||||
size_t phdr_index_;
|
||||
ssize_t max_note_size_;
|
||||
std::string name_filter_;
|
||||
NoteType type_filter_;
|
||||
bool use_filter_;
|
||||
bool is_valid_;
|
||||
bool retry_;
|
||||
|
||||
DISALLOW_COPY_AND_ASSIGN(NoteReader);
|
||||
};
|
||||
|
||||
ElfImageReader();
|
||||
~ElfImageReader();
|
||||
|
||||
@ -101,8 +172,37 @@ class ElfImageReader {
|
||||
//! \return `true` if the debug address was found.
|
||||
bool GetDebugAddress(VMAddress* debug);
|
||||
|
||||
//! \brief Return a NoteReader for this image, which scans all PT_NOTE
|
||||
//! segments in the image.
|
||||
//!
|
||||
//! The returned NoteReader is only valid for the lifetime of the
|
||||
//! ElfImageReader that created it.
|
||||
//!
|
||||
//! \param[in] max_note_size The maximum note size to read. Notes whose
|
||||
//! combined name, descriptor, and padding size are greater than
|
||||
//! \a max_note_size will be silently skipped. A \a max_note_size of -1
|
||||
//! indicates infinite maximum note size.
|
||||
//! \return A NoteReader object capable of reading notes in this image.
|
||||
std::unique_ptr<NoteReader> Notes(ssize_t max_note_size);
|
||||
|
||||
//! \brief Return a NoteReader for this image, which scans all PT_NOTE
|
||||
//! segments in the image, filtering by name and type.
|
||||
//!
|
||||
//! The returned NoteReader is only valid for the lifetime of the
|
||||
//! ElfImageReader that created it.
|
||||
//!
|
||||
//! \param[in] name The note name to match.
|
||||
//! \param[in] type The note type to match.
|
||||
//! \param[in] max_note_size The maximum note size to read. Notes whose
|
||||
//! combined name, descriptor, and padding size are greater than
|
||||
//! \a max_note_size will be silently skipped. A \a max_note_size of -1
|
||||
//! indicates infinite maximum note size.
|
||||
//! \return A NoteReader object capable of reading notes in this image.
|
||||
std::unique_ptr<NoteReader> NotesWithNameAndType(const std::string& name,
|
||||
NoteReader::NoteType type,
|
||||
ssize_t max_note_size);
|
||||
|
||||
private:
|
||||
class ProgramHeaderTable;
|
||||
template <typename PhdrType>
|
||||
class ProgramHeaderTableSpecific;
|
||||
|
||||
|
@ -54,27 +54,17 @@ void LocateExecutable(pid_t pid, bool is_64_bit, VMAddress* elf_address) {
|
||||
*elf_address = exe_mapping->range.Base();
|
||||
}
|
||||
|
||||
void ExpectElfImageWithSymbol(pid_t pid,
|
||||
VMAddress address,
|
||||
bool is_64_bit,
|
||||
std::string symbol_name,
|
||||
VMAddress expected_symbol_address) {
|
||||
ProcessMemoryLinux memory;
|
||||
ASSERT_TRUE(memory.Initialize(pid));
|
||||
ProcessMemoryRange range;
|
||||
ASSERT_TRUE(range.Initialize(&memory, is_64_bit));
|
||||
|
||||
ElfImageReader reader;
|
||||
ASSERT_TRUE(reader.Initialize(range, address));
|
||||
|
||||
void ExpectSymbol(ElfImageReader* reader,
|
||||
const std::string& symbol_name,
|
||||
VMAddress expected_symbol_address) {
|
||||
VMAddress symbol_address;
|
||||
VMSize symbol_size;
|
||||
ASSERT_TRUE(
|
||||
reader.GetDynamicSymbol(symbol_name, &symbol_address, &symbol_size));
|
||||
reader->GetDynamicSymbol(symbol_name, &symbol_address, &symbol_size));
|
||||
EXPECT_EQ(symbol_address, expected_symbol_address);
|
||||
|
||||
EXPECT_FALSE(
|
||||
reader.GetDynamicSymbol("notasymbol", &symbol_address, &symbol_size));
|
||||
reader->GetDynamicSymbol("notasymbol", &symbol_address, &symbol_size));
|
||||
}
|
||||
|
||||
void ReadThisExecutableInTarget(pid_t pid) {
|
||||
@ -87,12 +77,48 @@ void ReadThisExecutableInTarget(pid_t pid) {
|
||||
VMAddress elf_address;
|
||||
LocateExecutable(pid, am_64_bit, &elf_address);
|
||||
|
||||
ExpectElfImageWithSymbol(
|
||||
pid,
|
||||
elf_address,
|
||||
am_64_bit,
|
||||
"ElfImageReaderTestExportedSymbol",
|
||||
FromPointerCast<VMAddress>(ElfImageReaderTestExportedSymbol));
|
||||
ProcessMemoryLinux memory;
|
||||
ASSERT_TRUE(memory.Initialize(pid));
|
||||
ProcessMemoryRange range;
|
||||
ASSERT_TRUE(range.Initialize(&memory, am_64_bit));
|
||||
|
||||
ElfImageReader reader;
|
||||
ASSERT_TRUE(reader.Initialize(range, elf_address));
|
||||
|
||||
ExpectSymbol(&reader,
|
||||
"ElfImageReaderTestExportedSymbol",
|
||||
FromPointerCast<VMAddress>(ElfImageReaderTestExportedSymbol));
|
||||
|
||||
ElfImageReader::NoteReader::Result result;
|
||||
std::string note_name;
|
||||
std::string note_desc;
|
||||
ElfImageReader::NoteReader::NoteType note_type;
|
||||
|
||||
std::unique_ptr<ElfImageReader::NoteReader> notes = reader.Notes(-1);
|
||||
while ((result = notes->NextNote(¬e_name, ¬e_type, ¬e_desc)) ==
|
||||
ElfImageReader::NoteReader::Result::kSuccess) {
|
||||
}
|
||||
EXPECT_EQ(result, ElfImageReader::NoteReader::Result::kNoMoreNotes);
|
||||
|
||||
notes = reader.Notes(0);
|
||||
EXPECT_EQ(notes->NextNote(¬e_name, ¬e_type, ¬e_desc),
|
||||
ElfImageReader::NoteReader::Result::kNoMoreNotes);
|
||||
|
||||
// Find the note defined in elf_image_reader_test_note.S.
|
||||
constexpr char kCrashpadNoteName[] = "Crashpad";
|
||||
constexpr ElfImageReader::NoteReader::NoteType kCrashpadNoteType = 1;
|
||||
constexpr uint32_t kCrashpadNoteDesc = 42;
|
||||
notes = reader.NotesWithNameAndType(kCrashpadNoteName, kCrashpadNoteType, -1);
|
||||
ASSERT_EQ(notes->NextNote(¬e_name, ¬e_type, ¬e_desc),
|
||||
ElfImageReader::NoteReader::Result::kSuccess);
|
||||
EXPECT_EQ(note_name, kCrashpadNoteName);
|
||||
EXPECT_EQ(note_type, kCrashpadNoteType);
|
||||
EXPECT_EQ(note_desc.size(), sizeof(kCrashpadNoteDesc));
|
||||
EXPECT_EQ(*reinterpret_cast<decltype(kCrashpadNoteDesc)*>(¬e_desc[0]),
|
||||
kCrashpadNoteDesc);
|
||||
|
||||
EXPECT_EQ(notes->NextNote(¬e_name, ¬e_type, ¬e_desc),
|
||||
ElfImageReader::NoteReader::Result::kNoMoreNotes);
|
||||
}
|
||||
|
||||
// Assumes that libc is loaded at the same address in this process as in the
|
||||
@ -109,11 +135,15 @@ void ReadLibcInTarget(pid_t pid) {
|
||||
<< dlerror();
|
||||
VMAddress elf_address = FromPointerCast<VMAddress>(info.dli_fbase);
|
||||
|
||||
ExpectElfImageWithSymbol(pid,
|
||||
elf_address,
|
||||
am_64_bit,
|
||||
"getpid",
|
||||
FromPointerCast<VMAddress>(getpid));
|
||||
ProcessMemoryLinux memory;
|
||||
ASSERT_TRUE(memory.Initialize(pid));
|
||||
ProcessMemoryRange range;
|
||||
ASSERT_TRUE(range.Initialize(&memory, am_64_bit));
|
||||
|
||||
ElfImageReader reader;
|
||||
ASSERT_TRUE(reader.Initialize(range, elf_address));
|
||||
|
||||
ExpectSymbol(&reader, "getpid", FromPointerCast<VMAddress>(getpid));
|
||||
}
|
||||
|
||||
class ReadExecutableChildTest : public Multiprocess {
|
||||
|
30
snapshot/elf/elf_image_reader_test_note.S
Normal file
30
snapshot/elf/elf_image_reader_test_note.S
Normal file
@ -0,0 +1,30 @@
|
||||
// Copyright 2017 The Crashpad Authors. All rights reserved.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#define NOTE_ALIGN 4
|
||||
.section .note.crashpad.test,"a",%note
|
||||
.balign NOTE_ALIGN
|
||||
.type testnote, %object
|
||||
testnote:
|
||||
.long name_end - name // namesz
|
||||
.long desc_end - desc // descsz
|
||||
.long 1 // type
|
||||
name:
|
||||
.ascii "Crashpad\0"
|
||||
name_end:
|
||||
.balign NOTE_ALIGN
|
||||
desc:
|
||||
.long 42
|
||||
desc_end:
|
||||
.size testnote, .-testnote
|
@ -73,6 +73,7 @@
|
||||
'cpu_context_test.cc',
|
||||
'crashpad_info_client_options_test.cc',
|
||||
'elf/elf_image_reader_test.cc',
|
||||
'elf/elf_image_reader_test_note.S',
|
||||
'linux/debug_rendezvous_test.cc',
|
||||
'linux/exception_snapshot_linux_test.cc',
|
||||
'linux/process_reader_test.cc',
|
||||
@ -124,7 +125,7 @@
|
||||
'copies': [{
|
||||
'destination': '<(PRODUCT_DIR)',
|
||||
'files': [
|
||||
'linux/test_exported_symbols.sym',
|
||||
'elf/test_exported_symbols.sym',
|
||||
],
|
||||
}],
|
||||
'ldflags': [
|
||||
|
Loading…
x
Reference in New Issue
Block a user