Skip to content

Commit 8240d63

Browse files
Dev VMclaude
andcommitted
Emit naked functions as LLVM IR with inline asm instead of module asm
This fixes issue #4294 where LTO linking fails with "symbol already defined" errors for naked template functions. The root cause was that module-level assembly from multiple compilation units gets concatenated during LTO before COMDAT deduplication can occur. The fix emits naked functions as proper LLVM IR functions with: - The 'naked' attribute (suppresses prologue/epilogue generation) - LinkOnceODRLinkage for template instantiations - COMDAT groups for proper symbol deduplication during LTO - Inline asm containing the function body - OptimizeNone and NoInline attributes to prevent LLVM from cloning the function during optimization passes (which would duplicate labels) Labels in the inline asm use printLabelName() for consistency with label references generated by the asm parser, ensuring labels are properly quoted to match the format used in jump instructions. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 091a6e9 commit 8240d63

4 files changed

Lines changed: 403 additions & 84 deletions

File tree

gen/naked.cpp

Lines changed: 94 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include "dmd/template.h"
1717
#include "gen/dvalue.h"
1818
#include "gen/funcgenstate.h"
19+
#include "gen/functions.h"
1920
#include "gen/irstate.h"
2021
#include "gen/llvm.h"
2122
#include "gen/llvmhelpers.h"
@@ -24,6 +25,7 @@
2425
#include "ir/irfunction.h"
2526
#include "llvm/IR/InlineAsm.h"
2627
#include <cassert>
28+
#include <sstream>
2729

2830
using namespace dmd;
2931

@@ -128,9 +130,11 @@ class ToNakedIRVisitor : public Visitor {
128130
stmt->loc.toChars());
129131
LOG_SCOPE;
130132

133+
// Use printLabelName to match how label references are generated in asm-x86.h.
134+
// This ensures label definitions match the quoted format used in jump instructions.
131135
printLabelName(irs->nakedAsm, mangleExact(irs->func()->decl),
132136
stmt->ident->toChars());
133-
irs->nakedAsm << ":";
137+
irs->nakedAsm << ":\n";
134138

