Mendalami Bahasa C

Saya memberikan saran agar seseorang “mendalami bahasa C” jika ingin belajar reverse engineering. Ada pertanyaan menarik yang diajukan ke saya: sedalam apa belajarnya pak? apa yang harus dipelajari. Sesuai KBBI mendalami di sini berarti: meresapi; menyelami; mempelajari (menelaah, menyelidiki) dalam-dalam.

Saya tidak akan membahas dalam mengenai kenapa seseorang perlu memahami bahasa C, singkatnya: saat ini C masih dipakai di mana-mana, dan akan terus begitu untuk beberapa belas/puluh tahun mendatang. Kernel berbagai sistem operasi ditulis dalam C, berbagai library penting masih ditulis dalam C (library kompresi, enkripsi, image encoding/decoding, dsb), dan bahkan kebanyakan bahasa pemrograman lain diimplementasikan dalam C (misalnya Ruby, Python, dan PHP).

Di awal, pelajarilah dan pahamilah semua konsep dasar dalam bahasa C. Ini seharusnya tidak makan waktu lama. Bahasa C hanya punya beberapa tipe data dasar (void, char, short, int, long, float, dan double) masing-masing bisa signed atau unsigned. Kita bisa mendefinisikan sebuah konstanta dengan const. Tipe data lain adalah enum, union, dan struct (sudah pernah saya bahas di sini) semua tipe data bisa diberi nama dengan typedef.

Hanya ada beberapa sintaks loop (while, do while, dan for, semuanya dengan break dan continue) dan conditional (if/else, goto, dan switch/case/default). Sintaks pembuatan fungsi juga cukup sederhana, hanya perlu mengingat “return” untuk mengembalikan nilai.

Konsep manajemen memori dan string (array of characters) merupakan salah satu hal yang sering membuat pemula bingung. Jika Anda sudah berhasil membuat kode yang selalu lolos valgrind (artinya tanpa warning dan tanpa memory leak), maka Anda sudah lulus dalam pelajaran ini.

Berikutnya buatlah struktur data dalam bahasa C. Mulai dari yang sederhana seperti linked list. Setelah berhasil mencontek buku/website, cobalah menuliskan ulang struktur data tanpa mencontek. Seharusnya kalau sudah paham akan bisa. Lalu cobalah memakai fitur yang lebih rumit seperti function pointer. Pastikan ini juga lolos valgrind.

Cobalah memakai berbagai library C, bisa dimulai dari memakai berbagai fungsi di library C standar. Lalu diteruskan dengan library lain, misalnya zlib untuk kompresi data, expat untuk parsing XML, png untuk dekompresi file PNG.

Cobalah juga untuk memakai lingkungan yang berbeda. Sistem operasi yang berbeda, compiler yang berbeda, IDE yang berbeda. Supaya lebih paham yang mana yang merupakan bagian dari bahasa C, dan yang mana sekedar fitur IDE atau OS yang Anda pakai.

Menurut saya seseorang bisa dianggap cukup memahami bahasa C apabila sudah menyadari bahwa bahasa C itu sangat sederhana. Pertama yang harus disadari adalah ada bahasa C dan ada libray C.

Ketika belajar C, seseorang akan diberikan program “hello world”, seperti ini:

#include <stdio.h>

int main(int argc, char *argv){
   printf("hello world");
}

Tanpa menyadari apa itu gunanya include, dari mana printf berasal, dsb. Ketika baru belajar memang kita tidak perlu tahu itu semua, tapi jika ingin mendalami, kita harus mengerti peran: preprocessor, compiler, assembler, dan linker. Kita juga perlu memahami apa itu library, dan bagaimana membuat library sendiri (pernah saya bahas di sini).

Memprogram sistem embedded tanpa sistem operasi, misalnya microcontroller akan membuat kita sadar mengenai banyak hal yang mungkin tidak terpikirkan di desktop. Misalnya pernyataan sederhana

printf("hello world\n");

Di sistem embedded tanpa layar dan tanpa keyboard, mungkin Anda akan bertanya: ke mana outputnya? bagaimana kita membaca input?. Di sini akan disadari bahwa printf bukanlah bagian dari bahasa C, tapi bagian dari library C. Bahasa C bisa digunakan dengan berbagai library (di Linux saja ada pilihan: GNU LibC, diet libc, musl, dsb).

Di sinilah salah satu kelebihan bahasa C: ketika kita ingin tahu implementasi sebuah fungsi library apapun, kita bisa melihatnya dan biasanya dalam bahasa C juga (hanya sebagian yang memakai assembly). Beda misalnya dengan PHP: jika kita ingin tahu bagaimana fungsi strlen atau split di PHP diimplementasikan, maka yang harus kita baca adalah kode dalam bahasa C (bukan PHP).

