こういう文字列の末尾が共通となるようなRustのコードがある。
fn main() { print!("foobar\n"); print!("bar\n"); }
こうなってしまう。
$ rustc hoge.rs $ readelf -p .rodata hoge| grep bar [ 0] invalid args/rustc/82e1608dfa6e0b5569232559e3d385fea5a93112/library/core/src/fmt/mod.rsfoobar\n bar\n
これは当然ふたつめのbar\n
が不要なので消えていてほしい。
なぜこうなってしまうのか。
まず文字列リテラルがnull終端でないのが気になる。 少しコードを変更してみよう。
fn main() { print!("dummy\n\0"); print!("foobar\n\0"); print!("bar\n\0"); }
出力はちょっとましになったが、相変わらず期待した結果にならない。
$ readelf -p .rodata hoge| grep bar [ 5e] foobar\n [ 66] bar\n
リンカの問題だろうか。lldを試してみよう。
$ rustc -C link-arg=-fuse-ld=lld hoge.rs $ readelf -p .comment hoge String dump of section '.comment': [ 0] GCC: (Ubuntu 11.4.0-1ubuntu1~22.04) 11.4.0 [ 2b] rustc version 1.75.0 (82e1608df 2023-12-21) [ 58] Linker: Ubuntu LLD 14.0.0 $ readelf -p .rodata hoge| grep bar [ 70] foobar\n [ 98] thread 'extern "bar\n
なんだか変な並びになってわかりにくい。 ちょっと元のコードをいじってわかりやすくする。 あまりよいコードの変更ではないが、とりあえず。
e$ cat hoge.rs fn main() { print!("\0foobar\n\0"); print!("\0bar\n\0"); } $ rustc -C link-arg=-fuse-ld=lld hoge.rs $ readelf -p .rodata hoge| grep bar [ 58] foobar\n [ 61] bar\n
実はLLDはoptimizeフラグを明示的にいじってやらないとtail-optimized stringsの最適化をやってくれない。
- lld/OutputSections.cpp
static MergeSyntheticSection *createMergeSynthetic(StringRef name, uint32_t type, uint64_t flags, uint32_t addralign) { if ((flags & SHF_STRINGS) && config->optimize >= 2) return make<MergeTailSection>(name, type, flags, addralign); return make<MergeNoTailSection>(name, type, flags, addralign); }
これを引数に与えてみよう。
$ rustc -C link-arg=-fuse-ld=lld -C link-arg=-O2 hoge.rs $ readelf -p .rodata hoge| grep bar [ 58] foobar\n [ 61] bar\n
残念、効果がないみたいだ。
さて、LLDのコードをみてみるとSHF_STRINGS
というフラグの有無をみている。
こいつはそのセクションに含まれているものが文字列であることを示す。
また、実はmergeが行われるためにはSHF_MERGE
というフラグでmergeableであることを示していなければならない。これもついでにみていこう。
これらはリンク前のセクションについている必要がある。 そのためrustcでリンク前のオブジェクトファイルを生成して、文字列リテラルが含まれているセクションについて確認していく。
$ rustc --emit obj hoge.rs $ readelf -WS hoge.o There are 46 section headers, starting at offset 0x1260: Section Headers: [Nr] Name Type Address Off Size ES Flg Lk Inf Al (省略) [25] .rodata..L__unnamed_2 PROGBITS 0000000000000000 000278 000000 00 A 0 0 8 [26] .rodata..L__unnamed_7 PROGBITS 0000000000000000 000278 00000c 00 A 0 0 1 [29] .rodata..L__unnamed_8 PROGBITS 0000000000000000 000298 00004b 00 A 0 0 1 [32] .rodata..L__unnamed_9 PROGBITS 0000000000000000 000300 000009 00 A 0 0 1 [35] .rodata..L__unnamed_10 PROGBITS 0000000000000000 000320 000006 00 A 0 0 1 $ readelf -p .rodata..L__unnamed_9 hoge.o String dump of section '.rodata..L__unnamed_9': [ 1] foobar\n $ readelf -p .rodata..L__unnamed_10 hoge.o String dump of section '.rodata..L__unnamed_10': [ 1] bar\n
フラグを表すのはFlgである。
どうやら文字列リテラルを含んでいるセクションのフラグにはSHF_STRINGS
とSHF_MERGE
はついてないということがわかった。
実はここでsh_entsize
の値についても確認しておく必要がある。
というのも、リンカはここの値が0の場合mergeableと判定してくれないからだ。
- lld:ELF/InputFiles.cpp
template <class ELFT> bool ObjFile<ELFT>::shouldMerge(const Elf_Shdr &sec, StringRef name) { // On a regular link we don't merge sections if -O0 (default is -O1). This // sometimes makes the linker significantly faster, although the output will // be bigger. // // Doing the same for -r would create a problem as it would combine sections // with different sh_entsize. One option would be to just copy every SHF_MERGE // section as is to the output. While this would produce a valid ELF file with // usable SHF_MERGE sections, tools like (llvm-)?dwarfdump get confused when // they see two .debug_str. We could have separate logic for combining // SHF_MERGE sections based both on their name and sh_entsize, but that seems // to be more trouble than it is worth. Instead, we just use the regular (-O1) // logic for -r. if (config->optimize == 0 && !config->relocatable) return false; // A mergeable section with size 0 is useless because they don't have // any data to merge. A mergeable string section with size 0 can be // argued as invalid because it doesn't end with a null character. // We'll avoid a mess by handling them as if they were non-mergeable. if (sec.sh_size == 0) return false; // Check for sh_entsize. The ELF spec is not clear about the zero // sh_entsize. It says that "the member [sh_entsize] contains 0 if // the section does not hold a table of fixed-size entries". We know // that Rust 1.13 produces a string mergeable section with a zero // sh_entsize. Here we just accept it rather than being picky about it. uint64_t entSize = sec.sh_entsize; if (entSize == 0) return false;
sh_entsize
はreadelf -S
の出力のESに対応する。
各文字列リテラルを含むセクションはここの値が0になっていることがわかる。
なぜそのようなオブジェクトファイルを出力しているのか?というのはrustcのソースコードを読みにいかねばわからない。
まずrustcが生成するLLVM IRを読んで、文字列リテラルがどのような実装になっているかの雰囲気をつかもう。
$ rustc --emit llvm-ir hoge.rs
以下は関係ありそうな箇所を抜粋したもの。
(省略..) @alloc_33edafb4635cba7fe9b343830eb4d654 = private unnamed_addr constant <{ [9 x i8] }> <{ [9 x i8] c"\00foobar\0A\00" }>, align 1 @alloc_0dc7dcbb24079b5067fcd16f8d1d6fab = private unnamed_addr constant <{ ptr, [8 x i8] }> <{ ptr @alloc_33edafb4635cba7fe9b343830eb4d654, [8 x i8] c"\09\00\00\00\00\00\00\00" }>, align 8 @alloc_82ad759880ed30affa0db4ee70a74cbc = private unnamed_addr constant <{ [6 x i8] }> <{ [6 x i8] c"\00bar\0A\00" }>, align 1 @alloc_170c2fea5b9cc064c6e9c015b7a41c15 = private unnamed_addr constant <{ ptr, [8 x i8] }> <{ ptr @alloc_82ad759880ed30affa0db4ee70a74cbc, [8 x i8] c"\06\00\00\00\00\00\00\00" }>, align 8
ここで、たとえば\0foobar\n\0
は@alloc_33edafb4635cba7fe9b343830eb4d654 = private unnamed_addr constant <{ [9 x i8] }> <{ [9 x i8] c"\00foobar\0A\00" }>, align 1
のように表現されていることがわかる。
private unnamed_addr constant
とはなんぞや?
まずprivateはわかりやすいが、これはlinkage typesで同一モジュール内からのアクセスのみが許可されていることを表す。
unnamed_addr
はちょっとわかりにくいが、これはアドレスは重要ではなく内容のみが重要であることを表す。またconstant
もこのセクションで説明されているとおりで、まあ定数となる(変更されない)くらいで解釈して問題ない。
次に<{ [9 x i8] }> <{ [9 x i8] c"\00foobar\0A\00" }>, align 1
をみてみよう。
まず<{ [9 x i8] }>
についてだが、LLVMの型定義をみると、これは要素にarray typeをひとつ含んだpacked struct typeということになる。packedなstruct typeであるので、末尾にalign 1
も付与されている。
次に<{ [9 x i8] c"\00foobar\0A\00" }>
の意味だが、これはLLVMでの定数を表す表現で、中に文字列定数を一つ格納するstructの定数となる。
と、上記記述をみてみると、Rustの文字列リテラルは文字列定数をひとつ格納するstruct型の定数としてLLVMのGlobal Variableで表現されているようだ。
なのでllvm::GlobalVariable
を生成している箇所を追っていこう。
Topic: LDCの場合
Rust側のコードを覗く前に、ここまでちゃんと生成できてない例しかみてないので、そもそもちゃんと動くの?前提が正しいのか?みたいな話がある。 そこで別のLLVMベースのコンパイラ・LDCでみてみる。
まずはそもそもLLDで文字列リテラルのmergeがちゃんと行われているのか?を実験してみよう。
$ cat app.d import std.stdio; void main() { writeln("foobar"); writeln("bar"); } $ ldc2 --linker=lld app.d $ readelf -p .rodata app| grep bar [ a5b] foobar [ ea6] bar $ ldc2 --linker=lld -L-O2 app.d $ readelf -p .rodata app| grep bar [ 392] foobar
リンカオプションに最適化フラグを与えるとちゃんとmergeしてくれていることがわかる。
次にセクションのフラグ情報をみてみる。
$ ldc2 --output-o app.d $ readelf -WS app.o | grep \.rodata [50] .rodata.str1.1 PROGBITS 0000000000000000 0013df 00000b 01 AMS 0 0 1 [51] .rodata.str1.16 PROGBITS 0000000000000000 0013f0 000169 01 AMS 0 0 16 $ readelf -p .rodata.str1.1 app.o String dump of section '.rodata.str1.1': [ 0] foobar [ 7] bar
.rodata.str1.1
セクションのsh_entsize
が1に、フラグがAMS
になっている(=mergeとstringsがセットされている)ことが確認できた。
これで期待どおりにバイナリを吐いている、ということはわかったが、LDCはできてRustができてないのはなんで?となる。
生成するLLVM IRをみてみよう。
$ ldc2 --output-ll app.d $ cat app.ll (省略) @.str = private unnamed_addr constant [7 x i8] c"foobar\00" ; [#uses = 1] @.str.1 = private unnamed_addr constant [4 x i8] c"bar\00" ; [#uses = 1]
private unnamed_addr constant
だから、Rust同様にLLVMのGlobalVariableな実装になっている。
LDCのコードは比較的追いやすい。
まず、このGlobalVariableを生成している箇所はgen/irstate.cpp
のgetCachedStringLiteralImpl関数にある。
- ldc:gen/irstate.cpp
template <typename F> LLGlobalVariable * getCachedStringLiteralImpl(llvm::Module &module, llvm::StringMap<LLGlobalVariable *> &cache, llvm::StringRef key, F initFactory) { auto iter = cache.find(key); if (iter != cache.end()) { return iter->second; } LLConstant *constant = initFactory(); auto gvar = new LLGlobalVariable(module, constant->getType(), true, LLGlobalValue::PrivateLinkage, constant, ".str"); gvar->setUnnamedAddr(LLGlobalValue::UnnamedAddr::Global); cache[key] = gvar; return gvar; }
GlobalVariableのコンストラクタの定義は以下のようになっている。
/// GlobalVariable ctor - This creates a global and inserts it before the /// specified other global. GlobalVariable(Module &M, Type *Ty, bool isConstant, LinkageTypes Linkage, Constant *Initializer, const Twine &Name = "", GlobalVariable *InsertBefore = nullptr, ThreadLocalMode = NotThreadLocal, std::optional<unsigned> AddressSpace = std::nullopt, bool isExternallyInitialized = false);
Type *Ty
とConstant *Initializer
が重要そうだ。
なのでinitFactoryにわたってくる関数をみてみる。D言語の文字列リテラルの場合はgen/llvmhelper.cppのbuildStringLiteralConstant関数が渡されてくる。
- ldc:gen/llvmhelper.cpp
llvm::Constant *buildStringLiteralConstant(StringExp *se, uint64_t bufferLength) { const auto stringLength = se->numberOfCodeUnits(); assert(bufferLength >= stringLength); if (se->sz == 1 && bufferLength <= stringLength + 1) { const DString data = se->peekString(); const bool nullTerminate = (bufferLength == stringLength + 1); return llvm::ConstantDataArray::getString( gIR->context(), {data.ptr, data.length}, nullTerminate); } Type *dtype = se->type->toBasetype(); Type *cty = dtype->nextOf()->toBasetype(); LLType *ct = DtoMemType(cty); LLArrayType *at = LLArrayType::get(ct, bufferLength); std::vector<LLConstant *> vals; vals.reserve(bufferLength); for (uint64_t i = 0; i < stringLength; ++i) { vals.push_back(LLConstantInt::get(ct, se->getCodeUnit(i), false)); } const auto nullChar = LLConstantInt::get(ct, 0, false); for (uint64_t i = stringLength; i < bufferLength; ++i) { vals.push_back(nullChar); } return LLConstantArray::get(at, vals); }
文字列リテラルが直接渡されてくる今回の場合は以下のケースだけ着目すればよい。
const DString data = se->peekString(); const bool nullTerminate = (bufferLength == stringLength + 1); return llvm::ConstantDataArray::getString( gIR->context(), {data.ptr, data.length}, nullTerminate);
なのでここではllvm::ConstantDataArrayが返ってきてる。
constant->getType()が返す型はこのArray型の要素となるので、この場合はint8_tになる。
よってこのセクションは、
- 要素の型がint8_tなConstantDataArray型のconstantなGlobalVariable
- 内部のデータはnull終端な文字列リテラル
で構成されている。
rustcコードリーディング
rustcが直接LLVMのGlobalVariableを生成しているコードは二箇所ある。
この2つの関数をrustc_codegen_llvm
内でいろいろな箇所から呼んでいる。
extern "C" LLVMValueRef LLVMRustGetOrInsertGlobal(LLVMModuleRef M, const char *Name, size_t NameLen, LLVMTypeRef Ty) { Module *Mod = unwrap(M); StringRef NameRef(Name, NameLen); // We don't use Module::getOrInsertGlobal because that returns a Constant*, // which may either be the real GlobalVariable*, or a constant bitcast of it // if our type doesn't match the original declaration. We always want the // GlobalVariable* so we can access linkage, visibility, etc. GlobalVariable *GV = Mod->getGlobalVariable(NameRef, true); if (!GV) GV = new GlobalVariable(*Mod, unwrap(Ty), false, GlobalValue::ExternalLinkage, nullptr, NameRef); return wrap(GV); } extern "C" LLVMValueRef LLVMRustInsertPrivateGlobal(LLVMModuleRef M, LLVMTypeRef Ty) { return wrap(new GlobalVariable(*unwrap(M), unwrap(Ty), false, GlobalValue::PrivateLinkage, nullptr)); }
先にも述べた通り、これらの関数はいろいろな箇所から呼ばれており、逐一探すのは骨が折れる。なのでちょっとアドホックなやり方で特定しよう。
rustcが生成するLLVM IRの出力をちょっと振り返ってみよう。
(省略..) @alloc_33edafb4635cba7fe9b343830eb4d654 = private unnamed_addr constant <{ [9 x i8] }> <{ [9 x i8] c"\00foobar\0A\00" }>, align 1 @alloc_0dc7dcbb24079b5067fcd16f8d1d6fab = private unnamed_addr constant <{ ptr, [8 x i8] }> <{ ptr @alloc_33edafb4635cba7fe9b343830eb4d654, [8 x i8] c"\09\00\00\00\00\00\00\00" }>, align 8 @alloc_82ad759880ed30affa0db4ee70a74cbc = private unnamed_addr constant <{ [6 x i8] }> <{ [6 x i8] c"\00bar\0A\00" }>, align 1 @alloc_170c2fea5b9cc064c6e9c015b7a41c15 = private unnamed_addr constant <{ ptr, [8 x i8] }> <{ ptr @alloc_82ad759880ed30affa0db4ee70a74cbc, [8 x i8] c"\06\00\00\00\00\00\00\00" }>, align 8
ここで各GlobalVariableの定義は、alloc_xxxxx...
のような命名規則になっていることがわかる。
この命名規則を利用しているのは、rustc_codegen_llvm/src/common.rs
のscalar_to_backend
関数である。
fn scalar_to_backend(&self, cv: Scalar, layout: abi::Scalar, llty: &'ll Type) -> &'ll Value { let bitsize = if layout.is_bool() { 1 } else { layout.size(self).bits() }; match cv { Scalar::Int(int) => { (省略...) } Scalar::Ptr(ptr, _size) => { let (prov, offset) = ptr.into_parts(); let (base_addr, base_addr_space) = match self.tcx.global_alloc(prov.alloc_id()) { GlobalAlloc::Memory(alloc) => { let init = const_alloc_to_llvm(self, alloc); let alloc = alloc.inner(); let value = match alloc.mutability { Mutability::Mut => self.static_addr_of_mut(init, alloc.align, None), _ => self.static_addr_of(init, alloc.align, None), }; if !self.sess().fewer_names() && llvm::get_value_name(value).is_empty() { let hash = self.tcx.with_stable_hashing_context(|mut hcx| { let mut hasher = StableHasher::new(); alloc.hash_stable(&mut hcx, &mut hasher); hasher.finish::<Hash128>() }); llvm::set_value_name(value, format!("alloc_{hash:032x}").as_bytes()); } (value, AddressSpace::DATA) }
llvm::GlobalVariableを生成しているのはstatic_addr_of
関数内になる。
(実際の呼び出し階層はstatic_addr_of
->static_addr_of_mut
->define_private_global(rustc_codegen_llvm/src/declare.rs)
->LLVMRustInsertPrivateGlobal(RustWrapper.cpp)
のようになっている)
なんでstatic_addr_of
のほうかとわかるのかというと、llvm::LLVMSetGlobalConstant(gv, True);
を呼んでるのがこちらのパスだけなので、LLVM IRの内容と照らし合わせるとこちらになるとわかる。
ここまででどのパスでGlobalVariableの生成するかはわかった が型情報をコードから追うのはなかなか大変そうだ。
(追記: 別にそんなことはなかった)
ここの型を決める箇所は以下のコードで常にconst_struct
を呼び出しており、これはConstStruct型となる。
pub fn const_alloc_to_llvm<'ll>(cx: &CodegenCx<'ll, '_>, alloc: ConstAllocation<'_>) -> &'ll Value { let alloc = alloc.inner(); let mut llvals = Vec::with_capacity(alloc.provenance().ptrs().len() + 1); (省略...) cx.const_struct(&llvals, true) }
ここで再びLLVM IRに目を戻してみる。
@alloc_33edafb4635cba7fe9b343830eb4d654 = private unnamed_addr constant <{ [9 x i8] }> <{ [9 x i8] c"\00foobar\0A\00" }>, align 1
ここでLDCでの定義を思い返してみると、ここはarray type(正確にはArrayDataType)だった。これでだんだんみえてきた。ここの型がArray-likeでなければSHF_MERGE
/SHF_STRINGS
は付与されない、ということだろう。
rustcの中でこれらのフラグをつけているわけではないことがわかったので、ここでLLVMのコード生成側に返ってみよう。
型情報によってmergeable判定を決めている箇所は、LLVMのTargetLoweringObjectFile.cppのgetKindForGlobal関数となっている。
- llvm:lib/Target/TargetLoweringObjectFile.cpp
SectionKind TargetLoweringObjectFile::getKindForGlobal(const GlobalObject *GO, const TargetMachine &TM){ (省略...) // If initializer is a null-terminated string, put it in a "cstring" // section of the right width. if (ArrayType *ATy = dyn_cast<ArrayType>(C->getType())) { if (IntegerType *ITy = dyn_cast<IntegerType>(ATy->getElementType())) { if ((ITy->getBitWidth() == 8 || ITy->getBitWidth() == 16 || ITy->getBitWidth() == 32) && IsNullTerminatedString(C)) { if (ITy->getBitWidth() == 8) return SectionKind::getMergeable1ByteCString(); if (ITy->getBitWidth() == 16) return SectionKind::getMergeable2ByteCString(); assert(ITy->getBitWidth() == 32 && "Unknown width"); return SectionKind::getMergeable4ByteCString(); } }
SectionKind::Mergeable1ByteCStringやSectionKind::Mergeable4ByteCStringを返す条件として、セクションの型が整数型のArrayTypeにキャスト可能な型であること・セクション内の文字列がnull終端となっていることなどが求められている。
これらのSectionKindはSHF_MERGE
とSHF_STRINGS
に影響することが以下の関数でわかる。
static unsigned getELFSectionFlags(SectionKind K) { unsigned Flags = 0; (省略...) if (K.isMergeableCString() || K.isMergeableConst()) Flags |= ELF::SHF_MERGE; if (K.isMergeableCString()) Flags |= ELF::SHF_STRINGS; return Flags; }
まとめ
結論としては、Rustは文字列リテラルをLLVMの世界では要素として文字列定数をひとつ持つpacked struct型の定数をグローバル変数として表現するが、LLVMはArray型の定数でかつnull終端である文字列定数しかMerge可能な文字列と判定できないため、文字列リテラルを格納しているセクションにmergeableかつstringsであることを表すフラグを付与できないということになる。
今回Rustが意図してこのような文字列リテラルの実装になっているかまでは調べきることはできなかったが、機会があればまた調査したい。