2009-03-09:

Automagiczna lista funkcji w C++

c++:medium:assembler:windows:linux:macosx
Historia zaczyna się jak zwykle. Pisałem pewną aplikację, która generowała pewne pliki testowe. Pliki były do siebie podobne, więc wyciągnąłem wspólny czynnikprzed nawias - zrobiłem generowanie wspólnej podstawy pliku, a potem porobiłem funkcje które zmieniały podstawowy plik (file shader, tylko w GF 15200 GTX! ;>), po czym ów nowy plik był zapisywany. Oczywiście musiałem posiadać pewną tablicę/wektor z tymi funkcjami, którą grzecznie uzupełniałem o każdą dopisaną funkcję. Po 38 funkcji się znudziłem...

Powstał więc problem - kto za mnie będzie dopisywał każdą funkcję do listy. Niestety, na asystenta mnie jeszcze nie stać, więc pozostało to zrzucić na kompilator.

Wszystko co od teraz napiszę dotyczyć będzie GCC, a konkretniej kompilatora g++. Prawdopodobnie da się to zrobić w identyczny sposób na innych kompilatorach, tylko wystarczy trochę poniższe makra pozmieniać. Kod testowałem na MinGW GCC 3.4.5 na Viście, GCC 4.1.2 20061115 na Debianie i GCC 4.0.1 (Apple Inc. build 5490) na Mac OS X.

Przyszły mi do głowy dwa pomysły:
1) Wykorzystać mechanizm eksportów, i run-time parsować tablicę eksportów pliku wykonywalnego (czy to EAT w PE, czy analogiczne mechanizmy w ELF / MACH-O). Jest to dość między-kompilatorowe, ale na każdy OS będzie trzeba klepać parsing eksportów na nowo - a to będzie z 30-50 linii per OS.
2) Wykorzystać pewną właściwość assemblera (GNU AS) używanego przez GCC, i stworzyć nową sekcję i wrzucać w nią kolejne pointery na funkcje. Rozwiązanie zadziała prawdopodobnie tylko na kompilatorze GCC (lub innych opartych o GNU AS... są takie?), ale z bardzo niewielkimi zmianami ruszy na (prawie) każdym OS'ie gdzie jest GCC.

To pierwsze wiem jak zrobić, więc z oczywistych powodów wybrałem to drugie ;>

Tak na prawdę całość rozbija się o trzy, proste, ale tworzone ponad godzinę (przez linuxowego gcc --;), makra:
1) LISTED_FUNC_LIST_START(a) - makro służy do inicjacji listy funkcji, w parametrze przyjmuje nazwę listy (a konkretniej, nazwę nowo-utworzonej tablicy pointerów na funkcje)
2) LISTED_FUNC(a) - makro do definiowania funkcji, gdzie 'a' to nazwa funkcji
3) LISTED_FUNC_LIST_END - makro kończące listę funkcji

Pierwsze makro wygląda następująco:

