web-dev-qa-db-ja.com

アプリケーションバイナリインターフェイス(ABI)とは何ですか?

ABIが何であるかを明確に理解できませんでした。ウィキペディアの記事を教えてはいけません。私がそれを理解できれば、私はここにそのような長い投稿を投稿しません。

これは、さまざまなインターフェイスに関する私の考え方です。

テレビのリモコンは、ユーザーとテレビの間のインターフェイスです。これは既存のエンティティですが、それ自体では役に立ちません(機能を提供しません)。リモコンのこれらの各ボタンのすべての機能は、テレビに実装されています。

Interface:これは、その機能のfunctionalityconsumerの間の「既存のエンティティ」層です。インターフェース自体は何もしません。背後にある機能を呼び出すだけです。

現在、ユーザーが誰であるかに応じて、さまざまなタイプのインターフェースがあります。

Command Line Interface(CLI)コマンドは既存のエンティティであり、コンシューマはユーザーであり、機能は背後にあります。

functionality:このインターフェイスを説明する目的を解決するソフトウェア機能。

existing entities:コマンド

consumer:ユーザー

グラフィカルユーザーインターフェイス(GUI)ウィンドウ、ボタンなどは既存のエンティティであり、消費者はユーザーであり、機能は背後にあります。

functionality:このインターフェイスについて説明している問題を解決するソフトウェア機能。

existing entities:ウィンドウ、ボタンなど。

consumer:ユーザー

Application Programming Interface(API)関数(より正確には)インターフェイス(インターフェイスベースのプログラミング)は既存のエンティティであり、ここのコンシューマは別のプログラムではありませんユーザー、そして再び機能がこのレイヤーの背後にあります。

functionality:このインターフェイスについて説明している問題を解決するソフトウェア機能。

existing entities:関数、インターフェイス(関数の配列)。

consumer:別のプログラム/アプリケーション。

Application Binary Interface(ABI)ここからが私の問題の始まりです。

functionality: ???

existing entities: ???

consumer: ???

  • さまざまな言語でソフトウェアを作成し、さまざまな種類のインターフェイス(CLI、GUI、およびAPI)を提供しましたが、ABIを提供したことがあるかどうかはわかりません。

ウィキペディアによると:

ABIは次のような詳細をカバーします

  • データ型、サイズ、および配置。
  • 関数の引数がどのように渡され、戻り値が取得されるかを制御する呼び出し規約。
  • システムコール番号と、アプリケーションがオペレーティングシステムに対してシステムコールを行う方法。

他のABIは次のような詳細を標準化します

  • c ++名のマングリング、
  • 例外伝播、および
  • 同じプラットフォーム上のコンパイラー間の呼び出し規則ですが、クロスプラットフォームの互換性は必要ありません。
  • 誰がこれらの詳細を必要としますか? OSとは言わないでください。アセンブリプログラミングを知っています。リンクと読み込みの仕組みを知っています。私は内部で何が起こるかを正確に知っています。

  • なぜC++の名前のマングリングが入ったのですか?バイナリレベルで話していると思います。なぜ言語が入ってくるのですか?

とにかく、私は [PDF] System V Application Binary InterfaceEdition 4.1(1997-03-18) をダウンロードして、その内容を確認しました。まあ、それのほとんどは意味がありませんでした。

  • ELF ファイル形式を説明する2つの章(4番目と5番目)が含まれているのはなぜですか?実際、これらはこの仕様の重要な2つの章にすぎません。残りの章は「プロセッサ固有」です。とにかく、私はそれが完全に異なるトピックだと思います。 ELFファイル形式の仕様ABIであると言わないでください。定義によると、インターフェイスになる資格はありません。

  • 私たちは非常に低いレベルで話しているので、非常に具体的でなければなりません。しかし、「命令セットアーキテクチャ(ISA)」にどのように特有なのかわかりません。

  • Microsoft WindowsのABIはどこにありますか?

だから、これらは私を悩ませている主要なクエリです。

398
claws

「ABI」を理解する簡単な方法の1つは、「API」と比較することです。

APIの概念についてはすでにご存じでしょう。ライブラリやOSなどの機能を使用する場合は、APIを使用します。 APIは、外部コンポーネントの機能にアクセスするためにコードで使用できるデータ型/構造、定数、関数などで構成されています。

ABIは非常に似ています。 APIのコンパイル済みバージョン(または機械語レベルのAPI)と考えてください。ソースコードを記述するとき、APIを介してライブラリにアクセスします。コードがコンパイルされると、アプリケーションはABIを介してライブラリ内のバイナリデータにアクセスします。 ABIは、コンパイルされたアプリケーションが(APIと同様に)外部ライブラリにアクセスするために使用する構造とメソッドを、より低いレベルでのみ定義します。

