GNU/Linux >> Linux の 問題 >  >> Linux

インラインアセンブリでsyscallまたはsysenterを介してシステムコールを呼び出す方法は?

明示的なレジスタ変数

https://gcc.gnu.org/onlinedocs/gcc-8.2.0/gcc/Explicit-Register-Variables.html#Explicit-Reg-Vars)

以下の理由から、これは現在、レジスター制約よりも一般的に推奨されるアプローチであると考えています。

  • r8 を含むすべてのレジスタを表すことができます 、 r9r10 システム コール引数に使用されるもの:GCC インライン アセンブリで Intel x86_64 レジスタ r8 から r15 にレジスタ制約を指定する方法
  • これは、魔法のレジスタ制約名を持たない ARM などの x86 以外の ISA にとって唯一の最適なオプションです:ARM GCC インライン アセンブリで個々のレジスタを制約として指定する方法は? (一時レジスタ + clobbers + および追加の mov 命令を使用する以外に)
  • この構文は、S -> rsi などの単一文字のニーモニックを使用するよりも読みやすいと主張します。

レジスタ変数は、たとえば glibc 2.29 で使用されます。次を参照してください:sysdeps/unix/sysv/linux/x86_64/sysdep.h .

main_reg.c

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    register int64_t rax __asm__ ("rax") = 1;
    register int rdi __asm__ ("rdi") = fd;
    register const void *rsi __asm__ ("rsi") = buf;
    register size_t rdx __asm__ ("rdx") = size;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi), "r" (rsi), "r" (rdx)
        : "rcx", "r11", "memory"
    );
    return rax;
}

void my_exit(int exit_status) {
    register int64_t rax __asm__ ("rax") = 60;
    register int rdi __asm__ ("rdi") = exit_status;
    __asm__ __volatile__ (
        "syscall"
        : "+r" (rax)
        : "r" (rdi)
        : "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world\n";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub アップストリーム。

コンパイルして実行:

gcc -O3 -std=c99 -ggdb3 -ffreestanding -nostdlib -Wall -Werror \
  -pedantic -o main_reg.out main_reg.c
./main.out
echo $?

出力

hello world
0

比較のために、以下はインライン アセンブリで syscall または sysenter を介してシステム コールを呼び出す方法に類似していますか?同等のアセンブリを生成します:

main_constraint.c

#define _XOPEN_SOURCE 700
#include <inttypes.h>
#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (1), "D" (fd), "S" (buf), "d" (size)
        : "rcx", "r11", "memory"
    );
    return ret;
}

void my_exit(int exit_status) {
    ssize_t ret;
    __asm__ __volatile__ (
        "syscall"
        : "=a" (ret)
        : "0" (60), "D" (exit_status)
        : "rcx", "r11", "memory"
    );
}

void _start(void) {
    char msg[] = "hello world\n";
    my_exit(my_write(1, msg, sizeof(msg)) != sizeof(msg));
}

GitHub アップストリーム。

両方の逆アセンブル:

objdump -d main_reg.out

main_reg.c はほぼ同じです。 1:

Disassembly of section .text:

0000000000001000 <my_write>:
    1000:   b8 01 00 00 00          mov    $0x1,%eax
    1005:   0f 05                   syscall 
    1007:   c3                      retq   
    1008:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    100f:   00 

0000000000001010 <my_exit>:
    1010:   b8 3c 00 00 00          mov    $0x3c,%eax
    1015:   0f 05                   syscall 
    1017:   c3                      retq   
    1018:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
    101f:   00 

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   bf 01 00 00 00          mov    $0x1,%edi
    102a:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   ba 0d 00 00 00          mov    $0xd,%edx
    1043:   b8 01 00 00 00          mov    $0x1,%eax
    1048:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104f:   0a 
    1050:   0f 05                   syscall 
    1052:   31 ff                   xor    %edi,%edi
    1054:   48 83 f8 0d             cmp    $0xd,%rax
    1058:   b8 3c 00 00 00          mov    $0x3c,%eax
    105d:   40 0f 95 c7             setne  %dil
    1061:   0f 05                   syscall 
    1063:   c3                      retq   

したがって、GCC が必要に応じてこれらの小さなシステムコール関数をインライン化したことがわかります。

