asm etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster
asm etiketine sahip kayıtlar gösteriliyor. Tüm kayıtları göster

12 Temmuz 2019 Cuma

Dinamik Olarak Oluşturan Kodu Çalıştırmak ve JIT mimarisine Düşük Seviyeli Bir Bakış

Hafıza yönetim API'lerinden olan VirtualAlloc fonksiyonunun dokümantasyonunda aşağıdaki gibi açıklama var.

To execute dynamically generated code, use VirtualAlloc to allocate memory and the VirtualProtect function to grant PAGE_EXECUTE access.

Burada hepimizin anlayacağı üzere diyor ki;
Dinamik olarak oluşturulan kodu çalıştırmak için, VirtualAlloc fonksiyonu ile hafıza da tahsis yap ve VirtualProtect fonksiyonu ile de PAGE_EXECUTE yetkisi ver.

Tamam da burada dinamik olarak oluşturulan kod nasıl bir kod olmalı? Java , Delphi, C, C++, C# yoksa Javascript ya da başka bir programlama dili kodu mu?
Elbette hiçbiri değil, CPU'nun anlayacağı kodlar olmalı.

Öyle ise; CPU'nun anlayacağı kodları oluşturup, çalıştırmaya çalışalım. 5 + 4 işlemini CPU'nun anlayacağı şekle dönüştürüp, çalıştırmasını isteyeceğiz.

mov eax,5
add eax,4
ret
Yukarıdaki kodda eax register'a 5 değerini yazıyoruz.Daha sonra ise add komutu ile de üzerine 4 ekliyoruz. ret komutu ile de eax register'daki son değeri geriye döndürüyoruz.



Yanlız buraya kadar yazdığımız kodlar yine CPU'nun anlayabileceği kodlar değil. Yukarıda yazdığımız mov eax,5 ... gibi sembolik kodları VirtualAlloc fonksiyonu ile çalıştırmak istersek yine çalıştıramayacağız.

77C3DBA8 | B8 05 00 00 00           | mov eax,5                               |
77C3DBAD | 83 C0 04                 | add eax,4                               |
77C3DBB0 | C3                       | ret                                     |

Delphi Byte Array

Array [1..9] of Byte = (
$B8, $05, $00, $00, $00, $83, $C0, $04, $C3
);

C/C++ Byte Array

{
0xB8, 0x05, 0x00, 0x00, 0x00, 0x83, 0xC0, 0x04, 0xC3
};
O nedenle yukarıdaki görselde görüleceği üzere asm kodları yerine, byte karşılığını alıp, VirtualAlloc fonksiyonu ile hafıza da tahsis ettiğimiz alana yerleştireceğiz, sonrasında flProtect parametresine de Memory Protection Constants değerlerinden PAGE_EXECUTE ile başlayan değerlerden birini parametre olarak geçeceğiz. O zaman bu cek-caklı vaatlerimizi bir kenara bırakıp, kodlamaya başlayalım.

Aşağıda işletim sistemi api'lerinin kullanımına olanak veren, bildiğim dillerde hazırladığım örnekleri inceleyebilirsiniz.

C#

using System;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;

unsafe class Program
{
    // winnt.h    
    const int MEM_COMMIT = 0x00001000;
    const int MEM_RESERVE = 0x00002000;
    const int MEM_RELEASE = 0x00008000;
    const int PAGE_EXECUTE_READWRITE = 0x40;

    [DllImport("kernel32.dll", SetLastError = true)]
    unsafe static extern IntPtr VirtualAlloc(void* lpAddress, UIntPtr dwSize, int flAllocationType, int flProtect);

    [DllImport("kernel32.dll", SetLastError = true)]
    unsafe static extern bool VirtualFree(IntPtr lpAddress, UIntPtr dwSize, int dwFreeType);


    public delegate int FunctionPtr();
    static byte[] ByteCode = new byte[] { 0xB8, 0x05, 0x00, 0x00, 0x00, 0x83, 0xC0, 0x04, 0xC3 };

    const string FileName = "ByteCode.bin";
    const string ByteCodeURL = "https://github.com/ismailkocacan/Experiment/tree/master/JIT_ExecuteDynamicallyGeneratedCode/tests/" + FileName;

