GNU/Linux >> Belajar Linux >  >> Linux

Bagaimana cara memanggil panggilan sistem melalui syscall atau sysenter di inline assembly?

Variabel register eksplisit

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

Saya percaya ini sekarang umumnya menjadi pendekatan yang direkomendasikan atas batasan register karena:

  • dapat mewakili semua register, termasuk r8 , r9 dan r10 yang digunakan untuk argumen panggilan sistem:Bagaimana cara menentukan batasan register pada register x86_64 Intel x86_64 r8 hingga r15 dalam rakitan inline GCC?
  • itu satu-satunya opsi optimal untuk ISA lain selain x86 seperti ARM, yang tidak memiliki nama batasan register ajaib:Bagaimana cara menentukan register individual sebagai batasan dalam perakitan inline ARM GCC? (selain menggunakan register sementara + clobbers + dan instruksi mov ekstra)
  • Saya berpendapat bahwa sintaks ini lebih mudah dibaca daripada menggunakan mnemonik satu huruf seperti S -> rsi

Variabel register digunakan misalnya di glibc 2.29, lihat: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 upstream.

Kompilasi dan jalankan:

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

Keluaran

hello world
0

Sebagai perbandingan, analogi berikut dengan Bagaimana cara memanggil panggilan sistem melalui syscall atau sysenter di rakitan inline? menghasilkan rakitan yang setara:

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 upstream.

Pembongkaran keduanya dengan:

objdump -d main_reg.out

hampir identik, ini adalah main_reg.c satu:

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   

Jadi kita melihat bahwa GCC menyejajarkan fungsi syscall kecil tersebut seperti yang diinginkan.

my_write dan my_exit keduanya sama, tetapi _start di main_constraint.c sedikit berbeda:

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 

Sangat menarik untuk mengamati bahwa dalam hal ini GCC menemukan penyandian setara yang sedikit lebih pendek dengan memilih:

    104b:   89 c7                   mov    %eax,%edi

untuk menyetel fd ke 1 , yang sama dengan 1 dari nomor syscall, daripada lebih langsung:

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

Untuk diskusi mendalam tentang konvensi pemanggilan, lihat juga:Apa konvensi pemanggilan untuk panggilan sistem UNIX &Linux (dan fungsi ruang pengguna) di i386 dan x86-64

Diuji di Ubuntu 18.10, GCC 8.2.0.


Pertama-tama, Anda tidak dapat menggunakan GNU C Basic asm(""); dengan aman sintaks untuk ini (tanpa batasan input/output/clobber). Anda memerlukan Extended asm untuk memberi tahu kompiler tentang register yang Anda ubah. Lihat inline asm di manual GNU C dan wiki tag rakitan inline untuk tautan ke panduan lain untuk detail tentang hal-hal seperti "D"(1) berarti sebagai bagian dari asm() pernyataan.

Anda juga membutuhkan asm volatile karena itu tidak tersirat untuk Extended asm pernyataan dengan 1 atau lebih operan keluaran.

Saya akan menunjukkan cara mengeksekusi panggilan sistem dengan menulis program yang menulis Hello World! ke output standar dengan menggunakan write() panggilan sistem. Inilah sumber program tanpa implementasi dari panggilan sistem yang sebenarnya :

#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;
}

Anda dapat melihat bahwa saya menamai fungsi panggilan sistem kustom saya sebagai my_write untuk menghindari bentrok nama dengan write "normal". , disediakan oleh libc. Sisa dari jawaban ini berisi sumber my_write untuk i386 dan amd64.

i386

Panggilan sistem di i386 Linux diimplementasikan menggunakan vektor interupsi ke-128, mis. dengan memanggil int 0x80 dalam kode rakitan Anda, setelah mengatur parameter yang sesuai sebelumnya, tentu saja. Hal yang sama dapat dilakukan melalui SYSENTER , tetapi sebenarnya menjalankan instruksi ini dicapai oleh VDSO yang dipetakan secara virtual ke setiap proses yang berjalan. Sejak SYSENTER tidak pernah dimaksudkan sebagai pengganti langsung dari int 0x80 API, itu tidak pernah langsung dieksekusi oleh aplikasi userland - sebagai gantinya, ketika sebuah aplikasi perlu mengakses beberapa kode kernel, itu memanggil rutinitas yang dipetakan secara virtual di VDSO (itulah call *%gs:0x10 dalam kode Anda adalah untuk), yang berisi semua kode yang mendukung SYSENTER petunjuk. Ada cukup banyak karena cara kerja instruksi yang sebenarnya.

