關於C語言, GCC/MSVC中,如何寫出一個真正意義上的不依賴庫的程序?

int main()
{
return 0;
}

上面這段代碼其實也是要依賴庫的哈。


要是鏈接的問題。編譯器頂多是配合一下鏈接器。也不是語言本身的問題。

只要你小心翼翼沒有真的用依賴庫裏東西,你總能把程序鏈接為不依賴那些庫的樣子。否則,就是工具鏈的缺陷了。

MSVC 家族的 linker(LINK.EXE)除了 /NODEFAULTLIB 選項外,還能根據程序運行的環境(MSVC 裏叫子系統),通過 /SUBSYSTEM 選項下達必要的指示。LINK.EXE 支持不同運行環境,除了大家常用的的 CONSOLE(命令行界面,例子:nslookup、xcopy)和 WINDOWS(圖形界面程序,例子:記事本、MS Word),還有:

  • NATIVE 不依賴任何 Windows 子系統(例如 Win32),可以用內核 API 和服務。例子:開機登錄界面出現前運行的 chkdsk,各種內核驅動程序。
  • POSIX 無人問津的過時玩意兒。Windows 子系統之一的 POSIX 子系統。
  • BOOT_APPLICATION 這個子系統都不是 Windows 子系統。只是能和 Windows 啟動過程兼容的原生程序。例子:Windows 啟動管理器、Windows 內核載入器、Windows 內存測試程序。

和操作系統完全不沾邊也行:

  • EFI_APPLICATIONEFI_BOOT_SERVICE_DRIVEREFI_ROMEFI_RUNTIME_DRIVER 就是各種 UEFI 程序。

編程語言也不是限定了是 C/C++。C# 也行的。在遷移工具鏈、庫到新平臺等情況下可能發生依賴尚未實現的情況。解決起來都是類似套路。

具體到沒有語言 runtime 的情況,一般是先搞個 dummy type system(Platfrom.cs)。大概就是這種畫風。空著問題也不大的才能叫 intrinsic 嘛。

// https://github.com/dotnet/runtime/blob/master/src/coreclr/src/tools/aot/ILCompiler.TypeSystem.ReadyToRun.Tests/CoreTestAssembly/Platform.cs
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable 649
#pragma warning disable 169

namespace System
{
// Dummy core types to allow us compiling this assembly as a core library so that the type
// system tests dont have a dependency on a real core library.

public class Object
{
internal IntPtr m_pEEType;

public virtual bool Equals(object other)
{
return false;
}

public virtual int GetHashCode()
{
return 0;
}

public virtual string ToString() { return null; }

~Object()
{
}
}

public struct Void { }
public struct Boolean { }
public struct Char { }
public struct SByte { }
public struct Byte { }
public struct Int16 { }
public struct UInt16 { }
public struct Int32 { }
public struct UInt32 { }
public struct Int64 { }
public struct UInt64 { }
public struct IntPtr { }
public struct UIntPtr { }
public struct Single { }
public struct Double { }
public abstract class ValueType { }
public abstract class Enum : ValueType { }
public struct Nullable& where T : struct { }

public sealed class String { }
public abstract class Array : System.Collections.IList { }
public abstract class Delegate { }
public abstract class MulticastDelegate : Delegate { }

public struct RuntimeTypeHandle { }
public struct RuntimeMethodHandle { }
public struct RuntimeFieldHandle { }

public class Attribute { }

public class ThreadStaticAttribute : Attribute { }

public class Array& : Array, System.Collections.Generic.IList& { }

public class Exception { }

public ref struct TypedReference
{
private readonly ByReference& _value;
private readonly RuntimeTypeHandle _typeHandle;
}

public ref struct ByReference& { }
}

namespace System.Collections
{
interface IEnumerable { }

interface ICollection : IEnumerable { }

interface IList : ICollection { }
}

namespace System.Collections.Generic
{
interface IEnumerable& { }

interface ICollection& : IEnumerable& { }

interface IList& : ICollection& { }
}