ABIは、外部ライブラリを使用するアプリケーションに関して重要です。特定のライブラリを使用するようにプログラムがビルドされ、そのライブラリが後で更新される場合、そのアプリケーションを再コンパイルする必要はありません(エンドユーザーの観点からは、ソースがない場合があります)。更新されたライブラリが同じABIを使用している場合、プログラムを変更する必要はありません。ライブラリへのインターフェイス(プログラムが本当に気にするすべて)は、内部の動作が変更されている場合でも同じです。同じABIを持つ2つのバージョンのライブラリは、同じ低レベルインターフェイスを持っているため、「バイナリ互換」と呼ばれることがあります(古いバージョンを新しいバージョンに置き換えることができ、大きな問題はありません)。

ABIの変更が避けられない場合があります。この場合、そのライブラリを使用するプログラムは、新しいバージョンのライブラリを使用するように再コンパイルしない限り機能しません。 ABIが変更されてもAPIが変更されない場合、古いライブラリバージョンと新しいライブラリバージョンは「ソース互換」と呼ばれることがあります。これは、あるライブラリバージョン用にコンパイルされたプログラムは他のライブラリバージョンでは動作しないが、一方のライブラリバージョン用に記述されたソースコードは、再コンパイルされると他方で動作することを意味します。

このため、ライブラリ作成者は、ABIを安定させようとする傾向があります(混乱を最小限に抑えるため)。 ABIを安定に保つとは、関数インターフェイス(戻りの型と数、型、および引数の順序)、データ型またはデータ構造の定義、定義済み定数などを変更しないことを意味します。新しい関数とデータ型を追加できますが、既存のものはそのままにしておく必要があります同じ。たとえば、16ビットのデータ構造フィールドを32ビットのフィールドに展開すると、そのデータ構造を使用するコンパイル済みのコードは、そのフィールド(またはそれに続くもの)に正しくアクセスできなくなります。データ構造体のメンバーへのアクセスは、コンパイル中にメモリアドレスとオフセットに変換されます。データ構造が変更された場合、これらのオフセットは、コードがそれらが指すと期待しているものを指しておらず、結果はせいぜい予測不能です。

ABIは、Assemblyを使用してコードとのやり取りを期待しない限り、必ずしも明示的に提供するものではありません。 (たとえば)CアプリケーションとPascalアプリケーションは、コンパイル後に同じABIを使用するため、言語固有ではありません。

Edit:SysV ABI docsのELFファイル形式に関する章に関する質問について:この情報が含まれる理由は、ELF形式がオペレーティングシステムとアプリケーション間のインターフェース。 OSにプログラムを実行するように指示すると、プログラムが特定の方法でフォーマットされ、(たとえば)バイナリの最初のセクションが特定のメモリオフセットで特定の情報を含むELFヘッダーであると想定されます。これは、アプリケーションが自身に関する重要な情報をオペレーティングシステムに伝達する方法です。非ELFバイナリ形式(a.outやPEなど)でプログラムをビルドすると、ELF形式のアプリケーションを想定しているOSは、バイナリファイルを解釈したり、アプリケーションを実行したりできなくなります。これは、1つのバイナリ形式から別の形式に変換できるエミュレーションレイヤーの種類で再コンパイルまたは実行せずに、WindowsアプリをLinuxマシンで直接(またはその逆に)実行できない大きな理由の1つです。

IIRC、Windowsは現在 Portable Executable (またはPE)形式を使用しています。そのWikipediaページの「外部リンク」セクションには、PE形式に関する詳細情報へのリンクがあります。

また、C++の名前のマングリングに関する注意事項について:ABIは、互換性のために名前のマングリングを行うC++コンパイラの「標準化された」方法を定義できます。つまり、ライブラリを作成し、そのライブラリを使用するプログラムを開発する場合は、私とは異なるコンパイラーを使用でき、異なる名前マングリングスキームのために結果のバイナリーに互換性がないことを心配する必要はありません。これは、新しいバイナリファイル形式を定義している場合、またはコンパイラまたはリンカーを記述している場合にのみ実際に使用されます。

428
bta

アセンブリとOSレベルでの動作を知っている場合、特定のABIに準拠しています。 ABIは、戻り値が配置されるパラメーターの受け渡し方法などを管理します。多くのプラットフォームでは、選択できるABIは1つだけであり、そのような場合、ABIは「物事の仕組み」にすぎません。

ただし、ABIは、C++でのクラス/オブジェクトの配置方法なども管理します。これは、モジュールの境界を越えてオブジェクト参照を渡したい場合、または異なるコンパイラでコンパイルされたコードを混在させる場合に必要です。

また、32ビットバイナリを実行できる64ビットOSを使用している場合、32ビットコードと64ビットコードに対して異なるABIがあります。

一般に、同じ実行可能ファイルにリンクするコードは同じABIに準拠する必要があります。異なるABIを使用してコード間で通信する場合は、何らかの形式のRPCまたはシリアル化プロトコルを使用する必要があります。