Fungsi printf di atas sudah sangat kompleks untuk dijadikan contoh, jadi saya memakai fungsi sederhana saja: strlen. Ini implemenasi generik paling sederhana dari openbsd:

http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/lib/libc/string/strlen.c?rev=1.9&content-type=text/x-cvsweb-markup

size_t
strlen(const char *str)
{
	const char *s;

	for (s = str; *s; ++s)
		;
	return (s - str);
}

Tentunya ada ribuan cara untuk mengimplementasikan strlen. dan kadang untuk optimasi digunakan assembly. Saat ini banyak compiler C yang terbuka source codenya (GCC, Clang, Watcom, dsb) dan juga semua librarynya, jadi semua internal C bisa dipelajari.

Khusus untuk yang ingin belajar C untuk reverse engineering. Pelajarilah bahwa biasanya compiler C bisa menghasilkan kode assembly langsung dalam bentuk teks, dan kita bisa membandingkan kode C dengan kode assembly yang dihasilkan compiler. Sebagian informasi mengenai cara menghasilkan teks assembly bisa dibaca di sini.

Sebagai penutup. Jika Anda sudah bisa menganggap bahasa C sebagai bahasa yang sederhana maka kemungkinan Anda sudah mendapatkan pencerahan dan sudah mulai paham bahasa C.

Signature email jaman kuliah

Dulu waktu kuliah, saya punya signature email seperti ini:

main(i){putchar((i-1)["Xme]i_l"]+(i++))&&(8-i)&&main(i);} 


Ternyata masih ada beberapa orang yang ingat, dan masih banyak yang penasaran apa artinya (cuma satu kata: Yohanes) danĀ  kok bisa muncul seperti itu?.

Pertama, menurut standar C lama, sebuah fungsi tanpa kembalian akan mengembalikan sebuah int, dan parameter yang tanpa tipe juga adalah sebuah int, jadi fungsi di atas sama dengan:

int main(int i){
   putchar((i-1)["Xme]i_l"]+(i++))
         &&(8-i)&&
           main(i);
 } 

Perhatikan juga bahwa di C, sifat operator && adalah short circuit, artinya dalam A() && B() jika A() mengembalikan false, maka B tidak dieksekusi:

#include <stdio.h>

int A() {
   printf("Fungsi A dipanggil\n");
   return 0;
}

int B() {
   printf("Fungsi B dipanggil\n");
   return 1;
}

int main(int argc, char *argv[])
{
   if (A() && B()) {
           printf("A dan B mengembalikan TRUE\n");
   }
   return 0;
}

Bagian main di atas sama saja dengan ini:

#include <stdio.h>

int main(int argc, char *argv[])
{
   if (A()) {
       if (B()) {
           printf("A dan B mengembalikan TRUE\n");
       }
   }
   return 0;
}

Jadi kode signature saya bisa dijadikan if juga seperti ini:

int main(int i){
   if (putchar((i-1)["Xme]i_l"]+(i++))) {
           if (8-i) {
               if (main(i)) {
               }
           }
   }
 } 

Perhatikan beberapa bisa diperjelas, misalnya (8-i) akan true jika (8-i) !=0, atau selama i != 8. Karena if (main()) kosong, maka bisa dihilangkan if-nya.

int main(int i){
   if (putchar((i-1)["Xme]i_l"]+(i++))) {
           if (i!=8) {
              main(i);
           }
   }
 } 

Di C, sebuah array adalah sebuah pointer dan sebuah string adalah array of characters. Di C:

    int *array = (int *)malloc(sizeof(int)*10);
    int index = 1;
    array[index] = 10;
    //syntax array access di atas sama dengan:
    *(array + index) = 10;
    //penjumlahan sifatnya komutatif
    *(index + array) = 10;
    //jadi ini juga sama:
    index[array] = 10; 

Untuk lebih jelasnya, string saya keluarkan, dan notasinya diperbaiki:

const char *str = "Xme]i_l";
int main(int i){
   if (putchar(str[i-1]+(i++))) {
           if (i!=8) {
              main(i);
           }
   }
 } 

Kita lihat fungsi putchar di manual:

fputc, fputs, putc, putchar, puts – output of characters and strings

Di bagian return value:

fputc(), putc() and putchar() return the character written as an unsigned char cast to an int or EOF on
error.

Catatan: karena fungsi putchar tidak dideklarasikan, dan saya tidak menginclude apapun, maka dianggap kembaliannya int, dan ada warning dari compiler.

Dalam kasus saya, putchar ini akan selalu mengembalikan 1, karena saya memprint satu karakter setiap waktu. Jadi kita sederhanakan lagi:

const char *str = "Xme]i_l";
int main(int i){
   putchar(str[i-1]+(i++));
   if (i!=8) {
      main(i);
   }   
 } 

Operator ++ (post increment) akan dilakukan setelah sebuah ekspresi, jadi dalam kasus ini bisa disederhanakan:

