Zwracanie hard coded strings i trochę o samym return

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

  1. Na stos zostaje wrzucony adres zmiennej
    addr.pri fffffff8
    	push.pri
  2. Dynamicznie na stercie zostaje zarezerwowana odopowiednia ilość pamięci
    heap 8
  3. Adres do tej pąmięci zostaje wrzucony na stos przed parametrami
    push.alt
  4. Kiedy sterowanie wróci do funkcji która wywołała call ze stosu pobierane są dwa parametry
    pop.pri
    pop.alt
  5. Wartości są kopiowane
    movs 8
  6. 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();
}

Jeden komentarz o “Zwracanie hard coded strings i trochę o samym return

Dodaj komentarz