my_write そして my_exit どちらも同じですが、 _start main_constraint.c で 少し異なります:

0000000000001020 <_start>:
    1020:   c6 44 24 ff 00          movb   $0x0,-0x1(%rsp)
    1025:   48 8d 74 24 f3          lea    -0xd(%rsp),%rsi
    102a:   ba 0d 00 00 00          mov    $0xd,%edx
    102f:   48 b8 68 65 6c 6c 6f    movabs $0x6f77206f6c6c6568,%rax
    1036:   20 77 6f 
    1039:   48 89 44 24 f3          mov    %rax,-0xd(%rsp)
    103e:   b8 01 00 00 00          mov    $0x1,%eax
    1043:   c7 44 24 fb 72 6c 64    movl   $0xa646c72,-0x5(%rsp)
    104a:   0a 
    104b:   89 c7                   mov    %eax,%edi
    104d:   0f 05                   syscall 
    104f:   31 ff                   xor    %edi,%edi
    1051:   48 83 f8 0d             cmp    $0xd,%rax
    1055:   b8 3c 00 00 00          mov    $0x3c,%eax
    105a:   40 0f 95 c7             setne  %dil
    105e:   0f 05                   syscall 
    1060:   c3                      retq 

この場合、GCC が以下を選択して、わずかに短い同等のエンコーディングを見つけたことを観察するのは興味深いことです:

    104b:   89 c7                   mov    %eax,%edi

fd を設定する 1 へ 、これは 1 に等しい より直接的なものではなく、syscall 番号から:

    1025:   bf 01 00 00 00          mov    $0x1,%edi    

呼び出し規約の詳細については、以下も参照してください:i386 および x86-64 での UNIX および Linux システム コール (およびユーザー空間関数) の呼び出し規約とは

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


まず第一に、GNU C Basic asm(""); を安全に使用することはできません この構文 (入力/出力/クロバーの制約なし)。変更するレジスターについてコンパイラーに伝えるには、拡張 asm が必要です。 "D"(1) などの詳細については、GNU C マニュアルのインライン asm と、他のガイドへのリンクについてインライン アセンブリ タグ wiki を参照してください。 asm() の一部としての意味

asm volatile も必要です Extended asm では暗黙的ではないため 1 つ以上の出力オペランドを持つステートメント。

Hello World! と書くプログラムを書いて、システムコールを実行する方法を紹介します。 write() を使用して標準出力に システムコール。実際のシステム コールを実装していないプログラムのソースは次のとおりです:

#include <sys/types.h>

ssize_t my_write(int fd, const void *buf, size_t size);

int main(void)
{
    const char hello[] = "Hello world!\n";
    my_write(1, hello, sizeof(hello));
    return 0;
}

カスタム システム コール関数に my_write という名前を付けたことがわかります。 「通常の」 write との名前の衝突を避けるために 、libc によって提供されます。この回答の残りの部分には、 my_write のソースが含まれています i386 および amd64 用。

i386

i386 Linux のシステム コールは、128 番目の割り込みベクトルを使用して実装されます。 int 0x80 を呼び出して もちろん、事前にそれに応じてパラメーターを設定して、アセンブリコードで。 SYSENTER 経由で同じことを行うことができます 、しかし実際にこの命令を実行することは、実行中の各プロセスに仮想的にマッピングされた VDSO によって実現されます。 SYSENTER以降 int 0x80 を直接置き換えることを意図したものではありませんでした API であり、ユーザーランド アプリケーションによって直接実行されることはありません。代わりに、アプリケーションが何らかのカーネル コードにアクセスする必要がある場合、VDSO で仮想的にマップされたルーチンを呼び出します (これが call *%gs:0x10 SYSENTER をサポートするすべてのコードが含まれています。 命令。命令が実際にどのように機能するかという理由で、かなりの量があります。

これについて詳しく知りたい場合は、このリンクをご覧ください。カーネルと VDSO に適用される技術のかなり簡単な概要が含まれています。 (x86) Linux システム コールの決定版ガイド - getpid などのシステム コールも参照してください。 と clock_gettime 非常にシンプルで、カーネルはユーザー空間で実行されるコードとデータをエクスポートできるため、VDSO がカーネルに入る必要がなく、sysenter よりもはるかに高速です。

遅い int $0x80 を使用する方がはるかに簡単です 32 ビット ABI を呼び出します。

// i386 Linux
#include <asm/unistd.h>      // compile with -m32 for 32 bit call numbers
//#define __NR_write 4
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "int $0x80"
        : "=a" (ret)
        : "0"(__NR_write), "b"(fd), "c"(buf), "d"(size)
        : "memory"    // the kernel dereferences pointer args
    );
    return ret;
}

