これがそうであるように見えるプラットフォームで動作する絶対的な最低限のものは、
.globl main
main:
pushl $.LC0
call puts
addl $4, %esp
xorl %eax, %eax
ret
.LC0:
.string "Hello world"
しかし、これは多くの ABI 要件に違反しています。 ABI 準拠プログラムの最小要件は
.globl main
.type main, @function
main:
subl $24, %esp
pushl $.LC0
call puts
xorl %eax, %eax
addl $28, %esp
ret
.size main, .-main
.section .rodata
.LC0:
.string "Hello world"
オブジェクトファイル内の他のすべては、コンパイラがコードを可能な限り最適化していないか、オプションのいずれかです オブジェクト ファイルに書き込まれる注釈。
.cfi_*
特に、ディレクティブはオプションの注釈です。それらは必要です C++ 例外がスローされたときに関数がコール スタックにある可能性があるが、有用である場合に限ります。 スタック トレースを抽出する可能性がある任意のプログラムで。自明ではないコードをアセンブリ言語で手動で作成する場合は、その作成方法を学習する価値があります。残念ながら、それらは文書化されていません。現在、リンクする価値があると思われるものは見つかりません。
ライン
.section .note.GNU-stack,"",@progbits
アセンブリ言語を手で書いているかどうかについても知っておくことが重要です。これは別のオプションの注釈ですが、価値のある注釈です。これは、「このオブジェクト ファイルにはスタックを実行可能にする必要がない」ことを意味するためです。プログラム内のすべてのオブジェクト ファイルにこの注釈がある場合、カーネルはスタックを実行可能にしないため、セキュリティが少し向上します。
(あなたがすることを示すために スタックを実行可能にする必要がある場合は、"x"
と入力します ""
の代わりに . 「ネストされた関数」拡張機能を使用する場合、GCC はこれを行うことがあります。 (そうしないでください。))
GCC および GNU binutils によって (デフォルトで) 使用される "AT&T" アセンブリ構文には、3 種類の行があることに言及する価値があるでしょう:コロンで終わる単一のトークンを持つ行は、ラベルです。 (ラベルに表示できる文字のルールは覚えていません。) 最初の行 トークンはドットで始まり、ドットではありません コロンで終わる、アセンブラへのある種の指示です。それ以外はアセンブリ命令です。
関連:GCC/clang アセンブリ出力から「ノイズ」を除去する方法 .cfi
ディレクティブは直接役に立たず、プログラムはディレクティブがなくても機能します。 (例外処理とバックトレースに必要なスタックアンワインド情報なので、-fomit-frame-pointer
デフォルトで有効にできます。はい、gcc は C に対してもこれを発行します。)
価値のある Hello World プログラムを生成するために必要な asm ソース行の数に関する限り、明らかに libc 関数を使用してより多くの作業を行いたいと考えています。
@Zwolの答えには、元のCコードの最短の実装があります。
手作業でできること 、プログラムの終了ステータスを気にしない場合は、文字列を出力するだけです.
# Hand-optimized asm, not compiler output
.globl main # necessary for the linker to see this symbol
main:
# main gets two args: argv and argc, so we know we can modify 8 bytes above our return address.
movl $.LC0, 4(%esp) # replace our first arg with the string
jmp puts # tail-call puts.
# you would normally put the string in .rodata, not leave it in .text where the linker will mix it with other functions.
.section .rodata
.LC0:
.asciz "Hello world" # asciz zero-terminates
同等の C (同じセマンティクスを持つものではなく、最短の Hello World を要求しただけです):
int main(int argc, char **argv) {
return puts("Hello world");
}
その終了ステータスは実装定義ですが、間違いなく出力されます。 puts(3)
0..255 の範囲外の可能性がある「負でない数値」を返すため、Linux でプログラムの終了ステータスが 0 / 非ゼロであることについては何も言えません (プロセスの終了ステータスが低い 8 である場合)。 exit_group()
に渡される整数のビット システム コール (この場合は、main() を呼び出した CRT スタートアップ コードによる)。
JMP を使用して末尾呼び出しを実装することは標準的な方法であり、別の関数が戻った後に関数が何もする必要がない場合によく使用されます。 puts()
最終的に main()
を呼び出した関数に戻ります puts() が main() に戻り、次に main() が戻った場合と同様です。 main() の呼び出し元は、main() のスタックに置かれた引数を処理する必要があります。それらはまだそこにあるためです (ただし、変更されており、それを行うことが許可されています)。
gcc と clang は、スタック上の arg-passing スペースを変更するコードを生成しません。ただし、完全に安全で ABI に準拠しています。関数は、const
であっても、スタック上の引数を「所有」します。 .関数を呼び出す場合、スタックに置いた引数がまだそこにあると想定することはできません。同じまたは類似の引数で別の呼び出しを行うには、それらをすべて再度保存する必要があります。
また、これは puts()
を呼び出すことに注意してください main()
へのエントリと同じスタック配置で 、したがって、x86-32 別名 i386 System V ABI (Linux で使用) の最新バージョンで必要な 16B アライメントを維持する点で、ABI に準拠しています。
.string
.asciz
と同じように、文字列をゼロで終了します。 、しかし、私はそれを調べて確認しなければなりませんでした。 .ascii
を使用することをお勧めします または .asciz
データに終了バイトがあるかどうかを明確にするため。 ( write()
のような明示的な長さの関数で使用する場合は必要ありません )
x86-64 System V ABI (および Windows) では、引数はレジスタで渡されます。これにより、引数を再配置したり more を渡すことができるため、末尾呼び出しの最適化がはるかに簡単になります。 args (レジスタが不足しない限り)。これにより、コンパイラは実際にそれを喜んで実行します。 (私が言ったように、彼らは現在、スタック上の着信引数スペースを変更するコードを生成することを好まないため、ABI は許可されていることが明確であり、コンパイラによって生成された関数は、呼び出し先がスタック引数を上書きすると想定しています。 .)
ゴッドボルト コンパイラ エクスプローラーで確認できるように、clang または gcc -O3 は x86-64 に対してこの最適化を行います :
#include <stdio.h>
int main() { return puts("Hello World"); }
# clang -O3 output
main: # @main
movl $.L.str, %edi
jmp puts # TAILCALL
# Godbolt strips out comment-only lines and directives; there's actually a .section .rodata before this
.L.str:
.asciz "Hello World"
静的データ アドレスは常にアドレス空間の下位 31 ビットに収まり、実行可能ファイルは位置に依存しないコードを必要としません。それ以外の場合は mov
lea .LC0(%rip), %rdi
になります . (--enable-default-pie
で構成されている場合、gcc からこれを取得します 位置に依存しない実行可能ファイルを作成します。)
関数またはラベルのアドレスを GNU アセンブラーのレジスターにロードする方法
32 ビット x86 Linux int 0x80
を使用した Hello World システムコールを直接呼び出し、libc は使用しない
Linux システム コールを使用したアセンブリ言語の Hello, world を参照してください。 そこにある私の答えは、もともと SO Docs 用に書かれたもので、SO Docs が閉鎖されたときにそれを置く場所としてここに移動しました。それは実際にはここに属していなかったので、別の質問に移動しました.
関連:Linux用の非常に小さなELF実行可能ファイルの作成に関する旋風のチュートリアル。 exit() システム コールを実行するだけの実行可能な最小のバイナリ ファイル。それはバイナリ サイズを最小化することであり、ソース サイズや実際に実行される命令の数だけではありません。