135139
if (stmt->statement) {
136140
stmt->statement->accept(this);
@@ -144,108 +148,114 @@ void DtoDefineNakedFunction(FuncDeclaration *fd) {
144148
IF_LOG Logger::println("DtoDefineNakedFunction(%s)", mangleExact(fd));
145149
LOG_SCOPE;
146150

147-
// we need to do special processing on the body, since we only want
148-
// to allow actual inline asm blocks to reach the final asm output
151+
const char *mangle = mangleExact(fd);
152+
const auto &triple = *global.params.targetTriple;
149153

154+
// Clear the nakedAsm stream and collect the function body
150155
std::ostringstream &asmstr = gIR->nakedAsm;
156+
asmstr.str("");
151157

152-
// build function header
153-
154-
// FIXME: could we perhaps use llvm asmwriter to give us these details ?
158+
// Use the visitor to collect asm statements into nakedAsm
159+
ToNakedIRVisitor visitor(gIR);
160+
fd->fbody->accept(&visitor);
155161

156-
const char *mangle = mangleExact(fd);
157-
std::string fullmangle; // buffer only
162+
if (global.errors) {
163+
fatal();
164+
}
158165

159-
const auto &triple = *global.params.targetTriple;
160-
bool const isWin = triple.isOSWindows();
161-
bool const isDarwin = triple.isOSDarwin();
162-
163-
// osx is different
164-
// also mangling has an extra underscore prefixed
165-
if (isDarwin) {
166-
fullmangle += '_';
167-
fullmangle += mangle;
168-
mangle = fullmangle.c_str();
169-
170-
asmstr << "\t.section\t__TEXT,__text,regular,pure_instructions"
171-
<< std::endl;
172-
asmstr << "\t.globl\t" << mangle << std::endl;
173-
if (fd->isInstantiated()) {
174-
asmstr << "\t.weak_definition\t" << mangle << std::endl;
166+
// Get the collected asm string and escape $ characters for LLVM inline asm.
167+
// In LLVM inline asm, $N refers to operand N, so literal $ must be escaped as $$.
168+
std::string asmBody;
169+
{
170+
std::string raw = asmstr.str();
171+
asmBody.reserve(raw.size() * 2); // Worst case: all $ characters
172+
for (char c : raw) {
173+
if (c == '$') {
174+
asmBody += "$$";
175+
} else {
176+
asmBody += c;
177+
}
175178
}
176-
asmstr << "\t.p2align\t4, 0x90" << std::endl;
177-
asmstr << mangle << ":" << std::endl;
178179
}
179-
// Windows is different
180-
else if (isWin) {
181-
// mangled names starting with '?' (MSVC++ symbols) apparently need quoting
182-
if (mangle[0] == '?') {
183-
fullmangle += '"';
184-
fullmangle += mangle;
185-
fullmangle += '"';
186-
mangle = fullmangle.c_str();
187-
} else if (triple.isArch32Bit()) {
188-
// prepend extra underscore for Windows x86
189-
fullmangle += '_';
190-
fullmangle += mangle;
191-
mangle = fullmangle.c_str();
192-
}
180+
asmstr.str(""); // Clear for potential reuse
193181

194-
asmstr << "\t.def\t" << mangle << ";" << std::endl;
195-
// hard code these two numbers for now since gas ignores .scl and llvm
196-
// is defaulting to .type 32 for everything I have seen
197-
asmstr << "\t.scl\t2;" << std::endl;
198-
asmstr << "\t.type\t32;" << std::endl;
199-
asmstr << "\t.endef" << std::endl;
182+
// Get or create the LLVM function
183+
llvm::Module &module = gIR->module;
184+
llvm::Function *func = module.getFunction(mangle);
200185

186+
if (!func) {
187+
// Create function type using the existing infrastructure
188+
llvm::FunctionType *funcType = DtoFunctionType(fd);
189+
190+
// Create function with appropriate linkage
191+
llvm::GlobalValue::LinkageTypes linkage;
201192
if (fd->isInstantiated()) {
202-
asmstr << "\t.section\t.text,\"xr\",discard," << mangle << std::endl;
203-
} else {
204-
asmstr << "\t.text" << std::endl;
205-
}
206-
asmstr << "\t.globl\t" << mangle << std::endl;
207-
asmstr << "\t.p2align\t4, 0x90" << std::endl;
208-
asmstr << mangle << ":" << std::endl;
209-
} else {
210-
if (fd->isInstantiated()) {
211-
asmstr << "\t.section\t.text." << mangle << ",\"axG\",@progbits,"
212-
<< mangle << ",comdat" << std::endl;
213-
asmstr << "\t.weak\t" << mangle << std::endl;
193+
linkage = llvm::GlobalValue::LinkOnceODRLinkage;
214194
} else {
215-
asmstr << "\t.text" << std::endl;
216-
asmstr << "\t.globl\t" << mangle << std::endl;
195+
linkage = llvm::GlobalValue::ExternalLinkage;
217196
}
218-
asmstr << "\t.p2align\t4, 0x90" << std::endl;
219-
asmstr << "\t.type\t" << mangle << ",@function" << std::endl;
220-
asmstr << mangle << ":" << std::endl;
197+
198+
func = llvm::Function::Create(funcType, linkage, mangle, &module);
199+
} else if (!func->empty()) {
200+
// Function already has a body - this can happen if the function was
201+
// already defined (e.g., template instantiation in another module).
202+
// Don't add another body.
203+
return;
204+
} else if (func->hasFnAttribute(llvm::Attribute::Naked)) {
205+
// Function already has naked attribute - it was already processed
206+
return;
221207
}
222208

223-
// emit body
224-
ToNakedIRVisitor v(gIR);
225-
fd->fbody->accept(&v);
209+
// Set naked attribute - this tells LLVM not to generate prologue/epilogue
210+
func->addFnAttr(llvm::Attribute::Naked);
226211

227-
// We could have generated new errors in toNakedIR(), but we are in codegen
228-
// already so we have to abort here.
229-
if (global.errors) {
230-
fatal();
231-
}
212+
// Prevent optimizations that might clone or modify the function.
213+
// The inline asm contains labels that would conflict if duplicated.
214+
func->addFnAttr(llvm::Attribute::OptimizeNone);
215+
func->addFnAttr(llvm::Attribute::NoInline);
232216

233-
// emit size after body
234-
// llvm does this on linux, but not on osx or Win
235-
if (!(isWin || isDarwin)) {
236-
asmstr << "\t.size\t" << mangle << ", .-" << mangle << std::endl
237-
<< std::endl;
217+
// For template instantiations, set up COMDAT for deduplication
218+
if (fd->isInstantiated()) {
219+
func->setComdat(module.getOrInsertComdat(mangle));
238220
}
239221

240-
gIR->module.appendModuleInlineAsm(asmstr.str());
241-
asmstr.str("");
222+
// Set other common attributes
223+
func->addFnAttr(llvm::Attribute::NoUnwind);
224+
225+
// Create entry basic block
226+
llvm::BasicBlock *entryBB =
227+
llvm::BasicBlock::Create(gIR->context(), "entry", func);
242228

229+
// Save current insert point and switch to new function
230+
llvm::IRBuilderBase::InsertPoint savedIP = gIR->ir->saveIP();
231+
gIR->ir->SetInsertPoint(entryBB);
232+
233+
// Create inline asm - the entire function body is a single asm block
234+
// No constraints needed since naked functions handle everything in asm
235+
llvm::FunctionType *asmFuncType =
236+
llvm::FunctionType::get(llvm::Type::getVoidTy(gIR->context()), false);
237+
238+
llvm::InlineAsm *inlineAsm = llvm::InlineAsm::get(
239+
asmFuncType,
240+
asmBody,
241+
"", // No constraints
242+
true, // Has side effects
243+
false, // Not align stack
244+
llvm::InlineAsm::AD_ATT // AT&T syntax
245+
);
246+
247+
gIR->ir->CreateCall(inlineAsm);
248+
249+
// Naked functions don't return normally through LLVM IR
250+
gIR->ir->CreateUnreachable();
251+
252+
// Restore insert point
253+
gIR->ir->restoreIP(savedIP);
254+
255+
// Handle DLL export on Windows
243256
if (global.params.dllexport ||
244-
(global.params.targetTriple->isOSWindows() && fd->isExport())) {
245-
// Embed a linker switch telling the MS linker to export the naked function.
246-
// This mimics the effect of the dllexport attribute for regular functions.
247-
const auto linkerSwitch = std::string("/EXPORT:") + mangle;
248-
gIR->addLinkerOption(llvm::StringRef(linkerSwitch));
257+
(triple.isOSWindows() && fd->isExport())) {
258+
func->setDLLStorageClass(llvm::GlobalValue::DLLExportStorageClass);
249259
}
250260
}
251261

@@ -436,7 +446,7 @@ DValue *DtoInlineAsmExpr(Loc loc, FuncDeclaration *fd,
436446
LLSmallVector<LLValue *, 8> operands;
437447
LLSmallVector<LLType *, 8> indirectTypes;
438448
operands.reserve(n);
439-
449+
440450
Type *returnType = fd->type->nextOf();
441451
const size_t cisize = constraintInfo.size();
442452
const size_t minRequired = n + (returnType->ty == TY::Tvoid ? 0 : 1);
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
// Tests corner cases for naked functions with DMD-style inline asm.
2+
//
3+
// This tests:
4+
// 1. Stack manipulation (push/pop)
5+
// 2. Forward and backward jumps
6+
// 3. Nested labels
7+
// 4. Naked function calling convention
8+
9+
// REQUIRES: target_X86
10+
11+
// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -output-s -of=%t.s %s
12+
// RUN: FileCheck %s --check-prefix=ASM < %t.s
13+
14+
// RUN: %ldc -mtriple=x86_64-linux-gnu -O0 -run %s
15+
16+
module naked_asm_corner_cases;
17+
18+
// Test 1: Stack manipulation with push/pop
19+
// ASM-LABEL: stackManipulation:
20+
// ASM-NOT: pushq %rbp
21+
// ASM-NOT: movq %rsp, %rbp
22+
// ASM: pushq %rbx
23+
// ASM: movl $42, %eax
24+
// ASM: movl %eax, %ebx
25+
// ASM: movl %ebx, %eax
26+
// ASM: popq %rbx
27+
// ASM: retq
28+
extern(C) int stackManipulation() {
29+
asm { naked; }
30+
asm {
31+
push RBX; // Save callee-saved register
32+
mov EAX, 42;
33+
mov EBX, EAX; // Use the saved register
34+
mov EAX, EBX;
35+
pop RBX; // Restore
36+
ret;
37+
}
38+
}
39+
40+
// Test 2: Forward jump (jump to label defined later)
41+
// ASM-LABEL: forwardJump:
42+
// ASM: jmp .LforwardJump_skip
43+
// ASM: .LforwardJump_skip:
44+
// ASM: retq
45+
extern(C) int forwardJump() {
46+
asm { naked; }
47+
asm {
48+
mov EAX, 1;
49+
jmp skip; // Forward jump
50+
mov EAX, 0; // Should be skipped
51+
skip:
52+
ret;
53+
}
54+
}
55+
56+
// Test 3: Backward jump (loop)
57+
// ASM-LABEL: backwardJump:
58+
// ASM: .LbackwardJump_again:
59+
// ASM: incl %eax
60+
// ASM: cmpl $5, %eax
61+
// ASM: jl .LbackwardJump_again
62+
extern(C) int backwardJump() {
63+
asm { naked; }
64+
asm {
65+
xor EAX, EAX;
66+
again:
67+
inc EAX;
68+
cmp EAX, 5;
69+
jl again; // Backward jump
70+
ret;
71+
}
72+
}
73+
74+
// Test 4: Multiple control flow paths
75+
// ASM-LABEL: multiPath:
76+
// ASM: .LmultiPath_path1:
77+
// ASM: .LmultiPath_path2:
78+
// ASM: .LmultiPath_done:
79+
extern(C) int multiPath(int x) {
80+
asm { naked; }
81+
version(D_InlineAsm_X86_64) asm {
82+
// x is in EDI on SysV ABI
83+
test EDI, EDI;
84+
jz path1;
85+
jmp path2;
86+
path1:
87+
mov EAX, 10;
88+
jmp done;
89+
path2:
90+
mov EAX, 20;
91+
done:
92+
ret;
93+
}
94+
}
95+
96+
// Test 5: Runtime verification
97+
void main() {
98+
// Verify stack manipulation works
99+
assert(stackManipulation() == 42, "stackManipulation failed");
100+
101+
// Verify forward jump works
102+
assert(forwardJump() == 1, "forwardJump failed");
103+
104+
// Verify backward jump (loop) works
105+
assert(backwardJump() == 5, "backwardJump failed");
106+
107+
// Verify multi-path control flow
108+
assert(multiPath(0) == 10, "multiPath(0) failed");
109+
assert(multiPath(1) == 20, "multiPath(1) failed");
110+
}

0 commit comments

Comments
 (0)