2008-08-17:

Funkcje typu naked w gcc/g++

c:c++:assembler:gcc:g++:asm
Ostatnio miałem okazję tworzyć w C++ (MinGW g++) pewną małą bibliotekę do runtime-patchów. W pewnym momencie zaszła potrzeba stworzenia funkcji w całości w assemblerze, bez żadnych dodatków ze strony kompilatora, czyli po prostu chodziło o funkcję "naked". Niestety, o ile kompilatory rodem z Redmond udostępniają atrybut __declspec(naked) dla x86 [Visual C++ Language Reference - naked (C++)], to kompilatory z GNU Compiler Collection obsługują "naked" jedynie w portach kompilatorów dla ARM, AVR, IP2K i SPU [Using the GNU Compiler Collection (For GCC version 4.3.0) - Function Attributes]. Powstał więc pewien problem który miałby kilka rozwiązań:
1. Można by stworzyć zewnętrzny plik z funkcją w assemblerze, i kompilować go do obiektu (GNU AS), a następnie linkować z resztą projektu.
2. Jak wyżej, tylko skompilować nasm'em (Netwide Assembler) i wrzucić kod w stringa (ASCIIZ), a następnie stringa przerzutować na wskaźnik do funkcji (to może brzmieć dziwnie, ale zazwyczaj właśnie tak to robię).
3. Podpatrzeć jak gcc/g++ kompilują funkcję do assemblera, a następnie użyć inline assemblera do stworzenia funkcji.
Pierwszy punkt odrzuciłem ponieważ nie chciałem tworzyć dodatkowych plików źródłowych. Drugi punkt odpadł ponieważ kod miał być w miarę czytelny i łatwo modyfikowalny. Została więc trzecia metoda.

Podpatrzenie jak gcc/g++ kompilują jest dość trywialne - służy do tego opcja -S (zamiast pliku wykonywalnego/obiektowego zostaje utworzony plik .s z kodem w assemblerze) . W zależności czy wolimy składnie Intel czy AT&T można dodać dodatkową opcję -masm=intel (lub jej nie dodawać). Mi to osobiście bez różnicy, więc zostawiłem domyślną AT&T.
Testy przeprowadziłem na prostej funkcji gimme_five, która po prostu zwracała 5. Poniższy listing jest wynikiem kompilacji kodu C za pomocą MinGW gcc.

.globl _gimme_five
  .def _gimme_five; .scl 2; .type 32; .endef
_gimme_five:
  pushl %ebp
  movl %esp, %ebp
  movl $5, %eax
  popl %ebp
  ret

Najważniejsza w powyższym listingu jest deklaracja etykiety (3cia linia), czyli _gimme_five: (należy pamiętać że przy kompilacji MinGW gcc dodaje podkreślenie, linuxowe gcc tego nie robią, DJGPP również nie). Dwie linie wyżej, czyli .globl _gimme_five oraz ta z .def są opcjonalne. Pierwsza z nich jest przydatna gdy chcemy aby funkcja była widoczna w momencie linkowania (czyli gdybyśmy dodali static, to .globl by się nie pojawiło). Druga linia natomiast służy do określenia dodatkowych opcji, wpływających głównie na wygląd funkcji w pliku obiektowym [Using as (GNU Binutils version 2.17.90) - Assembler Directives] - osobiście ją całkowicie pominąłem.

Zoptymalizowany kod powyższej funkcji, wraz z koniecznymi dyrektywami assemblera wygląda następująco:

.globl _gimme_five
_gimme_five:
  movl $5, %eax
  ret

Przepisując to na C otrzymujemy następujący kod (trzeba dodać oczywiście deklarację funkcji, w końcu kompilator C musi wiedzieć iż taka funkcja istnieje):

int gimme_five(void);
__asm(
  ".globl _gimme_five\n"
  "_gimme_five:\n"
  "  movl $5, %eax\n"
  "  ret"
);

Kolejną sprawą jest zastosowanie powyższego schematu w C++ - dochodzi sprawa dekoracji nazw funkcji. Funkcja int gimme_five(void) zostanie w C++ (MinGW g++) przerobiona na __Z10gimme_fivev, w związku z czym należy dodać dodatkową etykietę z dekoracjami (o tyle dobrze że mogą być dwie etykiety do jednego miejsca naraz ;>):

int gimme_five(void);
__asm(
  ".globl __Z10gimme_fivev\n"
  ".globl _gimme_five\n"
  "__Z10gimme_fivev:\n"
  "_gimme_five:\n"
  "  movl $5, %eax\n"
  "  ret"
);

Oczywiście równie dobrze zamiast podwajania etykiet można deklarację funkcji wrzucić w extern "C" { }.

