2216ee4909
generation code from DWARFLinker. It adds command line option: --build-accelerator [none,DWARF] Build accelerator tables(default: none) =none - Do not build accelerators =DWARF - Build accelerator tables according to the resulting DWARF version DWARFv4: .debug_pubnames and .debug_pubtypes DWARFv5: .debug_names Differential Revision: https://reviews.llvm.org/D139638
542 lines
18 KiB
C++
542 lines
18 KiB
C++
//=== llvm-dwarfutil.cpp --------------------------------------------------===//
|
|
//
|
|
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
|
|
// See https://llvm.org/LICENSE.txt for license information.
|
|
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
|
|
//
|
|
//===----------------------------------------------------------------------===//
|
|
|
|
#include "DebugInfoLinker.h"
|
|
#include "Error.h"
|
|
#include "Options.h"
|
|
#include "llvm/DebugInfo/DWARF/DWARFContext.h"
|
|
#include "llvm/DebugInfo/DWARF/DWARFVerifier.h"
|
|
#include "llvm/MC/MCTargetOptionsCommandFlags.h"
|
|
#include "llvm/ObjCopy/CommonConfig.h"
|
|
#include "llvm/ObjCopy/ConfigManager.h"
|
|
#include "llvm/ObjCopy/ObjCopy.h"
|
|
#include "llvm/Option/Arg.h"
|
|
#include "llvm/Option/ArgList.h"
|
|
#include "llvm/Option/Option.h"
|
|
#include "llvm/Support/CRC.h"
|
|
#include "llvm/Support/CommandLine.h"
|
|
#include "llvm/Support/FileUtilities.h"
|
|
#include "llvm/Support/InitLLVM.h"
|
|
#include "llvm/Support/PrettyStackTrace.h"
|
|
#include "llvm/Support/Process.h"
|
|
#include "llvm/Support/Signals.h"
|
|
#include "llvm/Support/TargetSelect.h"
|
|
|
|
using namespace llvm;
|
|
using namespace object;
|
|
|
|
namespace {
|
|
enum ID {
|
|
OPT_INVALID = 0, // This is not an option ID.
|
|
#define OPTION(PREFIX, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM, \
|
|
HELPTEXT, METAVAR, VALUES) \
|
|
OPT_##ID,
|
|
#include "Options.inc"
|
|
#undef OPTION
|
|
};
|
|
|
|
#define PREFIX(NAME, VALUE) \
|
|
static constexpr StringLiteral NAME##_init[] = VALUE; \
|
|
static constexpr ArrayRef<StringLiteral> NAME(NAME##_init, \
|
|
std::size(NAME##_init) - 1);
|
|
#include "Options.inc"
|
|
#undef PREFIX
|
|
|
|
static constexpr opt::OptTable::Info InfoTable[] = {
|
|
#define OPTION(PREFIX, NAME, ID, KIND, GROUP, ALIAS, ALIASARGS, FLAGS, PARAM, \
|
|
HELPTEXT, METAVAR, VALUES) \
|
|
{ \
|
|
PREFIX, NAME, HELPTEXT, \
|
|
METAVAR, OPT_##ID, opt::Option::KIND##Class, \
|
|
PARAM, FLAGS, OPT_##GROUP, \
|
|
OPT_##ALIAS, ALIASARGS, VALUES},
|
|
#include "Options.inc"
|
|
#undef OPTION
|
|
};
|
|
|
|
class DwarfutilOptTable : public opt::GenericOptTable {
|
|
public:
|
|
DwarfutilOptTable() : opt::GenericOptTable(InfoTable) {}
|
|
};
|
|
} // namespace
|
|
|
|
namespace llvm {
|
|
namespace dwarfutil {
|
|
|
|
std::string ToolName;
|
|
|
|
static mc::RegisterMCTargetOptionsFlags MOF;
|
|
|
|
static Error validateAndSetOptions(opt::InputArgList &Args, Options &Options) {
|
|
auto UnknownArgs = Args.filtered(OPT_UNKNOWN);
|
|
if (!UnknownArgs.empty())
|
|
return createStringError(
|
|
std::errc::invalid_argument,
|
|
formatv("unknown option: {0}", (*UnknownArgs.begin())->getSpelling())
|
|
.str()
|
|
.c_str());
|
|
|
|
std::vector<std::string> InputFiles = Args.getAllArgValues(OPT_INPUT);
|
|
if (InputFiles.size() != 2)
|
|
return createStringError(
|
|
std::errc::invalid_argument,
|
|
formatv("exactly two positional arguments expected, {0} provided",
|
|
InputFiles.size())
|
|
.str()
|
|
.c_str());
|
|
|
|
Options.InputFileName = InputFiles[0];
|
|
Options.OutputFileName = InputFiles[1];
|
|
|
|
Options.BuildSeparateDebugFile =
|
|
Args.hasFlag(OPT_separate_debug_file, OPT_no_separate_debug_file, false);
|
|
Options.DoODRDeduplication =
|
|
Args.hasFlag(OPT_odr_deduplication, OPT_no_odr_deduplication, true);
|
|
Options.DoGarbageCollection =
|
|
Args.hasFlag(OPT_garbage_collection, OPT_no_garbage_collection, true);
|
|
Options.Verbose = Args.hasArg(OPT_verbose);
|
|
Options.Verify = Args.hasArg(OPT_verify);
|
|
|
|
if (opt::Arg *NumThreads = Args.getLastArg(OPT_threads))
|
|
Options.NumThreads = atoi(NumThreads->getValue());
|
|
else
|
|
Options.NumThreads = 0; // Use all available hardware threads
|
|
|
|
if (opt::Arg *Tombstone = Args.getLastArg(OPT_tombstone)) {
|
|
StringRef S = Tombstone->getValue();
|
|
if (S == "bfd")
|
|
Options.Tombstone = TombstoneKind::BFD;
|
|
else if (S == "maxpc")
|
|
Options.Tombstone = TombstoneKind::MaxPC;
|
|
else if (S == "universal")
|
|
Options.Tombstone = TombstoneKind::Universal;
|
|
else if (S == "exec")
|
|
Options.Tombstone = TombstoneKind::Exec;
|
|
else
|
|
return createStringError(
|
|
std::errc::invalid_argument,
|
|
formatv("unknown tombstone value: '{0}'", S).str().c_str());
|
|
}
|
|
|
|
if (opt::Arg *BuildAccelerator = Args.getLastArg(OPT_build_accelerator)) {
|
|
StringRef S = BuildAccelerator->getValue();
|
|
|
|
if (S == "none")
|
|
Options.AccelTableKind = DwarfUtilAccelKind::None;
|
|
else if (S == "DWARF")
|
|
Options.AccelTableKind = DwarfUtilAccelKind::DWARF;
|
|
else
|
|
return createStringError(
|
|
std::errc::invalid_argument,
|
|
formatv("unknown build-accelerator value: '{0}'", S).str().c_str());
|
|
}
|
|
|
|
if (Options.Verbose) {
|
|
if (Options.NumThreads != 1 && Args.hasArg(OPT_threads))
|
|
warning("--num-threads set to 1 because verbose mode is specified");
|
|
|
|
Options.NumThreads = 1;
|
|
}
|
|
|
|
if (Options.DoODRDeduplication && Args.hasArg(OPT_odr_deduplication) &&
|
|
!Options.DoGarbageCollection)
|
|
return createStringError(
|
|
std::errc::invalid_argument,
|
|
"cannot use --odr-deduplication without --garbage-collection");
|
|
|
|
if (Options.BuildSeparateDebugFile && Options.OutputFileName == "-")
|
|
return createStringError(
|
|
std::errc::invalid_argument,
|
|
"unable to write to stdout when --separate-debug-file specified");
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
static Error setConfigToAddNewDebugSections(objcopy::ConfigManager &Config,
|
|
ObjectFile &ObjFile) {
|
|
// Add new debug sections.
|
|
for (SectionRef Sec : ObjFile.sections()) {
|
|
Expected<StringRef> SecName = Sec.getName();
|
|
if (!SecName)
|
|
return SecName.takeError();
|
|
|
|
if (isDebugSection(*SecName)) {
|
|
Expected<StringRef> SecData = Sec.getContents();
|
|
if (!SecData)
|
|
return SecData.takeError();
|
|
|
|
Config.Common.AddSection.emplace_back(objcopy::NewSectionInfo(
|
|
*SecName, MemoryBuffer::getMemBuffer(*SecData, *SecName, false)));
|
|
}
|
|
}
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
static Error verifyOutput(const Options &Opts) {
|
|
if (Opts.OutputFileName == "-") {
|
|
warning("verification skipped because writing to stdout");
|
|
return Error::success();
|
|
}
|
|
|
|
std::string FileName = Opts.BuildSeparateDebugFile
|
|
? Opts.getSeparateDebugFileName()
|
|
: Opts.OutputFileName;
|
|
Expected<OwningBinary<Binary>> BinOrErr = createBinary(FileName);
|
|
if (!BinOrErr)
|
|
return createFileError(FileName, BinOrErr.takeError());
|
|
|
|
if (BinOrErr->getBinary()->isObject()) {
|
|
if (ObjectFile *Obj = static_cast<ObjectFile *>(BinOrErr->getBinary())) {
|
|
verbose("Verifying DWARF...", Opts.Verbose);
|
|
std::unique_ptr<DWARFContext> DICtx = DWARFContext::create(*Obj);
|
|
DIDumpOptions DumpOpts;
|
|
if (!DICtx->verify(Opts.Verbose ? outs() : nulls(),
|
|
DumpOpts.noImplicitRecursion()))
|
|
return createFileError(FileName,
|
|
createError("output verification failed"));
|
|
|
|
return Error::success();
|
|
}
|
|
}
|
|
|
|
// The file "FileName" was created by this utility in the previous steps
|
|
// (i.e. it is already known that it should pass the isObject check).
|
|
// If the createBinary() function does not return an error, the isObject
|
|
// check should also be successful.
|
|
llvm_unreachable(
|
|
formatv("tool unexpectedly did not emit a supported object file: '{0}'",
|
|
FileName)
|
|
.str()
|
|
.c_str());
|
|
}
|
|
|
|
class raw_crc_ostream : public raw_ostream {
|
|
public:
|
|
explicit raw_crc_ostream(raw_ostream &O) : OS(O) { SetUnbuffered(); }
|
|
|
|
void reserveExtraSpace(uint64_t ExtraSize) override {
|
|
OS.reserveExtraSpace(ExtraSize);
|
|
}
|
|
|
|
uint32_t getCRC32() { return CRC32; }
|
|
|
|
protected:
|
|
raw_ostream &OS;
|
|
uint32_t CRC32 = 0;
|
|
|
|
/// See raw_ostream::write_impl.
|
|
void write_impl(const char *Ptr, size_t Size) override {
|
|
CRC32 = crc32(
|
|
CRC32, ArrayRef<uint8_t>(reinterpret_cast<const uint8_t *>(Ptr), Size));
|
|
OS.write(Ptr, Size);
|
|
}
|
|
|
|
/// Return the current position within the stream, not counting the bytes
|
|
/// currently in the buffer.
|
|
uint64_t current_pos() const override { return OS.tell(); }
|
|
};
|
|
|
|
static Expected<uint32_t> saveSeparateDebugInfo(const Options &Opts,
|
|
ObjectFile &InputFile) {
|
|
objcopy::ConfigManager Config;
|
|
std::string OutputFilename = Opts.getSeparateDebugFileName();
|
|
Config.Common.InputFilename = Opts.InputFileName;
|
|
Config.Common.OutputFilename = OutputFilename;
|
|
Config.Common.OnlyKeepDebug = true;
|
|
uint32_t WrittenFileCRC32 = 0;
|
|
|
|
if (Error Err = writeToOutput(
|
|
Config.Common.OutputFilename, [&](raw_ostream &OutFile) -> Error {
|
|
raw_crc_ostream CRCBuffer(OutFile);
|
|
if (Error Err = objcopy::executeObjcopyOnBinary(Config, InputFile,
|
|
CRCBuffer))
|
|
return Err;
|
|
|
|
WrittenFileCRC32 = CRCBuffer.getCRC32();
|
|
return Error::success();
|
|
}))
|
|
return std::move(Err);
|
|
|
|
return WrittenFileCRC32;
|
|
}
|
|
|
|
static Error saveNonDebugInfo(const Options &Opts, ObjectFile &InputFile,
|
|
uint32_t GnuDebugLinkCRC32) {
|
|
objcopy::ConfigManager Config;
|
|
Config.Common.InputFilename = Opts.InputFileName;
|
|
Config.Common.OutputFilename = Opts.OutputFileName;
|
|
Config.Common.StripDebug = true;
|
|
std::string SeparateDebugFileName = Opts.getSeparateDebugFileName();
|
|
Config.Common.AddGnuDebugLink = sys::path::filename(SeparateDebugFileName);
|
|
Config.Common.GnuDebugLinkCRC32 = GnuDebugLinkCRC32;
|
|
|
|
if (Error Err = writeToOutput(
|
|
Config.Common.OutputFilename, [&](raw_ostream &OutFile) -> Error {
|
|
if (Error Err =
|
|
objcopy::executeObjcopyOnBinary(Config, InputFile, OutFile))
|
|
return Err;
|
|
|
|
return Error::success();
|
|
}))
|
|
return Err;
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
static Error splitDebugIntoSeparateFile(const Options &Opts,
|
|
ObjectFile &InputFile) {
|
|
Expected<uint32_t> SeparateDebugFileCRC32OrErr =
|
|
saveSeparateDebugInfo(Opts, InputFile);
|
|
if (!SeparateDebugFileCRC32OrErr)
|
|
return SeparateDebugFileCRC32OrErr.takeError();
|
|
|
|
if (Error Err =
|
|
saveNonDebugInfo(Opts, InputFile, *SeparateDebugFileCRC32OrErr))
|
|
return Err;
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
using DebugInfoBits = SmallString<10000>;
|
|
|
|
static Error addSectionsFromLinkedData(objcopy::ConfigManager &Config,
|
|
ObjectFile &InputFile,
|
|
DebugInfoBits &LinkedDebugInfoBits) {
|
|
if (isa<ELFObjectFile<ELF32LE>>(&InputFile)) {
|
|
Expected<ELFObjectFile<ELF32LE>> MemFile = ELFObjectFile<ELF32LE>::create(
|
|
MemoryBufferRef(LinkedDebugInfoBits, ""));
|
|
if (!MemFile)
|
|
return MemFile.takeError();
|
|
|
|
if (Error Err = setConfigToAddNewDebugSections(Config, *MemFile))
|
|
return Err;
|
|
} else if (isa<ELFObjectFile<ELF64LE>>(&InputFile)) {
|
|
Expected<ELFObjectFile<ELF64LE>> MemFile = ELFObjectFile<ELF64LE>::create(
|
|
MemoryBufferRef(LinkedDebugInfoBits, ""));
|
|
if (!MemFile)
|
|
return MemFile.takeError();
|
|
|
|
if (Error Err = setConfigToAddNewDebugSections(Config, *MemFile))
|
|
return Err;
|
|
} else if (isa<ELFObjectFile<ELF32BE>>(&InputFile)) {
|
|
Expected<ELFObjectFile<ELF32BE>> MemFile = ELFObjectFile<ELF32BE>::create(
|
|
MemoryBufferRef(LinkedDebugInfoBits, ""));
|
|
if (!MemFile)
|
|
return MemFile.takeError();
|
|
|
|
if (Error Err = setConfigToAddNewDebugSections(Config, *MemFile))
|
|
return Err;
|
|
} else if (isa<ELFObjectFile<ELF64BE>>(&InputFile)) {
|
|
Expected<ELFObjectFile<ELF64BE>> MemFile = ELFObjectFile<ELF64BE>::create(
|
|
MemoryBufferRef(LinkedDebugInfoBits, ""));
|
|
if (!MemFile)
|
|
return MemFile.takeError();
|
|
|
|
if (Error Err = setConfigToAddNewDebugSections(Config, *MemFile))
|
|
return Err;
|
|
} else
|
|
return createStringError(std::errc::invalid_argument,
|
|
"unsupported file format");
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
static Expected<uint32_t>
|
|
saveSeparateLinkedDebugInfo(const Options &Opts, ObjectFile &InputFile,
|
|
DebugInfoBits LinkedDebugInfoBits) {
|
|
objcopy::ConfigManager Config;
|
|
std::string OutputFilename = Opts.getSeparateDebugFileName();
|
|
Config.Common.InputFilename = Opts.InputFileName;
|
|
Config.Common.OutputFilename = OutputFilename;
|
|
Config.Common.StripDebug = true;
|
|
Config.Common.OnlyKeepDebug = true;
|
|
uint32_t WrittenFileCRC32 = 0;
|
|
|
|
if (Error Err =
|
|
addSectionsFromLinkedData(Config, InputFile, LinkedDebugInfoBits))
|
|
return std::move(Err);
|
|
|
|
if (Error Err = writeToOutput(
|
|
Config.Common.OutputFilename, [&](raw_ostream &OutFile) -> Error {
|
|
raw_crc_ostream CRCBuffer(OutFile);
|
|
|
|
if (Error Err = objcopy::executeObjcopyOnBinary(Config, InputFile,
|
|
CRCBuffer))
|
|
return Err;
|
|
|
|
WrittenFileCRC32 = CRCBuffer.getCRC32();
|
|
return Error::success();
|
|
}))
|
|
return std::move(Err);
|
|
|
|
return WrittenFileCRC32;
|
|
}
|
|
|
|
static Error saveSingleLinkedDebugInfo(const Options &Opts,
|
|
ObjectFile &InputFile,
|
|
DebugInfoBits LinkedDebugInfoBits) {
|
|
objcopy::ConfigManager Config;
|
|
|
|
Config.Common.InputFilename = Opts.InputFileName;
|
|
Config.Common.OutputFilename = Opts.OutputFileName;
|
|
Config.Common.StripDebug = true;
|
|
if (Error Err =
|
|
addSectionsFromLinkedData(Config, InputFile, LinkedDebugInfoBits))
|
|
return Err;
|
|
|
|
if (Error Err = writeToOutput(
|
|
Config.Common.OutputFilename, [&](raw_ostream &OutFile) -> Error {
|
|
return objcopy::executeObjcopyOnBinary(Config, InputFile, OutFile);
|
|
}))
|
|
return Err;
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
static Error saveLinkedDebugInfo(const Options &Opts, ObjectFile &InputFile,
|
|
DebugInfoBits LinkedDebugInfoBits) {
|
|
if (Opts.BuildSeparateDebugFile) {
|
|
Expected<uint32_t> SeparateDebugFileCRC32OrErr =
|
|
saveSeparateLinkedDebugInfo(Opts, InputFile,
|
|
std::move(LinkedDebugInfoBits));
|
|
if (!SeparateDebugFileCRC32OrErr)
|
|
return SeparateDebugFileCRC32OrErr.takeError();
|
|
|
|
if (Error Err =
|
|
saveNonDebugInfo(Opts, InputFile, *SeparateDebugFileCRC32OrErr))
|
|
return Err;
|
|
} else {
|
|
if (Error Err = saveSingleLinkedDebugInfo(Opts, InputFile,
|
|
std::move(LinkedDebugInfoBits)))
|
|
return Err;
|
|
}
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
static Error saveCopyOfFile(const Options &Opts, ObjectFile &InputFile) {
|
|
objcopy::ConfigManager Config;
|
|
|
|
Config.Common.InputFilename = Opts.InputFileName;
|
|
Config.Common.OutputFilename = Opts.OutputFileName;
|
|
|
|
if (Error Err = writeToOutput(
|
|
Config.Common.OutputFilename, [&](raw_ostream &OutFile) -> Error {
|
|
return objcopy::executeObjcopyOnBinary(Config, InputFile, OutFile);
|
|
}))
|
|
return Err;
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
static Error applyCLOptions(const struct Options &Opts, ObjectFile &InputFile) {
|
|
if (Opts.DoGarbageCollection ||
|
|
Opts.AccelTableKind != DwarfUtilAccelKind::None) {
|
|
verbose("Do debug info linking...", Opts.Verbose);
|
|
|
|
DebugInfoBits LinkedDebugInfo;
|
|
raw_svector_ostream OutStream(LinkedDebugInfo);
|
|
|
|
if (Error Err = linkDebugInfo(InputFile, Opts, OutStream))
|
|
return Err;
|
|
|
|
if (Error Err =
|
|
saveLinkedDebugInfo(Opts, InputFile, std::move(LinkedDebugInfo)))
|
|
return Err;
|
|
|
|
return Error::success();
|
|
} else if (Opts.BuildSeparateDebugFile) {
|
|
if (Error Err = splitDebugIntoSeparateFile(Opts, InputFile))
|
|
return Err;
|
|
} else {
|
|
if (Error Err = saveCopyOfFile(Opts, InputFile))
|
|
return Err;
|
|
}
|
|
|
|
return Error::success();
|
|
}
|
|
|
|
} // end of namespace dwarfutil
|
|
} // end of namespace llvm
|
|
|
|
int main(int Argc, char const *Argv[]) {
|
|
using namespace dwarfutil;
|
|
|
|
InitLLVM X(Argc, Argv);
|
|
ToolName = Argv[0];
|
|
|
|
// Parse arguments.
|
|
DwarfutilOptTable T;
|
|
unsigned MAI;
|
|
unsigned MAC;
|
|
ArrayRef<const char *> ArgsArr = ArrayRef(Argv + 1, Argc - 1);
|
|
opt::InputArgList Args = T.ParseArgs(ArgsArr, MAI, MAC);
|
|
|
|
if (Args.hasArg(OPT_help) || Args.size() == 0) {
|
|
T.printHelp(
|
|
outs(), (ToolName + " [options] <input file> <output file>").c_str(),
|
|
"llvm-dwarfutil is a tool to copy and manipulate debug info", false);
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
if (Args.hasArg(OPT_version)) {
|
|
cl::PrintVersionMessage();
|
|
return EXIT_SUCCESS;
|
|
}
|
|
|
|
Options Opts;
|
|
if (Error Err = validateAndSetOptions(Args, Opts))
|
|
error(std::move(Err), dwarfutil::ToolName);
|
|
|
|
InitializeAllTargets();
|
|
InitializeAllTargetMCs();
|
|
InitializeAllTargetInfos();
|
|
InitializeAllAsmPrinters();
|
|
|
|
ErrorOr<std::unique_ptr<MemoryBuffer>> BuffOrErr =
|
|
MemoryBuffer::getFileOrSTDIN(Opts.InputFileName);
|
|
if (BuffOrErr.getError())
|
|
error(createFileError(Opts.InputFileName, BuffOrErr.getError()));
|
|
|
|
Expected<std::unique_ptr<Binary>> BinOrErr =
|
|
object::createBinary(**BuffOrErr);
|
|
if (!BinOrErr)
|
|
error(createFileError(Opts.InputFileName, BinOrErr.takeError()));
|
|
|
|
Expected<FilePermissionsApplier> PermsApplierOrErr =
|
|
FilePermissionsApplier::create(Opts.InputFileName);
|
|
if (!PermsApplierOrErr)
|
|
error(createFileError(Opts.InputFileName, PermsApplierOrErr.takeError()));
|
|
|
|
if (!(*BinOrErr)->isObject())
|
|
error(createFileError(Opts.InputFileName,
|
|
createError("unsupported input file")));
|
|
|
|
if (Error Err =
|
|
applyCLOptions(Opts, *static_cast<ObjectFile *>((*BinOrErr).get())))
|
|
error(createFileError(Opts.InputFileName, std::move(Err)));
|
|
|
|
BinOrErr->reset();
|
|
BuffOrErr->reset();
|
|
|
|
if (Error Err = PermsApplierOrErr->apply(Opts.OutputFileName))
|
|
error(std::move(Err));
|
|
|
|
if (Opts.BuildSeparateDebugFile)
|
|
if (Error Err = PermsApplierOrErr->apply(Opts.getSeparateDebugFileName()))
|
|
error(std::move(Err));
|
|
|
|
if (Opts.Verify) {
|
|
if (Error Err = verifyOutput(Opts))
|
|
error(std::move(Err));
|
|
}
|
|
|
|
return EXIT_SUCCESS;
|
|
}
|