namespace System.Runtime.InteropServices
{
public enum LayoutKind
{
Sequential = 0, // 0x00000008,
Explicit = 2, // 0x00000010,
Auto = 3, // 0x00000000,
}

public sealed class StructLayoutAttribute : Attribute
{
internal LayoutKind _val;

public StructLayoutAttribute(LayoutKind layoutKind)
{
_val = layoutKind;
}

public LayoutKind Value { get { return _val; } }
public int Pack;
public int Size;
}

public sealed class FieldOffsetAttribute : Attribute
{
private int _val;
public FieldOffsetAttribute(int offset)
{
_val = offset;
}
public int Value { get { return _val; } }
}
}

namespace System.Runtime.CompilerServices
{
public sealed class IsByRefLikeAttribute : Attribute
{
}
}

大多數簡單定義的類型可以直接從 runtime 的源碼中導入(複製粘貼即可)。注意遵守 MIT 協議。複雜或是涉及未實現的依賴,則只留個聲明,定義留空(ref 分支下的源)。你可以根據需要增刪類型和成員。

接下來是平臺或運行環境庫。這裡我們準備一個 Uefi.cs 。因為 C 常常是事實上的 IDL,再不濟也大多能碰上 C/C++ 打頭陣。對於 C 家族的 C# 來說,這是相對容易的。

// (節選)
// 用 using 引入別名
// . . .
using UINT64 = System.UInt64;
using INT64 = System.Int64;
using UINT32 = System.UInt32;
using INT32 = System.Int32;
using UINT16 = System.UInt16;
using CHAR16 = System.Char;
using INT16 = System.Int16;
using BOOLEAN = System.Boolean;
using UINT8 = System.Byte;
using CHAR8 = System.Byte;
using INT8 = System.SByte;
using UINTN = nuint;
using INTN = nint;
using GUID = System.Guid;
using EFI_GUID = System.Guid;
using System;
using System.Runtime.InteropServices;
using IN = System.Runtime.InteropServices.InAttribute;
using OUT = System.Runtime.InteropServices.OutAttribute;
using OPTIONAL = System.Runtime.InteropServices.OptionalAttribute;
// . . .
using static Uefi;
using static EFI_STATUS;
using static EFI_ALLOCATE_TYPE;
using static EFI_KEY_SHIFT_STATE;
using static EFI_KEY_TOGGLE_STATE;
// . . .
static unsafe partial class Uefi {
public static readonly void* NULL;
public const BOOLEAN TRUE = true;
public const BOOLEAN FALSE = false;
}
// . . .
unsafe struct IPv6_ADDRESS {
fixed UINT8 Addr[16];
}
[StructLayout(LayoutKind.Explicit)]
unsafe struct EFI_IP_ADDRESS {
[FieldOffset(0)] public fixed UINT32 Addr[4];
[FieldOffset(0)] public EFI_IPv4_ADDRESS v4;
[FieldOffset(0)] public EFI_IPv6_ADDRESS v6;
}
// . . .
// 也可用 wrapper 替代 typedef
unsafe readonly struct EFI_HANDLE {
readonly void* value__;
EFI_HANDLE(void* value) =&> value__ = value;
public static implicit operator void*(EFI_HANDLE value) =&> value.value__;
public static implicit operator EFI_HANDLE(void* value) =&> new EFI_HANDLE(value);
// . . .
}
// . . .
// 為了更好的文檔支持,還可以對函數指針類型使用更複雜的 wrapper
unsafe readonly struct EFI_INPUT_RESET {
readonly delegate* unmanaged[Cdecl]& value__;
EFI_INPUT_RESET(delegate* unmanaged[Cdecl]& value) =&> value__ = value;
public static implicit operator delegate* unmanaged[Cdecl]&(EFI_INPUT_RESET value) =&> value.value__;
public static implicit operator EFI_INPUT_RESET(delegate* unmanaged[Cdecl]& value) =&> new EFI_INPUT_RESET(value);
// . . .
/// &

/// Reset the input device and optionally run diagnostics
/// &
/// &

Protocol instance pointer.& /// &


/// Driver may perform diagnostics on reset.
/// & /// &
/// &EFI_SUCCESS&
/// The device was reset.&

/// &EFI_DEVICE_ERROR&
/// The device is not functioning properly and could not be reset.&

/// &
public EFI_STATUS Invoke(
[IN] EFI_SIMPLE_TEXT_INPUT_PROTOCOL* This,
[IN] BOOLEAN ExtendedVerification)
=&> value__(This, ExtendedVerification);
}
/// &

