From 6468fc1abebaf4bfa05786e668a7dc81da5de12d Mon Sep 17 00:00:00 2001 From: Dmitriy Fomichev Date: Mon, 12 Mar 2018 17:44:41 +0300 Subject: [PATCH 1/9] Create 2018-03-13-steam-api-calls-forwarding.md Half translated article from habrahabr. --- .../2018-03-13-steam-api-calls-forwarding.md | 537 ++++++++++++++++++ 1 file changed, 537 insertions(+) create mode 100644 jekyll/_posts/2018-03-13-steam-api-calls-forwarding.md diff --git a/jekyll/_posts/2018-03-13-steam-api-calls-forwarding.md b/jekyll/_posts/2018-03-13-steam-api-calls-forwarding.md new file mode 100644 index 000000000..d2a7cc11f --- /dev/null +++ b/jekyll/_posts/2018-03-13-steam-api-calls-forwarding.md @@ -0,0 +1,537 @@ +--- +title: "The Steam API calls forwarding from Wine to GNU/Linux and vice versa using Nim" +author: Dmitry Fomichev +--- + +*This is an English version of the [Russian article](https://habrahabr.ru/post/349388/) originally posted on [habrahabr](https://habrahabr.ru/).* + +Players on the GNU/Linux platform have a lot of problems. One of them is the necessity to install an another Steam client for each Wine prefix used for Windows Steam games. The situation is getting worse if we consider the necessity to install a native Steam client for ported and cross-platform games also. +![](https://habrastorage.org/webt/h6/ad/8m/h6ad8m2tfoak1abb7kayqozqb9s.png) +But what if we find a way to use one client for all games? As a basis, we can take a native Steam client, and games for Windows will address it just like, for example, to OpenGL or the sound subsystem of GNU/Linux - through the Wine. The implementation of this approach will be discussed below. + +# In Wine veritas + +Wine can handle Windows libraries in two modes: native and built-in. The native library is perceived by Wine as a file with the extension `* .dll`, which it needs to load into memory and work with, as with the Windows entity. Wine handling all libraries, about which it knows nothing, exactly in this mode. The built-in mode implies that Wine must handle the access to the library in a special way and redirect all calls to a pre-created wrapper with the extension `* .dll.so`, which can access the underlying operating system and its libraries. More information about this can be read [here](https://wiki.winehq.org/Wine_Developer's_Guide/Architecture_Overview). + +Fortunately, most of the interaction with the Steam client occurs just through the library `steam_api.dll`. It means our task is reduced to implementing the wrapper for `steam_api.dll.so`, which will access `steam_api.dll`'s GNU/Linux counterpart - `libsteam_api.so`. + +Creation of such wrapper is a well [documented](https://wiki.winehq.org/Winelib_User%27s_Guide#Building_Winelib_DLLs) process. We need to take the original library for Windows, obtain a spec-file for it using `winedump`, write the implementation of all functions mentioned in the spec-file and compile/link all the sources we have written using `winegcc`. Or ask `winemaker` to handle all the routine work for us. + +# The devil is in the detail + +At first glance, the task is not so difficult. Especially considering that `winedump` can create wrappers automatically if there are header files of the original library. In our case header files are published by Valve for game developers on [the official site](https://partner.steamgames.com/). So, after creating a wrapper through `winedump`, enabling the built-in `steam_api.dll` mode via `winecfg` and compiling, we launched our own Steam, then the game itself and... The game is crashed! + + +
+trace:steam_api:SteamAPI_RestartAppIfNecessary_ ((uint32 )[hidden])
+trace:steam_api:SteamAPI_RestartAppIfNecessary_ () = (bool )0
+trace:steam_api:SteamAPI_Init_ ()
+Setting breakpad minidump AppID = [hidden]
+Steam_SetMinidumpSteamID:  Caching Steam ID:  [hidden] [API loaded no]
+trace:steam_api:SteamAPI_Init_ () = (bool )1
+trace:steam_api:SteamInternal_ContextInit_ ((void *)0x7ee468)
+trace:steam_api:SteamAPI_GetHSteamPipe_ ()
+trace:steam_api:SteamAPI_GetHSteamPipe_ () = (HSteamPipe )0x1
+trace:steam_api:SteamAPI_GetHSteamUser_ ()
+trace:steam_api:SteamAPI_GetHSteamUser_ () = (HSteamUser )0x1
+trace:steam_api:SteamAPI_GetHSteamPipe_ ()
+trace:steam_api:SteamAPI_GetHSteamPipe_ () = (HSteamPipe )0x1
+trace:steam_api:SteamInternal_CreateInterface_ ((char *)"SteamClient017")
+wine: Unhandled privileged instruction at address 0x7a3a3c92 (thread 0009), starting debugger...
+Unhandled exception: privileged instruction in 32-bit code (0x7a3a3c92).
+
+Note: this log is more informative than the wrapper generated by the above described method, but the essence of the problem have not changed by this fact. +
+ +Judging by the log, our wrapper works (!) exactly until the function `SteamInternal_CreateInterface` is called. What is wrong with it? After reading the documentation and correlating it with the header files, we can find that the function retrurns a pointer to an object of the `SteamClient` class. + +I think that those who are familiar with ABI C ++ already understand the crash origin. The root of the problem is the calling conventions. The C ++ standard does not imply the binary compatibility of programs compiled by different compilers, and in our case the Windows game is compiled by MSVC, while native Steam - by GCC. This problem is not observed for all calls to functions in `steam_api.dll` since they follow the calling conventions for the C language. Once a game receives an instance of the `SteamClient` class from the native Steam and tries to invoke its method (which follows the thiscall convention from C++), an error occurs. To fix the problem, we firstly need to identify key differences in the thiscall calling convention for both of compilers. + +MSVC | GCC +---------|-------- +Puts an object pointer to the ECX register. | Expects an object pointer on top of the stack. +Expects the stack cleanup by callee. | Expects the stack cleanup by caller. + +[[source](http://www.angelcode.com/dev/callconv/callconv.html#thiscall)] + +At this stage, it's worth making a little digression and mentioning that the attempts to solve the problem have already been made, and even quite successfully. There is a project named [SteamBridge](https://github.com/sirnuke/steambridge), which uses two separate Steam libraries - for Windows and for GNU/Linux. The Windows library is built using MSVC and calls the GNU/Linux library using the Wine. The GNU/Linux library is built using GCC and can call `libsteam_api.so` directly. The calling conventions problem is solved by inserting an assembler snippets at the Windows library side and by wrapping each object when it is passed to the MSVC code. This solution is somewhat redundant, since it requires an additional non-crossplatform compiler to build and introduces an extra entity, but the idea of wrapping the returned objects is robust. We will borrow it! + + +Fortunately for us, Wine already knows how to handle calling conventions. It is sufficient to declare a method with the `thiscall` attribute. Thus, we need to create wrappers of all virtual methods of all classes, and in the each method implementation just perform a call of the method from the original class (which pointer should be stored in the wrapper object). The example wrapper implementation will look like this: + + +```c++ +class ISteamClient_ +{ + public: + virtual HSteamPipe CreateSteamPipe() __attribute__((thiscall)); + ... // other methods + private: + ISteamClient * internal; +} +``` +```c++ +HSteamPipe ISteamClient_::CreateSteamPipe() +{ + TRACE("((ISteamClient *)%p)\n", this); + HSteamPipe result = this->internal->CreateSteamPipe(); + TRACE("() = (HSteamPipe)%p\n", result); + return result; +} +``` + + We also need to perform a similar operation but in oposite direction for classes passed from MSVC code to GCC, namely `CCallback` and` CCallResult`. This task is simple and boring, therefore the best solution is to delegate it to the script for code generation. After several attempts to build everything together, the game begins to work. + + +
+trace:steam_api:SteamAPI_RestartAppIfNecessary_ ((uint32 )[hidden])
+trace:steam_api:SteamAPI_RestartAppIfNecessary_ () = (bool )0
+trace:steam_api:SteamAPI_Init_ ()
+Setting breakpad minidump AppID = [hidden]
+Steam_SetMinidumpSteamID:  Caching Steam ID:  [hidden] [API loaded no]
+trace:steam_api:SteamAPI_Init_ () = (bool )1
+trace:steam_api:SteamInternal_ContextInit_ ((void *)0x7ee468)
+trace:steam_api:SteamAPI_GetHSteamPipe_ ()
+trace:steam_api:SteamAPI_GetHSteamPipe_ () = (HSteamPipe )0x1
+trace:steam_api:SteamAPI_GetHSteamUser_ ()
+trace:steam_api:SteamAPI_GetHSteamUser_ () = (HSteamUser )0x1
+trace:steam_api:SteamAPI_GetHSteamPipe_ ()
+trace:steam_api:SteamAPI_GetHSteamPipe_ () = (HSteamPipe )0x1
+trace:steam_api:SteamInternal_CreateInterface_ ((char *)"SteamClient017")
+trace:steam_api:SteamInternal_CreateInterface_ (): (ISteamClient *)0x7a7a04c8 wrapped as (ISteamClient_ *)0x7c49bc70
+trace:steam_api:SteamInternal_CreateInterface_ () = (ISteamClient_ *)0x7c49bc70
+trace:steam_api:GetISteamUser ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"SteamUser019")
+trace:steam_api:GetISteamUser () = (ISteamUser *)0x7c4bcc40
+trace:steam_api:GetISteamFriends ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"SteamFriends015")
+trace:steam_api:GetISteamFriends () = (ISteamFriends *)0x7c4b8650
+trace:steam_api:GetISteamUtils ((ISteamClient *)0x7c49bc70, (HSteamPipe )0x1, (char *)"SteamUtils008")
+trace:steam_api:GetISteamUtils () = (ISteamUtils *)0x7c4b7930
+trace:steam_api:GetISteamMatchmaking ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"SteamMatchMaking009")
+trace:steam_api:GetISteamMatchmaking () = (ISteamMatchmaking *)0x7c4c03c0
+trace:steam_api:GetISteamMatchmakingServers ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"SteamMatchMakingServers002")
+trace:steam_api:GetISteamMatchmakingServers () = (ISteamMatchmakingServers *)0x7c4b5450
+trace:steam_api:GetISteamUserStats ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMUSERSTATS_INTERFACE_VERSION011")
+trace:steam_api:GetISteamUserStats () = (ISteamUserStats *)0x7c4b5e10
+trace:steam_api:GetISteamApps ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMAPPS_INTERFACE_VERSION008")
+trace:steam_api:GetISteamApps () = (ISteamApps *)0x7c4b73a0
+trace:steam_api:GetISteamNetworking ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"SteamNetworking005")
+trace:steam_api:GetISteamNetworking () = (ISteamNetworking *)0x7c49cd40
+trace:steam_api:GetISteamRemoteStorage ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMREMOTESTORAGE_INTERFACE_VERSION014")
+trace:steam_api:GetISteamRemoteStorage () = (ISteamRemoteStorage *)0x7c4c1610
+trace:steam_api:GetISteamScreenshots ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMSCREENSHOTS_INTERFACE_VERSION003")
+trace:steam_api:GetISteamScreenshots () = (ISteamScreenshots *)0x7c4b70b0
+trace:steam_api:GetISteamHTTP ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMHTTP_INTERFACE_VERSION002")
+trace:steam_api:GetISteamHTTP () = (ISteamHTTP *)0x7c4b5c50
+trace:steam_api:GetISteamUnifiedMessages ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMUNIFIEDMESSAGES_INTERFACE_VERSION001")
+trace:steam_api:GetISteamUnifiedMessages () = (ISteamUnifiedMessages *)0x7c49e680
+trace:steam_api:GetISteamController ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"SteamController005")
+trace:steam_api:GetISteamController () = (ISteamController *)0x7c49bfd0
+trace:steam_api:GetISteamUGC ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMUGC_INTERFACE_VERSION009")
+trace:steam_api:GetISteamUGC () = (ISteamUGC *)0x7c49cad0
+trace:steam_api:GetISteamAppList ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMAPPLIST_INTERFACE_VERSION001")
+trace:steam_api:GetISteamAppList () = (ISteamAppList *)0x7c49c450
+trace:steam_api:GetISteamMusic ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMMUSIC_INTERFACE_VERSION001")
+trace:steam_api:GetISteamMusic () = (ISteamMusic *)0x7c49cbf0
+trace:steam_api:GetISteamMusicRemote ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMMUSICREMOTE_INTERFACE_VERSION001")
+trace:steam_api:GetISteamMusicRemote () = (ISteamMusicRemote *)0x7c49e710
+trace:steam_api:GetISteamHTMLSurface ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMHTMLSURFACE_INTERFACE_VERSION_003")
+trace:steam_api:GetISteamHTMLSurface () = (ISteamHTMLSurface *)0x7c49ccb0
+trace:steam_api:GetISteamInventory ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMINVENTORY_INTERFACE_V001")
+trace:steam_api:GetISteamInventory () = (ISteamInventory *)0x7c49d0c0
+trace:steam_api:GetISteamVideo ((ISteamClient *)0x7c49bc70, (HSteamUser )0x1, (HSteamPipe )0x1, (char *)"STEAMVIDEO_INTERFACE_V001")
+trace:steam_api:GetISteamVideo () = (ISteamVideo *)0x7c49cb60
+trace:steam_api:SetOverlayNotificationPosition ((ISteamUtils *)0x7c4b7930, (ENotificationPosition )0x2)
+trace:steam_api:SteamInternal_ContextInit_ ((void *)0x7ee468)
+trace:steam_api:SetWarningMessageHook ((ISteamUtils *)0x7c4b7930, (SteamAPIWarningMessageHook_t )0x52ebb0)
+
+
+ +It would seem: the fairy tale is over? And here not! + +# Welcome to the versions hell! + +Very soon it turns out that our design is completely viable only for games compiled using the same header files that we have available. And we only have the latest version of the Steam API. Other versions of headers Valve does not publish (even the latest headers were given under a closed license). On the other hand, our Steam is also of the latest version, but it does not prevent it from working with the old versions of the Steam API. How does it do that? + +The answer is hidden behind this line of the log: `trace:steam_api:SteamInternal_CreateInterface_ ((char *)"SteamClient017")`. It turns out that the client stores information about all classes of all versions of the Steam API, and the `steam_api.dll` only asks the client for an instance of the required class of the desired version. It remains only to find where exactly they are stored. First, let's try the straight approach: to find the "SteamClient016" string in the `libsteam_api.so`. Why not the "SteamClient017" string? Because we need to find a place were stored all the versions of Steam API instead of just the version of `libsteam_api.so` we have. + + +```bash +$ grep "SteamClient017" libsteam_api.so +Binary file libsteam_api.so matches +$ grep "SteamClient016" libsteam_api.so +$ +``` + +It seems that there is nothing similar in `libsteam_api.so`. Then we will try to walk through all the libraries of the Steam client. + +```bash +$ grep "SteamClient017" *.so +Binary file steamclient.so matches +Binary file steamui.so matches +$ grep "SteamClient016" *.so +Binary file steamclient.so matches +$ +``` + +And here is what we need! Let's curtain the Gabe Newell's portraint, if you have one, and open `steamclient.so` in the IDA. A quick keyword search shows an interesting set of strings which follow the pattern: `CAdapterSteamClient0XX`, where XX is the version number. What is even more curious, in the file there are lines with pattern `CAdapterSteamYYYY0XX`, where XX is also the version number, and YYYY is the name of the Steam API class for all other interfaces. The analysis of cross-references allows to find a table of virtual methods for each of the classes with such names easily. Thus, the summary scheme for each class will look like this: + +![](https://habrastorage.org/webt/hf/e4/4i/hfe44isvt4vsanajaro-oesru0k.png) + +The method table is found, but we have absolutely no information about the signatures of these methods. But this problem turned out to be [solvable](https://toster.ru/answer?answer_id=1156239)(the source in Russian) by calculating the maximum depth of the stack the method tries to access. So we can make an utility that will be receiving the `steamclient.so` library to the input, and forms a list of classes of all versions, as well as their methods at the output. It remains only to generate a wrapper code for the method calls conversion based on the list. The task does not look simple, especially considering that the method signature itself is not known to us, we know only the depth of the stack where the method call arguments end. The situation is aggravated by the peculiarities of the in-memory structures return, namely the presence of a hidden argument - pointer to the memory where the structure should be written. This pointer in all call conventions is extracted from the stack by the callee, so we can easily detect it by the `ret $4` instruction in the assembler code of methods in `steamclient.so`. But even so, the amount of non-trivial code generation is huge. + +# The Hero appears + +To any new or just not very popular programming language, the question of its niche first of all arises. Nim is no exception. It is often criticized for trying to "sit on all chairs at once", implying a big amount of features in the absence of one clear direction of development. Among such features it is possible to distinguish two: + +- compilation to C and, as a consequence, easy cross-platform compilation; + +- excellent metaprogramming support (the same language for both of run-time and compile-time code, direct manipulation with AST ). + +Именно это сочетание в результате и позволит сделать процесс написания обёртки безболезненным. +Для начала создадим основной файл `steam_api.nim` и файл с опциями компиляции `steam_api.nims`: + +```nim +const specname {.strdefine.} = "steam_api.spec" # spec файл пригодится во время компиляции, потому принимаем путь к нему через опцию `-d:specname=/path/to/steam_api.spec` с помощью прагмы {.strdefine.} и записываем в константу `specname`. +# Если опция не задана, в константу запишется значение по умолчанию — "steam_api.spec". +{.passL: "'" & specname & "'".} # Также передаем путь к spec файлу линкеру в качестве аргумента. + +# Описываем макрос TRACE из заголовочных файлов wine, который поможет нам при отладке +proc trace*(format: cstring) + {.varargs, importc: "TRACE", header: """#include +#include "wine/debug.h" +WINE_DEFAULT_DEBUG_CHANNEL(steam_api);""".} +# Прагма varargs указывает, что после первого аргумента могут быть ещё, прагма importc — как должно выглядеть имя при вызове в Си коде, прагма header — что должно быть помещено в шапку Си файла, где происходит вызов. +# Строго говоря, Nim понятия не имеет что такое TRACE. Зато теперь он знает, как можно вызвать TRACE в коде на Си. + +# Эта функция сгенерирована winedump'ом, потому включаем её в промежуточный код на Си почти без изменений. +{.emit:[""" +BOOL WINAPI DllMain(HINSTANCE instance, DWORD reason, void *reserved) +{ + """, trace, """("(%p, %u, %p)\n", instance, reason, reserved); // вызываем именно описанный нами макрос, чтобы не ломать зависимости от заголовочных файлов + switch (reason) + { + case DLL_WINE_PREATTACH: + return FALSE; /* prefer native version */ + case DLL_PROCESS_ATTACH: + DisableThreadLibraryCalls(instance); + NimMain(); // инициализируем сборщик мусора и рантайм Nim + break; + } + return TRUE; +} +"""].} +``` + + + +```nim +--app:lib # мы создаём библиотеку steam_api.dll.so, а не исполняемый файл +--passL:"-mno-cygwin" # несколько специальных опций передаём winegcc напрямую +--passC:"-mno-cygwin" # на самом деле это вовсе не опция, а макрос `--`, который эмулирует поведение опций компилятора +--passC:"-D__WINESRC__" # а сам файл написан на подмножестве языка Nim +--os:windows # хотя библиотека компилируется в linux, wine предоставляет нам функции WinAPI +--noMain # Мы создали свою функцию `DllMain`, поэтому не нужно, чтобы Nim создал ещё одну +--cc:gcc # явно указываем семейство компилятора C +# Дальше придётся использовать `switch`, так как макрос `--` не поддерживает точки в имени опции +switch("gcc.exe", "/usr/bin/winegcc") # а также путь к самому компилятору и линкеру +switch("gcc.linkerexe", "/usr/bin/winegcc") # я уже говорил что `switch` и `--` эквивалентны? +``` + + +Выглядит не очень-то и просто, но это лишь по причине того, что мы замахнулись на многое сразу. Здесь и кросскомпиляция, и импорт функций из заголовочных файлов Си, и особенности компиляции под Wine... Несмотря на кажущуюся сложность, ничего сложного не произошло, мы просто напрямую внедрили некоторые части исходного кода на Си, о которых Nim ничего не знает, и знать не может, а заодно описали для Nim как вызывать макрос TRACE из заголовочных файлов Wine (про сами эти файлы тоже рассказали). + +Теперь перейдём к самому вкусному — макросам и кодогенерации. Поскольку у нас нет полной информации о сигнатурах методов, мы будем эмулировать экземпляры классов из кода на Си, благо нам нужно эмулировать только виртуальную таблицу методов. Итак, пусть у нас есть файл, в котором описаны методы и классы Steam API следующим образом: +
+!CAdapterSteamYYY0XX
+[+]<глубина стека метода 1>
+[+]<глубина стека метода 2>
+...
+
+Знак `+` опционален и будет служить индикатором скрытого аргумента. +Этот файл можно получить, анализируя `steamclient.so`. Из него должна получиться таблица. Ключами к ней будут строки вида `CAdapterSteamYYYY0XX`, а значениями — массив ссылок на функции, вызывающие соответствующие методы в объекте, который является полем структуры, переданной в них неявно, через регистр `ECX`. Писать всё это на ассемблере не очень удобно, особенно учитывая, что неплохо было бы добавить какое-нибудь журналирование, поэтому выделим минимальный ассемблерный фрагмент: + +
+[...]
+[...]
+[...]
+[адрес возврата] <= ESP
+[аргумент 1]
+[аргумент 2]
+[???]
+
+
+```asm +push %ecx # помещаем в стек указатель на объект (он станет вторым аргументом) +push $<порядковый номер метода в таблице> # помещаем в стек номер метода (он будет самым первым аргументом) +# остальные аргументы сдвинутся на 3 (два помещённых в стек и адрес возврата) +call <функция Nim> # вызываем функцию, написанную на Nim +add $0x4, %esp # убираем из стека номер метода +pop %ecx # извлекаем указатель на объект +ret $<глубина стека> # удаляем из стека аргументы и возвращаемся +``` + +
+[адрес возврата в ассемблерный фрагмент] <= ESP
+[номер метода]
+[указатель на объект = %ecx]
+[адрес возврата]
+[аргумент 1]
+[аргумент 2]
+[???]
+
+
+ +
+[адрес возврата в ассемблерный фрагмент]
+[номер метода]
+[указатель на объект = %ecx]
+[адрес возврата]
+[аргумент 1]
+[аргумент 2]
+[???] <= ESP
+
+
+Осталось сгенерировать обозначенные функции Nim. Нужно сгенерировать по одной функции для каждой глубины стека встреченной в файле и ещё по одной для вызовов со скрытым аргументом. Далее будем называть эти функции псевдометодами для краткости. + +```nim +proc pseudoMethod4(methodNo: uint32, obj: ptr WrappedObject, retAddress: pointer, argument1: pointer) : uint64 {.cdecl.} = + # Название метода pseudoMethod<глубина стека> + # methodNo - порядковый номер метода в виртуальной таблице начиная с 0 + # obj - указатель на обертку объекта + # retAddress - адрес возврата в код игры (не используется) + # argument1 - аргумент, передаваемый в метод + # возвращаем uint64, так как наверняка неизвестно, будет ли возвращено 64 битное значение в регистрах EAX и EDX или 32 битное в EAX. + # прагма cdecl говорит компилятору, что он должен следовать соглашениям о вызовах Си + trace("Method No %d was called for obj=%p and return to %p\n", + methodNo, obj, retAddress) + trace("(%p)\n", argument1) + trace("Origin = %p\n", obj.origin) + let vtableaddr = obj.origin.vtable + trace("Origins VTable = %p\n", vtableaddr) # просто выводим всю информацию о методе для отладки + let maddr = cast[ptr proc(obj: pointer argument1: pointer): uint64](cast[uint32](vtableaddr) + methodNo*4) # вычисляем положение адреса оригинального метода + trace("Method address to call: %p\n", maddr) + let themethod = maddr[] # получаем адрес оригинального метода + trace("Method to call: %p\n", themethod) + let res = themethod(obj.origin, argument1) # вызываем оригинальный метод (соглашения о вызовах GCC) + trace("Result = %p\n", res) + return wrapIfNecessary(res) # если результат - указатель на объект, то оборачиваем его и возвращаем обёртку. +``` + +Оставим за скобками реализацию функции `wrapIfNecessary` и перейдём к описанию кода, который генерирует описанные выше фрагменты. Сначала прочитаем файл, в котором хранятся описания классов. Путь к файлу мы получим так же, как и путь к spec-файлу — через опцию компилятора. + + +```nim +from strutils import splitLines, split, parseInt +from tables import initTable, `[]`, `[]=`, pairs, Table +type + StackState* = tuple + # информация о стеке для конкретного метода + depth: int # глубина стека + swap: bool # индикатор наличия скрытого аргумента + Classes* = Table[string, seq[StackState]] ## таблица, которую мы хотим получить: ключи — имена классов (CAdapterSteamYYY0XX), значения — списки глубин стека каждого метода + +const cdfile {.strdefine.} = "" + # по аналогии с прошлым случаем, получаем путь к файлу из опций компилятора + +proc readClasses(): Classes {.compileTime.} = + # прагма compileTime явно указывает компилятору, что не нужно генерировать код для этой функции + result = initTable[string, seq[StackState]]() # result — неявная переменная, которая будет возвращена в конце функции + let filedata = slurp(cdfile) # во время компиляции файл читается функцией `slurp`, в то время как обычные функции работы с файлами недоступны + for line in filedata.splitLines(): + if line.len == 0: + continue + elif line[0] == '!': + let curstr = line[1..^1] # подстрока с первого по последний символ + result[curstr] = newSeq[StackState]() + else: + let depth = parseInt(line) + let swap = line[0] == '+' # в качестве индикатора скрытого аргумента служит знак "+" перед глубиной стека + # он не влияет на распознавание числа и очень легко проверяется + result[curstr].add((depth: depth, swap: swap)) # Именованный кортеж не требует особого конструктора с именем типа + # возврата нет, так как в result и так записано возвращаемое значение +``` + +Теперь мы получили таблицу классов. Поскольку функция `readClasses` не использует ничего, возможного только во время выполнения, мы смело можем вычислить её во время компиляции и записать результат в константу: `const classes = readClasses()`. Составим таблицу методов-обёрток, состоящих из ассемблерных вставок, описанных выше. + + +```nim +static: + # Ключевое слово static указывает, что работа с переменными происходит во время компиляции. + var declared: set[uint8] = {} # глубины стека, для которых нужно будет создать псевдометоды + var swpdeclared: set[uint8] = {} # глубины стека, для которых нужно будет создать псевдометоды со скрытым аргументом + +proc eachMethod(k: string, methods: seq[StackState], sink: NimNode): NimNode {.compileTime.} = + # создаёт декларацию функции и присваивает её `k`тому элементу в таблице с идентификатором `sink` + # NimNode - любой элемент АСД. В нашем случае это идентификатор на входе и список выражений на выходе. + result = newStmtList() # пустой список выражений языка + let kString = newStrLitNode k # превращение строки в узел АСД, означающий строку + # Unified Call Syntax позволяет записывать вызовы функций как душе угодно, конкретно верхний эквивалентен newStrLitNode(k), k.newStrLitNode() и k.newStrLitNode (стиль изменён для демострации) + result.add quote do: # quote - особый макрос, создающий АСД для участка кода, переданного ему в качестве аргумента, а `do` позволяет превратить в аргумент код под ним + `sink`[`kString`] = newSeq[MethodProc](2) # всё, что в кавычках будет подставлено в АСД без изменений + for i, v in methods.pairs(): + if v.swap: # подсчёт псевдометодов, которые предстоит создать + swpdeclared.incl(v.depth.uint8) # неявные преобразования типов не допускаются + else: + declared.incl(v.depth.uint8) + # Уже знакомая нам ассемблерная вставка в виде строки с комментариями. + # Необходимые значения вклеиваются в неё оператором конкатенации `&`. + # Тройные кавычки ведут себя также как в питоне. + let asmcode = """ + push %ecx # помещаем в стек указатель на объект + push $0x""" & i.toHex & """ # затем номер метода в виртуальной таблице + call `pseudoMethod""" & $v.depth & (if v.swap: "S" else: "") & #конструкции if-elif-else и case-of-else могут быть выражениями возвращающими результат + """` # вызываем псевдометод + add $0x4, %esp # убираем из стека номер метода + pop %ecx # возвращаем указатель на объект в регистр ECX и чистим от него стек + ret $""" & $(v.depth-4) & """ # чистим стек от остальных аргументов и возвращаемся +""" + var tstr = newNimNode(nnkTripleStrLit) # nnkTripleStrLit это тип узла АСД для строки в тройных кавычках + tstr.strVal = asmcode # превращаем строку в узел АСД эквивалентный этой строке + let asmstmt = newTree(nnkAsmStmt, newEmptyNode(), tstr) # а затем в узел АСД эквивалентный выражению `asm """<код>"""` + let methodname = newIdentNode("m" & k & $i) # создаём идентификатор метода как `m<имя класса><номер метода>` + result.add quote do: # вклеиваем в шаблон декларации функции и добавляем полученное АСД к общему списку + proc `methodname` () {.asmNoStackFrame, noReturn.} = # декларация функции + # прагма asmNoStackFrame должна указать компилятору, не создавать новый фрейм в стеке + # прагма noReturn говорит компилятору, что возврат сделан вручную и генерировать для этого код не нужно + `asmstmt` + # присваивание + add(`sink`[`kString`], `methodname`) # макросу quote не всегда удаётся правильно понять конструкцию с вклеенными кусками АСД, потому иногда приходится призывать на помощь UCS и видоизменить вызов +``` + +По полученным спискам строим псевдометоды. Процесс перебора списков оставлен за кадром. Также стоит отметить, что все процедуры, использованные нами — обычные функции Nim, оперирующие АСД и вызываемые из тела макроса (который тоже опущен). Магия интерпретации созданных АСД происходит при выходе из тела макроса. + +```nim +proc makePseudoMethod(stack: uint8, swp: bool): NimNode {.compileTime.} = + ## Создаёт АСД с декларацией псевдометода. + result = newProc(newIdentNode("pseudoMethod" & $stack & + (if swp:"S" else: ""))) # новая декларация пустой функции с именем "pseudoMethod<глубина стека>[S]" + # подход с `quote` тут не работает, так как аргументы генерируются динамически + result.addPragma(newIdentNode("cdecl")) # добавляем {.cdecl.} + let nargs = max(int(stack div 4) - 1 - int(swp), 0) # число реальных аргументов за вычетом самого объекта и скрытого аргумента, если он есть + let justargs = genArgs(nargs) # эта функция опущена, её результат - массив деклараций аргументов функции от "argument1: uint32" до "argument: uint32" + let origin = newIdentNode("origin") + let rmethod = newIdentNode("rmethod") + var mcall = genCall("rmethod", nargs) # эта функция тоже опущена, её результат - АСД вызова "rmethod(argument1, ... , argument)" + mcall.insert(1, origin) # вставка первым аргументом идентификатора оригинального объекта + var argseq = @[ # Аргументы самого псевдометода + newIdentNode("uint64"), # возвращаемое значение + newIdentDefs(newIdentNode("methodNo"), newIdentNode("uint32")), + # порядковый номер метода + newIdentDefs(newIdentNode("obj"), newIdentNode("uint32")), + # ссылка на объект (тип изменён на uint32 для простоты восприятия) + newIdentDefs(newIdentNode("retAddress"), newIdentNode("uint32")), + # адрес возврата + ] + if swp: + # если есть скрытый аргумент - добавляем его + argseq.add(newIdentDefs(newIdentNode("hidden"), newIdentNode("pointer"))) + # остальные аргументы добавляем в конец + argseq &= justargs[1..^1] + var originargs = @[ # Аргументы для декларации оригинального метода + newIdentNode("uint64"), + newIdentDefs(newIdentNode("obj"), newIdentNode("uint32")), + ] & justargs[1..^1] + let procty = newTree(nnkProcTy, newTree(nnkFormalParams, originargs), + newTree(nnkPragma, newIdentNode("cdecl"))) # сама декларация оригинального метода + let args = newTree(nnkFormalParams, argseq) + result[3] = args # подставляем аргументы в декларацию псевдометода + let tracecall = genTraceCall(nargs) # реализация опущена для простоты, результат - вызов trace со всеми аргументами, переданными в псевдометод + result.body = quote do: # подстановка тела функции + trace("Method No %d was called for obj=%p and return to %p\n", + methodNo, obj, retAddress) + `tracecall` + let wclass = cast[ptr WrappedClass](obj) # цена нашего упрощения декларации - необходимость преобразования `uint32` в `ptr WrappedClass` + let `origin` = cast[uint32](wclass.origin) + trace("Origin = %p\n", `origin`) + let vtableaddr = wclass.origin.vtable + trace("Origins VTable = %p\n", vtableaddr) + let maddr = cast[ptr `procty`](cast[uint32](vtableaddr) + shift*4) + trace("Method address to call: %p\n", maddr) + let `rmethod` = maddr[] + trace("Method to call: %p\n", `rmethod`) + if swp: + # для случая скрытого аргумента нужна ещё одна ассемблерная вставка, тут она показана не будет + let asmcall = genAsmHiddenCall("rmethod", "origin", nargs) # вставка меняет местами скрытый аргумент и указатель на объект, а также исправляет стек так, что скрытый аргумент перестаёт быть скрытым + result.body.add quote do: + trace("Hidden before = %p (%p) \n", hidden, cast[ptr cint](hidden)[]) + `asmcall` # вызов происходит внутри вставки + trace("Hidden result = %p (%p) \n", hidden, cast[ptr cint](hidden)[]) + return cast[uint64](hidden) + # зато для случая скрытого аргумента не нужно выполнять проверку необходимости обёртки, заранее известно, что возвращаемое значение не является указателем на объект + else: + # добавляем АСД самого вызова и проверку необходимости обёртки + result.body.add quote do: + let res = `mcall` + trace("Result = %p\n", res) + return wrapIfNecessary(res) # реализация `wrapIfNecessary` в эту статью не поместилась +``` + + +Самая сложная часть позади. Сложность её обусловлена необходимостью формирования и вставки динамического списка аргументов в несколько ключевых точек декларации псевдометода. Здесь не работает простой подход с шаблоном и подстановкой через quote, поэтому приходится собирать узлы АСД один за другим, что негативно сказывается на объеме и читаемости кода. Осталось написать сам макрос, из которого будут вызываться наши генераторы АСД. + + +```nim +macro makeTableOfVTables(sink: untyped): untyped = + # создаёт таблицу с массивами виртуальных методов каждого класса + # `sink` - переменная-назначение, куда всё будет записано. + result = newStmtList() # пустой список выражений + result.add quote do: # `sink` в аргументах макроса указан как untyped, но в теле макроса он чудесным образом превращается в узел АСД, то есть имеет тип NimNode + `sink` = initTable[string, seq[MethodProc]]() # создаём новую таблицу + let classes = readClasses() # та самая функция readClasses, которой мы разбирали файл во время компиляции + for k, v in classes.pairs: + result.add(eachMethod(k, v, sink)) # сначала создаём методы-обёртки + for i in declared: # напомню, что `declared` это глобальная переменная времени компиляции, по совместительству множество, которое мы определили и наполнили в eachMethod ранее. + result.insert(0, makePseudoMethod(i, false)) # псевдометоды вставляем до самих методов, поскольку Nim, как и Си, чувствителен к порядку определения функций + for i in swpdeclared: + result.insert(0, makePseudoMethod(i, true)) + when declared(debug): # если компилятору передан флаг `-d:debug`, выводим АСД в виде кода в stdout прямо во время компиляции, + echo(result.repr) # на случай если нужно будет посмотреть, как выглядит сгенерированный код + # магия макроса превращает наш `result` из NimNode обратно в `untyped`, то есть в код +# и вызов макроса. +var vtables: Table[string, seq[MethodProc]] +makeTableOfVTables(vtables) +``` + + +Похожим образом создаются объявления основных функций `steam_api.dll`. Для проброса вызовов обратно из GNU/Linux в игру форма уже известна и едина для всех версий Steam API, поэтому нет нужды в кодогенерации. Например, определение первого метода будет выглядеть так: + +```nim +proc run(obj: ptr WrappedCallback, p: pointer) {.cdecl.} = + # первый виртуальный метод класса CCallback. + trace("[%p](%p)\n", obj, p) + let originRun = (obj.origin.vtable + 0)[] # `+` определён отдельно для указателя и числа, чтобы избежать большого количества преобразований типов + let originObj = obj.origin + asm """ + mov %[obj], %%ecx # Метод игры ожидает увидеть указатель на объект в регистре ECX + mov %%esp, %%edi # ESP сохраняем в EDI, т.к. он не меняется при вызове + push %[p] # Помещаем аргумент в стек + call %[mcall] # вызываем метод + mov %%edi, %%esp # восстанавливаем стек + ::[obj]"g"(`originObj`), [p]"g"(`p`), [mcall]"g"(`originRun`) + :"eax", "edi", "ecx", "cc" +""" +``` + + +# Заключение +Итак, мы рассмотрели основные ключевые точки, позволяющие сгенерировать обёртку для Steam API во время компиляции. Какими бы сложными они не казались, такой подход, несомненно, выигрывает у ручного написания нескольких сотен однотипных методов. Nim написал все эти методы за нас. Кто-то может спросить: «А что там с отладкой всего этого ужаса?». Вопрос отладки кода времени компиляции действительно сложен. Единственное средство — это старые добрые отладочные сообщения `echo` (аналог `print` в Nim). К счастью в Nim есть функции `repr` и `treeRepr`, которые превращают АСД в строку кода и строку со структурной схемой узлов соответственно, что сильно упрощает отладку. + +Особо стоит отметить гибкость компилятора Nim. Компиляция в Си в сочетании с высококлассной поддержкой метапрограммирования позволяет рассматривать его и как сверхмощный препроцессор для Си, и как отдельный компилятор языка, не уступающего по возможностям Си, в обёртке приятного питоноподобного синтаксиса. + +Возможно, статья покажется слишком сумбурной, поскольку достаточно непросто описать сложную задачу и её решение, в которых язык раскрывается на полную мощность, простым и лаконичным образом. К сожалению, в рамках этой статьи не удалось описать ещё несколько аспектов, а именно: +- функцию `wrapIfNeccessary` и механизм определения имени объекта по указателю; +- формирование класса-обёртки на основе описанных методов; +- взаимодействие со Steam для загрузки игры; +- подробности реализации обёрток функций `steam_api.dll` (в статье речь шла только о виртуальных методах); +- утилиты для анализа `steamclient.so` и `libsteam_api.so`, эмуляция поведения стека; +- подводные камни и проблемы, которые возникли при поиске описанных в статье решений (сборщик мусора, игнорирование прагмы `asmNoStackFrame`, старые версии компилятора). + +Такие подробности, на мой взгляд, ещё сильнее ухудшили бы восприятие. Кроме того, статья не описывает реальный ход исследования и решения проблемы, а лишь представляет реконструкцию решения в угоду целостности повествования. + +Рабочее решение обозначенной в заголовке проблемы представлено в репозитории на github: +- в [ветке master](https://github.com/xomachine/SteamForwarder/tree/8f2b8cea17da8718dfd8a87fbd2677d475abb54a) реализация без использования Nim и хорошо работающая только с одной версией Steam API; +- в [ветке devel](https://github.com/xomachine/SteamForwarder/tree/a7dea4b6a87086b93d4165090d85ec8134985962) реализация с использованием Nim, о которой шла речь во второй половине статьи. + +Некоторые имена переменных и функций в оригинальном коде отличаются от примеров, данных в статье. Ссылки даны на коммит каждой ветки, являющийся верхним на момент публикации, чтобы не потерять актуальность со временем. + +Надеюсь, статья вызовет дополнительный интерес к языку программирования Nim и покажет читателям, что на нём можно писать нечто более сложное, чем `echo "Hello, world!"`. From 40feb171f9c86bb063175e074b73d20cfaad53b5 Mon Sep 17 00:00:00 2001 From: xomachine Date: Mon, 12 Mar 2018 18:00:41 +0300 Subject: [PATCH 2/9] Using todays date for Steam related article for easer changes checking --- ...lls-forwarding.md => 2018-03-12-steam-api-calls-forwarding.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename jekyll/_posts/{2018-03-13-steam-api-calls-forwarding.md => 2018-03-12-steam-api-calls-forwarding.md} (100%) diff --git a/jekyll/_posts/2018-03-13-steam-api-calls-forwarding.md b/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md similarity index 100% rename from jekyll/_posts/2018-03-13-steam-api-calls-forwarding.md rename to jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md From b9171732a4d02181badd486d011e15010a605014 Mon Sep 17 00:00:00 2001 From: xomachine Date: Mon, 12 Mar 2018 19:07:31 +0300 Subject: [PATCH 3/9] Added dynamic spoilers as a replace of missing tag --- .../2018-03-12-steam-api-calls-forwarding.md | 122 +++++++++++------- 1 file changed, 74 insertions(+), 48 deletions(-) diff --git a/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md b/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md index d2a7cc11f..32eb815ae 100644 --- a/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md +++ b/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md @@ -21,7 +21,8 @@ Creation of such wrapper is a well [documented](https://wiki.winehq.org/Winelib_ At first glance, the task is not so difficult. Especially considering that `winedump` can create wrappers automatically if there are header files of the original library. In our case header files are published by Valve for game developers on [the official site](https://partner.steamgames.com/). So, after creating a wrapper through `winedump`, enabling the built-in `steam_api.dll` mode via `winecfg` and compiling, we launched our own Steam, then the game itself and... The game is crashed! - + + Judging by the log, our wrapper works (!) exactly until the function `SteamInternal_CreateInterface` is called. What is wrong with it? After reading the documentation and correlating it with the header files, we can find that the function retrurns a pointer to an object of the `SteamClient` class. @@ -82,7 +83,8 @@ HSteamPipe ISteamClient_::CreateSteamPipe() We also need to perform a similar operation but in oposite direction for classes passed from MSVC code to GCC, namely `CCallback` and` CCallResult`. This task is simple and boring, therefore the best solution is to delegate it to the script for code generation. After several attempts to build everything together, the game begins to work. - + + It would seem: the fairy tale is over? And here not! @@ -156,9 +158,9 @@ The answer is hidden behind this line of the log: `trace:steam_api:SteamInternal ```bash -$ grep "SteamClient017" libsteam_api.so +$ grep "SteamClient017" libsteam_api.so Binary file libsteam_api.so matches -$ grep "SteamClient016" libsteam_api.so +$ grep "SteamClient016" libsteam_api.so $ ``` @@ -189,8 +191,9 @@ To any new or just not very popular programming language, the question of its ni Именно это сочетание в результате и позволит сделать процесс написания обёртки безболезненным. Для начала создадим основной файл `steam_api.nim` и файл с опциями компиляции `steam_api.nims`: - -```nim + + - -```nim + + Выглядит не очень-то и просто, но это лишь по причине того, что мы замахнулись на многое сразу. Здесь и кросскомпиляция, и импорт функций из заголовочных файлов Си, и особенности компиляции под Wine... Несмотря на кажущуюся сложность, ничего сложного не произошло, мы просто напрямую внедрили некоторые части исходного кода на Си, о которых Nim ничего не знает, и знать не может, а заодно описали для Nim как вызывать макрос TRACE из заголовочных файлов Wine (про сами эти файлы тоже рассказали). Теперь перейдём к самому вкусному — макросам и кодогенерации. Поскольку у нас нет полной информации о сигнатурах методов, мы будем эмулировать экземпляры классов из кода на Си, благо нам нужно эмулировать только виртуальную таблицу методов. Итак, пусть у нас есть файл, в котором описаны методы и классы Steam API следующим образом: +
 !CAdapterSteamYYY0XX
 [+]<глубина стека метода 1>
 [+]<глубина стека метода 2>
 ...
 
