LLD의 전역 변수를 Ctx로 캡슐화하고 컨텍스트 인자 전달로 치환해 전역 상태 의존을 줄인 과정과 효과, lld/Common과 LLVM 측 전역 상태의 한계, 그리고 라이브러리로서의 사용 시나리오를 정리한다.
LLD, the LLVM linker는 여러 바이너리 포맷(ELF, Mach-O, PE/COFF, WebAssembly)을 지원하는 성숙하고 빠른 링커다. 독립 실행형 프로그램으로 설계되어 코드베이스가 전역 상태에 크게 의존하는데, 이는 라이브러리 통합 관점에서 이상적이지 않다. RFC: Revisiting LLD-as-a-library design에서 요약했듯, 주요한 난점은 두 가지다.
llvm::sys::Process::Exit(val, /*NoCleanup=*/true)와 CrashRecoveryContext(내부적으로 longjmp)를 사용해 해결되었다.링커 API를 호출하는 방식은 매력적일 수 있다. 특히 LLVM을 정적으로 링크할 때 실행 파일 크기가 커지는 문제를 피하고 싶다면 더 그렇다. 하지만 나는 LLD를 별도 프로세스로 실행하는 것이 여전히 권장되는 접근이라고 본다. 그에는 여러 장점이 있다.
cl::opt와 ManagedStatic)가 격리된다.새 프로세스를 생성하면 빌드 시스템 관점의 이점이 있지만, LLD 내부의 전역 상태 사용 문제는 여전히 우려 사항이다. 이는 고급 사용 사례에서 고려해야 하는 요소다. 다음은 LLD 15 코드베이스에 존재하는 전역 변수들이다.
$ rg '^extern [^(]* \w+;' lld/ELF
lld/ELF/SyntheticSections.h
1290:extern InStruct in;
lld/ELF/Symbols.h
51:extern SmallVector<SymbolAux, 0> symAux;
lld/ELF/SymbolTable.h
87:extern std::unique_ptr<SymbolTable> symtab;
lld/ELF/InputSection.h
33:extern std::vector<Partition> partitions;
403:extern SmallVector<InputSectionBase *, 0> inputSections;
408:extern llvm::DenseSet<std::pair<const Symbol *, uint64_t>> ppc64noTocRelax;
lld/ELF/OutputSections.h
156:extern llvm::SmallVector<OutputSection *, 0> outputSections;
lld/ELF/InputFiles.h
43:extern std::unique_ptr<llvm::TarWriter> tar;
lld/ELF/Driver.h
23:extern std::unique_ptr<class LinkerDriver> driver;
lld/ELF/LinkerScript.h
366:extern std::unique_ptr<LinkerScript> script;
lld/ELF/Config.h
372:extern std::unique_ptr<Configuration> config;
406:extern std::unique_ptr<Ctx> ctx;
일부 전역 상태는 정적 멤버 변수로도 존재한다.
LLD는 전역 변수 의존을 줄이는 방향으로 변화해 왔다. 이는 라이브러리 통합 적합성을 높인다.
lld/Common에서 전역 변수 제거가 진행되었다.이러한 진전에 고무되어, 나는 ELF 포트에서 전역 변수를 제거하는 계획을 세웠다. 2022년에 섹션 초기화 병렬화를 가능하게 하는 작업의 일환으로, lld/ELF/Config.h에 struct Ctx를 도입했다. 계획은 다음과 같다.
Ctx로 이관한다.Ctx &ctx를 추가한다.lld::elf::ctx를 lld::elf::link 내부의 지역 변수로 바꾼다.Ctx로 캡슐화하기지난 2년 반 동안, 나는 전역 변수들을 Ctx 클래스로 계속 이관해 왔다. 예: 커밋.
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 590c19e6d88d..915c4d94e870 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -382,2 +382,10 @@ struct Ctx {
std::atomic<bool> hasSympart{false};
+ // A tuple of (reference, extractedFile, sym). Used by --why-extract=.
+ SmallVector<std::tuple<std::string, const InputFile *, const Symbol &>, 0>
+ whyExtractRecords;
+ // A mapping from a symbol to an InputFile referencing it backward. Used by
+ // --warn-backrefs.
+ llvm::DenseMap<const Symbol *,
+ std::pair<const InputFile *, const InputFile *>>
+ backwardReferences;
};
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 8315d43c776e..2ab698c91b01 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -1776,3 +1776,3 @@ static void handleUndefined(Symbol *sym, const char *option) {
if (!config->whyExtract.empty())
- driver->whyExtract.emplace_back(option, sym->file, *sym);
+ ctx->whyExtractRecords.emplace_back(option, sym->file, *sym);
}
@@ -1812,3 +1812,3 @@ static void handleLibcall(StringRef name) {
-void LinkerDriver::writeArchiveStats() const {
+static void writeArchiveStats() {
if (config->printArchiveStats.empty())
@@ -1834,3 +1834,3 @@ void LinkerDriver::writeArchiveStats() const {
++extracted[CachedHashStringRef(file->archiveName)];
- for (std::pair<StringRef, unsigned> f : archiveFiles) {
+ for (std::pair<StringRef, unsigned> f : driver->archiveFiles) {
unsigned &v = extracted[CachedHashString(f.first)];
2024년에는 전역 변수 관련 작업을 하지 않았다. 작업은 2024년 7월에 재개되었다. TarWriter, SymbolAux, Out, ElfSym, outputSections 등을 Ctx로 옮겼다.
struct Ctx {
Config arg;
LinkerDriver driver;
LinkerScript *script;
std::unique_ptr<TargetInfo> target;
...
};
명령줄 옵션을 보관하던 변수 config는 lld/ELF 전반에 만연해 있었다. 코드 가독성과 유지보수성을 높이기 위해 이를 ctx.arg로 이름을 바꾸었다(mold의 명명 방식).
lld/ELF 전반에 퍼져 있던 정적 저장 기간 변수도 제거했다. 예:
Ctx &ctx를 매개변수로 전달하기다음 단계는 수많은 함수와 클래스에 Ctx &ctx 매개변수를 추가하여, 점진적으로 전역 ctx에 대한 참조를 없애는 것이었다.
멤버 함수의 수정 범위를 최소화하기 위해 일부 클래스(예: SyntheticSection, OutputSection)에는 멤버 변수로 Ctx &ctx를 넣었다. 하지만 Symbol과 InputSection에는 적합하지 않았다. 단어 하나만 늘어나도 메모리 사용량이 크게 증가할 수 있기 때문이다.
// Writer.cpp
template <class ELFT> class Writer {
public:
LLVM_ELF_IMPORT_TYPES_ELFT(ELFT)
Writer(Ctx &ctx) : ctx(ctx), buffer(ctx.e.outputBuffer) {}
...
};
template <class ELFT> void elf::writeResult(Ctx &ctx) {
Writer<ELFT>(ctx).run();
}
...
bool elf::includeInSymtab(Ctx &ctx, const Symbol &b) {
if (auto *d = dyn_cast<Defined>(&b)) {
// Always include absolute symbols.
SectionBase *sec = d->section;
if (!sec)
return true;
assert(sec->isLive());
if (auto *s = dyn_cast<MergeInputSection>(sec))
return s->getSectionPiece(d->value).live;
return true;
}
return b.used || !ctx.arg.gcSections;
}
ctx 제거전역 ctx 변수의 참조 수가 0에 도달하면, 완전히 제거할 때가 된다. 나는 2024년 11월 16일에 이 변경을 적용했다.
diff --git a/lld/ELF/Config.h b/lld/ELF/Config.h
index 72feeb9d49cb..a9b7a98e5b54 100644
--- a/lld/ELF/Config.h
+++ b/lld/ELF/Config.h
@@ -539,4 +539,2 @@ struct InStruct {
std::unique_ptr<SymtabShndxSection> symTabShndx;
-
- void reset();
};
@@ -664,3 +662,2 @@ struct Ctx {
Ctx();
- void reset();
@@ -671,4 +668,2 @@ struct Ctx {
-LLVM_LIBRARY_VISIBILITY extern Ctx ctx;
-
// The first two elements of versionDefinitions represent VER_NDX_LOCAL and
diff --git a/lld/ELF/Driver.cpp b/lld/ELF/Driver.cpp
index 334dfc0e3ba1..631051c27381 100644
--- a/lld/ELF/Driver.cpp
+++ b/lld/ELF/Driver.cpp
@@ -81,4 +81,2 @@ using namespace lld::elf;
-Ctx elf::ctx;
-
static void setConfigs(Ctx &ctx, opt::InputArgList &args);
@@ -165,2 +114,3 @@ bool link(ArrayRef<const char *> args, llvm::raw_ostream &stdoutOS,
llvm::raw_ostream &stderrOS, bool exitEarly, bool disableOutput) {
+ Ctx ctx;
// This driver-specific context will be freed later by unsafeLldMain().
@@ -169,7 +119,2 @@ bool link(ArrayRef<const char *> args, llvm::raw_ostream &stdoutOS,
context->e.initialize(stdoutOS, stderrOS, exitEarly, disableOutput);
- context->e.cleanupCallback = []() {
- Ctx &ctx = elf::ctx;
- ctx.reset();
- ctx.partitions.emplace_back(ctx);
- };
context->e.logName = args::getFilenameWithoutExe(args[0]);
이 변경 이전에는, lld::elf::link를 여러 번 호출할 때 전역 ctx를 리셋하기 위해 cleanupCallback 함수가 필수였다.
전역 변수를 제거한 이후에는 cleanupCallback이 더 이상 필요 없다. 이제 생성자가 Ctx를 초기화하므로 reset 함수도 필요하지 않다.
lld/Common에서 전역 상태 제거lld/ELF 쪽에서 많은 진전이 있었지만, lld/Common에도 할 일이 많다. 진단(diagnostics), bump allocator 같은 공통 유틸리티 코드가 전역 lld::context()를 사용한다.
/// Returns the default error handler.
ErrorHandler &errorHandler();
void error(const Twine &msg);
void error(const Twine &msg, ErrorTag tag, ArrayRef<StringRef> args);
[[noreturn]] void fatal(const Twine &msg);
void log(const Twine &msg);
void message(const Twine &msg, llvm::raw_ostream &s = outs());
void warn(const Twine &msg);
uint64_t errorCount();
스레드 지역 변수(thread-local)를 쓰는 방법도 있지만, llvm/lib/Support/Parallel.cpp가 생성한 워커 스레드는 메인 스레드의 값을 상속하지 않는다. 우리는 Ctx &ctx에 직접 접근할 수 있으므로, 이를 활용한 컨텍스트 인지형(context-aware) API로 대체할 수 있다.
https://github.com/llvm/llvm-project/pull/112319에서 컨텍스트 인지형 진단 유틸리티를 도입했다.
log("xxx") => Log(ctx) << "xxx"message("xxx") => Msg(ctx) << "xxx"warn("xxx") => Warn(ctx) << "xxx"errorOrWarn(toString(f) + "xxx") => Err(ctx) << f << "xxx"error(toString(f) + "xxx") => ErrAlways(ctx) << f << "xxx"fatal("xxx") => Fatal(ctx) << "xxx"2024-11-16 시점에 lld/ELF에서는 log/warn/error/fatal을 제거했다.
하부 구현에서, lld::ErrorHandler::fatal과, 오류 한도에 도달했을 때(exitEarly가 true인 경우) lld::ErrorHandler::error는 exitLld(1)을 호출한다.
이 변화는 llvm::Twine 때문에 생기던 코드 사이즈 오버헤드를 크게 줄인다. 가장 단순한 Twine(123) 같은 경우에도, 생성된 코드는 값을 담는 스택 객체와 Twine 종류(kind)를 보유해야 한다.
lld/include/lld/Common/Memory.h의 lld::make는 전역 컨텍스트를 사용하는 할당 함수다. 소유권이 명확하다면 std::make_unique가 더 나은 선택일 수 있다.
지침:
lld::saver를 피하라.void message(const Twine &msg, llvm::raw_ostream &s = outs());처럼 lld::outs()를 사용하는 API를 피하라.lld/include/lld/Common/Memory.h의 lld::make를 피하라.ELFFileBase::init)에서의 fatal 오류([LLD][COFF] When using LLD-as-a-library, always prevent re-entrance on failures)LTO 링크 작업은 LLVM을 사용한다. LLVM의 전역 상태를 이해하는 것이 중요하다.
LLVM은 여러 LLVMContext 인스턴스를 동시에 할당·사용할 수 있지만, cl::opt와 ManagedStatic 같은 일부 전역 상태는 공유한다. 즉, 서로 다른 cl::opt 옵션 값을 가진 두 개의 LLVM 컴파일(LLD의 LTO 링크 작업 포함)을 동시에 실행할 수 없다. LLD의 전역 상태를 제거하더라도, 서로 다른 cl::opt 값을 사용해 링크하려면 새 LLD 프로세스를 생성해야 한다.
전역 상태를 벗어나려는 어떤 제안도 cl::opt 사용을 복잡하게 만들며, 현실적이지 않다.
LLD는 병렬화를 위해 llvm/Support/Parallel.h의 함수도 사용한다. 이 함수들은 getDefaultExecutor와 llvm::parallel::strategy 같은 전역 상태에 의존한다. Alexandre Ganea가 이 함수들을 컨텍스트 인지형으로 만드는 작업을 진행 중이다. (지난달 LLVM Developers' Meeting에서 직접 만나 반가웠다)
lld/Common/Driver.h의 lld::lldMain을 반복 호출할 수 있다. 드물게 fatal이 호출된 일부 상황에서는 lld::lldMain을 다시 호출하는 것이 안전하지 않을 수 있다. 두 스레드에서 lld::lldMain을 동시에 실행하는 것은 지원되지 않는다.
명령 LLD_IN_TEST=3 lld-link ...는 링크 과정을 세 번 실행하지만, 표준 출력/표준 오류로 진단을 출력하는 것은 마지막 호출뿐이다. lld/test/lit.cfg.py에는 COFF 포트가 테스트를 두 번 실행하도록 구성되어 있다([lld] Add test suite mode for running LLD main twice). 다른 포트는 이 모드가 동작하도록 추가 작업이 필요하다.