Autor Wątek: Dynamiczne wywoływanie funkcji  (Przeczytany 5545 razy)

Offline dynax

  • Użytkownik

# Kwiecień 01, 2013, 18:37:21
Piszę własną VM'kę z jej własnym językiem assembly. Napisałem już assembler i większość kodu samej maszyny. Chciałbym teraz dodać możliwość bezpośredniego wywoływania funkcji systemowych z kodu assembly (nazwałem to native extension). To znaczy, że przy starcie maszyny podawałbym listę jakiś plików DLL z których można by ładować funkcję. W nagłówku pliku z kodem podawałbym jakie funkcje chcę zaimportować i można by je spokojnie wołać z wnętrza kodu. Nie mam jednak zbyt dużego pojęcia w jaki sposób miałbym dynamicznie rozpoznać typ zaimportowanej funkcji i jak przekazać argumenty do niej. Jedyne co mam to adres początku stosu i adres końca stosu. Dzięki temu mogę wyciągnąć z wewnętrznej pamięci jakiś blok danych o ściśle niezdefiniowanej strukturze. Chciałbym  przy imporcie funkcji z pliku .dll pobrać jej sygnaturę, odpowiednio ją sparsować i dopiero na tej podstawie sformatować odpowiednio mój blok z danymi. Jest w ogóle taka możliwość? Jeśli nie to jaki polecacie inny sposób aby wykonać to o co mi chodzi? Czy jedyną opcją jest ręczne stworzenie listy plików .dll możliwych do zaimportowania i listy funkcji wraz z sygnaturami dla każdej z nich?

Pozdrawiam.

Offline Mr. Spam

  • Miłośnik przetworów mięsnych

Offline Rolek

  • Użytkownik

# Kwiecień 01, 2013, 19:18:04
Jeśli chodzi o natywny kod maszynowy to funkcja jest po prostu fragmentem (lub fragmentami) kodu bez żadnego opisu. Ten kod „spodziewa się”, że w chwili wywołania w określonym miejscu będą dane, na których ma działać. To kod wywołujący musi wiedzieć jak obchodzić się z kodem wywoływanym.
Nie ma nawet standardowego sposobu na odczytanie konwencji wywołania (czy argumenty idą przez rejestry czy stos, kto po wszystkim czyści stos caller czy callee, czy funkcja w ogóle wróci). Wszystko masz opisane w dokumentacji (jeśli jest) oraz w kodzie wysokopoziomowym (prototypy funkcji) i symbolach debugowych jeśli są dołączone do biblioteki.
« Ostatnia zmiana: Kwiecień 01, 2013, 19:19:42 wysłana przez Rolek »

Offline Xender

  • Użytkownik

# Kwiecień 01, 2013, 19:52:35
Jeśli piszesz to w C++ (w 11 łatwiej), to możesz wyciągnąć z headerów sygnatury funkcji za pomocą metaprogramowania szablonowego, być może w połączeniu z typeid (najlepiej wersją przyjmującą statyczny typ, nie wyrażenie).

Wyciągnięcie tego z dowolnej binarki może być nawet niemożliwe, bo jedynym symbolem, jaki na pewno zawsze jest eksportowany, jest nazwa funkcji, wskazująca na jej adres.

Za to jeśli chcesz to wyciągnąć z binarek, o których strukturze wewnętrznej możesz poczynić jakieś założenia (obecność symboli debugowych, zmanglowanie nazw przez konkretny kompilator, może nawet obecność jakichś innych metadanych), to może być łatwiej.

Zawsze można też zobaczyć, jak robią to inni - zdaje się, że AutoHotKey miał importowanie funkcji z binarek, ale nie wiem, jak sobie radził z typowaniem.

Offline hashedone

  • Użytkownik

# Kwiecień 02, 2013, 16:35:23
A nie jest najprościej napisać do tych dllek nagłówki? Wówczas ktoś kto używa funkcji "wbudowanych", musi dołączyć mimo wszystko nagłówek z deklaracjami funkcji. Tak robią chyba wszystkie nowoczesne systemy.

Offline dynax

  • Użytkownik

# Kwiecień 08, 2013, 20:42:05
W takim razie wykombinowałem coś innego. Zastanawiam się jednak czy możliwe jest aby wywołać funkcję z kolejnymi argumentami zapisanymi w jakiejś tablicy. Umiałbym to zrobić we wstawce assembly ale nie chcę ze względu na nie przenośność takiego rozwiązania na inne platformy. Czy jest takie coś możliwe dzięki tym nowym bajerom z C++0x, bo nie orientuje się zbytnio czy może coś takiego już dodali?

