2020-04-02 11:54:05 -07:00
|
|
|
//===- Driver.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 "Driver.h"
|
|
|
|
|
#include "Config.h"
|
|
|
|
|
#include "InputFiles.h"
|
2020-05-01 16:29:06 -07:00
|
|
|
#include "OutputSection.h"
|
2020-04-02 11:54:05 -07:00
|
|
|
#include "OutputSegment.h"
|
|
|
|
|
#include "SymbolTable.h"
|
|
|
|
|
#include "Symbols.h"
|
|
|
|
|
#include "Target.h"
|
|
|
|
|
#include "Writer.h"
|
|
|
|
|
|
|
|
|
|
#include "lld/Common/Args.h"
|
|
|
|
|
#include "lld/Common/Driver.h"
|
|
|
|
|
#include "lld/Common/ErrorHandler.h"
|
|
|
|
|
#include "lld/Common/LLVM.h"
|
|
|
|
|
#include "lld/Common/Memory.h"
|
|
|
|
|
#include "lld/Common/Version.h"
|
2020-05-05 16:37:34 -07:00
|
|
|
#include "llvm/ADT/DenseSet.h"
|
2020-04-21 13:37:57 -07:00
|
|
|
#include "llvm/ADT/StringExtras.h"
|
2020-04-02 11:54:05 -07:00
|
|
|
#include "llvm/ADT/StringRef.h"
|
|
|
|
|
#include "llvm/BinaryFormat/MachO.h"
|
|
|
|
|
#include "llvm/BinaryFormat/Magic.h"
|
2020-06-05 19:54:58 -07:00
|
|
|
#include "llvm/Config/config.h"
|
2020-05-14 12:43:51 -07:00
|
|
|
#include "llvm/Object/Archive.h"
|
2020-04-02 11:54:05 -07:00
|
|
|
#include "llvm/Option/ArgList.h"
|
|
|
|
|
#include "llvm/Option/Option.h"
|
|
|
|
|
#include "llvm/Support/MemoryBuffer.h"
|
2020-04-23 20:16:49 -07:00
|
|
|
#include "llvm/Support/Path.h"
|
2020-04-02 11:54:05 -07:00
|
|
|
|
|
|
|
|
using namespace llvm;
|
|
|
|
|
using namespace llvm::MachO;
|
|
|
|
|
using namespace llvm::sys;
|
|
|
|
|
using namespace lld;
|
|
|
|
|
using namespace lld::macho;
|
|
|
|
|
|
|
|
|
|
Configuration *lld::macho::config;
|
|
|
|
|
|
|
|
|
|
// Create prefix string literals used in Options.td
|
|
|
|
|
#define PREFIX(NAME, VALUE) const char *NAME[] = VALUE;
|
|
|
|
|
#include "Options.inc"
|
|
|
|
|
#undef PREFIX
|
|
|
|
|
|
|
|
|
|
// Create table mapping all options defined in Options.td
|
|
|
|
|
static const opt::OptTable::Info optInfo[] = {
|
|
|
|
|
#define OPTION(X1, X2, ID, KIND, GROUP, ALIAS, X7, X8, X9, X10, X11, X12) \
|
|
|
|
|
{X1, X2, X10, X11, OPT_##ID, opt::Option::KIND##Class, \
|
|
|
|
|
X9, X8, OPT_##GROUP, OPT_##ALIAS, X7, X12},
|
|
|
|
|
#include "Options.inc"
|
|
|
|
|
#undef OPTION
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
MachOOptTable::MachOOptTable() : OptTable(optInfo) {}
|
|
|
|
|
|
|
|
|
|
opt::InputArgList MachOOptTable::parse(ArrayRef<const char *> argv) {
|
|
|
|
|
// Make InputArgList from string vectors.
|
|
|
|
|
unsigned missingIndex;
|
|
|
|
|
unsigned missingCount;
|
|
|
|
|
SmallVector<const char *, 256> vec(argv.data(), argv.data() + argv.size());
|
|
|
|
|
|
|
|
|
|
opt::InputArgList args = ParseArgs(vec, missingIndex, missingCount);
|
|
|
|
|
|
|
|
|
|
if (missingCount)
|
|
|
|
|
error(Twine(args.getArgString(missingIndex)) + ": missing argument");
|
|
|
|
|
|
|
|
|
|
for (opt::Arg *arg : args.filtered(OPT_UNKNOWN))
|
|
|
|
|
error("unknown argument: " + arg->getSpelling());
|
|
|
|
|
return args;
|
|
|
|
|
}
|
|
|
|
|
|
2020-06-04 17:40:41 -07:00
|
|
|
static Optional<std::string> findLibrary(StringRef name) {
|
2020-06-05 11:18:33 -07:00
|
|
|
std::string stub = (llvm::Twine("lib") + name + ".tbd").str();
|
2020-06-04 17:40:41 -07:00
|
|
|
std::string shared = (llvm::Twine("lib") + name + ".dylib").str();
|
|
|
|
|
std::string archive = (llvm::Twine("lib") + name + ".a").str();
|
|
|
|
|
llvm::SmallString<260> location;
|
2020-06-03 19:20:35 +00:00
|
|
|
|
|
|
|
|
for (StringRef dir : config->searchPaths) {
|
2020-06-05 11:18:33 -07:00
|
|
|
for (StringRef library : {stub, shared, archive}) {
|
2020-06-04 17:40:41 -07:00
|
|
|
location = dir;
|
|
|
|
|
llvm::sys::path::append(location, library);
|
|
|
|
|
if (fs::exists(location))
|
|
|
|
|
return location.str().str();
|
|
|
|
|
}
|
2020-06-03 19:20:35 +00:00
|
|
|
}
|
2020-06-05 11:18:33 -07:00
|
|
|
return {};
|
2020-04-21 13:37:57 -07:00
|
|
|
}
|
|
|
|
|
|
2020-04-02 11:54:05 -07:00
|
|
|
static TargetInfo *createTargetInfo(opt::InputArgList &args) {
|
2020-06-15 12:27:30 +02:00
|
|
|
StringRef arch = llvm::Triple(LLVM_DEFAULT_TARGET_TRIPLE).getArchName();
|
|
|
|
|
config->arch = llvm::MachO::getArchitectureFromName(
|
|
|
|
|
args.getLastArgValue(OPT_arch, arch));
|
2020-06-05 19:54:58 -07:00
|
|
|
switch (config->arch) {
|
|
|
|
|
case llvm::MachO::AK_x86_64:
|
|
|
|
|
case llvm::MachO::AK_x86_64h:
|
|
|
|
|
return createX86_64TargetInfo();
|
|
|
|
|
default:
|
2020-06-15 12:27:30 +02:00
|
|
|
fatal("missing or unsupported -arch " + args.getLastArgValue(OPT_arch));
|
2020-06-05 19:54:58 -07:00
|
|
|
}
|
2020-04-02 11:54:05 -07:00
|
|
|
}
|
|
|
|
|
|
2020-04-21 13:37:57 -07:00
|
|
|
static std::vector<StringRef> getSearchPaths(opt::InputArgList &args) {
|
|
|
|
|
std::vector<StringRef> ret{args::getStrings(args, OPT_L)};
|
|
|
|
|
if (!args.hasArg(OPT_Z)) {
|
|
|
|
|
ret.push_back("/usr/lib");
|
|
|
|
|
ret.push_back("/usr/local/lib");
|
|
|
|
|
}
|
|
|
|
|
return ret;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-02 11:54:05 -07:00
|
|
|
static void addFile(StringRef path) {
|
|
|
|
|
Optional<MemoryBufferRef> buffer = readFile(path);
|
|
|
|
|
if (!buffer)
|
|
|
|
|
return;
|
|
|
|
|
MemoryBufferRef mbref = *buffer;
|
|
|
|
|
|
|
|
|
|
switch (identify_magic(mbref.getBuffer())) {
|
2020-05-14 12:43:51 -07:00
|
|
|
case file_magic::archive: {
|
|
|
|
|
std::unique_ptr<object::Archive> file = CHECK(
|
|
|
|
|
object::Archive::create(mbref), path + ": failed to parse archive");
|
|
|
|
|
|
|
|
|
|
if (!file->isEmpty() && !file->hasSymbolTable())
|
|
|
|
|
error(path + ": archive has no index; run ranlib to add one");
|
|
|
|
|
|
|
|
|
|
inputFiles.push_back(make<ArchiveFile>(std::move(file)));
|
|
|
|
|
break;
|
|
|
|
|
}
|
2020-04-02 11:54:05 -07:00
|
|
|
case file_magic::macho_object:
|
|
|
|
|
inputFiles.push_back(make<ObjFile>(mbref));
|
|
|
|
|
break;
|
2020-04-21 13:37:57 -07:00
|
|
|
case file_magic::macho_dynamically_linked_shared_lib:
|
|
|
|
|
inputFiles.push_back(make<DylibFile>(mbref));
|
|
|
|
|
break;
|
2020-06-05 11:18:33 -07:00
|
|
|
case file_magic::tapi_file: {
|
|
|
|
|
llvm::Expected<std::unique_ptr<llvm::MachO::InterfaceFile>> result =
|
|
|
|
|
TextAPIReader::get(mbref);
|
|
|
|
|
if (!result)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
std::unique_ptr<llvm::MachO::InterfaceFile> interface{std::move(*result)};
|
|
|
|
|
inputFiles.push_back(make<DylibFile>(std::move(interface)));
|
|
|
|
|
break;
|
|
|
|
|
}
|
2020-04-02 11:54:05 -07:00
|
|
|
default:
|
|
|
|
|
error(path + ": unhandled file type");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-05 16:37:34 -07:00
|
|
|
static std::array<StringRef, 6> archNames{"arm", "arm64", "i386",
|
|
|
|
|
"x86_64", "ppc", "ppc64"};
|
|
|
|
|
static bool isArchString(StringRef s) {
|
|
|
|
|
static DenseSet<StringRef> archNamesSet(archNames.begin(), archNames.end());
|
|
|
|
|
return archNamesSet.find(s) != archNamesSet.end();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// An order file has one entry per line, in the following format:
|
|
|
|
|
//
|
|
|
|
|
// <arch>:<object file>:<symbol name>
|
|
|
|
|
//
|
|
|
|
|
// <arch> and <object file> are optional. If not specified, then that entry
|
|
|
|
|
// matches any symbol of that name.
|
|
|
|
|
//
|
|
|
|
|
// If a symbol is matched by multiple entries, then it takes the lowest-ordered
|
|
|
|
|
// entry (the one nearest to the front of the list.)
|
|
|
|
|
//
|
|
|
|
|
// The file can also have line comments that start with '#'.
|
|
|
|
|
void parseOrderFile(StringRef path) {
|
|
|
|
|
Optional<MemoryBufferRef> buffer = readFile(path);
|
|
|
|
|
if (!buffer) {
|
|
|
|
|
error("Could not read order file at " + path);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
MemoryBufferRef mbref = *buffer;
|
|
|
|
|
size_t priority = std::numeric_limits<size_t>::max();
|
|
|
|
|
for (StringRef rest : args::getLines(mbref)) {
|
|
|
|
|
StringRef arch, objectFile, symbol;
|
|
|
|
|
|
|
|
|
|
std::array<StringRef, 3> fields;
|
|
|
|
|
uint8_t fieldCount = 0;
|
|
|
|
|
while (rest != "" && fieldCount < 3) {
|
|
|
|
|
std::pair<StringRef, StringRef> p = getToken(rest, ": \t\n\v\f\r");
|
|
|
|
|
StringRef tok = p.first;
|
|
|
|
|
rest = p.second;
|
|
|
|
|
|
|
|
|
|
// Check if we have a comment
|
|
|
|
|
if (tok == "" || tok[0] == '#')
|
|
|
|
|
break;
|
|
|
|
|
|
|
|
|
|
fields[fieldCount++] = tok;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch (fieldCount) {
|
|
|
|
|
case 3:
|
|
|
|
|
arch = fields[0];
|
|
|
|
|
objectFile = fields[1];
|
|
|
|
|
symbol = fields[2];
|
|
|
|
|
break;
|
|
|
|
|
case 2:
|
|
|
|
|
(isArchString(fields[0]) ? arch : objectFile) = fields[0];
|
|
|
|
|
symbol = fields[1];
|
|
|
|
|
break;
|
|
|
|
|
case 1:
|
|
|
|
|
symbol = fields[0];
|
|
|
|
|
break;
|
|
|
|
|
case 0:
|
|
|
|
|
break;
|
|
|
|
|
default:
|
|
|
|
|
llvm_unreachable("too many fields in order file");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!arch.empty()) {
|
|
|
|
|
if (!isArchString(arch)) {
|
|
|
|
|
error("invalid arch \"" + arch + "\" in order file: expected one of " +
|
|
|
|
|
llvm::join(archNames, ", "));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO: Update when we extend support for other archs
|
|
|
|
|
if (arch != "x86_64")
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!objectFile.empty() && !objectFile.endswith(".o")) {
|
|
|
|
|
error("invalid object file name \"" + objectFile +
|
|
|
|
|
"\" in order file: should end with .o");
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!symbol.empty()) {
|
|
|
|
|
SymbolPriorityEntry &entry = config->priorities[symbol];
|
|
|
|
|
if (!objectFile.empty())
|
|
|
|
|
entry.objectFiles.insert(std::make_pair(objectFile, priority));
|
|
|
|
|
else
|
|
|
|
|
entry.anyObjectFile = std::max(entry.anyObjectFile, priority);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
--priority;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 20:16:49 -07:00
|
|
|
// We expect sub-library names of the form "libfoo", which will match a dylib
|
|
|
|
|
// with a path of .*/libfoo.dylib.
|
|
|
|
|
static bool markSubLibrary(StringRef searchName) {
|
|
|
|
|
for (InputFile *file : inputFiles) {
|
|
|
|
|
if (auto *dylibFile = dyn_cast<DylibFile>(file)) {
|
|
|
|
|
StringRef filename = path::filename(dylibFile->getName());
|
|
|
|
|
if (filename.consume_front(searchName) && filename == ".dylib") {
|
|
|
|
|
dylibFile->reexport = true;
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-12 14:02:13 -04:00
|
|
|
static void handlePlatformVersion(opt::ArgList::iterator &it,
|
|
|
|
|
const opt::ArgList::iterator &end) {
|
|
|
|
|
// -platform_version takes 3 args, which LLVM's option library doesn't
|
|
|
|
|
// support directly. So this explicitly handles that.
|
|
|
|
|
// FIXME: stash skipped args for later use.
|
|
|
|
|
for (int i = 0; i < 3; ++i) {
|
|
|
|
|
++it;
|
|
|
|
|
if (it == end || (*it)->getOption().getID() != OPT_INPUT)
|
|
|
|
|
fatal("usage: -platform_version platform min_version sdk_version");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-02 11:54:05 -07:00
|
|
|
bool macho::link(llvm::ArrayRef<const char *> argsArr, bool canExitEarly,
|
|
|
|
|
raw_ostream &stdoutOS, raw_ostream &stderrOS) {
|
|
|
|
|
lld::stdoutOS = &stdoutOS;
|
|
|
|
|
lld::stderrOS = &stderrOS;
|
|
|
|
|
|
2020-04-29 15:42:32 -07:00
|
|
|
stderrOS.enable_colors(stderrOS.has_colors());
|
|
|
|
|
// TODO: Set up error handler properly, e.g. the errorLimitExceededMsg
|
|
|
|
|
|
2020-04-02 11:54:05 -07:00
|
|
|
MachOOptTable parser;
|
|
|
|
|
opt::InputArgList args = parser.parse(argsArr.slice(1));
|
|
|
|
|
|
|
|
|
|
config = make<Configuration>();
|
|
|
|
|
symtab = make<SymbolTable>();
|
|
|
|
|
target = createTargetInfo(args);
|
|
|
|
|
|
|
|
|
|
config->entry = symtab->addUndefined(args.getLastArgValue(OPT_e, "_main"));
|
|
|
|
|
config->outputFile = args.getLastArgValue(OPT_o, "a.out");
|
2020-04-28 16:58:22 -07:00
|
|
|
config->installName =
|
|
|
|
|
args.getLastArgValue(OPT_install_name, config->outputFile);
|
2020-04-21 13:37:57 -07:00
|
|
|
config->searchPaths = getSearchPaths(args);
|
2020-04-28 16:58:22 -07:00
|
|
|
config->outputType = args.hasArg(OPT_dylib) ? MH_DYLIB : MH_EXECUTE;
|
2020-04-21 13:37:57 -07:00
|
|
|
|
|
|
|
|
if (args.hasArg(OPT_v)) {
|
|
|
|
|
message(getLLDVersion());
|
|
|
|
|
std::vector<StringRef> &searchPaths = config->searchPaths;
|
|
|
|
|
message("Library search paths:\n" +
|
|
|
|
|
llvm::join(searchPaths.begin(), searchPaths.end(), "\n"));
|
|
|
|
|
freeArena();
|
|
|
|
|
return !errorCount();
|
|
|
|
|
}
|
2020-04-02 11:54:05 -07:00
|
|
|
|
2020-05-12 14:02:13 -04:00
|
|
|
for (opt::ArgList::iterator it = args.begin(), end = args.end(); it != end;
|
|
|
|
|
++it) {
|
|
|
|
|
const opt::Arg *arg = *it;
|
2020-04-02 11:54:05 -07:00
|
|
|
switch (arg->getOption().getID()) {
|
|
|
|
|
case OPT_INPUT:
|
|
|
|
|
addFile(arg->getValue());
|
|
|
|
|
break;
|
2020-06-03 19:20:35 +00:00
|
|
|
case OPT_l: {
|
|
|
|
|
StringRef name = arg->getValue();
|
2020-06-04 17:40:41 -07:00
|
|
|
if (Optional<std::string> path = findLibrary(name)) {
|
2020-06-03 19:20:35 +00:00
|
|
|
addFile(*path);
|
2020-06-04 17:40:41 -07:00
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
error("library not found for -l" + name);
|
2020-04-21 13:37:57 -07:00
|
|
|
break;
|
2020-06-03 19:20:35 +00:00
|
|
|
}
|
2020-05-12 14:02:13 -04:00
|
|
|
case OPT_platform_version: {
|
|
|
|
|
handlePlatformVersion(it, end); // Can advance "it".
|
|
|
|
|
break;
|
|
|
|
|
}
|
2020-04-02 11:54:05 -07:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-23 20:16:49 -07:00
|
|
|
// Now that all dylibs have been loaded, search for those that should be
|
|
|
|
|
// re-exported.
|
|
|
|
|
for (opt::Arg *arg : args.filtered(OPT_sub_library)) {
|
|
|
|
|
config->hasReexports = true;
|
|
|
|
|
StringRef searchName = arg->getValue();
|
|
|
|
|
if (!markSubLibrary(searchName))
|
|
|
|
|
error("-sub_library " + searchName + " does not match a supplied dylib");
|
|
|
|
|
}
|
|
|
|
|
|
2020-05-05 16:37:34 -07:00
|
|
|
StringRef orderFile = args.getLastArgValue(OPT_order_file);
|
|
|
|
|
if (!orderFile.empty())
|
|
|
|
|
parseOrderFile(orderFile);
|
|
|
|
|
|
[lld-macho] Support calls to functions in dylibs
Summary:
This diff implements lazy symbol binding -- very similar to the PLT
mechanism in ELF.
ELF's .plt section is broken up into two sections in Mach-O:
StubsSection and StubHelperSection. Calls to functions in dylibs will
end up calling into StubsSection, which contains indirect jumps to
addresses stored in the LazyPointerSection (the counterpart to ELF's
.plt.got).
Initially, the LazyPointerSection contains addresses that point into one
of the entry points in the middle of the StubHelperSection. The code in
StubHelperSection will push on the stack an offset into the
LazyBindingSection. The push is followed by a jump to the beginning of
the StubHelperSection (similar to PLT0), which then calls into
dyld_stub_binder. dyld_stub_binder is a non-lazily bound symbol, so this
call looks it up in the GOT.
The stub binder will look up the bind opcodes in the LazyBindingSection
at the given offset. The bind opcodes will tell the binder to update the
address in the LazyPointerSection to point to the symbol, so that
subsequent calls don't have to redo the symbol resolution. The binder
will then jump to the resolved symbol.
Depends on D78269.
Reviewers: ruiu, pcc, MaskRay, smeenai, alexshap, gkm, Ktwu, christylee
Subscribers: llvm-commits
Tags: #llvm
Differential Revision: https://reviews.llvm.org/D78270
2020-05-05 17:38:10 -07:00
|
|
|
// dyld requires us to load libSystem. Since we may run tests on non-OSX
|
|
|
|
|
// systems which do not have libSystem, we mock it out here.
|
|
|
|
|
// TODO: Replace this with a stub tbd file once we have TAPI support.
|
|
|
|
|
if (StringRef(getenv("LLD_IN_TEST")) == "1" &&
|
|
|
|
|
config->outputType == MH_EXECUTE) {
|
|
|
|
|
inputFiles.push_back(DylibFile::createLibSystemMock());
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-28 16:58:22 -07:00
|
|
|
if (config->outputType == MH_EXECUTE && !isa<Defined>(config->entry)) {
|
2020-04-02 11:54:05 -07:00
|
|
|
error("undefined symbol: " + config->entry->getName());
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-21 13:37:57 -07:00
|
|
|
createSyntheticSections();
|
|
|
|
|
|
2020-04-02 11:54:05 -07:00
|
|
|
// Initialize InputSections.
|
[lld-macho][re-land] Support .subsections_via_symbols
Summary:
This diff restores and builds upon @pcc and @ruiu's initial work on
subsections.
The .subsections_via_symbols directive indicates we can split each
section along symbol boundaries, unless those symbols have been marked
with `.alt_entry`.
We exercise this functionality in our tests by using order files that
rearrange those symbols.
Depends on D79668.
Reviewers: ruiu, pcc, MaskRay, smeenai, alexshap, gkm, Ktwu, christylee
Reviewed By: smeenai
Subscribers: thakis, llvm-commits, pcc, ruiu
Tags: #llvm
Differential Revision: https://reviews.llvm.org/D79926
2020-05-19 08:46:07 -07:00
|
|
|
for (InputFile *file : inputFiles) {
|
|
|
|
|
for (SubsectionMap &map : file->subsections) {
|
|
|
|
|
for (auto &p : map) {
|
|
|
|
|
InputSection *isec = p.second;
|
|
|
|
|
inputSections.push_back(isec);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-02 11:54:05 -07:00
|
|
|
|
|
|
|
|
// Write to an output file.
|
|
|
|
|
writeResult();
|
|
|
|
|
|
|
|
|
|
if (canExitEarly)
|
|
|
|
|
exitLld(errorCount() ? 1 : 0);
|
|
|
|
|
|
|
|
|
|
freeArena();
|
|
|
|
|
return !errorCount();
|
|
|
|
|
}
|