Skip to content

Commit 09dbfc9

Browse files
committed
Added ELF shared library support to LinkImportsTransformation.
Relates to #108
1 parent c2ce051 commit 09dbfc9

File tree

9 files changed

+944
-146
lines changed

9 files changed

+944
-146
lines changed

Biohazrd.Transformation/Biohazrd.Transformation.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
<ItemGroup>
88
<PackageReference Include="Kaisa" Version="0.0.1" />
9+
<PackageReference Include="LibObjectFile" Version="0.3.5" />
910
</ItemGroup>
1011

1112
<ItemGroup>

Biohazrd.Transformation/Common/LinkImportsTransformation.cs

Lines changed: 125 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
using Biohazrd.Transformation.Infrastructure;
22
using Kaisa;
3+
using LibObjectFile.Elf;
34
using System;
45
using System.Collections.Generic;
56
using System.Diagnostics;
67
using System.Diagnostics.CodeAnalysis;
78
using System.IO;
9+
using System.Linq;
810
using System.Text;
911

1012
namespace Biohazrd.Transformation.Common
@@ -46,10 +48,117 @@ public bool TrackVerboseImportInformation
4648
/// <remarks>You generally do not want to enable this option unless you have advanced needs for virtual methods to be exported.</remarks>
4749
public bool ErrorOnMissingVirtualMethods { get; set; }
4850

51+
private static ReadOnlySpan<byte> ElfFileSignature => new byte[] { 0x7F, 0x45, 0x4C, 0x46 }; // 0x7F "ELF"
52+
private static ReadOnlySpan<byte> WindowsArchiveSignature => new byte[] { 0x21, 0x3C, 0x61, 0x72, 0x63, 0x68, 0x3E, 0xA };// "!<arch>\n"
53+
private static int LongestSignatureLength => WindowsArchiveSignature.Length;
4954
public void AddLibrary(string filePath)
5055
{
5156
using FileStream stream = new(filePath, FileMode.Open, FileAccess.Read);
52-
Archive library = new(stream);
57+
58+
// Determine if the library is an ELF shared library or a Windows archive file
59+
Span<byte> header = stackalloc byte[LongestSignatureLength];
60+
if (stream.Read(header) != header.Length)
61+
{ throw new ArgumentException("The specified file is too small to be a library.", nameof(filePath)); }
62+
63+
stream.Position = 0;
64+
65+
if (header.StartsWith(WindowsArchiveSignature))
66+
{
67+
Archive library = new(stream);
68+
69+
// Enumerate all import and export symbols from the package
70+
foreach (ArchiveMember member in library.ObjectFiles)
71+
{
72+
if (member is ImportArchiveMember importMember)
73+
{
74+
SymbolImportExportInfo info = new(importMember);
75+
GetOrCreateSymbolEntry(importMember.Symbol).AddImport(filePath, info);
76+
}
77+
else if (member is CoffArchiveMember coffMember)
78+
{
79+
foreach (CoffSymbol coffSymbol in coffMember.Symbols)
80+
{ GetOrCreateSymbolEntry(coffSymbol.Name).AddExport(filePath); }
81+
}
82+
}
83+
}
84+
else if (header.StartsWith(ElfFileSignature))
85+
{
86+
ElfReaderOptions options = new() { ReadOnly = true };
87+
ElfObjectFile elf = ElfObjectFile.Read(stream, options);
88+
89+
// Determine the name to use for the import
90+
// In theory we could strip off the lib prefix and .so suffix here, however that will remove information required for loading the library via NativeLibrary.Load(string)
91+
// If we want to strip these we should handle it in the output stage instead (that way it's consistent across the entire library too.)
92+
string libraryFileName = Path.GetFileName(filePath);
93+
94+
// Find the .dynsym table
95+
// (We do not bother with the symbol hash table because we'll most likely end up needing nearly every symbol as it is and it simplifies our architecture.)
96+
const string sectionName = ".dynsym";
97+
ElfSymbolTable? symbolTable = elf.Sections.OfType<ElfSymbolTable>().FirstOrDefault(s => s.Name == sectionName);
98+
99+
if (symbolTable is null)
100+
{ throw new ArgumentException($"The specified ELF file does not contain a '{sectionName}' section."); }
101+
102+
// See https://refspecs.linuxbase.org/elf/gabi4+/ch4.symtab.html for details on what the symbol table entires mean
103+
// Note that the spec is not very specific about what is and isn't allowed in the dynamic symbol table, so we're probably overly defensive here
104+
// In general we ignore symbols which have OS and architecture-specific types or binding since we can't safely understand what they're for.
105+
foreach (ElfSymbol symbol in symbolTable.Entries)
106+
{
107+
// Ignore unnamed symbols
108+
if (String.IsNullOrEmpty(symbol.Name))
109+
{ continue; }
110+
111+
// Ignore symbols which are not publically accessible
112+
// (Default effectively means public)
113+
if (symbol.Visibility != ElfSymbolVisibility.Default && symbol.Visibility != ElfSymbolVisibility.Protected)
114+
{ continue; }
115+
116+
// Ignore symbols which are undefined
117+
if (symbol.Section.IsSpecial && symbol.Section.SpecialIndex == ElfNative.SHN_UNDEF)
118+
{ continue; }
119+
120+
// Ignore symbols which are not functions or objects (variables)
121+
if (symbol.Type != ElfSymbolType.Function && symbol.Type != ElfSymbolType.Object)
122+
{ continue; }
123+
124+
// Ignore symbols which are not global or weak
125+
// Note that we do not actually differentiate between global and weak symbols, the distinction only matters for static linking
126+
// See https://www.bottomupcs.com/libraries_and_the_linker.xhtml#d0e10440
127+
if (symbol.Bind != ElfSymbolBind.Global && symbol.Bind != ElfSymbolBind.Weak)
128+
{ continue; }
129+
130+
// Determine the import type of the symbol based on its section
131+
ImportType importType;
132+
switch (symbol.Section.Section?.Name)
133+
{
134+
case ".text":
135+
importType = ImportType.Code;
136+
break;
137+
case ".rodata":
138+
importType = ImportType.Const;
139+
break;
140+
case ".data":
141+
case ".bss":
142+
importType = ImportType.Data;
143+
break;
144+
case null when symbol.Section.IsSpecial:
145+
Debug.Fail($"ELF symbol from special section '{symbol.Section}'");
146+
continue;
147+
default:
148+
if (symbol.Section.Section?.Name is not null)
149+
{ Debug.Fail($"ELF symbol from unrecognized section '{symbol.Section.Section.Name}'"); }
150+
else
151+
{ Debug.Fail($"ELF symbol from unnamed/null section."); }
152+
continue;
153+
}
154+
155+
// Add the symbol to our lookup
156+
SymbolImportExportInfo info = new(libraryFileName, symbol, importType);
157+
GetOrCreateSymbolEntry(symbol.Name).AddImport(filePath, info);
158+
}
159+
}
160+
else
161+
{ throw new ArgumentException("The specified file does not appear to be in a compatible format.", nameof(filePath)); }
53162

54163
SymbolEntry GetOrCreateSymbolEntry(string symbol)
55164
{
@@ -61,21 +170,6 @@ SymbolEntry GetOrCreateSymbolEntry(string symbol)
61170
}
62171
return symbolEntry;
63172
}
64-
65-
// Enumerate all import and export symbols from the package
66-
foreach (ArchiveMember member in library.ObjectFiles)
67-
{
68-
if (member is ImportArchiveMember importMember)
69-
{
70-
SymbolImportExportInfo info = new(importMember);
71-
GetOrCreateSymbolEntry(importMember.Symbol).AddImport(filePath, info);
72-
}
73-
else if (member is CoffArchiveMember coffMember)
74-
{
75-
foreach (CoffSymbol coffSymbol in coffMember.Symbols)
76-
{ GetOrCreateSymbolEntry(coffSymbol.Name).AddExport(filePath); }
77-
}
78-
}
79173
}
80174