Miałbym wtedy informację ile argumentów przyjmuje konkretna funkcja i w jakiej formie. Na przykład wiedziałbym, że chcę wywołać puts(), miałym wskaźnik na tę funkcję i wiedziałbym, że przyjmuje ona jeden argument będący wskaźnikiem. Ściągnąłbym wtedy z mojego stosu jedną wartość, przetłumaczył adres z przestrzeni wirtualnej mojej VM'ki na adres fizyczny z pamięci i zapisał do jakiegoś vectora czy innego kontenera. Potem chciałbym wywołać funkcję podaną we wskaźniku z argumentami zapisanymi w tym vectorze.

Offline FoToN

  • Użytkownik

# Kwiecień 08, 2013, 21:30:21
Swego czasu jak pisałem własny język skryptowy, to wywoływanie zewnętrznych funkcji zrobiłem właśnie za pomocą wstawek asemblerowych i wstawiania argumentów w pętli na stos. Oprócz problemów z przenośnością takiego rozwiązania jest także problem z różnymi typami wywołań (fastcall, stdcall...).

Ogólnie teraz na szybko mogę zaproponować tworzenie funkcji pośredniej, która jako argument przyjmuje "coś" w czym będą przekazywane argumenty. Funkcje pośrednią powinno dać się wygenerować jakimś narzędziem (jak takie napiszesz) ;)

Offline dynax

  • Użytkownik

# Kwiecień 08, 2013, 23:16:35
To ma umieć wywołać dowolną funkcję załadowaną z DLL bez rekompilacji samej maszyny wirtualnej :)

Offline Xirdus

  • Redaktor

# Kwiecień 09, 2013, 00:30:24
Kurde, zdawało mi się że już tego posta pisałem... Ale cóż:

Najlepszym wyjściem, a przynajmniej takim, które na pewno jest wykonalne, to zrobić parser plików .h od tych DLL-ek (90% DLL-ek jest w C) który generowałby odpowiednie klasy/funkcje w twoim języku. Będzie sporo kombinowania, ale przynajmniej rozwiązany jest problem typów funkcji, argumentów i konwencji wywołania. A biblioteki DLL w innych językach sobie odpuść ;)

Offline Xender

  • Użytkownik

# Kwiecień 09, 2013, 03:51:35
Jeśli to ma być dowolna funkcja z dowolnej DLL, to umarł w butach, bo nawet, jeśli przeparsujesz headery, jak rodzi Xirdus, to musisz jeszcze zmanglować nazwy - to spory problem w C++, ale i w C trzeba cos zrobić (zależnie od konwencji wywołania, widywałem na przykład @<rozmiar_argumentów> jako sufix nazwy w DLLce).

Jeśli masz deklarację funkcji we własnym kodzie i masz dynamicznie typowane zmienne z VM (tutaj typu Boost::any), to łap:
#include <iostream>
#include <vector>
#include <exception>
#include <string>
#include <boost/any.hpp>

template <typename... Args>
struct TypenamesDummy{};

template <size_t... Indices>
struct IndicesDummy{};

template <typename Ret, typename... FuncArgs, typename T, typename... TypesBeingCounted, size_t... Indices>
Ret TypePackEnumeratingCallHelper(
Ret(*f)(FuncArgs...),
std::vector<boost::any> args,
TypenamesDummy<T, TypesBeingCounted...>,
IndicesDummy<Indices...>)
{
return TypePackEnumeratingCallHelper(f, args, TypenamesDummy<TypesBeingCounted...>(), IndicesDummy<sizeof...(TypesBeingCounted), Indices...>());
}

template <typename Ret, typename... FuncArgs, size_t... Indices>
Ret TypePackEnumeratingCallHelper(
Ret(*f)(FuncArgs...),
std::vector<boost::any> args,
TypenamesDummy<>,
IndicesDummy<Indices...>)
{
return f(boost::any_cast<FuncArgs>(args[Indices])...);
}

template <typename Ret, typename... Args>
Ret call(Ret(*f)(Args...), std::vector<boost::any> args)
{
using std::to_string;
if(args.size() != sizeof...(Args))
throw std::runtime_error("Invalid argument count - need "
+ to_string(sizeof...(Args)) + ", got "
+ to_string(args.size()) + ".");

return TypePackEnumeratingCallHelper(f, args, TypenamesDummy<Args...>(), IndicesDummy<>());
}

//-------------------------------------------------------
// MAGIC LINE BETWEEN SCARY LIBRARY CODE AND CLIENT CODE
//-------------------------------------------------------

int func(char, float)
{
return 2;
}

int main()
{
std::vector<boost::any> args = {'c', 4.0f};
try
{
int result = call(func, args);
std::cout << "Returned value: " << result << "\n";
}
catch(std::exception& e)
{
std::cout << e.what() << "\n";
}

    std::cout << "Hello world!" << std::endl;
    return 0;
}
Niniejszym udostępniam powyższy fragment kodu na licencji WTFPL. Kod rzuca wyjątki, jeśli liczba lub typ argumentów nie będzie się zgadzać - pierwsza sam sprawdzam, drugim zajmuje się any_cast.