さまざまな種類のインターフェイスを固定された特性セットに絞り込もうとしています。たとえば、インターフェイスを必ずしもコンシューマとプロデューサに分割する必要はありません。インターフェイスは、2つのエンティティが相互作用するための単なる規則です。

ABIは(部分的に)ISAに依存しない可能性があります。一部の側面(呼び出し規約など)はISAに依存しますが、他の側面(C++クラスレイアウトなど)は依存しません。

適切に定義されたABIは、コンパイラを作成する人々にとって非常に重要です。適切に定義されたABIがなければ、相互運用可能なコードを生成することは不可能です。

編集:明確にするための注意事項:

  • ABIの「バイナリ」は、文字列またはテキストの使用を除外しません。 C++クラスをエクスポートするDLLをリンクする場合は、そのどこかにメソッドと型シグネチャをエンコードする必要があります。そこでC++の名前マングリングが登場します。
  • ABIを提供しなかった理由は、大多数のプログラマーがABIを提供しないことです。 ABIは、プラットフォーム(つまり、オペレーティングシステム)を設計する同じ人によって提供され、広く使用されているABIを設計する特権を持つプログラマーはほとんどいません。
124
JesperE

実際にdo n't ABIが必要な場合if--

  • あなたのプログラムには機能がありません、そして
  • あなたのプログラムは、単独で実行されている単一の実行可能ファイル(つまり、組み込みシステム)であり、文字通り唯一実行されているものであり、他の何かと話す必要はありません。

簡略化された要約:

API:"ここに呼び出し可能なすべての関数があります。"

ABI:"これはhow関数を呼び出す。 "

ABIは、適切に機能するようにプログラムをコンパイルするためにコンパイラーとリンカーが従う一連の規則です。 ABIは複数のトピックをカバーしています:

  • おそらく、ABIの最大かつ最も重要な部分は、 プロシージャ呼び出し標準 であり、「呼び出し規約」としても知られています。呼び出し規約は、「関数」がアセンブリコードに変換される方法を標準化します。
  • ABIは、ライブラリ内の公開された関数のnamesをどのように表現して、他のコードがそれらのライブラリを呼び出し、どの引数を渡すべきかを知る方法も指示します。これは「名前マングリング」と呼ばれます。
  • また、ABIは、使用できるデータ型の種類、それらの配置方法、およびその他の低レベルの詳細を指示します。

ABIの中核であると考える呼び出し規約をさらに詳しく見てみましょう。

マシン自体には「機能」という概念はありません。 cのような高水準言語で関数を記述すると、コンパイラは_MyFunction1:のようなアセンブリコードの行を生成します。これはlabelであり、最終的にはアセンブラによってアドレスに解決されます。このラベルは、アセンブリコードの「関数」の「開始」を示します。高レベルコードでは、その関数を「呼び出す」ときに、実際に実行しているのは、CPUにそのラベルのアドレスにjumpを実行させ、そこで実行を継続することです。

ジャンプの準備として、コンパイラーは多くの重要なことをしなければなりません。呼び出し規則はチェックリストのようなもので、コンパイラーはこのすべてを行うために従います。

  • 最初に、コンパイラはアセンブリコードを少し挿入して現在のアドレスを保存します。これにより、「機能」が完了すると、CPUは適切な場所に戻って実行を継続できます。
  • 次に、コンパイラは引数を渡すアセンブリコードを生成します。
    • いくつかの呼び出し規約では、引数をスタックに配置するよう指示しています(特定の順序で)。
    • 他の規則では、引数は特定のレジスタに配置する必要があります(もちろん、データ型に依存)。
    • さらに他の規則では、スタックとレジスタの特定の組み合わせを使用する必要があります。
  • もちろん、以前にこれらのレジスタに重要なものがあった場合、それらの値は上書きされて永久に失われるため、呼び出し規約によっては、引数を入れる前にコンパイラがこれらのレジスタの一部を保存するように指示する場合があります。
  • これでコンパイラーは、以前に作成したラベル(_MyFunction1:)に移動するようCPUに指示するジャンプ命令を挿入します。この時点で、CPUは「機能」にあると見なすことができます。
  • 関数の最後に、コンパイラーはCPUが戻り値を正しい場所に書き込むようにするアセンブリコードを配置します。呼び出し規約により、戻り値を特定のレジスター(そのタイプに応じて)に入れるか、スタックに入れるかが決まります。
  • これで、クリーンアップの時間です。呼び出し規約により、コンパイラがクリーンアップアセンブリコードを配置する場所が決まります。
    • 一部の規則では、呼び出し元はスタックをクリーンアップする必要があるとしています。これは、「関数」が実行され、CPUが以前の場所にジャンプした後、実行される次のコードは非常に具体的なクリーンアップコードであることを意味します。
    • 他の慣習では、クリーンアップコードの特定の部分は「関数」の最後にあるべきであると述べていますbeforeジャンプバック。