81175
private bool Resolve(string symbolName, [NotNullWhen(true)] out string? dllFileName, [NotNullWhen(true)] out string? mangledName, ref DiagnosticAccumulator diagnosticsAccumulator, bool isFunction, bool isVirtualMethod)
@@ -106,7 +200,9 @@ static string MakeErrorMessage(string message, SymbolEntry symbolEntry)
106200

107201
foreach ((string library, SymbolImportExportInfo info) in symbolEntry.Sources)
108202
{
109-
if (info.IsImport)
203+
if (info.IsFromElf)
204+
{ builder.Append($"\n '{library}'"); } // The library and the DllFileName are the same for ELF sources, avoid printing the redundant info.
205+
else if (info.IsImport)
110206
{ builder.Append($"\n '{library}': Import from '{info.DllFileName}'"); }
111207
else
112208
{ builder.Append($"\n '{library}': Statically-linked export'"); }
@@ -273,6 +369,7 @@ private readonly struct SymbolImportExportInfo
273369
public ImportNameType ImportNameType { get; }
274370
public string? DllFileName { get; }
275371
public ushort OrdinalOrHint { get; }
372+
public bool IsFromElf { get; }
276373

277374
public SymbolImportExportInfo(ImportArchiveMember importMember)
278375
{
@@ -281,6 +378,17 @@ public SymbolImportExportInfo(ImportArchiveMember importMember)
281378
ImportNameType = importMember.ImportHeader.NameType;
282379
DllFileName = importMember.Dll;
283380
OrdinalOrHint = importMember.ImportHeader.OrdinalOrHint;
381+
IsFromElf = false;
382+
}
383+
384+
public SymbolImportExportInfo(string libraryFileName, ElfSymbol symbol, ImportType importType)
385+
{
386+
IsImport = true;
387+
ImportNameType = ImportNameType.Name; // ELFs do not have ordinal members
388+
DllFileName = libraryFileName;
389+
OrdinalOrHint = 0;
390+
ImportType = importType;
391+
IsFromElf = true;
284392
}
285393

286394
public bool IsEquivalentTo(SymbolImportExportInfo other)

THIRD-PARTY-NOTICES.md

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ Biohazrd incorporates third-party libraries licensed as follows.
55
- [ClangSharp](#clangsharp)
66
- [ClangSharp.Pathogen](#clangsharppathogen)
77
- [Kaisa the Sharp Librarian](#kaisa-the-sharp-librarian)
8+
- [LibObjectFile](#libobjectfile)
89
- [The LLVM Project](#the-llvm-project)
910

1011
<!-- /TOC -->
@@ -82,6 +83,34 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
8283
SOFTWARE.
8384
```
8485

86+
# LibObjectFile
87+
88+
```
89+
Copyright (c) 2019, Alexandre Mutel
90+
All rights reserved.
91+
92+
Redistribution and use in source and binary forms, with or without modification
93+
, are permitted provided that the following conditions are met:
94+
95+
1. Redistributions of source code must retain the above copyright notice, this
96+
list of conditions and the following disclaimer.
97+
98+
2. Redistributions in binary form must reproduce the above copyright notice,
99+
this list of conditions and the following disclaimer in the documentation
100+
and/or other materials provided with the distribution.
101+
102+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
103+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
104+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
105+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
106+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
107+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
108+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
109+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
110+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
111+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
112+
```
113+
85114
# The LLVM Project
86115

87116
```
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
using System;
2+
using System.ComponentModel;
3+
using System.Diagnostics;
4+
using System.IO;
5+
6+
namespace Biohazrd.Tests.Common
7+
{
8+
public static partial class LlvmTools
9+
{
10+
private static string? ClangPath = null;
11+
private static Exception? LlvmToolchainRootLocationFailureException = null;
12+
13+
private static string? TryFindClang(out Exception? exception)
14+
{
15+
if (ClangPath is not null)
16+
{
17+
exception = null;
18+
return ClangPath;
19+
}
20+
21+
if (LlvmToolchainRootLocationFailureException is not null)
22+
{
23+
exception = LlvmToolchainRootLocationFailureException;
24+
return null;
25+
}
26+
27+
// It's not super clear if Win32Exception.NativeErrorCode is actually errno on Unix-like systems when Process.Start fails due to a missing executable,
28+
// but it doesn't actually matter since ERROR_FILE_NOT_FOUND and ENOENT are both 2.
29+
const int ERROR_FILE_NOT_FOUND = 2;
30+
31+
// Check if Clang is present on the system PATH
32+
try
33+
{
34+
using Process clang = Process.Start("clang", "--version");
35+
clang.WaitForExit();
36+
37+
if (clang.ExitCode == 0)
38+
{
39+
exception = null;
40+
return ClangPath = "clang";
41+
}
42+
else
43+
{
44+
exception = new Exception("The Clang install found on the system PATH appears to be non-functional.");
45+
LlvmToolchainRootLocationFailureException = exception;
46+
return null;
47+
}
48+
}
49+
catch (Win32Exception ex) when (ex.NativeErrorCode == ERROR_FILE_NOT_FOUND)
50+
{ exception = new FileNotFoundException("Clang was not found on the system PATH."); }
51+
catch (Exception ex)
52+
{
53+
exception = new Exception($"The Clang install found on the system PATH appears to be unusable: {ex.Message}.", ex);
54+
LlvmToolchainRootLocationFailureException = exception;
55+
return null;
56+
}
57+
58+
// Find Clang from Visual Studio if the appropriate component is installed
59+
if (OperatingSystem.IsWindows())
60+
{
61+
try
62+
{
63+
// The other LLVM-related component (Microsoft.VisualStudio.Component.VC.Llvm.ClangToolset) is only for using clang-cl for building C++ MSBuild projects.
64+
VisualStudioLocator locator = new("Microsoft.VisualStudio.Component.VC.Llvm.Clang");
65+
string visualStudioRoot = locator.LocateVisualStudio();
66+
string visualStudioClangPath = Path.Combine(visualStudioRoot, "VC", "Tools", "Llvm", "bin", "clang.exe");
67+
68+
if (!File.Exists(visualStudioClangPath))
69+
{ throw new FileNotFoundException("Visual Studio install claims to have LLVM toolchain but clang.exe was not found.", visualStudioClangPath); }
70+
71+
exception = null;
72+
return ClangPath = visualStudioClangPath;
73+
}
74+
catch (Exception ex)
75+
{
76+
if (exception is not null)
77+
{ exception = new AggregateException(exception, ex); }
78+
else
79+
{ exception = ex; }
80+
}
81+
}
82+
83+
// Clang is not installed
84+
LlvmToolchainRootLocationFailureException = exception;
85+
return null;
86+
}
87+
88+
public static Exception? IsClangAvailable()
89+
{
90+
string? clangPath = TryFindClang(out Exception? exception);
91+
Debug.Assert(exception is not null || clangPath is not null);
92+
return exception;
93+
}
94+
95+
public static string GetClangPath()
96+
{
97+
string? clangPath = TryFindClang(out Exception? exception);
98+
99+
if (exception is not null)
100+
{
101+
Debug.Assert(clangPath is null);
102+
throw exception;
103+
}
104+
105+
Debug.Assert(clangPath is not null);
106+
return clangPath;
107+
}
108+
}
109+
}

0 commit comments

Comments
 (0)