środa, 12 sierpnia 2009

printf - based - listener

Potrzebowałem zrobić listener dowolnej ilości zmiennych oparty na funkcji o prototypie podobnym do

int printf ( const char * format, ... );

wyzwalany jakąś funkcją (w szczególnym przypadku callbackiem). Na przykład wyświetlający informacje typu FPS w grze, albo jako reakcja na event czy przychodzący z sieci pakiet. Dla uproszczenia tej notki załóżmy, że ma obsługiwać jedynie inty i floaty. Można oczywiście napisać wielką funkcję, w której w zależności od jakiejś zmiennej będziemy wywoływać w switchu printf(napis, int1), printf(napis, int1. int2) etc. aż do 1000 zmiennych różnych typów, czego na pewno nikt nie przekroczy. Ale można też to zrobić inaczej. Moje rozwiązanie jest proste - podajmy parametry bezpośrednio z poziomu asma. W dokumentacjach można wyczytać jak VS (w moim przypadku, bo go używam) wywołuje funkcje (calling convention), jednak warto to sprawdzić w praktyce.

Na początek robimy sobie małą aplikację testową:

#include
int main() {
printf("printf call %d %f\n", 1, 2.0f);
return 0;
}

Kompilujemy, odpalamy, i (o dziwo) na ekranie wyświetla się napis „printf call 1 2.000000”. Podejrzyjmy wygenerowaną binarkę w desassemblerze, np. w IDA (program jest dostępny w wersji Freeware – troszkę ograniczonej, ale dla przeciętnego człowieka oferuje i tak o wiele za dużo). Interesuje nas poniższy fragment (jeśli nie umiesz znaleźć go w swoim disasmie, warto się pouczyć podstaw RE, choćby z [1] czy [2], czasem się przydają nawet jeśli nie chcemy być RE).


.text:004113BE mov esi, esp
.text:004113C0 sub esp, 8
.text:004113C3 fld ds:dbl_415758
.text:004113C9 fstp [esp+0D4h+var_D4]
.text:004113CC push 1
.text:004113CE push offset aPrintfCallDF
.text:004113D3 call ds:printf
.text:004113D9 add esp, 10h

Jak widać parametry funkcji wrzucane są na stos od końca. Czyli zgodnie z opisem w dokumentacji. Jeśli chodzi o inty sprawa jest banalna - znany wszystkim push wartości. Więc np. zamiast

int i = 1;
int j = 2;
printf("printf call %d %d\n", i, j);

możemy zrobić równoważne:

int i = 1;
int j = 2;

__asm {
push j
push i
}

printf("printf call %d %d\n");

int add_ = 8;
__asm add esp, add_

Na początku wrzucamy zmienne na stos zgodnie z konwencją kompilatora, potem robimy call i na końcu zwiększamy wskaźnik stosu o tyle o ile zmniejszyliśmy go niejawnie przy pushowaniu. Jaki z tego jest zysk? Ano taki:

int tablica[5] = {1, 2, 3, 4, 5};

for(int i = 4; i >= 0; --i) {
int k = tablica[i];
__asm push k
}

printf("printf call %d %d %d %d %d\n");

int add_ = 4 * 5;
__asm add esp, add_

A co z floatami? FLD wrzuca float’a na stos FPU, natomiast FSTP kopiuje pierwszą wartość z tego stosu pod wskazany adres, po czym wykonuje POPa (btw. jest też wersja bez POPa - FST). Trzeba też pamiętać, że potrzebujemy aż 64 bitów.

float f = 3.14f;
int floatSize = 8;
__asm {
fld f
sub esp, floatSize
fstp qword ptr [esp]
}
printf("float = %f\n");
__asm add esp, floatSize

Samego listenera można teraz zaimplementować na tyle sposobów ilu jest koderów. W moim przypadku cały snippet wygląda tak:

#include
#include
#include

class PrintfListener {
enum Type {
T_INT,
T_FLOAT
};

struct Value {
Type type;
union V_ {
int* i;
float* f;
} val;

Value(int* i) { type = T_INT; val.i = i; }
Value(float* f) { type = T_FLOAT; val.f = f; }
};

std::vector value;
std::string str;
public:
void addParam(int* i) { value.push_back(Value(i)); }
void addParam(float* f) { value.push_back(Value(f)); }
void setString(const std::string& s) { str.assign(s); }

void use() {
std::vector::iterator it = value.end();
int totalSize = 0;
while(true) {
--it;

switch(it->type) {
case T_INT: {
int v = *(it->val.i);
__asm push v
totalSize += 4;
} break;
case T_FLOAT: {
float v = *(it->val.f);
__asm {
fld v
sub esp, 8
fstp qword ptr [esp]
}
totalSize += 8;
} break;
}

if(it == value.begin()) break;
}

printf(str.c_str());
__asm add esp, totalSize
}
};

int main() {
int i = 0;
float f = 0.0f;

PrintfListener listener;
listener.addParam(&i);
listener.addParam(&f);
listener.setString("i = %d, f = %f\n");

listener.use();

i = 10;
f = 3.14f;
listener.use();

return 0;
}

Ok, a jak to ugryźć inaczej? Można np. zastosować STLowy string stream i do printfa przekazywać gotowy poskładany string - ale wtedy właściwie tracimy największą zaletę formatującego printfa, więc przy takim podejściu byśmy już pewnie użyli iostream na konsoli i napisali inną funkcję wypisującą w przypadku aplikacji graficznej. Jak ktoś ma inne genialne sposoby (może da się to zrobić dużo prościej?) to będę wdzięczny za wszelkie info :)

[1] ReverseCraft by Gynvael – dobre źródło dla początkujących z RE: http://re.coldwind.pl/
[2] Introduction to Reverse Engineering Software by Mike Perry - świetne źródło, troszkę wiekowe, dla początkujących z RE, bardzo dużo informacji od podszewki: http://www.acm.uiuc.edu/sigmil/RevEng/

5 komentarzy:

  1. Hah, bardzo fajny pomysł ;> Kod dość haxxorski (tj taki jak lubie) ;> Aż miło popatrzeć ;>
    Thx for sharing ;>

    p.s. chyba ci #include w kodzie źle wyświetla (html entities?)

    OdpowiedzUsuń
  2. Thx. Faktycznie, w dodatku formatowanie troszkę się sypnęło. Będzie trzeba pokminić czemu. Teoretycznie plugin do bloga miał sam konwertować na entities ale jak widać tego nie robi.

    OdpowiedzUsuń
  3. Bardzo ciekawa notka. Lubie czytac o taki rzeczach :)

    Co do formatowania kodu, u siebie uzywam pluginu do Live writera, "Paste from VS", jednak czasem potrafi on pokaszanic caly text.
    Ogolnie blogspot ma cos nie tak z formatowaniem czasami.

    OdpowiedzUsuń
  4. Co do notki jeszcze. Visual ma ciekawa opcje, za pomoca ktorej mozna podejrzec wygenerowany kod wynikowy w asm.
    Wlasciwosci projektu a tam Configuration Preperties -> C++ -> Output Files -> Assembler Output.

    OdpowiedzUsuń
  5. O thx, nie wiedziałem o tej opcji :) A przyda się na pewno.

    OdpowiedzUsuń