多くの異なるABI /呼び出し規約があります。主なものは次のとおりです。

  • X86またはx86-64 CPU(32ビット環境)の場合:
    • CDECL
    • STDCALL
    • ファーストコール
    • ベクトルコール
    • シックル
  • X86-64 CPU(64ビット環境)の場合:
    • SYSTEMV
    • MSNATIVE
    • ベクトルコール
  • ARM CPU(32ビット)の場合
    • AAPCS
  • ARM CPU(64ビット)の場合
    • AAPCS64

ここ は、異なるABI用にコンパイルするときに生成されるアセンブリの違いを実際に示す素晴らしいページです。

もう1つ言及することは、ABIは関連性があるだけではないということですinsideプログラムの実行可能モジュール。プログラムがライブラリ関数を正しく呼び出すようにするために、リンカによって使用されるalsoコンピューター上で複数の共有ライブラリを実行しており、コンパイラーが使用しているABIを知っている限り、スタックを爆破することなくそれらから適切に関数を呼び出すことができます。

ライブラリ関数を呼び出す方法を理解するコンパイラは、非常に重要です。ホストされたプラットフォーム(つまり、OSがプログラムを読み込むプラットフォーム)では、カーネル呼び出しを行わずにプログラムを点滅させることさえできません。

27
Lakey

アプリケーションバイナリインターフェイス(ABI)はAPIに似ていますが、関数はソースコードレベルで呼び出し元にアクセスできません。バイナリ表現のみがアクセス可能/利用可能です。

ABIは、プロセッサアーキテクチャレベルまたはOSレベルで定義できます。 ABIは、コンパイラのコード生成フェーズが従う標準です。標準は、OSまたはプロセッサのいずれかによって修正されます。

機能性:実装言語または特定のコンパイラ/リンカー/ツールチェーンから独立して関数呼び出しを行うためのメカニズム/標準を定義します。 JNIやPython-Cインターフェイスなどを許可するメカニズムを提供します。

既存のエンティティ:マシンコード形式の機能。

コンシューマー:別の関数(別の言語の関数、別のコンパイラーによってコンパイルされた関数、または別のリンカーによってリンクされた関数を含む)。

17
alvin

機能:コンパイラ、アセンブリライター、リンカ、およびオペレーティングシステムに影響する一連のコントラクト。コントラクトは、関数がどのように配置されるか、パラメーターが渡される場所、パラメーターが渡される方法、関数がどのように機能するかを指定します。これらは一般に(プロセッサアーキテクチャ、オペレーティングシステム)タプルに固有です。

既存のエンティティ:パラメータレイアウト、関数セマンティクス、レジスタ割り当て。たとえば、ARMアーキテクチャには多数のABIがあります(APCS、EABI、GNU-EABI、多くの歴史的なケースを気にしないでください)-混合ABIを使用すると、境界を越えて呼び出すときにコードが単に機能しなくなります。

コンシューマ:コンパイラ、アセンブリライター、オペレーティングシステム、CPU固有のアーキテクチャ。

誰がこれらの詳細を必要としますか?コンパイラ、アセンブリライター、コード生成(またはアライメント要件)、オペレーティングシステム(割り込み処理、syscallインターフェイス)を行うリンカー。アセンブリプログラミングを行った場合、ABIに準拠していました!

C++名前マングリングは特別なケースです-リンカーと動的リンカーを中心とした問題-名前マングリングが標準化されていない場合、動的リンクは機能しません。以降、C++ ABIはC++ ABIと呼ばれます。これはリンカーレベルの問題ではなく、コード生成の問題です。 C++バイナリを取得したら、ソースから再コンパイルせずに別のC++ ABI(名前のマングリング、例外処理)と互換性を持たせることはできません。

ELFは、ローダーと動的リンカーを使用するためのファイル形式です。 ELFはバイナリコードとデータのコンテナ形式であり、コードのABIを指定します。 PE実行可能ファイルはABIではないため、厳密な意味でELFはABIであるとは考えません。

すべてのABIは命令セット固有です。 ARM ABIは、MSP430またはx86_64プロセッサでは意味がありません。

WindowsにはいくつかのABIがあります。たとえば、fastcallとstdcallは2つの一般的な使用ABIです。 syscall ABIもまた異なります。

10
Yann Ramin

少なくともあなたの質問の一部に答えさせてください。 Linux ABIがシステムコールにどのように影響するかの例と、それがなぜ有用か。

システムコールは、ユーザー空間プログラムがカーネル空間に何かを尋ねる方法です。これは、特定のレジスタに呼び出しと引数の数値コードを入れて、割り込みをトリガーすることで機能します。カーネルスペースへの切り替えが発生し、カーネルが数値コードと引数を検索し、リクエストを処理し、結果をレジスタに戻し、ユーザースペースへの切り替えをトリガーします。これは、たとえば、アプリケーションがメモリを割り当てたりファイルを開いたりするときに必要です(syscallsは "brk"および "open")。