/// EFI Boot Services Table.
/// &
unsafe readonly partial struct EFI_BOOT_SERVICES {
/// & /// The table header for the EFI Boot Services Table.
/// &
public readonly EFI_TABLE_HEADER Hdr;
// . . .
//EFI_CREATE_EVENT CreateEvent;
public readonly EFI_CREATE_EVENT CreateEvent;
//EFI_SET_TIMER SetTimer;
public readonly EFI_SET_TIMER SetTimer;
//EFI_WAIT_FOR_EVENT WaitForEvent;
public readonly EFI_WAIT_FOR_EVENT WaitForEvent;
// . . .
// 還能用 void* 填補未有語言綁定的部分。
// EFI_EXIT_BOOT_SERVICES ExitBootServices;
public readonly void* ExitBootServices;
// . . .
}
// . . .
/// & /// EFI System Table
/// &
unsafe readonly partial struct EFI_SYSTEM_TABLE {
/// & /// The table header for the EFI System Table.
/// &
public readonly EFI_TABLE_HEADER Hdr;
/// & /// A pointer to a null terminated string that identifies the vendor
/// that produces the system firmware for the platform.
/// &
public readonly CHAR16* FirmwareVendor;
/// & /// A firmware vendor specific value that identifies the revision
/// of the system firmware for the platform.
/// &
public readonly UINT32 FirmwareRevision;
/// & /// The handle for the active console input device. This handle must support
/// EFI_SIMPLE_TEXT_INPUT_PROTOCOL and EFI_SIMPLE_TEXT_INPUT_EX_PROTOCOL.
/// &
public readonly EFI_HANDLE ConsoleInHandle;
/// & /// A pointer to the EFI_SIMPLE_TEXT_INPUT_PROTOCOL interface that is
/// associated with ConsoleInHandle.
/// &
public readonly EFI_SIMPLE_TEXT_INPUT_PROTOCOL* ConIn;
/// & /// The handle for the active console output device.
/// &
public readonly EFI_HANDLE ConsoleOutHandle;
/// & /// A pointer to the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL interface
/// that is associated with ConsoleOutHandle.
/// &
public readonly EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* ConOut;
/// & /// The handle for the active standard error console device.
/// This handle must support the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL.
/// &
public readonly EFI_HANDLE StandardErrorHandle;
/// & /// A pointer to the EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL interface
/// that is associated with StandardErrorHandle.
/// &
public readonly EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL* StdErr;
/// & /// A pointer to the EFI Runtime Services Table.
/// &
public readonly EFI_RUNTIME_SERVICES* RuntimeServices;
/// & /// A pointer to the EFI Boot Services Table.
/// &
public readonly EFI_BOOT_SERVICES* BootServices;
/// & /// The number of system configuration tables in the buffer ConfigurationTable.
/// &
public readonly UINTN NumberOfTableEntries;
/// & /// A pointer to the system configuration tables.
/// The number of entries in the table is NumberOfTableEntries.
/// &
public readonly EFI_CONFIGURATION_TABLE* ConfigurationTable;
}
// (非必須)根據需要 port 一些庫函數。
// . . .
unsafe partial class BaseMemory {
// 雖然有 boot service,但也能從 SDK 裏抄來別的實現。
/// & /// Copies a source buffer to a destination buffer, and returns the destination buffer.
/// This function copies Length bytes from SourceBuffer to DestinationBuffer, and returns
/// DestinationBuffer. The implementation must be reentrant, and it must handle the case
/// where SourceBuffer overlaps DestinationBuffer.
/// If Length is greater than (MAX_ADDRESS - DestinationBuffer + 1), then ASSERT().
/// If Length is greater than (MAX_ADDRESS - SourceBuffer + 1), then ASSERT().
/// &
/// &

The pointer to the destination buffer of the memory copy.& /// &

The pointer to the source buffer of the memory copy.& /// &

The number of bytes to copy from SourceBuffer to DestinationBuffer.& /// &DestinationBuffer&
[System.Runtime.RuntimeExport(nameof(CopyMem))]
public static void* CopyMem([OUT] void* DestinationBuffer, [IN]/*CONST*/ void* SourceBuffer, [IN] UINTN Length) {
if (Length == 0) {
return DestinationBuffer;
}
//ASSERT((Length - 1) &<= (MAX_ADDRESS - (UINTN)DestinationBuffer)); //ASSERT((Length - 1) &<= (MAX_ADDRESS - (UINTN)SourceBuffer)); if (DestinationBuffer == SourceBuffer) { return DestinationBuffer; } return InternalMemCopyMem(DestinationBuffer, SourceBuffer, Length); } /// &

/// Copy Length bytes from Source to Destination.
/// &
/// &

The target of the copy request.& /// &

The place to copy from.& /// &

The number of bytes to copy.& /// &Destination&
static void* InternalMemCopyMem(
[OUT] void* DestinationBuffer,
[IN] /*CONST*/ void* SourceBuffer,
[IN] UINTN Length) {
//
// Declare the local variables that actually move the data elements as
// volatile to prevent the optimizer from replacing this function with
// the intrinsic memcpy()
//
/*volatile*/
UINT8* Destination8;
/*CONST*/
UINT8* Source8;
/*volatile*/
UINT32* Destination32;
/*CONST*/
UINT32* Source32;
/*volatile*/
UINT64* Destination64;
/*CONST*/
UINT64* Source64;
UINTN Alignment;

if ((((UINTN)DestinationBuffer 0x7) == 0) (((UINTN)SourceBuffer 0x7) == 0) (Length &>= 8)) {
if (SourceBuffer &> DestinationBuffer) {
Destination64 = (UINT64*)DestinationBuffer;
Source64 = (/*CONST*/ UINT64*)SourceBuffer;
while (Length &>= 8) {
*(Destination64++) = *(Source64++);
Length -= 8;
}

// Finish if there are still some bytes to copy
Destination8 = (UINT8*)Destination64;
Source8 = (/*CONST*/ UINT8*)Source64;
while (Length-- != 0) {
*(Destination8++) = *(Source8++);
}
} else if (SourceBuffer &< DestinationBuffer) { Destination64 = (UINT64*)((UINTN)DestinationBuffer + Length); Source64 = (/*CONST*/ UINT64*)((UINTN)SourceBuffer + Length); // Destination64 and Source64 were aligned on a 64-bit boundary // but if length is not a multiple of 8 bytes then they wont be // anymore. Alignment = Length 0x7; if (Alignment != 0) { Destination8 = (UINT8*)Destination64; Source8 = (/*CONST*/ UINT8*)Source64; while (Alignment-- != 0) { *(--Destination8) = *(--Source8); --Length; } Destination64 = (UINT64*)Destination8; Source64 = (/*CONST*/ UINT64*)Source8; } while (Length &> 0) {
*(--Destination64) = *(--Source64);
Length -= 8;
}
}
} else if ((((UINTN)DestinationBuffer 0x3) == 0) (((UINTN)SourceBuffer 0x3) == 0) (Length &>= 4)) {
if (SourceBuffer &> DestinationBuffer) {
Destination32 = (UINT32*)DestinationBuffer;
Source32 = (/*CONST*/ UINT32*)SourceBuffer;
while (Length &>= 4) {
*(Destination32++) = *(Source32++);
Length -= 4;
}

// Finish if there are still some bytes to copy
Destination8 = (UINT8*)Destination32;
Source8 = (/*CONST*/ UINT8*)Source32;
while (Length-- != 0) {
*(Destination8++) = *(Source8++);
}
} else if (SourceBuffer &< DestinationBuffer) { Destination32 = (UINT32*)((UINTN)DestinationBuffer + Length); Source32 = (/*CONST*/ UINT32*)((UINTN)SourceBuffer + Length); // Destination32 and Source32 were aligned on a 32-bit boundary // but if length is not a multiple of 4 bytes then they wont be // anymore. Alignment = Length 0x3; if (Alignment != 0) { Destination8 = (UINT8*)Destination32; Source8 = (/*CONST*/ UINT8*)Source32; while (Alignment-- != 0) { *(--Destination8) = *(--Source8); --Length; } Destination32 = (UINT32*)Destination8; Source32 = (/*CONST*/ UINT32*)Source8; } while (Length &> 0) {
*(--Destination32) = *(--Source32);
Length -= 4;
}
}
} else {
if (SourceBuffer &> DestinationBuffer) {
Destination8 = (UINT8*)DestinationBuffer;
Source8 = (/*CONST*/ UINT8*)SourceBuffer;
while (Length-- != 0) {
*(Destination8++) = *(Source8++);
}
} else if (SourceBuffer &< DestinationBuffer) { Destination8 = (UINT8*)DestinationBuffer + (Length - 1); Source8 = (/*CONST*/ UINT8*)SourceBuffer + (Length - 1); while (Length-- != 0) { *(Destination8--) = *(Source8--); } } } return DestinationBuffer; } // . . . }