#define LISTED_FUNC_LIST_START(a) \
/* 0 */ __asm (".section .fnc" SECT_ATTR); \
/* 1 */ __asm (".globl " FUNC_UNDERSCORE #a); \
/* 2 */ __asm (FUNC_UNDERSCORE #a ":"); \
/* 3 */ extern func_ptr a[1]

Dzieją się tutaj 4ry rzeczy. W linii oznaczonej /* 0 */ deklarowana jest nowa sekcja o nazwie .fnc. SECT_ATTR to zależny od OSu zestaw atrybutów funkcji. Tak na prawdę zestawy są dwa - na Linux-based OS'y, i na inne (Mac OS X / Windows). Na Linux-based OSy wygląda to następująco:
# define SECT_ATTR ",\"a\",@progbits"
Atrybut "a" oznacza "allocatable" - bez niego sekcja nie zostanie wczytana do pamięci procesu. Natomiast "@progbits" oznacza po prostu "w tej sekcji są dane".
Jeżeli zaś chodzi o Mac OS X'a czy Windowsa:
# define SECT_ATTR ",\"dr\""
Atrybuty "dr" to kolejno data oraz read-only.
Wróćmy do LISTED_FUNC_LIST_START. Linia /* 1 */ to globalne wyeksportowanie symbolu - listy funkcji o nazwie z parametru. Makro FUNC_UNDERSCORE to dla Mac OS X'a i Windows "_", oraz ciąg pusty "" dla Linux-based OS'ów.
Linia /* 2 */ to zadeklarowanie etykiety (oznaczenia miejsca - adresu następujących danych) na poziomie assemblera.
A linia /* 3 */ to zadeklarowanie tejże tablicy/listy funkcji na poziomie C++. Oczywiście a[1] oznacza w tym wypadku "tablica z PRZYNAJMNIEJ jednym elementem", a nie "tablica o wielkości jednego elementu" - ponieważ w C++ nie ma kontroli granic tablic, możemy użyć takiego triku. Prawdopodobnie jest on zupełnie zbędny i a[] by wystarczyło, ale nvm, niech już sobie będzie.
Czyli streszczając, powyższe makro tworzy sekcje ".fnc" i deklaruje tablicę funkcji na poziomie asma, linkera i C++.

Czas na drugie makro, czyli deklaracja pojedynczej funkcji:

#define LISTED_FUNC(a) \
/* 0 */ __asm (".section .fnc" SECT_ATTR); \
/* 1 */ __asm (".long " FUNC_UNDERSCORE #a); \
/* 2 */ __asm (".text"); \
/* 3 */ extern "C" void a()

Linia /* 0 */ jest identyczna jak powyżej, z jedną uwagą - wszystko od niej do momentu zmiany sekcji będzie DOPISYWANE (słowo klucz) do sekcji .fnc.
Linia /* 1 */ to po prostu wrzucenie w dane miejsce ADRESU funkcji "a" (w listingu asma pojawi się ".long _nazwa", czyli po prostu "wstaw tutaj ADRES funkcji _nazwa")
Przedostatnia linia, /* 2 */, to "powrót" do pisania w sekcji kodu (".text" to skrócone ".section .text") - została ona dodana po kilku empirycznych próbach z paroma identycznymi funkcjami ;D
Ostatnia, /* 3 */ to po prostu deklaracja funkcji (po niej MUSI nastąpić ciało funkcji). Jedna uwaga co do zastosowanego tutaj extern "C" - bez tego w przypadku C++ funkcja "a" miała by w nazwie pewne dekoracje (np. zamiast alamakota było by __Z10alamakotav), a jako że w linii /* 1 */ korzystamy z tej nazwy, to musieli byśmy tam wstawić nazwę z dekoracją, co okazało się nietrywialne (z poziomu __asm, głównie przez liczbę przy Z ;p). W związku z czym można albo zrezygnować z dekoracji (jako że wszystkie funkcje na liście mają i tak różne nazwy, i identyczny prototyp, to to nie robi żadnej różnicy), albo wprowadzić dodatkową nazwę dla funkcji (alias) - czyli po prostu w sekcji .text wyemitować symbol, analogicznie jak w poprzednim makrze. Okazało się że drugie rozwiązanie nie działa na GCC pod Linux-based OS'ami, ponieważ ów kompilator tam chce być fajny, i wyrzuca wszystkie globalne wstawki asma na początek listingu (przez co aliasy nie działają jak powinny). Więc zostało rozwiązanie pierwsze - extern "C".

Czas na ostatnie makro, które kończy listę:

#define LISTED_FUNC_LIST_END \
/* 0 */ __asm (".section .fnc" SECT_ATTR); \
/* 1 */ __asm (".long 0")

Jak widać jest trywialne - na koniec sekcji .fnc (/* 0 */) wrzucamy LONG'a o wartości 0 (/* 1 */).

Jak tego użyć? Rzućmy okiem na poniższy kod:

#include <cstdio>

// Func pointer typedef
typedef void (*func_ptr)();

// Defines
#ifdef __unix__
# define FUNC_UNDERSCORE ""
# define SECT_ATTR ",\"a\",@progbits"
#else
# define FUNC_UNDERSCORE "_"
# define SECT_ATTR ",\"dr\""
#endif

#define LISTED_FUNC_LIST_START(a) \
/* 0 */ __asm (".section .fnc" SECT_ATTR); \
/* 1 */ __asm (".globl " FUNC_UNDERSCORE #a); \
/* 2 */ __asm (FUNC_UNDERSCORE #a ":"); \
/* 3 */ extern func_ptr a[1]

#define LISTED_FUNC_LIST_END \
/* 0 */ __asm (".section .fnc" SECT_ATTR); \
/* 1 */ __asm (".long 0")

#define LISTED_FUNC(a) \
/* 0 */ __asm (".section .fnc" SECT_ATTR); \
/* 1 */ __asm (".long " FUNC_UNDERSCORE #a); \
/* 2 */ __asm (".text"); \
/* 3 */ extern "C" void a()

// Listed functions
LISTED_FUNC_LIST_START(my_function_list);

// 1st func
LISTED_FUNC(first_func)
{
 puts("first function");
}

// 2nd func
LISTED_FUNC(second_func)
{
 puts("second function");
}

// 3rd func
LISTED_FUNC(third_func)
{
 puts("third function");
}

LISTED_FUNC_LIST_END;
// End of listed functions

int
main()
{
 func_ptr *ptr;

 for(ptr = my_function_list; *ptr; ptr++)
   (*ptr)();

 return 0;
}


Czyli na górze makra, potem LISTED_FUNC_LIST_START(my_function_list); które zadeklaruje tablice pointerów na funkcje my_function_list, potem jakieś 3 funkcje deklarowane w sposób LISTED_FUNC(first_func), i na koniec LISTED_FUNC_LIST_END;. A w funkcji main zwykłe przechodzenie tablicy i wykonywanie funkcji, jedna po drugiej.

Tak to wygląda na Viście:

12:42:33 gynvael >ver

Microsoft Windows [Wersja 6.0.6001]
Ansi hack ver 0.004b by gynvael.coldwind//vx

12:42:40 gynvael >g++ --version
g++ (GCC) 3.4.5 (mingw special)
Copyright (C) 2004 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE


12:42:43 gynvael >g++ test.cpp -Wall -Wextra

12:42:48 gynvael >a
first function
second function
third function

12:42:49 gynvael >


Tak na Debianie:

12:48:31 gynvael:debianvm> uname -a
Linux debianvm 2.6.18-6-686-bigmem #1 SMP Sat Dec 27 10:38:36 UTC 2008 i686 GNU/Linux
12:53:19 gynvael:debianvm> g++ --version
g++ (GCC) 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

12:53:37 gynvael:debianvm> g++ ltest.cpp -Wall -Wextra
12:53:47 gynvael:debianvm> ./a.out
first function
second function
third function
12:53:49 gynvael:debianvm>


A tak na OS X:

Mac:~ gynvael$ uname -a
Darwin Mac.local 9.6.0 Darwin Kernel Version 9.6.0: Mon Nov 24 17:37:00 PST 2008; root:xnu-1228.9.59~1/RELEASE_I386 i386
Mac:~ gynvael$ g++ --version
i686-apple-darwin9-g++-4.0.1 (GCC) 4.0.1 (Apple Inc. build 5490)
Copyright (C) 2005 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Mac:~ gynvael$ g++ -Wall -Wextra test.cpp
Mac:~ gynvael$ ./a.out
first function
second function
third function
Mac:~ gynvael$


I tyle.

P.S. kompilacja z -g na Debianie coś mi nie działa, może rzucę potem okiem wtf
P.S.2. powyższy kod powinien działać na platformach 32-bitowych, tj na 64-bitowych nie zadziała
P.S.3. Unavowed przetestował powyższe na Linux-based OS'ie na procesorze z rodziny ARM - trzeba tam zrobić jedną zmianę, mianowicie z atrybutów sekcji wywalić ",@progbits" (samo "a" ma zostać). Output z jego ARMowej maszynki:

<:tmp>$ uname -a
Linux 2.6.16.16 #1 Tue May 16 21:45:07 CEST 2006 armv4tl GNU/Linux
<:tmp>$ gcc --version
gcc (GCC) 4.1.2 20061115 (prerelease) (Debian 4.1.1-21)
Copyright (C) 2006 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ g++ -o test test.cc
./test
<:tmp>$ ./test
first function
second function
third function
<:tmp>$

Comments:

2009-03-09 11:19:47 = Agares
{
Fajnie. A może by tak wersja pod Visual Studio(czyt. kompilator który jest w zestawie z VS)? ;)
}
2009-03-09 13:17:30 = ranides
{
/*
Po owocnych dyskusjach z deusem, moja propozycja tak wygląda, może innym też się przyda. przenośne, duża kontrola nad tym, kiedy grupę tworzymy i kiedy grupę uruchamiamy */

#include <iostream>
using namespace std;

#define ACTION(name, code) struct name##_t { name##_t() code } name;

struct Group {

ACTION(first,
{
cout << "1" << endl;
})

ACTION(second,
{
cout << "2" << endl;
})

ACTION(last,
{
cout << "3" << endl;
})

};

int main() {

Group(); // run all now!

struct Group2 {
ACTION(a, { cout << "A" << endl; })
ACTION(b, { cout << "B" << endl; })
ACTION(c, { cout << "C" << endl; })
};

Group2(); // run all now!

return 0;
}
}
2009-03-10 00:54:46 = Gynvael Coldwind
{
@Agares
Rzucę potem okiem na VS ;>
Ewentualnie popatrz na to co ranides napisał ;>

@ranides
Pomysłowy sposób ;> Dało by radę w moim przypadku takie rozwiązanie, noo i bardziej przenośne ;>
Good work ;>
}
2009-03-10 12:20:44 = Artur
{
poza tamatem ale: chlopaki jaka ksiazke do C++ byscie polecili? c i asm znam juz
}
2009-03-10 23:15:39 = Gynvael Coldwind
{
@Artur
Hmm, ja nie mam zielonego pojęcia prawdę mówiąc, dawno nie miałem w łapkach żadnej sensownej książki o programowaniu ;<
}
2009-03-11 01:15:24 = Patrykuss
{
@Artur. Jeżeli chodzi Ci o totalny wstęp za kilka złotych, to polecam coś z serii "żółtych" Helionowych (czyt. Kierzkowski i s-ka).

Jeżeli nie, to Biblia C++(?). Nie pamiętam dokładnie tytułu ale na półce w księgarni poznasz ją na pewno. Chyba najgrubsza z dostępnych ;).
}
2009-03-11 01:32:08 = Artur
{
dzienki Patrykuss sprawdze biblie C++, pytam bo jest sporo tytulow a niektorzy autorzy nie maja za bardzo talentu do przekazywania wiedzy co nie znaczy ze nie sa dobrymi koderami.

ps. Gynvael a jakies ksiazki do pisanie sterownikow jak np. twoj ExcpHook mozesz polecic?
}
2009-03-11 02:10:58 = Gynvael Coldwind
{
@Artur
Mam nadzieje że nikt nie napisał książki o pisaniu takich sterowników jak ExcpHook ;> Lepiej się uczyć pisać ładnie i działający kod ;D

Anyway, odpowiadając na pytanie, mi bardzo pomogła w ogarnięciu tematu książeczka "Rootkity: Sabotowanie Jądra Windows" (http://helion.pl/ksiazki/rootki.htm).
}
2009-03-13 17:42:09 = j00ru
{
Noo dobra, proste/ladne rozwiazania juz byly, czas na cos odemnie ; D
W sumie jakos specjalnie bardziej funkcjonalne to nie jest, ale przedstawia pewien odmienny schemat dzialania ;>

#include <cstdio>
#include <cstdlib>
#include <windows.h>
using namespace std;

// Func pointer typedef
typedef void (*func_ptr)();


/* Function signature */
#define FUNC_START 0x0BADC0DE

/* Placing a label right before the function declarations to obtain the
* initial search address */
#define START_FUNCTION_LIST(a)
extern DWORD a;
__asm("_" #a ":")

/* Use this macro inside every accessory function with its name as the parameter */
#define FUNCTION_START(a)
__asm("jmp $+8");
__asm("jmp _" #a);
__asm(".long 0x0BADC0DE");

#define FUNC_NUMBER 3 /* Number of the func signatures being searched at run-time */


func_ptr* ParseFunctions(void* StartAddr)
{
func_ptr* ret = (func_ptr*)malloc(1024); // enough for us
BYTE* ptr = (BYTE*)StartAddr;
int i=0;

do
{
if(*(DWORD*)ptr == FUNC_START)
{
printf("Found at 0x%.8x...
",(DWORD)ptr);
ret[i++] = (func_ptr)(ptr-2);
}
ptr++;
}
while(i<FUNC_NUMBER);

ret[i] = 0;
return ret;
}

START_FUNCTION_LIST(FunctionList);

extern "C" void Func1()
{
FUNCTION_START(Func1);
puts("Func1()");
}

extern "C" void Func2()
{
FUNCTION_START(Func2);
puts("Func2()");
}

extern "C" void Func3()
{
FUNCTION_START(Func3);
puts("Func3()");
}

int main()
{
func_ptr* ptr = ParseFunctions(&FunctionList);

for( ;*ptr;ptr++ )
{
(*ptr)();
}

return 0;
}


Czyli generalnie caly trick polega na tym ze na "poczatku" (nie do konca, prolog funkcji jest wrzucany wczesniej dlatego stosujemy ten trick z podwojnym jmp) kazdej procki ktora chcemy automatycznie wywolywac wrzucamy pewien ciag - sygnature - ktora okresla przynaleznosc do samo-odpalajacej sie grupy ;>
Sygnatura powinna byc ofc tak dobrana, zeby nie zainstaly zadne kolizje z kawalkami niezwiazanego kodu, w przykladzie uzylem zwyklego DWORDa o wartosci 0x0BADC0D3.

Na poczatku main() lecimy po pamieci szukajac N wystapien tych sygnaturek i spisujemy znalezione adresy, a potem je odpalamy ;> Tyle.
}
2009-03-14 04:21:27 = Gynvael Coldwind
{
@j00ru
Thx za kolejny ciekawy pomysł ;>

Ostatnio deus podsunął mi pewien pomysł ze zmiennymi globalnymi, który jeszcze bardziej upraszcza sprawę (kurcze muszę jakiś tag <code> wprowadzić):

#include <vector>
#include <cstdio>

using namespace std;

typedef void (*func_ptr)();

int vector_push_back_wrapper(vector<func_ptr> &a, func_ptr b) { a.push_back(b); return 0; }

#define LISTED_FUNC_LIST_START(a)
vector<func_ptr> a;

#define LISTED_FUNC(b,a)
void a();
static int b##_##a##_var = vector_push_back_wrapper(b, a);
void a()

#define LISTED_FUNC_LIST_END(a)
static int a##_end_var = vector_push_back_wrapper(a, NULL)

// Listed functions
LISTED_FUNC_LIST_START(my_function_list);

// 1st func
LISTED_FUNC(my_function_list, first_func)
{
puts("first function");
}

// 2nd func
LISTED_FUNC(my_function_list, second_func)
{
puts("second function");
}

// 3rd func
LISTED_FUNC(my_function_list, third_func)
{
puts("third function");
}

LISTED_FUNC_LIST_END(my_function_list);
// End of listed functions

int
main()
{
func_ptr *ptr;

for(ptr = &my_function_list[0]; *ptr; ptr++)
(*ptr)();

return 0;
}

}

Add a comment:

Nick:
URL (optional):
Math captcha: 3 ∗ 4 + 8 =