Poniżej teoria, trochę mój braindump. You have been warned...

Teoria jest taka: żeby móc użyć wektora, o którym mówisz, musisz zastosować type erasure, żeby uzyskać zmienną, która może przechować wszystko, ale w compile time nie wie, co właśnie przechowuje - musi za to wiedzieć to w runtime, czyli mieć dodatkowe pole z ID typu.
Przy wywołaniu zaś chcesz dokonać sprawdzenia faktycznego typu tak opakowanej zmiennej, czyli porównać ID zapisane w opakowanej zmiennej z ID typu, który przyjmuje funkcja.

Rozbijając na pojedyncze problemy:
- Type erasure - tu 2 podstawowe opcje:
a) reinterpret_cast<void*>, ale tylko ze wskaźnikiem na zmienną, której własność należy do scope'u, który jest zewnętrzny do takiego wskaźnika, żeby mieć pewność, że wykona się destruktor i że nie skończymy ze wskaźnikiem do już zniszczonej zmiennej. Chyba, że będziesz używać tylko typów mających trait std::is_trivially_destructible, takich jak np. PODy lub typy podstawowe - wtedy nie trzeba wywoływać destruktora, więc opakowanie może trzymać taką zmienną na własność.
b) Jeśli chcesz obiekty, żeby opakowanie trzymało obiekty na własność (we własnym scope), to można użyć szablonowego struktury-wrappera z wirtualnym destruktorem, dziedziczącego po bazie.

- Dynamiczne ID/porównywanie typu: dla void* typeid, dla wersji z polimorficznym opakowaniem dynamic_cast.

Dobra wiadomość: to wszystko zostało już zrobione i istnieje pod nazwą boost::any, gotowe do użycia.

Jest jeszcze trzeci problem - pozyskanie typu argumentu funkcji. Jest to możliwe dzięki TMP. Analogicznie do delegatów, przed C++11 trzeba było hardkodować wersje w różną liczbą parametrów szablonowych, teraz można użyć variadic templates.

Offline dynax

  • Użytkownik

# Kwiecień 09, 2013, 11:40:08
Najlepszym wyjściem, a przynajmniej takim, które na pewno jest wykonalne, to zrobić parser plików .h od tych DLL-ek (90% DLL-ek jest w C) który generowałby odpowiednie klasy/funkcje w twoim języku. Będzie sporo kombinowania, ale przynajmniej rozwiązany jest problem typów funkcji, argumentów i konwencji wywołania. A biblioteki DLL w innych językach sobie odpuść ;)

O samo parsowanie headerów się w tym momencie nie martwimy :) To jest temat na zupełnie inną dyskusję. W tym momencie zakładam, że obok .dll leży sobie jakiś plik, powiedzmy XML, w którym mam ładną listę wszystkich eksportowanych funkcji, wraz z listą argumentów i konwencją wywołania. Na razie w ten sposób ręcznie przepisałem sporą część msvcrt :D
Parser headerów brzmi całkiem fajnie, jest jakiś darmowy i otwarty parser bison/flex z opisaną składnią C++?

Cytat: Xender
Jeśli to ma być dowolna funkcja z dowolnej DLL, to umarł w butach, bo nawet, jeśli przeparsujesz headery, jak rodzi Xirdus, to musisz jeszcze zmanglować nazwy - to spory problem w C++, ale i w C trzeba cos zrobić (zależnie od konwencji wywołania, widywałem na przykład @<rozmiar_argumentów> jako sufix nazwy w DLLce).

Ano, trochę to denerwujące, że każdy ma własną konwencje nazywania funkcji wewnątrz .dll. Biblioteki C na przykład praktycznie nigdy nie manglują nazw, natomiast C++ zazwyczaj dorzucają sporo info o funkcjach. Najgłupsze jest to, że każdy kompilator ma osobną konwencje. MSVC wrzuca w tablice eksportów całą deklarację funkcji, często razem z konwencją wołania. G++ natomiast ma własny system opisujące te deklaracje, który co prawda łatwiej się parsuje ale wygląda dość syfiaście.

Dzięki za kod. Zaraz sprawdzę. Szkoda tylko, że będę musiał dorzucić do projektu boost. Miałem taką cichą nadzieję, że C++11 poratuje mnie w tym przypadku.

BTW. Te szablony ze zmienną liczbą argumentów są seksi ;)

Offline FoToN

  • Użytkownik

# Kwiecień 09, 2013, 13:41:32
Poszukałem trochę w google i znalazłem to: http://stackoverflow.com/questions/1128150/win32-api-to-enumerate-dll-export-functions