現在、システムコールには「brk」などの短い名前と対応するオペコードがあり、これらはシステム固有のヘッダーファイルで定義されています。これらのオペコードが同じである限り、再コンパイルすることなく、異なる更新されたカーネルで同じコンパイル済みユーザーランドプログラムを実行できます。したがって、プリコンパイルされたバイナリ、つまりABIで使用されるインターフェイスがあります。

7
snies

ABIとAPIを区別する最善の方法は、それが何のために使用されているのかを知ることです。

X86-64には通常1つのABIがあります(x86 32ビットには別のセットがあります):

http://www.x86-64.org/documentation/abi.pdf

https://developer.Apple.com/library/mac/documentation/DeveloperTools/Conceptual/LowLevelABI/140-x86-64_Function_Calling_Conventions/x86_64.html

http://people.freebsd.org/~obrien/AMD64-elf-abi.pdf

Linux + FreeBSD + MacOSXは、若干のバリエーションがあります。また、Windows x64には独自のABIがあります。

http://eli.thegreenplace.net/2011/09/06/stack-frame-layout-on-x86-64/

ABIを知っていて、他のコンパイラもそれに続くと仮定すると、バイナリは理論的には互いに呼び出し(特にライブラリAPI)を知っており、スタックまたはレジスタなどによってパラメータを渡します。または、関数を呼び出すとどのレジスタが変更されるか基本的に、これらの知識はソフトウェアが互いに統合するのに役立ちます。レジスタ/スタックレイアウトの順序がわかれば、アセンブリに記述されたさまざまなソフトウェアを問題なく簡単につなぎ合わせることができます。

ただし、APIは異なります。

これは、引数が定義された高レベルの関数名であり、これらのAPIを使用して異なるソフトウェアの一部がビルドされた場合、相互に呼び出すことができます。ただし、SAME ABIの追加要件を順守する必要があります。

たとえば、以前はWindowsはPOSIX APIに準拠していました。

https://en.wikipedia.org/wiki/Windows_Services_for_UNIX

https://en.wikipedia.org/wiki/POSIX

そしてLinuxもPOSIXに準拠しています。ただし、バイナリを単に移動してすぐに実行することはできません。ただし、POSIX準拠のAPIで同じ名前を使用しているため、Cで同じソフトウェアを使用し、別のOSで再コンパイルして、すぐに実行できます。

APIは、ソフトウェアの統合を容易にするためのものです-プリコンパイル段階。そのため、コンパイル後、ソフトウェアはまったく異なるように見えることがあります-ABIが異なる場合

ABIは、バイナリ/アセンブリレベルでソフトウェアの正確な統合を定義するためのものです。

4
Peter Teoh

共有ライブラリ内のコードを呼び出したり、コンパイルユニット間でコードを呼び出したりするには、オブジェクトファイルに呼び出しのラベルを含める必要があります。 C++は、データの非表示を強制し、オーバーロードされたメソッドを許可するために、メソッドラベルの名前をマングルします。同じABIを明示的にサポートしない限り、異なるC++コンパイラのファイルを混在させることはできません。

3
Justin Smith

概要

ABI(アプリケーションバイナリインターフェイス)を定義する正確なレイヤーには、さまざまな解釈と強い意見があります。

私の見解では、ABIは特定のAPIの特定/プラットフォームと見なされるものの主観的な慣習です。 ABIは、特定のAPIに対して「変更しない」、またはランタイム環境(エグゼキューター、ツール、リンカー、コンパイラー、jvm、およびOS)によって対処される規則の「残り」です。

インターフェースの定義:ABI、API

Joda-timeのようなライブラリを使用する場合は、joda-time-<major>.<minor>.<patch>.jarへの依存関係を宣言する必要があります。ライブラリはベストプラクティスに従い、 Semantic Versioning を使用します。これにより、3つのレベルでAPIの互換性が定義されます。

  1. パッチ-コードをまったく変更する必要はありません。ライブラリはいくつかのバグを修正するだけです。
  2. マイナー-追加後にコードを変更する必要はありません
  3. メジャー-インターフェイス(API)が変更され、コードの変更が必要になる場合があります。

同じライブラリの新しいメジャーリリースを使用するために、他の多くの規則が引き続き尊重されます。

  • ライブラリに使用されるバイナリ言語(Javaの場合、Javaバイトコードを定義するJVMターゲットバージョン)
  • 呼び出し規約
  • JVMの規則
  • リンク規則
  • 実行時の規則これらはすべて、使用するツールによって定義および管理されます。

Javaのケーススタディ

たとえば、Javaは、ツールではなく正式なJVM仕様でこれらすべての規則を標準化しました。この仕様により、他のベンダーは互換性のあるライブラリを出力できる異なるツールセットを提供できました。

Javaには、ABIに関する2つの興味深いケーススタディがあります。Scalaバージョンと Dalvik 仮想マシンです。

Dalvik仮想マシンがABIを破った