    static void Main(string[] args)
    {
        IntPtr P  = VirtualAlloc(null, (UIntPtr)ByteCode.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        Marshal.Copy(ByteCode, 0, P, ByteCode.Length);
        FunctionPtr functionPtr = (FunctionPtr)Marshal.GetDelegateForFunctionPointer(P, typeof(FunctionPtr));
        int result = functionPtr();
        VirtualFree(P, (UIntPtr)ByteCode.Length, MEM_RELEASE);
    }

    static byte[] GetByteCode()
    {
        WebClient client = new WebClient();
        byte[] codeData = client.DownloadData(ByteCodeURL);
        return codeData;
    }

    static void SaveToFile()
    {
        File.WriteAllBytes(FileName, ByteCode);
    }
}


C

#include <Windows.h>

typedef int(*FunctionPtr)();

byte BYTE_CODE[] = { 0xB8, 0x04, 0x00, 0x00, 0x00, 0x83, 0xC0, 0x03, 0xC3 };
const int CODE_SIZE = sizeof BYTE_CODE;

int main()
{
 PVOID P = VirtualAlloc(NULL, CODE_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 memcpy(P, BYTE_CODE, CODE_SIZE);
 FunctionPtr functionPtr = (FunctionPtr)P;
 int result = functionPtr();
 VirtualFree(P, CODE_SIZE, MEM_RELEASE);
}

Delphi

program Project2;
 
{$APPTYPE CONSOLE}
{$R *.res}
 
uses
 Winapi.Windows,
 System.SysUtils;
 
type
 TFunctionPtr = function: Integer;
 
const
 BYTE_CODE: Array [1 .. 9] of Byte = ($B8, $05, $00, $00, $00, $83, $C0, $04, $C3);
 CODE_SIZE = sizeof(BYTE_CODE);
 
var
 P: Pointer;
 Result: Integer;
 FunctionPtr: TFunctionPtr;
begin
 P := VirtualAlloc(nil, CODE_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 CopyMemory(P, @BYTE_CODE, CODE_SIZE);
 FunctionPtr := TFunctionPtr(P);
 Result := FunctionPtr();
 VirtualFree(P, CODE_SIZE, MEM_RELEASE);
end.

Yukarıda yaptıklarımızı kısaca açıklayacak olursak;
- VirtualAlloc ile hafızada CODE_SIZE array'in boyu kadar yer açtık.

- PAGE_EXECUTE_READWRITE değerini geçerek bu hafıza bloğunun çalıştırabilir, okunup , yazılabilir olduğunu belirttik.

- CopyMemory ile çalıştırmak istediğimiz kodları hafızaya yerleştirdik.

- VirtualAlloc bize hafızada tahsis ettiği bloğunun başlagıç pointer'ını döndürmüştü.O adreside bir fonksiyon pointer'ına cast edip, çağırdık.

Konunun başlığı "Dinamik Olarak Oluşturan Kodu Çalıştırmak ve JIT mimarisine Düşük Seviyeli Bir Bakış" yazıyor ama ben debugger'da kodu kendim manuel yazdım.
Bu nasıl dinamik oluşturma diyebilirsiniz.Evet haklısınız.
Şimdi kodu dinamik olarak bize oluşturan bir Assembler'a ihtiyacımız var...
"Burada biz daha çok çalıştırma kısmını incelemiş olduk."

Dahası; .Net ve Java gibi run-time bağlı dillerde kullanılan bir teknik. Derlenmiş MSIL ya da Java Byte kodu tekrar CPU'nun anlayacağı koda dönüştürüp çalıştırma işi, işte bu şekilde gerçekleşiyor.Başka bir deyişle "JIT Compile" diye ifade edilmekte...

Şuanda aklıma gelen, gelmeyen pek çok farklı amaç için kullanılabilir.
Injection, dağıtık kod, sanal makine vs.
Kaynak kodlara buradan ulaşabilirsiniz.

1 Eylül 2018 Cumartesi

Calling Convention Kavramı / Fonksiyon Çağırım Düzeni

Fonksiyon çağırım düzeni (Calling Convention kısaca CC diyelim) özetle;
fonksiyon ya da procedure parametrelerinin hangilerinin CPU register veya stack'de, hangi sıra ile tahsis edileceğidir.

Delphi, C/C++ gibi dillerde fonksiyon ya da procedure'e ekleyeceğimiz CC ile (register, pascal, cdecl, stdcall, safecall, winapi, __fastcall gibi) fonksiyonun çağırım düzeni belirtilebilir.

Delphi'de varsayılan CC register'dır. Yani fonksiyon ya da procedure'nin sonuna herhangi CC belirtilmediğinde varsayılan olarak register çalışır.Stack frame oluşturmak yerine, ECX, EDX, EAX gibi CPU register'ları kullandığı için, fonksiyonun çalışma hızı en efektif ve hızlı olanıdır.Aşağıdaki fonksiyonda X ve Y parametreleri stack yerine CPU register'larda tahsis edilecektir.

function Hesapla(X, Y: Integer): Integer; 
function Hesapla(X, Y: Integer): Integer; register;
Yukarıdaki her iki fonksiyon da aynı CC sahiptir diyebiliriz.
C/C++'da varsayılan CC cdecl'dir. cdecl adı üstünde c decleration kısaltmasıdır.Tüm parametreler stack'de tutulur. C/C++

int Hesapla(int X, int Y);
int __cdecl Hesapla(int X, int Y);  
Delphi

function Hesapla(X, Y: Integer): Integer; cdecl;
Yukarıdaki bilgilere göre;
Varsayılan bir C fonksiyonu ile, varsayılan bir delphi fonksiyonunu karşılaştırdığımızda, mantıken ve de teknik olarak delphi fonksiyonun daha hızlı çalışması gerekir.
Çünkü C'de "varsayılan olarak", fonksiyon parametreleri stack'de tahsis ediliyorken, delphi'de 3 adet parametreye kadarı CPU register'larda geriye kalanlar ise stack'de tahsis edilir. CPU register'ları stack'den daha hızlıdır.

C'de delphi'de olduğu gibi parametreleri stack yerine CPU register'da tahsis etmek için __fastcall CC kullanılır. Kısaca C'nin __fastcall'ı ile Delphin'nin register CC'ı aynıdır diyebiliriz. Delphi

function Hesapla(X, Y: Integer): Integer; register;
begin
end;
Hesapla(1,2);
ASM Çıktısı

Project1.dpr.45: Hesapla(1,2);
00419500 BA02000000       mov edx,$00000002
00419505 B801000000       mov eax,$00000001
0041950A E881DDFFFF       call Hesapla
C/C++

int __fastcall Hesapla(int X, int Y) {
}
Hesapla(1,2);
ASM Çıktısı

main.cpp.48: Hesapla(1,2);
0040125B BA02000000       mov edx,$00000002
00401260 B801000000       mov eax,$00000001
00401265 E8DEFFFFFF       call Hesapla(int,int)
Dokümanlar winapi ya da stdcall'ın aslında bir CC olmadığını söylüyor.
Daha çok windows işletim sistemindeki API'lerde bu CC görebiliriz. winwindef.h içerisinde tanımı şöyle;
#define WINAPI __stdcall
Örneğin CreateFile fonksiyonun tanımında görebiliriz. fileapi.h

WINBASEAPI
HANDLE
WINAPI
CreateFileA(
   _In_ LPCSTR lpFileName,
   _In_ DWORD dwDesiredAccess,
   _In_ DWORD dwShareMode,
   _In_opt_ LPSECURITY_ATTRIBUTES lpSecurityAttributes,
   _In_ DWORD dwCreationDisposition,
   _In_ DWORD dwFlagsAndAttributes,
   _In_opt_ HANDLE hTemplateFile
   );
Delphi zaten bir çok windows api fonksiyonunu hali hazırda bize WinApi.Windows.pas uniti içersinde sunuyor.

function CreateFileA(lpFileName: LPCSTR; dwDesiredAccess, dwShareMode: DWORD;
 lpSecurityAttributes: PSecurityAttributes; dwCreationDisposition, dwFlagsAndAttributes: DWORD;
 hTemplateFile: THandle): THandle; stdcall;
function CreateFileA; external kernelbase name 'CreateFileA';
Yine dokümanlar pascal CC'nin ise geriye dönük uyumluluk için korumaya devam edildiğini söylüyor. Şimdi örnek procedure ya da fonksiyonların CPU penceresindeki kodunu inceleyelim.
(View -> Debug Windows -> CPU Windows -> Entire CPU) ya da CTRL + ALT + C

program Project1;

{$APPTYPE CONSOLE}

{$R *.res}

uses

 System.SysUtils;

procedure Procedure1(A, B, C, D, E: Integer);
begin
end;

procedure Procedure2(A, B, C, D, E: Integer); register;
begin
end;

procedure Procedure3(A, B, C, D, E: Integer); pascal;
begin
end;

procedure Procedure4(A, B, C, D, E: Integer); cdecl;
begin
end;

procedure Procedure5(A, B, C, D, E: Integer); stdcall;
begin
end;

procedure Procedure6(A, B, C, D, E: Integer); safecall;
begin
end;

begin
 Procedure1(1, 5, 7, 9, 10);
 Procedure2(1, 5, 7, 9, 10);
 Procedure3(1, 5, 7, 9, 10);
 Procedure4(1, 5, 7, 9, 10);
 Procedure5(1, 5, 7, 9, 10);
 Procedure6(1, 5, 7, 9, 10);
end.

Project1.dpr.40: Procedure1(1, 5, 7, 9, 10);
00419500 6A09             push $09
00419502 6A0A             push $0a
00419504 B907000000       mov ecx,$00000007
00419509 BA05000000       mov edx,$00000005
0041950E B801000000       mov eax,$00000001
00419513 E878DDFFFF       call Procedure1
Project1.dpr.41: Procedure2(1, 5, 7, 9, 10);
00419518 6A09             push $09
0041951A 6A0A             push $0a
0041951C B907000000       mov ecx,$00000007
00419521 BA05000000       mov edx,$00000005
00419526 B801000000       mov eax,$00000001
0041952B E878DDFFFF       call Procedure2
Project1.dpr.42: Procedure3(1, 5, 7, 9, 10);
00419530 6A01             push $01
00419532 6A05             push $05
00419534 6A07             push $07
00419536 6A09             push $09
00419538 6A0A             push $0a
0041953A E881DDFFFF       call Procedure3
Project1.dpr.43: Procedure4(1, 5, 7, 9, 10);
0041953F 6A0A             push $0a
00419541 6A09             push $09
00419543 6A07             push $07
00419545 6A05             push $05
00419547 6A01             push $01
00419549 E87ADDFFFF       call Procedure4
0041954E 83C414           add esp,$14
Project1.dpr.44: Procedure5(1, 5, 7, 9, 10);
00419551 6A0A             push $0a
00419553 6A09             push $09
00419555 6A07             push $07
00419557 6A05             push $05
00419559 6A01             push $01
0041955B E870DDFFFF       call Procedure5
Project1.dpr.45: Procedure6(1, 5, 7, 9, 10);
00419560 6A0A             push $0a
00419562 6A09             push $09
00419564 6A07             push $07
00419566 6A05             push $05
00419568 6A01             push $01
0041956A E869DDFFFF       call Procedure6
0041956F E8C4F1FEFF       call @CheckAutoResult
Project1.dpr.46: end.
main.cpp

#pragma hdrstop

#pragma argsused

#ifdef _WIN32

#include 

#else

typedef char _TCHAR;

#define _tmain main

#endif

#include 

void Procedure1(int A, int B, int C, int D, int E) {

}

void __fastcall Procedure2(int A, int B, int C, int D, int E) {

}

void pascal Procedure3(int A, int B, int C, int D, int E) {

}

void __cdecl Procedure4(int A, int B, int C, int D, int E) {

}

void __stdcall Procedure5(int A, int B, int C, int D, int E) {

}

void Procedure6(int A, int B, int C, int D, int E) {

}

int _tmain(int argc, _TCHAR* argv[]) {

  Procedure1(1, 5, 7, 9, 10);
  Procedure2(1, 5, 7, 9, 10);
  Procedure3(1, 5, 7, 9, 10);
  Procedure4(1, 5, 7, 9, 10);
  Procedure5(1, 5, 7, 9, 10);
  Procedure6(1, 5, 7, 9, 10);
  return 0;
}

Yukarıdaki çıktıları incelediğimizde, parametrelerin hangi sırayla (sağdan sola veya soldan sağa doğru) stack'e ittirildiğini(push) ya da hangi CPU register'da (ECX,EDX, EAX) tahsis edildiğini somut bir şekilde görebiliriz. Delphi, C/C++ gibi dillerde fonksiyonlarda çağırım düzenini kısaca bu şekilde ifade edebiliriz.

.net dillerinde ise fonksiyon çağırım düzeni, DLLImport attribute sınıfında bulunan CallingConvention özelliği kullanarak belirtilebilir.Bu tür managed dillerden, umanaged system fonksiyonlarını çağırmanın diğer bir adı ise Platform Invoke ya da kısaca P/Invoke diye ifade edilir.

[DllImport("User32.dll", CharSet = CharSet.Unicode, CallingConvention = CallingConvention.StdCall)]
public static extern int MessageBox(IntPtr hWnd, string lpText, string lpCaption, int uType);

static void Main(string[] args)
{
   MessageBox(IntPtr.Zero, "Bu bir mesajdır", "Bu da bir başlıktır", 0);
}
Java'da ise "bildiğim kadarıyla" "dil seviyesinde" .net'deki DllImport'a benzer bir sınıf yok. Aşağıdaki kaynaklara göz atabilirsiniz.
x86 calling conventions
Calling Conventions
Pitfalls of converting
Delphi Language Guide (Delphi for Microsoft Win32 Delphi for the Microsoft .NET Framework)