Zajęcia 10: wstawki assemblerowe¶
Data: 28.05.2024
Materiały dodatkowe¶
Jak używać assemblera w programach napisanych w C¶
Jeśli chcemy użyć instrukcji assemblera w kodzie napisanym w języku C, mamy kilka możliwości:
Uznać, że wcale nie chcemy dotykać assemblera, napisać przenośny kod i zaufać optymalizatorowi.
Użyć rozszerzeń kompilatora niezależnych od architektury (
__vector__
,__builtin_popcount
, …).Użyć rozszerzeń kompilatora zależnych od architektury (tzw. intrinsics, np.
_mm_aesenc_si128
).Użyć wstawki assemblerowej (
__asm__
).Napisać całą funkcję w assemblerze i wywołać ją w C.
Wstawki assemblerowe są najbardziej delikatnym i niebezpiecznym z powyższych mechanizmów.
Wstawki assemblerowe w gcc¶
Składnia wstawki assemblerowej jest zależna od kompilatora. W przypadku gcc i clanga jest ona następująca:
__asm__ <opcjonalnie volatile> (
"<kod assemblera>"
: <lista wyjść>
: <lista wejść>
: <lista nadpisanych rejestrów>
);
W gcc, pisząc wstawkę assemblerową efektywnie definiujemy nową instrukcję assemblera w kompilatorze, która będzie traktowana przez backend kompilatora na równi ze zwykłymi. Tak jak w przypadku zwykłych instrukcji, kompilator musi dokładnie znać zachowanie naszej wstawki – jej wejścia, wyjścia oraz możliwości optymalizacji.
Domyślnie, kod wstawki podlega optymalizacjom przepływu danych – gcc zakłada, że wstawka
assemblera nie ma skutków ubocznych (innych niż zapisanie wyjść) i może usunąć wstawkę
gdy jej wyjścia nie są używane, przestawić ją z innym kodem, bądź zduplikować. Żeby tego
uniknąć, możemy użyć słowa volatile
– to zagwarantuje, że nasza wstawka wykona się
dokładnie jeden raz, gdy zostanie używa.
Kod assemblera we wstawce jest szablonem tekstu, który zostanie wyemitowany bezpośrednio do pliku assemblerowego będącego wyjściem kompilatora. W tym szablonie gcc podstawi zaalokowane wejścia i wyjścia w odpowiednie miejsca.
Listy wejść i wyjść opisują dane, których będzie używać nasza wstawka. Każdy wpis opisuje
zmienną (bądź wyrażenie) w C, która zostanie podstawiona w odpowiednim miejscu szablonu
oraz ograniczenia na miejsce, w którym powinna zostać umieszczona – np. "g"
oznacza
rejestr ogólnego przeznaczenia, a "m"
oznacza pamięć.
Lista nadpisanych rejestrów opisuje, co nasza wstawka modyfikuje jako skutki uboczne. Możemy
tam użyć nazw rejestrów, napisać "cc"
jeśli nasza wstawka modyfikuje rejestr znaczników
(rflags
i jego odpowiedniki), bądź napisać "memory"
, jeśli nasza wstawka modyfikuje
struktury w pamięci (inne niż te przekazane jako wyjścia).
Przykład (bezużyteczny) wstawki assemblerowej:
int a, b, c;
// a = b + c;
__asm__ (
"addl %1, %0"
: "=g"(a)
: "gm"(b), "0"(c)
: "cc"
);
W tym wypadku mówimy, że gcc może umieścić b
w rejestrze bądź pamięci (dwa różne ograniczenia),
a
musi umieścić w rejestrze, a c
musi umieścić w tym samym rejestrze, co argument 0
(czyli a). Kompilator wykona odpowiednią alokację rejestrów i podstawi wybrane rejestry
(bądź adres pamięci) w miejsce %0
i %1
w trakcie emitowania kodu assemblera.
Czasem potrzebujemy dokładniejszego ograniczenia lokalizacji wejścia/wyjścia do konkretnego
rejestru (np. w przypadku syscalli). Żeby to zrobić, używamy ograniczenia "g"
w połączeniu ze zmienną zdefiniowaną w następujący sposób:
register int b __asm__("eax");
// a = b + c;
__asm__ (
"addl %%eax, %0"
: "=g"(a)
: "g"(b), "0"(c)
: "cc"
);
Wstawki assemblerowe mogą zawierać więcej niż jedną instrukcję (choć należy tego unikać)
– oddzielamy je wtedy przez ;
bądź \n
w tekście szablonu. Jeśli potrzebujemy
zdefiniować etykietę wewnątrz wstawki, nie powinniśmy nadawać jej nazwy (będzie ona kolidować,
jeśli kompilator postanowi zduplikować kod wstawki) – zamiast tego, powinniśmy użyć etykiet
lokalnych (https://sourceware.org/binutils/docs/as/Symbol-Names.html).
Dla przykładu, wykonanie syscalla read
na x86_64:
ssize_t read (int fd_, void *ptr_, size_t len_) {
register int sys_nr __asm__("eax") = __NR_read;
register ssize_t res __asm__("rax");
register int fd __asm__("edi") = fd_;
register void *ptr __asm__("rsi") = ptr_;
register size_t len __asm__("rdx") = len_;
__asm__ volatile (
"syscall"
: "=g"(res)
: "g"(sys_nr), "g"(fd), "g"(ptr), "g"(len)
: "cc", "rcx", "r11", "memory"
);
return res;
}