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)