Niestety powyższa metoda uniemożliwia korzystanie z nazw argumentów funkcji. Ale z drugiej strony to może nawet lepiej - kompilator z Microsoft przy __declspec(naked) też nie najlepiej sobie radzi z nazwanymi argumentami, a konkretniej to generuje niedziałający kod (kilka razy zdarzyło mi się szukać kilka godzin buga tylko po to by się przekonać że wygenerowany kod w asmie jest niedostosowany do braku prologu/epilogu funkcji).
Obejście tego problemu polega na stworzeniu dwóch funkcji - wrappera "naked" w assemblerze, oraz normalnej funkcji w C która by z wrappera była wywoływana. Wrapper ustawiałby argumenty/środowisko, dzięki czemu funkcja mogła by być zbudowana normalnie.

OK, tyle na teraz ;>

Update 1:
Poprawiłem terminatory linii. Bylo \r\n a powinno być \n (przy czym obie wersje powinny działać... na Windowsie; nie jestem pewien czy ruszyłyby na innych systemach).

Update 2:
Oczywiście, zamiast rozbijać linie w ten sposób:
__asm("line1 \n"
"line2 \n"
"line3");

...możemy użyć backslasha i zrobić to w ten sposób (imo wygodniejszy i czytelniejszy, chociaż do perfekcji jeszcze trochę mu brakuje):
__asm("line1 \n\
line2 \n\
line3");


Update 3:
See also: Lazy Coding - DLLExport with naked functions.

Comments:

2008-08-18 00:32:15 = ged_
{
"wstawki asm w gcc"
"latwo modyfikowalne"

it does not compute :)
}
2010-03-09 11:59:22 = Kuba
{
Czytając w Internecie artykuły o osdevie natknąłem się na to: "-fno-leading-underscore" - flaga do gcc wyłączająca podkreślenia przy funkcjach.
(żródło - http://osdev.pl/wiki/index.php/Kurs_pisania_OS_cz._III)
}
2011-05-13 10:46:40 = Krzysztof
{
Ostatnio z racji studiów informatycznych zaczynam wkręcać się w assemblera i myślałem, że dobrze mi idzie, ale czytając Twoje artykuły widzę ile jeszcze mi brakuje do przyzwoitego poziomu. Tak czy inaczej gratuluję wiedzy :)
}
2011-12-22 11:34:32 = bl4de
{
Przy kompilacji z ustawioną składnią Intela zauważyłem, że znak _ musi znaleźć się w linijce, gdzie zaczyna się definicja f-cji:

int gimme_five() {
__asm (
".globl gimme_five\n"
".type gimme_five, @function\n"
"gimme_five:\n" // <- o, tutaj powinno być _gimme_five :)
" mov eax,0x5\n"
" pop rbp\n"
" ret"
);
}

W przeciwnym wypadku powoduje to błąd kompilacji:

gcc -masm=intel -Wall -g -o test test.c

(...)
test.c: Assembler messages:
test.c:23: Error: symbol `gimme_five' is already defined


Przy okazji: czy jest jakiś sposób, by uniknąć ostrzeżenia kompilatora, że f-cja int gimme_five() nie zwraca zadeklarowanego typu? Bo instrukcja powrotu z f-cj znajduje się przecież we wstawce asemblerowej i kompilator tego nie uwzględnia, gdy kompilujemy program gcc z opcją -Wall
}
2011-12-23 18:39:05 = Gynvael Coldwind
{
@bl4de
Hehe chyba trochę nie do końca dokładnie przeczytałeś post :)
Zauważ, że ja nie definiuje funkcji gimme_five w C (tj. nie daje { } po int gimmie_five()), a jedynie ją deklaruje (tj. po int gimmie_five() jest średnik).

Przy definiowaniu funkcji w taki sposób jak Ty to zrobiłeś... no cóż, w zasadzie gcc ma rację - redefiniujesz funkcję.

Bottom line: jeśli chcesz zrobić funkcję typu naked, to nie definiuj funkcji w C, a jedynie ją zadeklaruj.
}
2011-12-27 15:27:52 = bl4de
{
Aaa, faktycznie :P

Mea culpa :)

Teraz wszystko jasne. Dziękuję za sprostowanie :)
}
2012-08-12 17:44:34 = Mrowqa
{
Bardzo ciekawe - świetny artykuł
;)
Niedawno pisałem aplikację i miałem z tym problem. Rozwiązałem go w dość nieładny sposób - kod funkcji naked wrzuciłem na koniec maina* i wywoływałem ją z wstawki w środku... Pisałem w Visualu i nie wiedziałem, że jest taka opcja, by zrobić sobie funkcję naked :P Ciągle dowiaduję się czegoś nowego ;)

* przed chwilą zrobiłem test (bo nie byłem pewny :P) i okazuje się, iż w Visualu (VC++ 2010 Exp) nie wolno - w każdym razie bez szperania w ustawieniach czego oczywiście nie dokonałem ^^ - umieszczać wstawek w zasięgu globalnym - "error C2059: syntax error : '__asm'"

Pozdrawiam,
Mrowqa
}
2020-03-01 18:58:05 = Gynvael Coldwind
{
test
}

Add a comment:

Nick:
URL (optional):
Math captcha: 1 ∗ 6 + 10 =