#![deny(clippy::arithmetic_side_effects)]
use crate::args::LinkOptions;
use crate::diag::DiagnosticSink;
use crate::error::{Error, Result};
use crate::symbols::{Resolution, SymbolName, SymbolTable, SymbolUse, resolve_symbols_with};
use super::inputs::{Inputs, LtoMode};
use super::resolve::{ComdatHook, ElfRules};
#[cfg(feature = "plugin")]
pub use plugin_link::{SymbolFacts, Winner, symbol_resolution};
#[cfg(feature = "plugin")]
use crate::plugin::{FileResolution, SymbolResolution};
#[derive(Clone, Debug, Default)]
pub struct IrSymbols<'a> {
pub names: Vec<SymbolName<'a>>,
pub uses: Vec<SymbolUse>,
pub comdats: Vec<&'a [u8]>,
pub comdat_of: Vec<u32>,
pub discarded: Vec<bool>,
pub claim: usize,
}
impl<'a> IrSymbols<'a> {
pub fn discard_comdats(&mut self, discarded: Vec<bool>) {
for (use_, &comdat) in self.uses.iter_mut().zip(&self.comdat_of) {
let Some(key) = comdat.checked_sub(1) else {
continue;
};
if matches!(use_, SymbolUse::Definition { .. })
&& discarded.get(key as usize).copied().unwrap_or(false)
{
*use_ = SymbolUse::Ignore;
}
}
self.discarded = discarded;
}
#[must_use]
pub fn defined_names(&self) -> Vec<SymbolName<'a>> {
self.names
.iter()
.zip(&self.uses)
.filter(|(_, use_)| matches!(use_, SymbolUse::Definition { .. }))
.map(|(&name, _)| name)
.collect()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum IrKind {
LlvmBitcode,
GccSlim,
GccFat,
}
#[must_use]
pub fn ir_error(file: &str, kind: IrKind, mode: LtoMode) -> Error {
let (what, driver, plugin) = match kind {
IrKind::LlvmBitcode => ("LLVM bitcode", "clang", "LLVMgold.so"),
IrKind::GccSlim | IrKind::GccFat => ("GCC LTO", "gcc", "liblto_plugin.so"),
};
Error::Plugin(match mode {
LtoMode::NoPlugin => format!(
"{file}: {what} input needs an LTO plugin: link it through the compiler \
driver ({driver} -flto), which passes -plugin {plugin}"
),
LtoMode::Unsupported => format!(
"{file}: {what} input needs an LTO plugin, and this qld was built without \
plugin support (cargo feature `plugin`)"
),
LtoMode::Claim => {
format!("{file}: no LTO plugin claimed this {what} input (it needs {plugin})")
}
LtoMode::AfterLto | LtoMode::Generated => format!(
"{file}: {what} input is needed by the code LTO generated, but was not part \
of LTO (it defines a symbol that only the generated code references)"
),
})
}
#[derive(Debug, Default)]
pub struct LtoLink {
#[cfg(feature = "plugin")]
session: Option<crate::plugin::Session>,
}
impl LtoLink {
#[must_use]
pub fn is_active(&self) -> bool {
#[cfg(feature = "plugin")]
{
self.session.is_some()
}
#[cfg(not(feature = "plugin"))]
{
false
}
}
pub fn finish(self, diagnostics: &dyn DiagnosticSink) -> Result<()> {
#[cfg(feature = "plugin")]
if let Some(session) = self.session {
return session.finish(diagnostics);
}
let _ = diagnostics;
Ok(())
}
}
pub fn resolve<'a, F: crate::elf::read::ElfFormat>(
options: &LinkOptions,
diagnostics: &dyn DiagnosticSink,
rules: &ElfRules,
inputs: &mut Inputs<'a, F>,
) -> Result<(SymbolTable<'a>, Resolution<'a>, LtoLink)> {
#[cfg(feature = "plugin")]
if !options.plugins.is_empty() {
return plugin_link::resolve(options, diagnostics, rules, inputs);
}
let _ = (options, diagnostics);
let mut symbols = SymbolTable::new();
let mut comdat = ComdatHook::default();
let resolution = resolve_symbols_with(&mut symbols, rules, &mut inputs.files, &mut comdat)?;
Ok((symbols, resolution, LtoLink::default()))
}
#[cfg(feature = "plugin")]
mod plugin_link {
use std::sync::Arc;
use std::time::{Duration, Instant};
use hashbrown::HashMap;
use rayon::prelude::*;
use super::{
ComdatHook, ElfRules, Error, FileResolution, Inputs, IrSymbols, LinkOptions, LtoLink,
LtoMode, Resolution, Result, SymbolName, SymbolResolution, SymbolTable, SymbolUse,
resolve_symbols_with,
};
use crate::diag::{Diagnostic, DiagnosticSink};
use crate::elf::dso;
use crate::elf::export::{self, Mode, VersionScript};
use crate::elf::inputs::{self, ElfInput, InputRole};
use crate::elf::object::split_version;
use crate::elf::read::consts::{STB_LOCAL, STV_HIDDEN, STV_INTERNAL};
use crate::elf::resolve::AUX_COMDAT;
use crate::ids::FileId;
use crate::input::FileFormat;
use crate::plugin::options::{PluginFlavor, classify, missing_environment};
use crate::plugin::{
ClaimedFile, InputFile, PluginMessage, Session, SessionOptions, SymbolKind, Visibility,
};
use crate::script::Pattern;
use crate::symbols::{DefinitionKind, LoadHook, RoundFile, RoundHook, SymbolFlags};
const LTO_REGULAR: SymbolFlags = SymbolFlags::backend(8);
const LTO_DYNAMIC: SymbolFlags = SymbolFlags::backend(9);
const LTO_HIDDEN: SymbolFlags = SymbolFlags::backend(10);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Winner {
#[default]
Undefined,
Ir,
Regular,
Shared,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct SymbolFacts {
pub undefined: bool,
pub prevailing: bool,
pub winner: Winner,
pub regular_ref: bool,
pub dynamic_ref: bool,
pub export_all: bool,
pub listed: bool,
pub visibility: Visibility,
pub script_local: bool,
pub relocatable: bool,
pub wrapped: bool,
}
#[must_use]
pub fn symbol_resolution(facts: &SymbolFacts) -> SymbolResolution {
if facts.undefined {
return match facts.winner {
Winner::Undefined => SymbolResolution::Undefined,
Winner::Ir => SymbolResolution::ResolvedIr,
Winner::Regular => SymbolResolution::ResolvedExec,
Winner::Shared => SymbolResolution::ResolvedDyn,
};
}
if !facts.prevailing {
return match facts.winner {
Winner::Ir => SymbolResolution::PreemptedIr,
Winner::Regular | Winner::Shared | Winner::Undefined => {
SymbolResolution::PreemptedRegular
}
};
}
if facts.relocatable || facts.regular_ref || facts.wrapped {
return SymbolResolution::PrevailingDef;
}
let visible = matches!(
facts.visibility,
Visibility::Default | Visibility::Protected
) && !facts.script_local
&& (facts.dynamic_ref || facts.export_all || facts.listed);
if visible {
SymbolResolution::PrevailingDefIronlyExp
} else {
SymbolResolution::PrevailingDefIronly
}
}
fn exit_on_fatal(message: &PluginMessage) {
eprintln!("{}: error: {}", crate::PROGRAM_NAME, message.text);
std::process::exit(1);
}
struct Driver<'o> {
options: &'o LinkOptions,
diagnostics: &'o dyn DiagnosticSink,
session: Option<Session>,
claims: usize,
claim_time: Duration,
}
impl Driver<'_> {
fn session(&mut self) -> Result<&mut Session> {
if self.session.is_none() {
let mut session = Session::new(SessionOptions {
fatal_hook: self.options.exit_on_plugin_fatal.then_some(exit_on_fatal),
..SessionOptions::from_link_options(self.options)
})?;
for (path, plugin_options) in &self.options.plugins {
let flavor = PluginFlavor::detect(path, plugin_options);
let missing =
missing_environment(flavor, plugin_options, |name| std::env::var_os(name));
if !missing.is_empty() {
self.diagnostics.emit(Diagnostic::warning(format!(
"{}: {} not set: the plugin cannot compile LTO inputs outside the \
compiler driver",
path.display(),
missing.join(" and ")
)));
}
for option in plugin_options {
if classify(flavor, option).exits_process() {
self.diagnostics.emit(Diagnostic::warning(format!(
"{}: -plugin-opt={option}: the plugin ends the link after \
writing its output",
path.display()
)));
}
}
}
session.load_plugins(&self.options.plugins, self.diagnostics)?;
self.session = Some(session);
}
self.session
.as_mut()
.ok_or_else(|| Error::Internal("LTO plugin session missing".into()))
}
fn claim<'a, F: crate::elf::read::ElfFormat>(
&mut self,
file: &ElfInput<'a, F>,
handle: usize,
known_used: bool,
) -> Result<Option<IrSymbols<'a>>> {
let start = Instant::now();
let mut input = input_file(file, handle)?;
input.known_used = known_used;
let diagnostics = self.diagnostics;
let claim = self.claims;
let session = self.session()?;
let claimed = session.claim(&input, diagnostics)?;
let symbols = claimed
.map(|claimed| ir_symbols(file, claimed, claim))
.transpose()?;
if symbols.is_some() {
self.claims = self.claims.saturating_add(1);
}
self.claim_time = self.claim_time.saturating_add(start.elapsed());
Ok(symbols)
}
}
fn input_file<F: crate::elf::read::ElfFormat>(
file: &ElfInput<'_, F>,
handle: usize,
) -> Result<InputFile> {
let Some(data) = file.file else {
return Err(Error::Internal(format!(
"{}: LTO input not loaded",
file.display()
)));
};
let bytes = data.data();
let size = u64::try_from(bytes.len()).unwrap_or(u64::MAX);
let handle = u64::try_from(handle).unwrap_or(u64::MAX);
let within = data
.parent()
.and_then(|parent| file.table().get(parent))
.and_then(|archive| {
let base = archive.data().as_ptr() as usize;
let offset = (bytes.as_ptr() as usize).checked_sub(base)?;
let end = offset.checked_add(bytes.len())?;
(end <= archive.data().len()).then(|| (archive.path(), offset))
});
Ok(match within {
Some((path, offset)) => InputFile::new(
path,
u64::try_from(offset).unwrap_or(u64::MAX),
size,
handle,
),
None => InputFile::new(data.path(), 0, size, handle),
})
}
fn ir_symbols<'a, F: crate::elf::read::ElfFormat>(
file: &ElfInput<'a, F>,
claimed: &ClaimedFile,
claim: usize,
) -> Result<IrSymbols<'a>> {
let mut arena = Vec::new();
let mut spans = Vec::with_capacity(claimed.symbols.len());
let mut keys: HashMap<&[u8], u32> = HashMap::new();
let mut key_order: Vec<&[u8]> = Vec::new();
for symbol in &claimed.symbols {
let start = arena.len();
arena.extend_from_slice(&symbol.name);
if let Some(version) = &symbol.version {
arena.push(b'@');
arena.extend_from_slice(version);
}
let comdat = match &symbol.comdat_key {
Some(key) => match keys.get(key.as_slice()) {
Some(&index) => index,
None => {
key_order.push(key);
let index = u32::try_from(key_order.len())
.map_err(|_| Error::Limit("too many COMDAT keys".into()))?;
keys.insert(key, index);
index
}
},
None => 0,
};
spans.push((start, arena.len(), comdat));
}
let mut key_spans = Vec::with_capacity(key_order.len());
for key in &key_order {
let start = arena.len();
arena.extend_from_slice(key);
key_spans.push((start, arena.len()));
}
let table = file.table();
let id = table.add_bytes(
format!("{} (LTO symbol names)", file.display()),
Arc::from(arena),
)?;
let data: &'a [u8] = table
.get(id)
.map(crate::input::InputFile::data)
.unwrap_or_default();
let slice = |start: usize, end: usize| -> Result<&'a [u8]> {
data.get(start..end)
.ok_or_else(|| Error::Internal("LTO symbol name buffer".into()))
};
let wrap = file.wrap();
let mut names = Vec::with_capacity(spans.len());
let mut uses = Vec::with_capacity(spans.len());
let mut comdat_of = Vec::with_capacity(spans.len());
for (symbol, &(start, end, comdat)) in claimed.symbols.iter().zip(&spans) {
let mut name = slice(start, end)?;
if symbol.kind.is_undefined() {
name = wrap.redirect(name);
}
let (base, version) = split_version(name);
names.push(SymbolName::with_version(base, version));
let aux = if comdat == 0 { 0 } else { AUX_COMDAT };
uses.push(match symbol.kind {
SymbolKind::Definition => SymbolUse::Definition {
kind: DefinitionKind::Regular,
aux,
},
SymbolKind::WeakDefinition => SymbolUse::Definition {
kind: DefinitionKind::Weak,
aux,
},
SymbolKind::Common => SymbolUse::Definition {
kind: DefinitionKind::Common,
aux: symbol.size & !AUX_COMDAT,
},
SymbolKind::Undefined => SymbolUse::Reference { weak: false },
SymbolKind::WeakUndefined => SymbolUse::Reference { weak: true },
});
comdat_of.push(comdat);
}
let comdats = key_spans
.iter()
.map(|&(start, end)| slice(start, end))
.collect::<Result<Vec<_>>>()?;
Ok(IrSymbols {
names,
uses,
discarded: vec![false; comdats.len()],
comdats,
comdat_of,
claim,
})
}
struct ClaimHook<'d, 'o, 'a> {
driver: &'d mut Driver<'o>,
comdat: ComdatHook<'a>,
}
impl<'a, F: crate::elf::read::ElfFormat> RoundHook<ElfInput<'a, F>> for ClaimHook<'_, '_, 'a> {
fn load_hook(&self) -> Option<&dyn LoadHook<ElfInput<'a, F>>> {
self.comdat.load_hook()
}
fn after_load(
&mut self,
round: usize,
files: &mut [RoundFile<'_, ElfInput<'a, F>>],
) -> Result<()> {
for round_file in files.iter_mut() {
let file = &mut *round_file.file;
if file.lto_mode() != LtoMode::Claim || file.pending_ir().is_none() {
continue;
}
match self.driver.claim(file, round_file.id.index(), true)? {
Some(symbols) => {
file.ir = Some(Box::new(symbols));
file.object = None;
}
None => file.claim_declined()?,
}
}
self.comdat.after_load(round, files)
}
}
struct Context {
export_all: bool,
relocatable: bool,
shared: bool,
script: Option<VersionScript>,
patterns: Vec<Pattern>,
wrap: Vec<Vec<u8>>,
}
impl Context {
fn new<F: crate::elf::read::ElfFormat>(
options: &LinkOptions,
files: &[ElfInput<'_, F>],
) -> Result<Self> {
let mode = Mode::new(options, files.iter().any(|f| f.shared.is_some()));
let export_all = mode.shared
|| (options.export_dynamic
&& mode.dynamic
&& mode.kind != crate::args::OutputKind::StaticPie);
let (script, patterns) = export::read_scripts(options)?;
Ok(Self {
export_all,
relocatable: options.kind == crate::args::OutputKind::Relocatable,
shared: mode.shared,
script,
patterns,
wrap: options.wrap.iter().map(|w| w.as_bytes().to_vec()).collect(),
})
}
fn wrapped(&self, name: &[u8]) -> bool {
if self.wrap.is_empty() {
return false;
}
let base = name
.strip_prefix(b"__wrap_")
.or_else(|| name.strip_prefix(b"__real_"))
.unwrap_or(name);
self.wrap.iter().any(|w| w == name || w == base)
}
}
fn needed_by_ir<F: crate::elf::read::ElfFormat>(
files: &[ElfInput<'_, F>],
symbols: &SymbolTable<'_>,
resolution: &Resolution<'_>,
) -> Vec<bool> {
let mut needed = vec![false; files.len()];
for (index, file) in files.iter().enumerate() {
let Some(ir) = &file.ir else {
continue;
};
let ids = resolution.symbol_ids(FileId::new(index));
for (use_, &id) in ir.uses.iter().zip(ids) {
if *use_ != (SymbolUse::Reference { weak: false }) {
continue;
}
let def = symbols.definition(id);
if def.kind != DefinitionKind::Shared {
continue;
}
let owner = def.file.index();
let Some(shared) = files.get(owner).and_then(|f| f.shared.as_ref()) else {
continue;
};
let unversioned = shared
.symbols
.get(def.index as usize)
.and_then(|&dynsym| shared.elf.symbol_version(dynsym as usize).ok())
.is_some_and(|version| version.info.is_none());
if unversioned && let Some(slot) = needed.get_mut(owner) {
*slot = true;
}
}
}
needed
}
fn mark_usage<F: crate::elf::read::ElfFormat>(
files: &[ElfInput<'_, F>],
symbols: &SymbolTable<'_>,
resolution: &Resolution<'_>,
) {
files.par_iter().enumerate().for_each(|(index, file)| {
let id = FileId::new(index);
if !resolution.is_live(id) || file.ir.is_some() {
return;
}
let ids = resolution.symbol_ids(id);
if let Some(object) = &file.object {
let table = object.elf.symbols();
for (local, &symbol) in ids.iter().enumerate() {
if !matches!(object.uses.get(local), Some(SymbolUse::Ignore) | None) {
symbols.set_flags(symbol, LTO_REGULAR);
}
if let Some(raw) = local
.checked_add(object.first_global)
.and_then(|i| table.get_raw(i))
&& raw.binding() != STB_LOCAL
&& matches!(raw.visibility(), STV_HIDDEN | STV_INTERNAL)
{
symbols.set_flags(symbol, LTO_HIDDEN);
}
}
for &(local, _) in &object.group_ignored {
if let Some(&symbol) = ids.get(local as usize) {
symbols.set_flags(symbol, LTO_REGULAR);
}
}
} else if let Some(shared) = &file.shared {
for (local, &symbol) in ids.iter().enumerate() {
if !matches!(shared.uses.get(local), Some(SymbolUse::Ignore) | None) {
symbols.set_flags(symbol, LTO_DYNAMIC);
}
}
} else if file.role == InputRole::Internal {
for &symbol in ids {
symbols.set_flags(symbol, LTO_REGULAR);
}
}
});
}
fn file_resolution<F: crate::elf::read::ElfFormat>(
context: &Context,
files: &[ElfInput<'_, F>],
symbols: &SymbolTable<'_>,
resolution: &Resolution<'_>,
handle: usize,
claimed: &ClaimedFile,
) -> FileResolution {
let ids = resolution.symbol_ids(FileId::new(handle));
let values = claimed
.symbols
.iter()
.enumerate()
.map(|(index, symbol)| {
let undefined = symbol.kind.is_undefined();
let Some(&id) = ids.get(index) else {
return if undefined {
SymbolResolution::Undefined
} else {
SymbolResolution::PreemptedRegular
};
};
let def = symbols.definition(id);
let winner = match def.kind {
DefinitionKind::Undefined | DefinitionKind::Lazy => Winner::Undefined,
DefinitionKind::Shared => Winner::Shared,
DefinitionKind::Regular | DefinitionKind::Weak | DefinitionKind::Common => {
if files.get(def.file.index()).is_some_and(|f| f.ir.is_some()) {
Winner::Ir
} else {
Winner::Regular
}
}
};
let prevailing = !undefined
&& winner == Winner::Ir
&& def.file.index() == handle
&& def.index as usize == index;
let flags = symbols.flags(id);
let name = symbols.name(id);
let script_local = context.shared
&& name.version().is_none()
&& context
.script
.as_ref()
.and_then(|script| script.lookup(name.bytes()))
.is_some_and(|(_, local)| local);
let listed = name.version().is_none()
&& context.patterns.iter().any(|p| p.matches(name.bytes()));
symbol_resolution(&SymbolFacts {
undefined,
prevailing,
winner,
regular_ref: flags.contains(LTO_REGULAR),
dynamic_ref: flags.contains(LTO_DYNAMIC),
export_all: context.export_all,
listed,
visibility: if flags.contains(LTO_HIDDEN) {
Visibility::Hidden
} else {
symbol.visibility
},
script_local,
relocatable: context.relocatable,
wrapped: context.wrapped(name.bytes()) || context.wrapped(&symbol.name),
})
})
.collect();
FileResolution::Included(values)
}
pub(super) fn resolve<'a, F: crate::elf::read::ElfFormat>(
options: &LinkOptions,
diagnostics: &dyn DiagnosticSink,
rules: &ElfRules,
inputs: &mut Inputs<'a, F>,
) -> Result<(SymbolTable<'a>, Resolution<'a>, LtoLink)> {
let mut driver = Driver {
options,
diagnostics,
session: None,
claims: 0,
claim_time: Duration::ZERO,
};
let timing = options.timing.as_ref();
let start = Instant::now();
let lap = |what: &str| {
if let Some(timing) = timing {
timing.write_line(&format!(
"qld: lto {what}: {:.1} ms",
start.elapsed().as_secs_f64() * 1000.0
));
}
};
for index in 0..inputs.files.len() {
let Some(file) = inputs.files.get(index) else {
break;
};
if !file.needs_claim() || file.file.is_none() {
continue;
}
let claimed = driver.claim(file, index, false)?;
let Some(file) = inputs.files.get_mut(index) else {
break;
};
match claimed {
Some(symbols) => file.lazy_names = symbols.defined_names(),
None => file.use_native_lazy_names()?,
}
}
let mut symbols = SymbolTable::new();
let resolution = {
let mut hook = ClaimHook {
driver: &mut driver,
comdat: ComdatHook::default(),
};
resolve_symbols_with(&mut symbols, rules, &mut inputs.files, &mut hook)?
};
let Some(mut session) = driver.session.take() else {
return Ok((symbols, resolution, LtoLink::default()));
};
if let Some(timing) = timing {
timing.write_line(&format!(
"qld: lto claims ({} files, plugin loading included): {:.1} ms",
driver.claims,
driver.claim_time.as_secs_f64() * 1000.0
));
}
lap("first resolution");
if !inputs.files.iter().any(|file| file.ir.is_some()) {
return Ok((
symbols,
resolution,
LtoLink {
session: Some(session),
},
));
}
if options.kind != crate::args::OutputKind::Relocatable {
dso::bind_unextracted(&inputs.files, &symbols, &resolution);
let kept = needed_by_ir(&inputs.files, &symbols, &resolution);
let _ = dso::plan_needed_with(&inputs.files, &symbols, rules, &resolution, &kept);
}
mark_usage(&inputs.files, &symbols, &resolution);
let context = Context::new(options, &inputs.files)?;
let files = &inputs.files;
let mut claim = 0usize;
let output = session.all_symbols_read(
|claimed| {
let this = claim;
claim = claim.saturating_add(1);
let handle = usize::try_from(claimed.handle).unwrap_or(usize::MAX);
let current = files
.get(handle)
.and_then(|file| file.ir.as_ref())
.is_some_and(|ir| ir.claim == this);
if !current || !resolution.is_live(FileId::new(handle)) {
return FileResolution::NotIncluded;
}
file_resolution(&context, files, &symbols, &resolution, handle, claimed)
},
diagnostics,
)?;
lap("code generation");
if output.errors > 0 {
return Err(Error::Reported {
errors: output.errors,
});
}
let table = inputs
.files
.first()
.ok_or_else(|| Error::Internal("no internal input file".into()))?
.table();
let objects = output
.files
.iter()
.map(|path| table.load_path(path))
.collect::<Result<Vec<_>>>()?;
let live: Vec<bool> = (0..inputs.files.len())
.map(|index| resolution.is_live(FileId::new(index)))
.collect();
drop(resolution);
drop(symbols);
let old = std::mem::take(&mut inputs.files);
let mut files: Vec<ElfInput<'a, F>> = Vec::with_capacity(old.len());
let mut others = Vec::new();
let mut placed = false;
for (index, mut file) in old.into_iter().enumerate() {
if file.ir.is_some() {
if !placed {
placed = true;
for &id in &objects {
let Some(object) = table.get(id) else {
continue;
};
if !matches!(object.format(), FileFormat::Elf(i) if i.is_relocatable()) {
others.push(id);
continue;
}
session.new_input(
&InputFile::new(
object.path(),
0,
u64::try_from(object.data().len()).unwrap_or(u64::MAX),
u64::try_from(files.len()).unwrap_or(u64::MAX),
),
diagnostics,
)?;
files.push(file.lto_object(object, file.position));
}
}
continue;
}
file.prepare_after_lto(live.get(index).copied().unwrap_or(false));
files.push(file);
}
inputs::add_after_lto(
&mut files,
options,
inputs.target,
&others,
&output.libraries,
&output.library_paths,
)?;
inputs.files = files;
let mut symbols = SymbolTable::new();
let mut comdat = ComdatHook::default();
let resolution = resolve_symbols_with(&mut symbols, rules, &mut inputs.files, &mut comdat)?;
lap("second resolution");
Ok((
symbols,
resolution,
LtoLink {
session: Some(session),
},
))
}
}