const char *str = "Xme]i_l";
int main(int i){
   putchar(str[i-1]+i);
   i++;
   if (i!=8) {
      main(i);
   }   
 } 

Sekarang ke bagian “magic”-nya. Di sistem operasi Windows, Linux atau POSIX yang lain, ketika program dijalankan, maka program dalam C akan menerima jumlah parameter dan isi parameternya

int main(int argc, char *argv[])

Jika kita deklarasikan tanpa argv, maka hanya jumlah parameternya yang kita dapatkan. Meskipun biasanya namanya argc dan argv, nama parameternya tentunya boleh apa saja

int main(int jumlah_argumen)

Jika program dijalankan tanpa parameter, maka jumlah argumennya adalah 1, yaitu nama program saat ini (yang tidak kita pedulikan di program ini). Jadi sebenarnya program tersebut dipanggil dengan

main(1);

Perhatikan bahwa “main” adalah sebuah fungsi di C, dan seperti fungsi apapun, bisa dipanggil bebas. Dalam kasus ini, saya memanggil (rekursif) main, dengan nilai i yang ditambahkan terus.

Jika ingin kode yang sangat jelas tiap langkahnya seperti ini:

#include <stdio.h>

const char *str = "Xme]i_l";
int main(int i){
   printf("\ni = %d\n", i);
   printf("str[i-1] = %c +%d = %c\n", str[i-1], i, str[i-1] + i);
   putchar(str[i-1]+i);
   i++;
   if (i!=8) {
      main(i);
   }   
} 

Dan outputnya:

[email protected]:~$ ./a.out 

i = 1
str[i-1] = X +1 = Y
Y
i = 2
str[i-1] = m +2 = o
o
i = 3
str[i-1] = e +3 = h
h
i = 4
str[i-1] = ] +4 = a
a
i = 5
str[i-1] = i +5 = n
n
i = 6
str[i-1] = _ +6 = e
e
i = 7
str[i-1] = l +7 = s

Demikian keisengan jaman kuliah dulu. Sekedar tambahan: dulu terinspirasi dari International Obfuscated C Code Contest, kode saya ini sangat sederhana dibandingkan para pemenang IOCCC.

System programming

Banyak orang yang terkesima dengan orang yang memprogram di level kernel sistem operasi; banyak juga yang menganggap wilayah kompilasi atau interpretasi itu sesuatu yang rumit, dan sebaiknya diterima saja, tidak perlu dipelajari.

Sebagian orang bersifat tidak mau tahu. Misalnya ada yang bilang: mengapa belajar assembly atau C/C++, bahasanya nggak dipakai, mendingan belajar PHP, Ruby, Java atau .NET. Orang-orang tersebut tidak menyadari bahwa compiler dan atau runtime bahasa-bahasa tersebut dibuat dengan C dan C++. Sebagian besar library yang dipakai di berbagai bahasa juga diimplementasikan di C/C++.

Sebagian kemampuan prosessor juga tidak akan bisa dimanfaatkan secara maksimal oleh bahasa tingkat tinggi, tanpa bantuin assembly dan atau C/C++. Contoh terbaru adalah instruksi-instruksi STTNI (String & Text New Instructions) yang diperkernalkan di prosessor baru Intel dengan arsitektur Nehalem untuk mempercepat pemrosesan teks (dan parsing XML) tidak akan bisa dilakukan.

Bahkan jika Anda tidak memprogram dalam bahasa C sekalipun, pengetahuan tersebut bisa berguna, misalnya Anda bisa mengoptimasi penggunaan memori dan mengoptimasi kecepatan di PHP. Lihat juga posting saya mengenai Kritik PHP.

Sekarang pertanyaan berikutnya yang mungkin muncul adalah: bagaimana saya belajar pemrograman sistem?

Continue reading System programming

Struct vs Union

Posting ini merupakan jawaban saya pada sebuah pertanyaan di milis linux-programming (Juli 2007). Saya posting di sini karena mungkin akan berguna bagi pemula dalam C.

Struct berguna untuk mengelompokkan data. Contoh: struktur mahasiswa mungkin memiliki NIM, nama, dst. Rasanya ini mudah dimengerti.

Union: untuk memberi beberapa nama untuk satu lokasi memori. Ini yang biasanya yang sulit dimengerti oleh yang baru belajar C. Saya berikan beberapa contoh:

#include <stdio.h>

union {
      int a;
      int b;
} a_dan_b;


int main() 
{
	a_dan_b.a = 5;
	printf("%d\n", a_dan_b.b);
	return 0;
}

Apa hasil keluaran program itu? Jawabnya adalah 5, karena a dan b menempati lokasi memori yang sama. Kita bisa menambahkan banyak variabel di union. Jika tipe variabel itu sama, maka nilainya akan sama. Kita bisa membuat union dari tipe yang berbeda juga, misalnya:

Continue reading Struct vs Union