web-dev-qa-db-ja.com

GNUアセンブラを使用してx86_64でprintfを呼び出す

GNUアセンブラで使用するためにAT&T構文を使用してプログラムを作成しました:

            .data
format:   .ascii "%d\n"  

            .text
            .global main  
main:
            mov    $format, %rbx
            mov    (%rbx), %rdi
            mov    $1, %rsi
            call     printf
            ret

[〜#〜] gcc [〜#〜]を使用してアセンブルおよびリンクします:

gcc -o main main.s

次のコマンドで実行します。

。/メイン

プログラムを実行すると、セグメンテーション違反が発生します。 gdbを使用すると、printf not foundと表示されます。動作しない「.extern printf」を試しました。誰かがprintfを呼び出す前にスタックポインタを保存し、[〜#〜] ret [〜#〜]の前に復元することを提案しましたが、どうすればよいですか?

9
L's World

このコードにはいくつかの問題があります。 Linuxで使用される AMD64 System V ABI 呼び出し規約には、いくつかの要件があります。 [〜#〜] call [〜#〜] の直前に、スタックが少なくとも16バイト(または32バイト)整列されている必要があります。

入力引数領域の終わりは、16(__m256がスタックに渡された場合は32)バイト境界に揃えられます。

[〜#〜] c [〜#〜] ランタイムがmain関数を呼び出した後、リターンポインタが [〜#〜] call [〜#〜] 。 16バイト境界に再調整するには、単に Push any汎用レジスターをスタックにスタックして [〜#〜] pop [〜#〜] 最後にオフにします。

呼び出し規約では、 [〜#〜] al [〜#〜] に、可変引数関数に使用されるベクトルレジスタの数が含まれている必要があります。

%alは、可変数の引数を必要とする関数に渡されるベクトル引数の数を示すために使用されます

printfは可変引数関数なので、 [〜#〜] al [〜#〜] を設定する必要があります。この場合、ベクトルレジスタにパラメータを渡さないため、 [〜#〜] al [〜#〜] を0に設定できます。

$ formatポインターが既にアドレスである場合も、それを逆参照します。だからこれは間違っています:

mov  $format, %rbx
mov  (%rbx), %rdi 

これは、フォーマットのアドレスを受け取り、それを [〜#〜] rbx [〜#〜] に配置します。次に、 [〜#〜] rbx [〜#〜] でそのアドレスの8バイトを取得し、 [〜#〜] rdi [〜に配置します#〜] [〜#〜] rdi [〜#〜] は、文字自体ではなく、文字列への pointer である必要があります。 2つの行は次のように置き換えることができます。

lea  format(%rip), %rdi

これはRIP相対アドレス指定を使用します。

また、 [〜#〜] nul [〜#〜] 文字列を終了する必要があります。 .asciiを使用する代わりに、x86プラットフォームでは.ascizを使用できます。

プログラムの作業バージョンは次のようになります。

# global data  #
    .data
format: .asciz "%d\n"
.text
    .global main
main:
  Push %rbx
  lea  format(%rip), %rdi
  mov  $1, %esi           # Writing to ESI zero extends to RSI.
  xor %eax, %eax          # Zeroing EAX is efficient way to clear AL.
  call printf
  pop %rbx
  ret

その他の推奨事項/提案

また、64ビットLinux ABIから、呼び出し規約には特定のレジスターの保持を尊重するために作成する関数も必要であることを認識しておく必要があります。レジスターのリストとそれらを保存する必要があるかどうかは次のとおりです。

enter image description here

Preserved across Register 列でYesと表示されているレジスタは、関数全体で確実に保持される必要があるレジスタです。関数mainは他の [〜#〜] c [〜#〜] 関数と同じです。


読み取り専用であることがわかっている文字列/データがある場合は、.rodataではなく.section .rodataを使用して.dataセクションに配置できます。


64ビットモードの場合:32ビットレジスタであるデスティネーションオペランドがある場合、CPUはレジスタを64ビットレジスタ全体にゼロ拡張します。これにより、命令エンコーディングのバイトを節約できます。


実行可能ファイルが位置に依存しないコードとしてコンパイルされている可能性があります。次のようなエラーが表示される場合があります。

共有オブジェクトを作成するとき、シンボル `printf @@ GLIBC_2.2.5 'に対するR_X86_64_PC32の再配置は使用できません。 -fPICで再コンパイル

これを修正するには、次のように外部関数printfを呼び出す必要があります。

call printf@plt 

Procedure Linkage Table(PLT) を介して外部ライブラリ関数を呼び出します。

20
Michael Petch

同等のcファイルから生成されたアセンブリコードを確認できます。
test.cでgcc -o - -S -fno-asynchronous-unwind-tables test.cを実行しています

#include <stdio.h>
int main() {
   return printf("%d\n", 1);
}

これはアセンブリコードを出力します。

        .file   "test.c"
        .section        .rodata
.LC0:
        .string "%d\n"
        .text
        .globl  main
        .type   main, @function
main:
        pushq   %rbp
        movq    %rsp, %rbp
        movl    $1, %esi
        movl    $.LC0, %edi
        movl    $0, %eax
        call    printf
        popq    %rbp
        ret
        .size   main, .-main
        .ident  "GCC: (GNU) 6.1.1 20160602"
        .section        .note.GNU-stack,"",@progbits

これにより、printfを呼び出して変更できるアセンブリコードのサンプルが提供されます。


コードと比較して、次の2つを変更する必要があります。

  • %rdiはフォーマットを指す必要があり、%rbxを参照しないでください。これはmov $format, %rdiで実行できます
  • printfには可変数の引数があるため、mov $0, %eaxを追加する必要があります

これらの変更を適用すると、次のようになります。

    .data
format: .ascii "%d\n"  
.text
    .global main  
main:
  mov  $format, %rdi
  mov  $1, %rsi
  mov  $0, %eax
  call printf
  ret

そして、それを実行して印刷します:

1

2
mpromonet