Po nieznacznym przerobieniu kodu udało się stworzyć coś, co zdaje się działać dla DLL-ek wyplutych przez kompilator Microsoftu:
#include <Windows.h>
#include <winnt.h>
#include <assert.h>
#include <stdio.h>
#include <DbgHelp.h>

int main(int argc, const char* args[])
{
wchar_t path[512];
mbstowcs(path, args[1], 512);

HMODULE lib = LoadLibraryEx(path, NULL, DONT_RESOLVE_DLL_REFERENCES);
assert(((PIMAGE_DOS_HEADER)lib)->e_magic == IMAGE_DOS_SIGNATURE);
PIMAGE_NT_HEADERS header = (PIMAGE_NT_HEADERS)((BYTE *)lib + ((PIMAGE_DOS_HEADER)lib)->e_lfanew);
assert(header->Signature == IMAGE_NT_SIGNATURE);
assert(header->OptionalHeader.NumberOfRvaAndSizes > 0);
PIMAGE_EXPORT_DIRECTORY exports = (PIMAGE_EXPORT_DIRECTORY)((BYTE *)lib + header->
OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);
PVOID names = (BYTE *)lib + exports->AddressOfNames;
char tmp[512];
for (int i = 0; i < exports->NumberOfNames; i++)
{
UnDecorateSymbolName((const char*)((BYTE *)lib + ((DWORD *)names)[i]), tmp, 512, UNDNAME_COMPLETE);

printf("Export: %s\n", tmp);
}

return 0;
}

Dla DLL w C dostajemy niestety tylko nazwy funkcji (sprawdziłem dla msvcrt.dll), ale dla tych napisanych w C++ da się uzyskać już nieco więcej:
Export: public: __thiscall Dupa::Dupa(void)
Export: public: __thiscall Dupa::~Dupa(void)
Export: public: class Dupa & __thiscall Dupa::operator=(class Dupa const &)
Export: bool __cdecl aaa(int,float)
Export: public: int __thiscall Dupa::bar(void)
Export: public: void __thiscall Dupa::foo(void)
Export: public: double __thiscall Dupa::sfghw(int,float,char const *)

Wydaje mi się, że adres metody z klasy lub funkcji C++ możesz normalnie pozyskać za pomocą GetProcAddress (podajesz udekorowaną nazwę). Więc dla C++ na Windowsie dla bibliotek skompilowanych za pomocą Visual Studio możesz uzyskać pełne deklaracje funkcji. Dla GCC pewnie też się jakoś da.

[Edit]: A i nie daję żadnej gwarancji, że ten kod będzie działał zawsze. To jest bardziej na zasadzie "u mnie działa" ;)
« Ostatnia zmiana: Kwiecień 09, 2013, 13:43:26 wysłana przez FoToN »

Offline dynax

  • Użytkownik

# Kwiecień 09, 2013, 14:31:35
http://stackoverflow.com/questions/4353116/listing-the-exported-functions-of-a-dll

Ja wcześniej trafiłem na takie coś. Ale tak jak mówiłem - dla bibliotek C nie zwraca listy argumentów i konwencji wołania.

Offline Kos

  • Użytkownik
    • kos.gd

# Kwiecień 10, 2013, 16:28:26
Jeśli to ma być dowolna funkcja z dowolnej DLL, to umarł w butach, bo nawet, jeśli przeparsujesz headery, jak rodzi Xirdus, to musisz jeszcze zmanglować nazwy - to spory problem w C++, ale i w C trzeba cos zrobić (zależnie od konwencji wywołania, widywałem na przykład @<rozmiar_argumentów> jako sufix nazwy w DLLce).

Manglowanie nazw w C? Seriously? Good lord! Który kompilator tak robi?

BTW jest taka pythonowa biblioteka ctypes która służy dokładnie do tego - ładowanie w locie bibliotek dynamicznych i wołanie funkcji z nich (+ konwersja między typami C a Pythonowymi; bardzo fajnie daje radę). Na windzie obsługuje dwa smaki dllek:

Cytuj
cdll loads libraries which export functions using the standard cdecl calling convention, while windll libraries call functions using the stdcall calling convention

Offline Krzysiek K.

  • Redaktor
    • DevKK.net

# Kwiecień 10, 2013, 16:33:01
Cytuj
Manglowanie nazw w C? Seriously? Good lord! Który kompilator tak robi?
Każdy windowsowy, bo tak zapisuje się nazwy funkcji eksportowanych/importowanych z DLL w WinAPI.

Offline dynax

  • Użytkownik

# Kwiecień 10, 2013, 17:09:42
BTW jest taka pythonowa biblioteka ctypes która służy dokładnie do tego - ładowanie w locie bibliotek dynamicznych i wołanie funkcji z nich (+ konwersja między typami C a Pythonowymi; bardzo fajnie daje radę)

I w sumie o to samo mi chodzi. Fajnie jakby pokazali jak oni to u siebie robią ;)