objdump -M intel -d a.out
-d
object dump ile disassemble eder, tamamını disassemble etmek için-D
,-M
ile intel işlemci mimarisine göre çıktı almak istediğimizi belirtiyoruz. Bu şekilde daha okunabilir.
GNU Debugger da bunu yapmak için ise gdb yi açıyoruz ve aşağıdaki komutu giriyoruz çalışmaz ise alttakileri.
(gdb) set dis intel
(gdb) set disassembly intel
(gdb) set disassembly-flavor intel
Programdan çıktığımızda ve tekrar girdiğimizde her seferinde bu işlemi yapmak zorundayız, bunu istemiyorsak.
echo "set dis intel" > ~/.gdbinit
echo "set disassembly intel" > ~/.gdbinit
echo "set disassembly-flavor intel" > ~/gdbinit
gcc/g++ derleyicisinde bir C programını derlerken genelde yazılımcılar kaynak kodu okunamaz olsun isterler, eğer derlediğimizde halen kaynak kodunun okunmasını istiyorsak -g
parametresini vermemiz gerekiyor.
g++ -g -o deneme deneme.c
bu şekilde derlersek gdb programın kaynak kodunu okuyabilir. gdb -q deneme
diyerek programı gdb de açıyoruz.
(gdb) list
programın kaynak kodunu ekrana basar.
(gdb) disasseble main
dediğimizde main fonksiyonunu disassemble eder.
(gdb) break main
main fonksiyonuna bir breakpoint koyuyoruz, program main fonksiyonuna gelesiye kadar çalışır, main fonksiyonuna geldiğinde durur. Komut bize bir adres döndürür buradan anlıyoruz ki main fonksiyonu çalıştıktan sonra çalışan komut o adreste.
(gdb) run
dediğimizde programı çalıştırır break point varsa durdurur.
(gdb) info register rip
rip (register instruction pointer) için bilgi verir, hangi adresi işaret ediyor. (gdb) i r rip
şeklinde de çalışır.
o/ octal notation (8'lik taban, gösterim)
x/ hexadecimal (16'lık taban, 0-9 A-F)
u/ unsigned notation 10 base (10'luk taban)
t/ binary notation (2'lik taban)
(gdb) x 0x555555555149
/ (gdb) x [adres]
verirsek bize o adreste bulunan instruction'u gösterir.
(gdb) x/o $rip
register instruction pointer'ın içinde bulunan adresi getirir.
(gdb) x/u $rip
register instruction pointer'ın içindeki adresi 10'luk tabana çeviripte gösterir.
/b single byte (1 byte)
/h halfword (2 byte - WORD olarak bilinir)
/w word (4 byte - DWORD denir (double word) - 4 byte'ı ifade eder)
/g giant (8 byte)
(gdb) x/2x $rip # register instruction pointer'ın adresi ile birlikte 2 adresi daha hexadecimal olarak göster.
(gdb) x/xb $rip # register instruction pointer'ın adresinden başla 1 byte göster hex olarak.
(gdb) x/xh $rip # register instruction pointer'ın adresinden başla 2 byte göster hex olarak.
(gdb) x/xw $rip # register instruction pointer'ın adresinden başla 4 byte göster hex olarak.
(gdb) x/tw $rip # register instruction pointer'ın adresinden başla 4 byte göster binary olarak.
(gdb) x/uw $rip # register instruction pointer'ın adresinden başla 4 byte göster decimal olarak.
(gdb) x/ow $rip # register instruction pointer'ın adresinden başla 4 byte göster octal olarak.
Platformdan platforma registerların isimleri değişebilir, registerları görmek için (gdb) info registers
(gdb) info register eip/rip
register'ın ismi eip olabilir, ripte olabilir.
(gdb) nexti
next instruction, bir sonraki instruction'u yapar, yaani kodu bir adım daha ilerletir ve çalıştırır.
(gdb) x/s 0x80484e0
0x80484e0 adresinden başlayarak string okur, adresteki stringi getirir.
(gdb) x/20i 0x80484e0
Adresindeki 20 instruction'u göster.
1 Karakter 1 byte dir ve bildiğimiz gibi 1 byte 8 bittir, yaani 1 byte 2^8 = 256 farklı değer alabilir demektir. ASCII tablosunda 1 byte'ın yalnızca 7 biti kullanılır buda 2^7 = 128 farklı sayı demektir ve ASCII tablosunda da 128 karakter vardır.
Type | Storage Size | Value Range |
---|---|---|
char | 1 byte | -128 to 127 or 0 to 255 |
unsigned char | 1 byte | 0 to 255 |
signed char | 1 byte | -128 to 127 |
int | 2 or 4 bytes | -32,768 to 32,767 or -2,147,483,648 to 2,147,483,647 |
unsigned int | 2 or 4 bytes | 0 to 65,535 or 0 to 4,294,967,295 |
short | 2 bytes | -32,768 to 32,767 |
unsigned short | 2 bytes | 0 to 65,535 |
long | 8 bytes | -9223372036854775808 to 9223372036854775807 |
unsigned long | 8 bytes | 0 to 18446744073709551615 |
İnanmıyorsanız deneyin :)
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <float.h>
int main(int argc, char** argv) {
printf("CHAR_BIT : %d\n", CHAR_BIT);
printf("CHAR_MAX : %d\n", CHAR_MAX);
printf("CHAR_MIN : %d\n", CHAR_MIN);
printf("INT_MAX : %d\n", INT_MAX);
printf("INT_MIN : %d\n", INT_MIN);
printf("LONG_MAX : %ld\n", (long) LONG_MAX);
printf("LONG_MIN : %ld\n", (long) LONG_MIN);
printf("SCHAR_MAX : %d\n", SCHAR_MAX);
printf("SCHAR_MIN : %d\n", SCHAR_MIN);
printf("SHRT_MAX : %d\n", SHRT_MAX);
printf("SHRT_MIN : %d\n", SHRT_MIN);
printf("UCHAR_MAX : %d\n", UCHAR_MAX);
printf("UINT_MAX : %u\n", (unsigned int) UINT_MAX);
printf("ULONG_MAX : %lu\n", (unsigned long) ULONG_MAX);
printf("USHRT_MAX : %d\n", (unsigned short) USHRT_MAX);
return 0;
}
Bir integer 4 bytlık yer kaplar, şunu unutmamak lazım bir adres en fazla 1 byte'lık veri tutabilir. Eğer biz 1 sasıyısını RAM de tutacaksak 00000001 şeklinde bir adreste tutabiliriz fakat veri int olarak atandığı için 4 bytlık yer kaplayacaktır. Yaani RAM de 00000000 00000000 00000000 0000001 şeklinde depolanacaktır ve 4 adresi işgal edecektir.
Burada adreste RAM de veriyi tutarken, endianness kavramı devreye giriyor, tutulacak değerin önemli biti en baştada olabilir en sondada.
Program çalıştırıldığında oluşturulur, programın assembly kodlarının tutulduğu bölgedir, boyutu runtime sırasında değiştirilemez. Program çalıştığında rip
bu segmentin en başdaki ilk komutu, instruction'u gösterir, daha sonra bu instruction'un boyutu alınır ve eklenir ve bir sonraki instruction bulunur en son aşamada ise rip
in gösterdiği instruction çalıştırılır.
data ve bss setmentlerinde, program sırasında oluşturulan global ve static değişkenler tutulur. Eğer değişken initialize olarak atanmış ise data da, eğer initialize atanmamış ise bss segmentinde tutulur. data & bss segmentlerinin boyutu runtime sırasında değiştirilemez.
Heap segmenti programcının direkt erişip işlem yapabildiği alandır. Programcı malloc
, calloc
, free
gibi fonksiyonlarla heap alanında işlemler yapabilir. Heap alanında sıralı olarak yer ayırtılır.
Stack veri yapısı FILO (First In Last Out) şeklindedir, ilk giren en son çıkar. Heap alanındakinden farklı olarak adresler sıralı değildir.
Kodu disassemble ettiğimizde
call
komutunu görürüz,call
komutu fonksiyon çağırmaya yarar. Fonksiyonlarstack
veri yapısı ile çalışır. Bir C kodu çalışırken baştan aşağıya doğru çalışır ve her komutun adresi ardı ardına sıralanmıştır fakatprintf()
gibi bir fonksiyon çağrılmışsaprintf()
fonksiyonunun başladığı bir adres vardır ve ordan itibaren komutları çalıştırmaya devam eder.printf()
fonksiyonu sona erdiğinde isemain()
fonksiyonunda kaldığı yerden program çalışmaya devam eder, peki bu nasıl oluyo? Stack veri yapısı ile.ESP: Stack Pointer,
push
ekleme yaparpop
ise çıkarma yapar bu nedenle stacks are not fixes. Stack pointer her zaman stack'e son ekleneni gösterir.EBP: Local Base Pointer, burada stack frame vardır ve 2 tane pointer vardır bu pointerlardan biri SFP Saved Frame Pointer, SFP stack pointer'ın gösterdiğinin bir öncekini tutar. Diğer pointer ise return address tir buda main fonksiyonundaki instruction'a geri döneceği adresi tutar.