還需要一些編譯器 contracts 或 runtime hacks(RuntimeHelpers.cs)。這一部分高度可變。常常要翻冷門資料。

// . . .
namespace System.Runtime {
internal sealed class RuntimeExportAttribute : Attribute {
public RuntimeExportAttribute(string entry) { }
}
}
namespace Internal.Runtime.CompilerHelpers {
using System.Runtime;
class StartupCodeHelpers {
[RuntimeExport("RhpReversePInvoke2")]
static void RhpReversePInvoke2() { }
[RuntimeExport("RhpReversePInvokeReturn2")]
static void RhpReversePInvokeReturn2() { }
[RuntimeExport("__fail_fast")]
static void FailFast() { for (; ; ) { } }
[RuntimeExport("RhpPInvoke")]
static void RhpPInvoke() { }
[RuntimeExport("RhpPInvokeReturn")]
static void RhpPInvokeReturn() { }
}
}
// . . .

然後就可上 helloworld (Program.cs)了。

// 愚蠢的 C# 竟然沒有 #include
// . . .
using System;
using System.Runtime.InteropServices;
using IN = System.Runtime.InteropServices.InAttribute;
using OUT = System.Runtime.InteropServices.OutAttribute;
using OPTIONAL = System.Runtime.InteropServices.OptionalAttribute;
using static BaseMemory;
using static Uefi;
using static EFI_STATUS;
using static EFI_ALLOCATE_TYPE;
using static EFI_KEY_SHIFT_STATE;
using static EFI_KEY_TOGGLE_STATE;
using static UefiEnvironment; // 目前僅捕獲 ImageHandle 和 SystemTable,沒別的用途。
// . . .
unsafe class Program {
static void Main() { } // 沒什麼用,讓 CSC 高興。
static EFI_STATUS LastStatus;
static void SetLastStatus(EFI_STATUS value) =&> LastStatus = value;
static EFI_STATUS GetLastStatus() =&> LastStatus;
static void Initialize(EFI_HANDLE ImageHandle, EFI_SYSTEM_TABLE* SystemTable) {
UefiEnvironment.Initialize(ImageHandle, SystemTable);
}
static void Exit(EFI_STATUS ExitStatus) {
SystemTable-&>BootServices-&>Exit.Invoke(ImageHandle, ExitStatus, 0, null);
}
static void ExitIfError(EFI_STATUS Status) {
if (EFI_ERROR(Status)) {
Exit(Status);
}
}
static void ExitIfError() =&> ExitIfError(LastStatus);
static void WaitForKeyEvent() {
EFI_STATUS Status; UINTN Index;
if (EFI_ERROR(Status = SystemTable-&>BootServices-&>WaitForEvent.Invoke(
1, SystemTable-&>ConIn-&>WaitForKey, Index))) {
SetLastStatus(Status);
}
}
// . . .
[System.Runtime.RuntimeExport(nameof(EfiMain))]
static unsafe EFI_STATUS EfiMain(
[IN] EFI_HANDLE ImageHandle, [IN] EFI_SYSTEM_TABLE* SystemTable) {
Initialize(ImageHandle, SystemTable);
EFI_STATUS Status;
string hello = "Hello, world!
";
fixed (CHAR16* p = hello) {
if (EFI_ERROR(Status = SystemTable-&>ConOut-&>OutputString.Invoke(
SystemTable-&>ConOut, p))) {
return Status;
}
}
string msg = "Press any key to exit this application . . .
";
fixed (CHAR16* p = msg) {
if (EFI_ERROR(Status = SystemTable-&>ConOut-&>OutputString.Invoke(
SystemTable-&>ConOut, p))) {
return Status;
}
}
WaitForKeyEvent();
ExitIfError();
return EFI_SUCCESS;
}
}

