ELF 共用資料庫重新導向

ELF 共用物件規定

在 Linux 和 Fuchsia 等以 ELF 為基礎的系統上,進入共用物件的機器碼 (即 GN 語言中的 shared_library()loadable_module() 目標) 必須使用 -fPIC 編譯器和連結器選項建構。

這與可執行程式碼不同,後者會改用 -fPIE。這會產生比 -fPIC 更小且更快的程式碼,但無法用於共用物件。

Fuchsia 建構作業不支援為 Linux 產生共用程式庫,但 Fuchsia 會定義個別的工具鍊執行個體,用於編譯可執行檔和共用程式庫。

這個獨立的工具鍊在內部建構規則中稱為「shlib」工具鍊,甚至是「隨附工具鍊」,一律會透過在基本工具鍊標籤後方附加 -shared 後置字串來命名。舉例來說,//build/toolchain/fuchsia:x64-shared 是基本 //build/toolchain/fuchsia:x64 工具鍊的 shlib 工具鍊。

ELF 共用程式庫重新導向

Fuchsia 建構作業會實作一項功能,確保 ELF shared_library()loadbable_module() 目標一律會以共用程式庫工具鍊建構,如上一節所述。

Fuchsia 開發人員不需要瞭解這項資訊,因為這完全是透明的:照常編寫 shared_library() 目標,完全不必擔心 ELF / 非 ELF。

本節的其餘部分將說明實作方式,首先需要瞭解為何需要這項功能,並提供幾個實務範例:

以可連結至執行檔或共用程式庫物件的 C++ 靜態程式庫 (例如 libutil) 為例,由於這兩種二進位檔都需要使用不同的編譯器標記建構程式碼,因此其中一種做法是定義兩個目標,如下所示:

  # A variant of the library to be linked into executables.
  static_library("libutil-static") {
    sources = [ ... ]
    ...
  }

  # A variant of the library to be linked into shared libraries.
  static_library("libutil-shared") {
    sources = [ ... ]
    ...
    if (is_fuchsia) {
      cflags = [ "-fPIC" ]  # Required for shared library code on Fuchsia.
    }
  }

  executable('program') {
    sources = [ ... ]
    deps = [ ":libutil-static" ]
  }

  shared_library('foo') {
    sources = [ ... ]
    deps = [ ":libutil-shared" ]
  }

這樣做雖然可行,但有許多不便之處:

  • 程式庫需要兩個目標定義,而非一個,且必須保持同步。即使不建構 Fuchsia 二進位檔也是如此。

  • 您必須在每個共用變數定義中加入明確的 is_fuchsia 檢查和編譯器標記,這很細微且容易出錯,也會讓這些定義變得不夠抽象。

    (部分內容可透過 GN 設定簡化,但仍有許多內容)。

  • 使用程式庫的任何目標都必須明確選取其中一個變體。

為了讓情況更貼近現實,假設 libutil 程式庫也依附於另一個 liblog 靜態資料庫。後者也需要提供靜態和共用變體,如下所示:

  static_library("liblog-static") {
    sources = [ ... ]
    ...
  }

  static_library("liblog-shared") {
    sources = [ ... ]
    if (is_fuchsia) {
      cflags = [ "-fPIC" ]
    }
  }

  static_library("libutil-static") {
    ...
    deps = [ ":liblog-static" ]
  }

  static_library("libutil-shared") {
    ...
    if (is_fuchsia) {
      cflags = [ "-fPIC" ]  # Required for shared library code.
    }
    deps = [ ":liblog-shared" ]
  }

  ... same as above

以下依附元件圖表說明此情況:

  program ->
      libutil-static ->
          liblog-static

  foo ->
      libutil-shared ->
          liblog-shared

讓所有這些目標定義和依附元件保持適當同步,既繁瑣又會使建構規則的抽象程度和實用性大幅降低。

使用專屬工具鍊建構 ELF 共用物件程式碼,可避免 libutil 及其依附元件的目標重複。例如:

# The following definition is global and should normally be put
# in the BUILDCONFIG.gn file