+ Знак `+` опционален и будет служить индикатором скрытого аргумента. Этот файл можно получить, анализируя `steamclient.so`. Из него должна получиться таблица. Ключами к ней будут строки вида `CAdapterSteamYYYY0XX`, а значениями — массив ссылок на функции, вызывающие соответствующие методы в объекте, который является полем структуры, переданной в них неявно, через регистр `ECX`. Писать всё это на ассемблере не очень удобно, особенно учитывая, что неплохо было бы добавить какое-нибудь журналирование, поэтому выделим минимальный ассемблерный фрагмент: - + + + + +{% highlight assembler %} push %ecx # помещаем в стек указатель на объект (он станет вторым аргументом) push $<порядковый номер метода в таблице> # помещаем в стек номер метода (он будет самым первым аргументом) # остальные аргументы сдвинутся на 3 (два помещённых в стек и адрес возврата) @@ -268,8 +277,10 @@ call <функция Nim> # вызываем функцию, написанну add $0x4, %esp # убираем из стека номер метода pop %ecx # извлекаем указатель на объект ret $<глубина стека> # удаляем из стека аргументы и возвращаемся -``` - +{% endhighlight %} + + + + + + + Осталось сгенерировать обозначенные функции Nim. Нужно сгенерировать по одной функции для каждой глубины стека встреченной в файле и ещё по одной для вызовов со скрытым аргументом. Далее будем называть эти функции псевдометодами для краткости. - -```nim + + + + +Оставим за скобками реализацию функции `wrapIfNecessary` и перейдём к описанию кода, который генерирует описанные выше фрагменты. Сначала прочитаем файл, в котором хранятся описания классов. Путь к файлу мы получим так же, как и путь к spec-файлу — через опцию компилятора. - -```nim + + + Теперь мы получили таблицу классов. Поскольку функция `readClasses` не использует ничего, возможного только во время выполнения, мы смело можем вычислить её во время компиляции и записать результат в константу: `const classes = readClasses()`. Составим таблицу методов-обёрток, состоящих из ассемблерных вставок, описанных выше. - -```nim + + + По полученным спискам строим псевдометоды. Процесс перебора списков оставлен за кадром. Также стоит отметить, что все процедуры, использованные нами — обычные функции Nim, оперирующие АСД и вызываемые из тела макроса (который тоже опущен). Магия интерпретации созданных АСД происходит при выходе из тела макроса. - -```nim + + + Самая сложная часть позади. Сложность её обусловлена необходимостью формирования и вставки динамического списка аргументов в несколько ключевых точек декларации псевдометода. Здесь не работает простой подход с шаблоном и подстановкой через quote, поэтому приходится собирать узлы АСД один за другим, что негативно сказывается на объеме и читаемости кода. Осталось написать сам макрос, из которого будут вызываться наши генераторы АСД. - -```nim + + Похожим образом создаются объявления основных функций `steam_api.dll`. Для проброса вызовов обратно из GNU/Linux в игру форма уже известна и едина для всех версий Steam API, поэтому нет нужды в кодогенерации. Например, определение первого метода будет выглядеть так: - -```nim + + + # Заключение Итак, мы рассмотрели основные ключевые точки, позволяющие сгенерировать обёртку для Steam API во время компиляции. Какими бы сложными они не казались, такой подход, несомненно, выигрывает у ручного написания нескольких сотен однотипных методов. Nim написал все эти методы за нас. Кто-то может спросить: «А что там с отладкой всего этого ужаса?». Вопрос отладки кода времени компиляции действительно сложен. Единственное средство — это старые добрые отладочные сообщения `echo` (аналог `print` в Nim). К счастью в Nim есть функции `repr` и `treeRepr`, которые превращают АСД в строку кода и строку со структурной схемой узлов соответственно, что сильно упрощает отладку. From d277c1f502392d4d517ae20ab2df058de9e2cf59 Mon Sep 17 00:00:00 2001 From: xomachine Date: Mon, 12 Mar 2018 21:19:46 +0300 Subject: [PATCH 4/9] Translated the class memory layout scheme --- .../2018-03-12-steam-api-calls-forwarding.md | 2 +- .../news/images/winesteam/ClassLayoutEng.svg | 751 ++++++++++++++++++ 2 files changed, 752 insertions(+), 1 deletion(-) create mode 100644 jekyll/assets/news/images/winesteam/ClassLayoutEng.svg diff --git a/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md b/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md index 32eb815ae..989b161f3 100644 --- a/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md +++ b/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md @@ -177,7 +177,7 @@ $ And here is what we need! Let's curtain the Gabe Newell's portraint, if you have one, and open `steamclient.so` in the IDA. A quick keyword search shows an interesting set of strings which follow the pattern: `CAdapterSteamClient0XX`, where XX is the version number. What is even more curious, in the file there are lines with pattern `CAdapterSteamYYYY0XX`, where XX is also the version number, and YYYY is the name of the Steam API class for all other interfaces. The analysis of cross-references allows to find a table of virtual methods for each of the classes with such names easily. Thus, the summary scheme for each class will look like this: -![](https://habrastorage.org/webt/hf/e4/4i/hfe44isvt4vsanajaro-oesru0k.png) +The class memory layout The method table is found, but we have absolutely no information about the signatures of these methods. But this problem turned out to be [solvable](https://toster.ru/answer?answer_id=1156239)(the source in Russian) by calculating the maximum depth of the stack the method tries to access. So we can make an utility that will be receiving the `steamclient.so` library to the input, and forms a list of classes of all versions, as well as their methods at the output. It remains only to generate a wrapper code for the method calls conversion based on the list. The task does not look simple, especially considering that the method signature itself is not known to us, we know only the depth of the stack where the method call arguments end. The situation is aggravated by the peculiarities of the in-memory structures return, namely the presence of a hidden argument - pointer to the memory where the structure should be written. This pointer in all call conventions is extracted from the stack by the callee, so we can easily detect it by the `ret $4` instruction in the assembler code of methods in `steamclient.so`. But even so, the amount of non-trivial code generation is huge. diff --git a/jekyll/assets/news/images/winesteam/ClassLayoutEng.svg b/jekyll/assets/news/images/winesteam/ClassLayoutEng.svg new file mode 100644 index 000000000..728b1bb2f --- /dev/null +++ b/jekyll/assets/news/images/winesteam/ClassLayoutEng.svg @@ -0,0 +1,751 @@ + +image/svg+xmlCAdapterSteamYYY0XX +Class Info +Method 1 +Method 2 +... +0x00000008 +0xXXXXXXXX + \ No newline at end of file From 9a6368d81668865b2531bb2fcf222e47da987a00 Mon Sep 17 00:00:00 2001 From: Dmitriy Fomichev Date: Mon, 12 Mar 2018 23:20:10 +0300 Subject: [PATCH 5/9] Update 2018-03-12-steam-api-calls-forwarding.md The other half of translated text --- .../2018-03-12-steam-api-calls-forwarding.md | 355 +++++++++--------- 1 file changed, 180 insertions(+), 175 deletions(-) diff --git a/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md b/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md index 989b161f3..70b2b3c71 100644 --- a/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md +++ b/jekyll/_posts/2018-03-12-steam-api-calls-forwarding.md @@ -189,35 +189,37 @@ To any new or just not very popular programming language, the question of its ni - excellent metaprogramming support (the same language for both of run-time and compile-time code, direct manipulation with AST ). -Именно это сочетание в результате и позволит сделать процесс написания обёртки безболезненным. -Для начала создадим основной файл `steam_api.nim` и файл с опциями компиляции `steam_api.nims`: + +This combination will make the wrapper writing process painless. +First, we create the main file `steam_api.nim` and a file with compilation options: `steam_api.nims` +