Jika Anda ingin membaca lebih lanjut tentang ini, lihat tautan ini. Ini berisi ikhtisar yang cukup singkat tentang teknik yang diterapkan di kernel dan VDSO. Lihat juga Panduan Definitif untuk (x86) Panggilan Sistem Linux - beberapa panggilan sistem seperti getpid dan clock_gettime sangat sederhana kernel dapat mengekspor kode + data yang berjalan di ruang pengguna sehingga VDSO tidak perlu memasukkan kernel, membuatnya jauh lebih cepat daripada sysenter bisa jadi.

Lebih mudah menggunakan int $0x80 yang lebih lambat untuk memanggil ABI 32-bit.

// 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;
}

Seperti yang Anda lihat, menggunakan int 0x80 API relatif sederhana. Nomor syscall menuju ke eax register, sedangkan semua parameter yang diperlukan untuk syscall masuk ke masing-masing ebx , ecx , edx , esi , edi , dan ebp . Nomor panggilan sistem dapat diperoleh dengan membaca file /usr/include/asm/unistd_32.h .

Prototipe dan deskripsi fungsi tersedia di bagian ke-2 manual, jadi dalam hal ini write(2) .

Kernel menyimpan/mengembalikan semua register (kecuali EAX) sehingga kita dapat menggunakannya sebagai operan input saja ke asm inline. Lihat Apa konvensi pemanggilan untuk panggilan sistem UNIX &Linux (dan fungsi ruang pengguna) di i386 dan x86-64

Perlu diingat bahwa daftar clobber juga berisi memory parameter, yang berarti bahwa instruksi yang tercantum dalam memori referensi daftar instruksi (melalui buf parameter). (Input penunjuk ke asm sebaris tidak menyiratkan bahwa memori yang menunjuk ke juga merupakan masukan. Lihat Bagaimana saya bisa menunjukkan bahwa memori *ditunjuk* oleh argumen ASM sebaris dapat digunakan?)

amd64

Hal-hal terlihat berbeda pada arsitektur AMD64 yang menggunakan instruksi baru yang disebut SYSCALL . Ini sangat berbeda dengan SYSENTER aslinya instruksi, dan pasti jauh lebih mudah digunakan dari aplikasi userland - itu benar-benar menyerupai CALL normal , sebenarnya, dan mengadaptasi int 0x80 lama ke SYSCALL yang baru cukup banyak sepele. (Kecuali menggunakan RCX dan R11 alih-alih tumpukan kernel untuk menyimpan RIP dan RFLAGS ruang pengguna sehingga kernel tahu ke mana harus kembali).

Dalam hal ini, nomor panggilan sistem masih diteruskan dalam register rax , tetapi register yang digunakan untuk menyimpan argumen sekarang hampir sama dengan konvensi pemanggilan fungsi:rdi , rsi , rdx , r10 , r8 dan r9 dalam urutan itu. (syscall sendiri menghancurkan rcx jadi r10 digunakan sebagai pengganti rcx , membiarkan fungsi pembungkus libc hanya menggunakan 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;
}

(Lihat kompilasi di Godbolt)

Perhatikan betapa praktis satu-satunya hal yang perlu diubah adalah nama register, dan instruksi aktual yang digunakan untuk melakukan panggilan. Ini sebagian besar berkat daftar masukan/keluaran yang disediakan oleh sintaks rakitan inline gcc yang diperluas, yang secara otomatis menyediakan instruksi pemindahan yang sesuai yang diperlukan untuk menjalankan daftar instruksi.

"0"(callnum) kendala pencocokan dapat ditulis sebagai "a" karena operan 0 (kode "=a"(ret) output) hanya memiliki satu register untuk dipilih; kami tahu itu akan memilih EAX. Gunakan mana saja yang menurut Anda lebih jelas.

Perhatikan bahwa OS non-Linux, seperti MacOS, menggunakan nomor panggilan yang berbeda. Dan bahkan konvensi penerusan arg yang berbeda untuk 32-bit.


Linux
  1. Tabel panggilan sistem Linux atau lembar contekan untuk Majelis

  2. Bagaimana cara mencetak nomor di perakitan NASM?

  3. Panggil Syscalls dari Java

  1. Cara mengkonfigurasi Virtualisasi di Redhat Linux

  2. Cara Mengubah Nama Host di Linux

  3. Cara Meng-upgrade Paket di Ubuntu melalui Command Line

  1. Bagaimana cara memetakan tumpukan untuk panggilan sistem clone () di linux?

  2. x86_64 Perakitan Kebingungan Panggilan Sistem Linux

  3. Panggilan sistem Linux tercepat