Z pewnością kilka osób zna pewnego rodzaju sztuczkę która powoduje crash kompilatora.
Wygląda to mnie więcej tak
public functionTest(){ return "Hard coded string"; }
( działa na localu jak i na webkompilatorze amxx.pl 😉 )
Nie ma żadnego uzasadnienia w zasadach i ograniczenia języka pawn który by nie pozwalał na tego typu kod
jest to raczej bug w kodzie kompilatora.
Trochę o samym zwracaniu wartości z funkcji.
Możemy wydzielić dwie sytuacje :
Pierwsza kiedy zwracamy wartość ( stałą , ze zmiennej , true/false etc. )
Tutaj jest prosto zwraca wartość ląduje w rejestrze PRI ( czym jest rejestr PRI pod koniec wpisu ) skąd może zostać pobrana przez funkcje wywołująca.
Dwa przykłady
public test(){ return 1; } public test2(){ new var = 1; return var; }
i kod assemblerowy tych funkcji ( usunąłem informacje dla debuggera )
proc ; test const.pri 1 retn proc ; test2 ;$lcl var fffffffc push.c 1 ;$exp load.s.pri fffffffc ;$exp stack 4 retn
W pierwszym przypadku kod assemblerowy jest prosty
const.pri 1
wartość rejestru pri zostaje ustawiona na 1 tyle.
w drugiej funkcji widzimy najpierw odłożenie wartości na stos ( 1 )
potem pobranie tej wartości i zapisanie do pri
load.s.pri fffffffc
warto zauważyć że przy load.s.pri dostęp uzyskujemy poprzez rejestr FRM i offset.
Pod koniec czyszczony jest stos tzn. do rejestru STK zostaje dodana wartość w zależności ile zmiennych było rejestrowanych w funkcji itp. ( stack 4 )
Samo pobranie wartości return przez funkcję wygląda bardzo prosto
public test6(){ new var; var = test2(); }
proc ; test6 ;$lcl var fffffffc push.c 0 ;$exp push.c 0 call test2 stor.s.pri fffffffc ;$exp stack 4 zero.pri retn
Najpierw następuje wrzucenie wartości na stos ( rejestracja zmiennej )
Potem wywołanie funkcji( push i call ) push oznacza ilość parametrów w tym przypadku 0
Po callu następuje pobranie wartości i przypisanie do zmiennej ( stor.s.pri fffffffc )
Tak jak poprzednio „podnosimy” stos i ustawiamy rejestr PRI na 0 ponieważ funkcja nie zwraca żadnej wartości. ( czyli w sumie zwraca 😛 )
Druga kiedy zwracamy tablice ( string to też tablica ):
Jest to trochę bardziej skomplikowane niż w pierwszym przypadku ale schemat jest prosty
Wszystko to dzieje się przed wywołaniem funkcji czyli callem
- Na stos zostaje wrzucony adres zmiennej do której będzie zapisywane to co zwróci funkcji już PO powrocie do funkcji wywołującej
- Dynamicznie na stercie zostaje zarezerwowana odpowiednia ilość pamięci
- Adres do tej pamięci zostaje wrzucony na stos przed parametrami
- Funkcja zwracając tablice zapisuje do tej zarezerwowanej pamięci
- Kiedy sterowanie wróci do funkcji która wywołała call ze stosu pobierane są dwa parametry adres zmiennej do której ma być zapisana zwracana tablica oraz adres dynamicznie zarezerwowanej pamięci
- Wartości są kopiowane
- Pamięć na stercie jest zwalniana
Przykład
public test3(){ new var[ 2 ] = { 0 , 1 }; return var; } public test5(){ new varReturn[ 2 ]; varReturn = test3(); }
i kod assemblerowy
proc ; test3 ;$lcl var fffffff8 stack fffffff8 zero.pri addr.alt fffffff8 movs 8 addr.pri fffffff8 ;$exp load.s.alt c movs 8 stack 8 retn proc ; test5 ;$lcl varReturn fffffff8 stack fffffff8 zero.pri addr.alt fffffff8 fill 8 addr.pri fffffff8 push.pri heap 8 push.alt push.c 0 call test3 pop.pri pop.alt movs 8 heap fffffff8 ;$exp stack 8 zero.pri retn
W proc test3 widzimy na początku zarezerwowanie pamięci wielkości 2 komórek ( 8 bajtów )
Skopiowanie do pri 0 które w tym przypadku oznacza adres danych przechowywanych w sekcji DATA
{ 0 , 1 }
Tak wszystkie tego typu dane wpadają do sekcji data i są tam trzymane więc należy z tym uważać
Wcześniej dodałem wpis który porusza lekko ten temat ( http://darkgl.pl/index.php/2012/12/28/zarzadzanie-ciagami-znakow-w-pamieci-pluginu/ )
Zapisanie adresu do przed chwilą zarezerwowanej pamięci do rejestru ALT
Oraz skopiowanie 8 bajtów z pamięci pod adresem z PRI do pamięci pod adresem przechowywanym w ALT
Dalszy kod stanowi właśnie return tej funkcji
addr.pri fffffff8 ;$exp load.s.alt c movs 8
Mamy tu ponownie pobranie adresu zwracanej zmiennej do PRI oraz pobranie adresu pamięci zarezerwowanej w heap do ALT
i następuje kopiowanie 8 bajtów
Procedura test5 wygląda troche skomplikowanie ale jest w sumie prosta i jest w niej kod schematu który opisałem wyżej
stack fffffff8 zero.pri addr.alt fffffff8 fill 8
Rezerwowanie pamięci pod zmienną i wypełnienie jej zerami
- Na stos zostaje wrzucony adres zmiennej
addr.pri fffffff8 push.pri
- Dynamicznie na stercie zostaje zarezerwowana odopowiednia ilość pamięci
heap 8
- Adres do tej pąmięci zostaje wrzucony na stos przed parametrami
push.alt
- Kiedy sterowanie wróci do funkcji która wywołała call ze stosu pobierane są dwa parametry
pop.pri pop.alt
- Wartości są kopiowane
movs 8
- Pamięć na stercie jest zwalniana
heap fffffff8
I tyle 🙂
Teraz dlaczego nie ma przeciwskazań do zwracania hard coded strings
Co jest nam potrzebne do zwracania ?
– Wielkość danych
– Ich adres
i te wszystkie dane są dostępne dla hard coded strings co pokazuje ta funkcja
public test4(){ new var[ 4 ] = "asd"; return var; }
btw. można tego użyć takiego podejście jako obejście tego problemu ze zwracaniem 😉
Kod asemblerowy
proc ; test4 ;$lcl var fffffff0 stack fffffff0 const.pri 8 addr.alt fffffff0 movs 10 addr.pri fffffff0 ;$exp load.s.alt c movs 10 stack 10 retn
Widzimy tutaj
Zarezerwowanie pamięci
stack fffffff0
Zapisanie adresu ciągu „asd” w sekcji DATA do PRI
const.pri 8
Skopiowanie adresu zmiennej do ALT
addr.alt fffffff0
I samo skopiowanie danych
movs 10
Mamy wszystkie potrzebne dane czy ten sam ( a przynajmniej podobny ) kod nie może być wykorzystany do zapisania danych przy return w pamięci na stercie 🙂 ?
Rejestry :
Bardzo krótko
PRI to alias na rejestr EAX ( do poczytania na wikipedii ) a ALT to alias na EDX
Służą do przechowywania różnego rodzaju wartości roboczych. Rejestrów innych też jest troche i są to
FRM wskaźnik na „stack frame” czyli ramkę stosu „w której” przechowywane są wartości potrzebne do powrotu kiedy
funkcja zakończy swoje działanie.
CIP adres aktualnie wykonywanej instruckji ( adres w pamięci )
DAT offset do miejsca gdzie zaczyna się sekcja data
COD offset do miejsca gdzie zaczyna się sekcja kodu
STP przechowuje adres gdzie zaczyna się stos
STK aktualna pozycja gdzie kończy się stos odkładając na stos odkładamy „w dół” więc stos dązdy od STP do zera , jest to tak zrobione
aby lepiej wykorzystac dostępna pamięć dla stosu i sterty ( heap ).
HEA aktualna pozycja gdzie kończy się sterta. Allokując pamieć dynamicznie jest ona rezerwowana właśnie na stercie.
Nie ma czegoś takiego jak rejestr z flagami. ( http://en.wikipedia.org/wiki/FLAGS_register )
Testowy plugin którego używałem przy pisaniu tego wpisu
public test(){ return 1; } public test2(){ new var = 1; return var; } public test3(){ new var[ 2 ] = { 0 , 1 }; return var; } public test4(){ new var[ 4 ] = "asd"; return var; } public test5(){ new varReturn[ 4 ]; varReturn = test3(); } public test6(){ new var; var = test2(); }
Brawo ale jeszcze mało rozumiem (assembler 0 skill)