好了,開始編譯吧。

csce.exe /debug:embedded /noconfig /nostdlib /runtimemetadataversion:v4.0.30319 *.cs /out:EFIApp.exe /langversion:latest /unsafe

/nostdlib 是關鍵,免得和 dummy type system 衝突。

再 AOT 編譯。首選當然是新版的 Microsoft.Dotnet.ILCompiler (有個同名的舊版相關項目,不要搞混了哦)。可以用 nuget 下載 win-x64 的 runtime 包(runtime.win-x64.microsoft.dotnet.ilcompiler,源目錄: https://dotnetfeed.blob.core.windows.net/dotnet-core/index.json),裡面能找到 ilc.exe 。

ilc.exe EFIApp.exe -o EFIApp.obj --systemmodule EFIApp --map EFIApp.map -O

最後鏈接成可以裸機執行的 UEFI 程序。/subsystem:EFI_APPLICATION 指定了子系統。要用 x64 Native Tools。

link.exe /subsystem:EFI_APPLICATION EFIApp.obj /entry:EfiMain /incremental:no /out:EFIApp.EFI

用 Diskpart 或 DiscUtils 創建鏡像文件,載入到虛擬機裏吧。我在鏡像裏放入了一套 UEFI shell 環境,方便調用。

當然,這只是 helloworld。要是為了能寫出複雜的程序,還得把 StdLib 在新環境裏實現一遍。


通常在你寫一個程序並使用gcc編譯時,gcc會自動將main函數作為__libc_start_main的一個函數指針來調用。而__libc_start_main是由_start函數調用的,_start函數也是Linux操作系統指定的程序入口點,這些工作都由gcc通過鏈接crt1.o提前幫你完成了,因此如果你想寫一個不依賴任何庫的程序,你需要自己實現_start函數。可以參考docker中提供的一個最小的鏡像hello-world,其中就有一段類似hello-world的C語言代碼。

#include &

const char message[] =
"
"
DOCKER_GREETING "
"
"This message shows that your installation appears to be working correctly.
"
"
"
"To generate this message, Docker took the following steps:
"
" 1. The Docker client contacted the Docker daemon.
"
" 2. The Docker daemon pulled the "" DOCKER_IMAGE "" image from the Docker Hub.
"
" (" DOCKER_ARCH ")
"
" 3. The Docker daemon created a new container from that image which runs the
"
" executable that produces the output you are currently reading.
"
" 4. The Docker daemon streamed that output to the Docker client, which sent it
"
" to your terminal.
"
"
"
"To try something more ambitious, you can run an Ubuntu container with:
"
" $ docker run -it ubuntu bash
"
"
"
"Share images, automate workflows, and more with a free Docker ID:
"
" https://hub.docker.com/
"
"
"
"For more examples and ideas, visit:
"
" https://docs.docker.com/get-started/
"
"
";

void _start() {
syscall(SYS_write, 1, message, sizeof(message) - 1);
syscall(SYS_exit, 0);
}

注意為了不依賴任何glibc的庫函數,所有的操作都需要通過系統調用。

參考資料

Linux x86 Program Start Up?

dbp-consulting.com圖標

GCC示例。

選項-nostdlib -nostdinc -fno-builtin

視情況禁用stack protector.

手工ld鏈接目標文件,使用自定義的鏈接腳本。入口點需要和你的入口函數對應(但不需要是main())。至少要收集.text .data .bss三個段,必要的話注意對齊和lma/vma對應。棧怎麼初始化看你的意圖了,這裡先不作假定。

基本思路其實就是不鏈接libc(也不引入對接libc的代碼),以防萬一可以禁用builtin函數。在拋棄這些的情況下,預設的鏈接腳本已經沒法工作(連原先約定好的入口點都不存在了),所以需要定製一下。這樣下來,OS自帶的私貨也可以屏蔽掉。

佈局和邏輯編寫處理得當的話,這種程序可以在用戶態下運行。畢竟鏈接器正常輸出了可執行映像,基礎還是有的。


GCC 的話,你沒法擺脫 libgcc,就算你寫沒有任何外部庫和系統調用的純 C 代碼,也會觸發到 libgcc 的調用,因為並非所有 C 語言代碼都可以一比一完全映射成裸彙編,

比如:

有些平臺沒有整數除法指令(或者連整數乘法指令都沒有),那麼你寫個 x * y ,或者 x / y 的整數運算,在代碼生成階段都可能會調用到 libgcc 裡面的模擬乘除法實現。

比如 32 位平臺進行 64 位的整數運算,加減運算,編譯器可能會生成 inline 的彙編指令,但是乘除法代碼量比較多,本身相對複雜,也許會生成一個函數調用,靠 libgcc 的軟體乘除法來模擬。如果你想在 16 位平臺下進行 32 位整數運算也有類似問題。

還有沒有浮點數運算單元的平臺,都需要靠 libgcc 來提供模擬,你程序裏沒用浮點還好,用了的話可能就會有 libgcc 依賴。

舉個例子,x86 下你的程序使用了 64 位整數運算:

int64_t foo(int64_t x, int64_t y) {
int64_t z = x * y;
int64_t w = x / y;
return (z + w);
}

如果是 32 位下面 -O2 編譯,生成的代碼就是:

movl 48(%esp), %ecx
movl 56(%esp), %ebx
movl 52(%esp), %ebp
movl 60(%esp), %eax
movl %ebx, 8(%esp)
imull %ecx, %eax
movl %ecx, (%esp)
imull %ebx, %ebp
addl %eax, %ebp
movl %ecx, %eax
mull %ebx
movl %eax, %esi
movl 60(%esp), %eax
movl %edx, %edi
addl %ebp, %edi
movl %eax, 12(%esp)
movl 52(%esp), %eax
movl %eax, 4(%esp)
call ___divdi3

你會發現 64 位乘法被展開成兩次 32 位乘法和加法操作了,因為乘法實現代碼量比較短,gcc 選擇直接 inline 指令展開,而後面除法相對複雜,gcc 覺得每個整數除法都 inline 展開的話,太虧了,弄個函數調用吧,所以你看到後面有個 __divdi3 的調用,就是做這個事情的。

你如果編譯時加了 「-nostdlib -fno-builtin -nodefaultlibs」就會報錯,說找不到 __divdi3 函數,

那麼我是不是可以通過避免在32位平臺下使用 64 位整數來去掉 libgcc 的依賴呢?

其實並不能,你不知道 gcc 到底將哪些操作弄成了 inline 指令展開,哪些操作弄成了 libgcc 函數調用,傳統 arm 下沒有整數除法,risc v 標準指令連乘法都沒有,很多嵌入式平臺沒有硬浮點支持。不光是數學運算,還有異常處理,棧操作,都有可能觸發 libgcc 調用。

歸根結底是你完全沒有辦法猜測 gcc 生成代碼時哪些地方調用了 libgcc,哪些地方直接 inline 指令展開,目標平臺不一樣,編譯參數不一樣,結果都不相同

所以你需要記住的是 libgcc 是 gcc 的一部分,即便你固定了目標平臺就是 x86,編譯參數就是這幾個,代碼裏繞著圈子走,盡量不觸碰需要 libgcc 調用的寫法,需要模擬時自己模擬,你以為萬事大吉?

不是,當你升級 gcc 版本時,你就發現全部都掛了。

所以不管你寫嵌入式代碼,操作系統實驗,或者普通應用程序,你可以挑戰 libc,把它的依賴幹掉,但是別去挑戰 libgcc,它是 gcc 的一部分。


操作系統的IO操作基本都是以系統調用的方式提供,一般的libc會封裝好這些操作,假如不鏈接libc的話就需要自己用彙編的方式使用系統調用了,而且c語言提供了asm關鍵字來實現彙編內聯,用彙編實現系統調用和libc裏提供的syscall函數的使用方法大同小異。

我不太會寫彙編,先瞎寫一下,如果有錯誤或者不規範的地方希望各位指證出來。

char msg[] = "hello,world!
";
int write(int fd, void* buf, unsigned int count)
{
register int res __asm__("rax");
asm volatile(
"mov $1,%%rax;"
"syscall;"
:);
return res;
}
static inline void exit(int status)
{
asm volatile(
"mov $60,%%rax;"
"syscall"
:);
}
void _start()
{
write(1, msg, sizeof(msg));
exit(0);
}

然後用gcc xxx.c -nostdlib -nostdinc -nostartfiles -fno-builtin -fno-stack-protector編譯


推薦閱讀:
相關文章