ご覧のとおり、int 0x80 を使用して API は比較的単純です。システムコールの番号は eax になります syscall に必要なすべてのパラメータがそれぞれ ebx に入ります。 、 ecxedxesiedi 、および ebp .システム コール番号は、ファイル /usr/include/asm/unistd_32.h を読み取ることで取得できます。 .

関数のプロトタイプと説明は、マニュアルの 2 番目のセクションにあるため、この場合は write(2) .

カーネルはすべてのレジスタ (EAX を除く) を保存/復元するため、それらをインライン asm への入力専用オペランドとして使用できます。 i386 および x86-64 での UNIX および Linux システム コール (およびユーザー空間関数) の呼び出し規則は何ですか

clobber リストには memory も含まれていることに注意してください。 これは、命令リストにリストされている命令が (buf を介して) メモリを参照することを意味します。 パラメータ)。 (インライン asm へのポインター入力は、ポイントされたメモリが入力でもあることを意味するものではありません。インライン ASM 引数によって *ポイントされた* メモリが使用される可能性があることをどのように示すことができますか? を参照してください。)

amd64

SYSCALL と呼ばれる新しい命令を備えた AMD64 アーキテクチャでは、状況が異なります。 .元の SYSENTER とは大きく異なります ユーザーランドアプリケーションから使用する方がはるかに簡単です-通常の CALL に似ています 、実際には、古い int 0x80 を適応させています 新しい SYSCALL へ かなり些細なことです。 (ただし、カーネル スタックの代わりに RCX と R11 を使用してユーザー空間の RIP と RFLAGS を保存し、カーネルがどこに戻るかを認識します)。

この場合、システム コールの番号はレジスタ rax に渡されます。 、ただし、引数を保持するために使用されるレジスタは、関数呼び出し規則とほぼ一致するようになりました:rdirsirdxr10r8r9 その順序で。 (syscall それ自体が rcx を破壊します だから r10 rcx の代わりに使用されます 、libc ラッパー関数が mov r10, rcx だけを使用できるようにします / syscall .)

// x86-64 Linux
#include <asm/unistd.h>      // compile without -m32 for 64 bit call numbers
// #define __NR_write 1
ssize_t my_write(int fd, const void *buf, size_t size)
{
    ssize_t ret;
    asm volatile
    (
        "syscall"
        : "=a" (ret)
        //                 EDI      RSI       RDX
        : "0"(__NR_write), "D"(fd), "S"(buf), "d"(size)
        : "rcx", "r11", "memory"
    );
    return ret;
}

(Godbolt でのコンパイルを参照してください)

実際に変更が必要だったのは、レジスタ名と、呼び出しに使用された実際の命令だけであったことに注意してください。これは主に、命令リストの実行に必要な適切な移動命令を自動的に提供する gcc の拡張インライン アセンブリ構文によって提供される入力/出力リストのおかげです。

"0"(callnum) 一致する制約は "a" のように記述できます オペランド 0 ("=a"(ret) 出力) 選択できるレジスタは 1 つだけです。 EAXを選択することはわかっています。より明確な方を使用してください。

Linux 以外の OS (MacOS など) では、異なる呼び出し番号が使用されることに注意してください。さらに、32 ビット用のさまざまな引数渡し規則。


Linux
  1. Linuxでclone()システムコールのスタックをmmapする方法は?

  2. アセンブリの Linux システム コール テーブルまたはチートシート

  3. アセンブリNASMで数値を出力するには?

  1. 最速の Linux システム コール

  2. パラメータをLinuxシステムコールに渡す方法は?

  3. Java から Syscall を呼び出す

  1. RedhatLinuxで仮想化を構成する方法

  2. Linuxでホスト名を変更する方法

  3. コマンドラインを介してUbuntuでパッケージをアップグレードする方法