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