Dalvik VMには、Javaバイトコードとは異なるタイプのバイトコードが必要です。 Dalvikライブラリは、Javaバイトコードを(同じAPIを使用して)Dalvikに変換することにより取得されます。このようにして、元のjoda-time-1.7.2.jarで定義された同じAPIの2つのバージョンを取得できます。 joda-time-1.7.2.jarおよびjoda-time-1.7.2-dalvik.jarと呼ぶことができます。スタック指向の標準Java vms:Oracleのもの、IBMのもの、open Javaまたはその他のABIを使用します。 2番目のABIはDalvikの周りのものです。

Scalaの連続リリースには互換性がありません

Scalaには、マイナーScalaバージョン:2.X間のバイナリ互換性がありません。このため、同じAPI "io.reactivex" %% "rxscala"% "0.26.5"には3つのバージョンがあります(将来的にはさらに):Scala 2.10、2.11、および2.12用。変更点 今のところわかりません 、しかしバイナリは互換性がありません。おそらく最新バージョンでは、ライブラリを古い仮想マシンで使用できなくするもの、おそらくリンク/命名/パラメーターの規則に関連するものが追加されます。

Javaの連続したリリースには互換性がありません

Javaには、JVMのメジャーリリースである4,5,6,7,8,9にも問題があります。下位互換性のみを提供します。 Jvm9は他のすべてのバージョンに対してコンパイル/ターゲット(javacの-targetオプション)を実行する方法を知っていますが、JVM 4はJVM 5をターゲットにしたコードを実行する方法を知りません。この非互換性は、さまざまなソリューションのおかげでレーダーの下を飛びます:

  1. セマンティックバージョニング:ライブラリがより高いJVMを対象とする場合、通常はメジャーバージョンを変更します。
  2. JVM 4をABIとして使用すれば、安全です。
  3. Java 9では、特定のターゲットJVMのバイトコードを同じライブラリに含める方法に関する仕様が追加されています。

なぜAPI定義から始めたのですか?

APIとABIは、互換性の定義方法に関する単なる規則です。下位層は、多数の高レベルのセマンティクスに関して汎用的です。そのため、いくつかの規則を簡単に作成できます。最初の種類の規則は、メモリアライメント、バイトエンコーディング、呼び出し規則、ビッグエンディアンエンコーディングとリトルエンディアンエンコーディングなどに関するものです。さらに、他の記述、リンク規則、 中間バイトコード のような実行可能な規則を取得しますJavaが使用するものや、GCCが使用するLLVM IRなど。 3番目に、ライブラリの検索方法、それらのロード方法に関する規則を取得します(Javaクラスローダーを参照)。概念がどんどん高くなるにつれて、与えられたものとみなす新しい規則があります。 セマンティックバージョニング に達していないのはそのためです。 majorバージョンでは暗黙的または折りたたみです。 <major>-<minor>-<patch>-<platform/ABI>を使用してセマンティックバージョニングを修正できます。これは実際にすでに起こっていることです:プラットフォームはすでにrpmdlljar(JVMバイトコード)、war(jvm + webサーバー)、apk2.11(特定のScala)バージョンなどです。 APKを言うとき、APIの特定のABIの部分について既に話します。

APIは別のABIに移植できます

