================================ Zajęcia 10: wstawki assemblerowe ================================ Data: 11.05.2021 .. contents:: Materiały dodatkowe =================== - https://gcc.gnu.org/onlinedocs/gcc-8.3.0/gcc/Using-Assembly-Language-with-C.html 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: 1. Uznać, że wcale nie chcemy dotykać assemblera, napisać przenośny kod i zaufać optymalizatorowi. 2. Użyć rozszerzeń kompilatora niezależnych od architektury (``__vector__``, ``__builtin_popcount``, ...). 3. Użyć rozszerzeń kompilatora zależnych od architektury (tzw. intrinsics, np. ``_mm_aesenc_si128``). 4. Użyć wstawki assemblerowej (``__asm__``). 5. 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__ ( "" : : : ); 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; }