if (is_fuchsia) {
  # Name of the toolchain used to build ELF shared library code.
  # This toolchain adds the `-fPIC` option to all build commands
  # by default so there is no need to add it in target definitions.
  shlib_toolchain = "${current_toolchain}-shared"
} else {
  # Shared library code can be built directly in the current toolchain
  # on non-Fuchsia platforms.
  shlib_toolchain = current_toolchain
}

# The following is part of a BUILD.gn file

static_library("liblog") {
  ...
}

static_library("libutil") {
  ...
  deps = [ ":liblog" ]
}

executable('program') {
  sources = [ ... ]
  deps = [ ":libutil" ]
}

shared_library("foo") {
  sources = [ ... ]
  deps = [ ":libutils($shlib_toolchain)" ]
}

現在對應的依附元件圖如下:

  program ->
      libutil ->
          liblog

  foo ->
      libutil($shlib_toolchain) ->
          liblog($shlib_toolchain)

上述配置可解決大多數原始問題,原因如下:

  • 明確的 is_fuchsia 檢查和 -fPIC 編譯器旗標新增作業,已完全從靜態資料庫定義中抽象化。

  • libutil 目標不需要擔心要選取哪個依附元件變體。

另一方面,每個 shared_library() 執行個體仍需從 shlib_toolchain 中仔細選取靜態資料庫和來源集依附元件,才能連結至適當的程式碼變體。

Fuchsia 建構作業會使用最後一個技巧解決這個最後一個問題:在基本工具鍊中使用重新導向群組目標,參照 shblib 工具鍊中的實際共用程式庫目標,如下所示:

# Set to true if the current toolchain's target platform is based
# on ELF and requires an shlib toolchain. This would normally be
# defined in BUILDCONFIG.gn for Fuchsia toolchains.
_requires_shlib_toolchain = ...

if (_requires_shlib_toolchain && current_toolchain != shlib_toolchain) {
  # A simple group that depends on the same target built in
  # the shblib_toolchain. Note that `public_deps` instead of `deps`
  # is required when crossing toolchain boundaries for proper
  # linking.
  group("bar") {
    public_deps = [ ":bar($shlib_toolchain)" ]
  }
} else {
  # The target that actually builds the shared library in
  # the shlib_toolchain, or the base one for non-Fuchsia platforms.
  # It will pick its dependencies in the same toolchain context.
  shared_library("bar") {
   ...
   deps = [ ":libutil" ]
  }
}

executable("program2") {
  deps = [ ":bar" ]
}

這會在 Fuchsia 上建立下列依附元件圖表:

  program2 -->
      bar -->                           # redirection group
          bar(shblib_toolchain) -->     # real shared_library()
              libutil(shlib_toolchain)

非 Fuchsia 基礎工具鍊則會取得:

  program2 -->
      bar -->                           # real shared_library()
          libutil

為進一步簡化使用方式,Fuchsia 建構會在 BUILDCONFIG.gn 中重新定義 shared_library() 範本,向 BUILD.gn 檔案隱藏這項實作詳細資料,讓檔案能以最自然的方式編寫,例如:

### This would appear in BUILDCONFIG.gn to ensure that `shared_library()`
### does ELF shared library redirection automatically when needed.

if (_requires_shlib_toolchain) {
  template("shared_library") {
    if (current_toolchain != shlib_toolchain) {
      group(target_name) {
        public_deps = [ ":${target_name}(${shlib_toolchain})" ]
      }
    } else {
      # Ensure the built-in shared_library() function is called.
      target("shared_library", target_name) {
        forward_variables_from(invoker, "*")
      }
    }
  }
}

### This would appear in regular BUILD.gn files

# Invokes the custom `shared_library()` template instead
# of the built-in one. On Fuchsia systems, this will create a
# redirection group that refers to a real shared_library()
# target in the shlib_toolchain. On a non-Fuchsia system, this
# simply defines a real shared_library() target.

shared_library("bar") {
  ...
  deps = [ ":libutil" ]
}

executable("program2") {
  ...
  deps = [ ":bar" ]
}

詳情請參閱 BUILDCONFIG.gn 中的 shared_library() 定義