抽象化の最上位レベル(最高のAPIに対して記述されたソースは、他の下位レベルの抽象化に再コンパイル/移植できます。

Rxscalaのソースがあるとしましょう。 Scalaツールが変更された場合、それらを再コンパイルできます。 JVMが変更された場合、高レベルの概念に煩わされることなく、古いマシンから新しいマシンに自動的に変換できます。移植は難しいかもしれませんが、他のクライアントには役立ちます。まったく異なるアセンブラーコードを使用して新しいオペレーティングシステムを作成する場合、トランスレーターを作成できます。

言語間で移植されたAPI

reactive streams のような複数の言語に移植されたAPIがあります。一般に、特定の言語/プラットフォームへのマッピングを定義します。 APIは、人間の言語または特定のプログラミング言語で正式に定義されたマスター仕様であると主張します。他のすべての「マッピング」はある意味でABIであり、それ以外は通常のABIよりも多くのAPIです。 RESTインターフェースでも同じことが起こります。

3
raisercostin

私もABIを理解しようとしていましたが、JesperEの答えはとても役に立ちました。

非常に単純な観点から、バイナリ互換性を考慮してABIを理解しようとする場合があります。

KDE wikiは、ライブラリをバイナリ互換として定義します。「以前のバージョンのライブラリに動的にリンクされたプログラムが、再コンパイルすることなく新しいバージョンのライブラリで実行し続ける場合」。動的リンクの詳細については、 静的リンク動的リンク

次に、ライブラリにバイナリ互換性を持たせるために必要な最も基本的な側面のみを見てみましょう(ライブラリにソースコードの変更がない場合)。

  1. 同じ/下位互換性のある命令セットアーキテクチャ(プロセッサ命令、レジスタファイル構造、スタック構成、メモリアクセスタイプ、サイズ、レイアウト、およびプロセッサが直接アクセスできる基本データタイプのアライメント)
  2. 同じ呼び出し規約
  3. 同じ名前のマングリング規則(FortranプログラムがC++ライブラリ関数を呼び出す必要がある場合に必要になることがあります)。

確かに、他にも多くの詳細がありますが、これは主にABIもカバーしています。

より具体的には、あなたの質問に答えるために、上記から推測できます:

ABI機能:バイナリ互換性

既存のエンティティ:既存のプログラム/ライブラリ/ OS

消費者:ライブラリ、OS

お役に立てれば!

1
blue_whale

用語ABIは、2つの異なるが関連する概念を指すために使用されます。

コンパイラについて話すときは、ソースレベルの構造からバイナリ構造への変換に使用される規則を指します。データ型はどのくらいですか?スタックはどのように機能しますか?関数にパラメーターを渡す方法は?どのレジスタを呼び出し元と呼び出し先で保存する必要がありますか?

ライブラリについて話すときは、コンパイルされたライブラリによって提示されるバイナリインターフェイスを指します。このインターフェイスは、ライブラリのソースコード、コンパイラが使用するルール、場合によっては他のライブラリから選択された定義など、多くの要因の結果です。

ライブラリを変更すると、APIを壊さずにABIを壊す可能性があります。たとえば、次のようなインターフェースを備えたライブラリを考えます。

void initfoo(FOO * foo)
int usefoo(FOO * foo, int bar)
void cleanupfoo(FOO * foo)

アプリケーションプログラマは次のようなコードを記述します

int dostuffwithfoo(int bar) {
  FOO foo;
  initfoo(&foo);
  int result = usefoo(&foo,bar)
  cleanupfoo(&foo);
  return result;
}

アプリケーションプログラマはFOOのサイズやレイアウトを気にしませんが、アプリケーションバイナリはfooのハードコードされたサイズで終わります。ライブラリプログラマがfooに余分なフィールドを追加し、誰かが古いライブラリバイナリで新しいライブラリバイナリを使用する場合、ライブラリはメモリアクセスの範囲外になる可能性があります。

ライブラリの作成者がAPIを次のように設計した場合、OTOH。

FOO * newfoo(void)
int usefoo(FOO * foo, int bar)
void deletefoo((FOO * foo, int bar))

アプリケーションプログラマは次のようなコードを記述します

int dostuffwithfoo(int bar) {
  FOO * foo;
  foo = newfoo();
  int result = usefoo(&foo,bar)
  deletefoo(&foo);
  return result;
}

その場合、アプリケーションバイナリはFOOの構造について何も知る必要がなく、ライブラリ内にすべて隠すことができます。ただし、その代償として、ヒープ操作が関係します。

1
plugwash

ABIは、呼び出しが成功することを確認するために、呼び出し元と呼び出し先の間で一貫している必要があります。スタックの使用、レジスタの使用、ルーチン終了時のスタックポップ。これらはすべて、ABIの最も重要な部分です。

アプリケーションバイナリインターフェイス(ABI)

機能性:

  • プログラマーのモデルから基礎となるシステムのドメインデータ型、サイズ、アライメント、呼び出し規約への変換。これは、関数の引数の受け渡し方法と戻り値の取得方法を制御します。システムコール番号と、アプリケーションがオペレーティングシステムに対してシステムコールを行う方法。高レベル言語コンパイラの名前マングリングスキーム、例外伝播、および同じプラットフォーム上のコンパイラ間の呼び出し規則。ただし、クロスプラットフォームの互換性は必要ありません...

既存のエンティティ:

  • プログラムの実行に直接関与する論理ブロック:ALU、汎用レジスタ、I/Oのメモリ/ I/Oマッピング用レジスタなど.

消費者:

  • 言語プロセッサリンカー、アセンブラ...

これらは、ビルドツールチェーンが全体として機能することを保証する必要がある人が必要とします。あるモジュールをアセンブリ言語で記述し、別のモジュールをPythonで記述し、独自のブートローダーではなくオペレーティングシステムを使用したい場合、「アプリケーション」モジュールは「バイナリ」境界を越えて機能し、そのような「インターフェース」の同意が必要です。

さまざまな高水準言語のオブジェクトファイルをアプリケーションにリンクする必要があるため、C++の名前マングリング。 Visual C++で構築されたWindowsへのシステムコールを行うGCC標準ライブラリの使用を検討してください。

JVMは他のアイデアを持っているかもしれませんが、ELFは解釈のためのオブジェクトファイルからのリンカーの1つの可能な期待です。

Windows RTストアアプリの場合、ビルドツールチェーンを連携させたい場合は、ARM ABIを検索してみてください。

1
Chawathe Vipul

要するに、哲学では、kindの物だけがうまくいくことができ、ABIはkindのソフトウェアのものが一緒に動作するものとして見ることができます。

1
smwikipedia

Linux共有ライブラリの最小実行可能ABIの例

共有ライブラリのコンテキストでは、「安定したABIを持つ」ことの最も重要な意味は、ライブラリの変更後にプログラムを再コンパイルする必要がないことです。

たとえば、次のとおりです。

  • 共有ライブラリを販売している場合、新しいリリースごとにライブラリに依存するすべてを再コンパイルするという煩わしさをユーザーに与えません

  • ユーザーのディストリビューションにある共有ライブラリに依存するクローズドソースプログラムを販売している場合、ターゲットOSの特定のバージョンでABIが安定していることが確実であれば、少ないビルド済みをリリースしてテストできます。

    これは、システム内の多くのプログラムがリンクしているC標準ライブラリの場合に特に重要です。

次に、これの最小限の具体的な実行可能な例を提供します。

main.c

#include <assert.h>
#include <stdlib.h>

#include "mylib.h"

int main(void) {
    mylib_mystruct *myobject = mylib_init(1);
    assert(myobject->old_field == 1);
    free(myobject);
    return EXIT_SUCCESS;
}

mylib.c

#include <stdlib.h>

#include "mylib.h"

mylib_mystruct* mylib_init(int old_field) {
    mylib_mystruct *myobject;
    myobject = malloc(sizeof(mylib_mystruct));
    myobject->old_field = old_field;
    return myobject;
}

mylib.h

#ifndef MYLIB_H
#define MYLIB_H

typedef struct {
    int old_field;
} mylib_mystruct;

mylib_mystruct* mylib_init(int old_field);

#endif

以下を使用してコンパイルおよび実行します。

cc='gcc -pedantic-errors -std=c89 -Wall -Wextra'
$cc -fPIC -c -o mylib.o mylib.c
$cc -L . -shared -o libmylib.so mylib.o
$cc -L . -o main.out main.c -lmylib
LD_LIBRARY_PATH=. ./main.out

ここで、ライブラリのv2について、mylib_mystructという新しいフィールドをnew_fieldに追加するとします。

次のようにold_fieldの前にフィールドを追加した場合:

typedef struct {
    int new_field;
    int old_field;
} mylib_mystruct;

そして、main.outではなくライブラリを再構築すると、アサートは失敗します!

これは、次の行が原因です。

myobject->old_field == 1

構造体の最初のintにアクセスしようとするAssemblyを生成しました。これは、予想されるnew_fieldではなくold_fieldになりました。

したがって、この変更によりABIが破損しました。

ただし、new_fieldの後にold_fieldを追加する場合:

typedef struct {
    int old_field;
    int new_field;
} mylib_mystruct;

その後、古い生成されたアセンブリは引き続き構造体の最初のintにアクセスし、プログラムは引き続き動作します。これは、ABIが安定しているためです。

GitHubのこの例の完全に自動化されたバージョン です。

このABIを安定させる別の方法は、mylib_mystructopaque struct として扱い、メソッドヘルパーを介してそのフィールドにのみアクセスすることです。これにより、ABIを安定させやすくなりますが、より多くの関数呼び出しを行うとパフォーマンスのオーバーヘッドが発生します。

API vs ABI

前の例では、new_fieldold_fieldの前に追加すると、ABIのみが破損し、APIは破損しなかったことに注意するのは興味深いことです。

これが意味することは、ライブラリに対してmain.cプログラムを再コンパイルしていれば、それは関係なく動作するということです。

ただし、たとえば関数のシグネチャを変更した場合は、APIも破損します。

mylib_mystruct* mylib_init(int old_field, int new_field);

その場合、main.cはコンパイルを完全に停止します。

セマンティックAPI vsプログラミングAPI

APIの変更は、セマンティックの変更という3番目のタイプに分類することもできます。

通常、セマンティックAPIは、APIが行うことを想定した自然言語の記述であり、通常はAPIドキュメントに含まれています。

したがって、プログラムビルド自体を壊すことなく、セマンティックAPIを壊すことができます。

たとえば、変更した場合

myobject->old_field = old_field;

に:

myobject->old_field = old_field + 1;

そうすれば、プログラミングAPIもABIも壊れませんでしたが、セマンティックAPIはmain.c壊れます。

プログラムでコントラクトAPIをチェックする方法は2つあります。

  • 多数のコーナーケースをテストします。簡単ですが、いつでも見逃す可能性があります。
  • 正式な検証 。困難ですが、ドキュメントとテストを「人間」/機械検証可能な方法に本質的に統合し、正確性の数学的証明を生成します!もちろん、正式な説明にバグがない限り;-)

C/C++共有ライブラリABIを破壊するすべてのリスト

TODO:究極のリストを見つける/作成する:

Javaの最小限の実行可能な例

Javaのバイナリ互換性とは?

Ubuntu 18.10、GCC 8.2.0でテスト済み。