Cara mengompilasi file DOS COM oleh kompiler GCC

Artikel diterbitkan 9 Desember 2014
Pembaruan untuk 2018: RenéRebe membuat video yang menarik berdasarkan artikel ini ( bagian 2 )

Akhir pekan lalu saya berpartisipasi dalam Ludum Dare # 31 . Tetapi bahkan sebelum konferensi diumumkan, karena hobi terakhir saya , saya ingin membuat game jadul di bawah DOS. Platform target adalah DOSBox. Ini adalah cara paling praktis untuk menjalankan aplikasi DOS, terlepas dari kenyataan bahwa semua prosesor x86 modern sepenuhnya kompatibel dengan yang lama, hingga 8086 16-bit.

Saya berhasil membuat dan menunjukkan game DOS Defender di konferensi. Program ini bekerja dalam mode nyata dari 80386 32-bit. Semua sumber daya dibangun ke dalam file COM yang dapat dieksekusi, tidak ada dependensi eksternal, sehingga seluruh game dikemas dalam biner 10 kilobyte.



Anda membutuhkan joystick atau gamepad untuk bermain. Saya menyertakan dukungan mouse dalam rilis untuk Ludum Dare demi presentasi, tetapi kemudian menghapusnya karena itu tidak berfungsi dengan baik.

Bagian yang paling menarik secara teknis adalah tidak ada alat pengembangan DOS yang diperlukan untuk membuat gim ! Saya hanya menggunakan kompiler Linux C biasa (gcc). Pada kenyataannya, Anda bahkan tidak dapat membuat Defender DOS untuk DOS. Saya melihat DOS hanya sebagai platform tertanam, yang merupakan satu-satunya bentuk di mana DOS masih ada sampai sekarang . Bersama dengan DOSBox dan DOSEMU, ini adalah seperangkat alat yang cukup nyaman.

Jika Anda hanya tertarik pada bagian praktis dari pengembangan, buka bagian "Cheat on GCC", di mana kami akan menulis program DOS COM "Hello, World" dengan GCC Linux.

Menemukan alat yang tepat


Ketika saya memulai proyek ini, saya tidak memikirkan GCC. Pada kenyataannya, saya pergi dengan cara ini ketika saya menemukan paket bcc (Bruce's C Compiler) untuk Debian, yang mengumpulkan binari 16-bit untuk 8086. Itu diadakan untuk mengkompilasi boot loader x86 dan hal-hal lain, tetapi bcc juga dapat digunakan untuk mengkompilasi file DOS COM. Itu membuat saya tertarik.

Untuk referensi: Intel 8086 mikroprosesor 16-bit dirilis pada tahun 1978. Itu tidak memiliki fitur aneh prosesor modern: tidak ada perlindungan memori, tidak ada instruksi floating point, dan hanya 1 MB RAM addressable. Semua desktop dan laptop x86 modern masih dapat berpura-pura menjadi prosesor 16-bit 8086 ini empat puluh tahun yang lalu, dengan pengalamatan terbatas yang sama dan semua itu. Ini adalah kompatibilitas yang terbelakang. Fungsi seperti ini disebut mode nyata . Ini adalah mode di mana semua komputer x86 boot. OS modern segera beralih ke mode terlindungi dengan pengalamatan virtual dan multitasking yang aman. DOS tidak melakukan itu.

Sayangnya, bcc bukan kompiler ANSI C. Ini mendukung subset K&R C, serta kode assembler x86 bawaan. Tidak seperti kompiler 8086 C lainnya, ia tidak memiliki konsep pointer “jauh” atau “panjang”, sehingga kode assembler internal diperlukan untuk mengakses segmen memori lain (VGA, jam, dll.). Catatan: sisa-sisa "long pointer" 8086 ini masih tersimpan di Win32 API: LPSTR , LPWORD , LPDWORD , dll. Assembler built-in itu bahkan tidak sebanding dengan GCC assembler built-in. Dalam assembler, Anda perlu memuat variabel secara manual dari tumpukan, dan karena bcc mendukung dua konvensi pemanggilan yang berbeda, variabel dalam kode harus dikodekan secara keras sesuai dengan satu atau beberapa konvensi lainnya.

Mengingat keterbatasan ini, saya memutuskan untuk mencari alternatif.

DJGPP


DJGPP - GCC port di bawah DOS. Sebuah proyek yang sangat mengesankan yang mentransfer hampir seluruh POSIX di bawah DOS. Banyak program port-DOS yang dibuat di DJGPP. Tapi dia hanya membuat program 32-bit untuk mode terproteksi. Jika dalam mode terproteksi Anda perlu bekerja dengan perangkat keras (misalnya, VGA), program membuat permintaan ke layanan antarmuka mode terproteksi DOS (DPMI). Jika saya menggunakan DJGPP, saya tidak dapat membatasi diri saya pada satu biner mandiri, karena saya harus memiliki server DPMI. Kinerja juga menderita dari permintaan untuk DPMI.

Mendapatkan alat yang diperlukan untuk DJGPP itu sulit, untuk sedikitnya. Untungnya, saya menemukan proyek build-djgpp yang berguna yang menjalankan semuanya, setidaknya di Linux.

Entah ada kesalahan serius, atau binari DJGPP resmi terinfeksi virus lagi , tetapi ketika saya memulai program saya di DOSBox, kesalahan "Bukan COFF: periksa virus" terus muncul. Untuk memverifikasi lebih lanjut bahwa virus tidak ada di komputer saya sendiri, saya mengatur lingkungan DJGPP pada Raspberry Pi saya, yang bertindak sebagai ruang bersih. Perangkat berbasis ARM ini tidak dapat terinfeksi virus x86. Dan masih ada masalah yang sama muncul, dan semua hash biner adalah sama antara mesin, jadi itu bukan salahku.

Jadi mengingat ini dan masalah DPMI, saya mulai mencari lebih jauh.

Menipu gcc


Apa yang akhirnya saya setujui adalah trik rumit “menipu” GCC untuk membuat file DOS COM dalam mode nyata. Triknya berfungsi hingga 80386 (yang biasanya Anda butuhkan). Prosesor 80386 diluncurkan pada tahun 1985 dan menjadi mikroprosesor x86 32-bit pertama. GCC masih mematuhi set instruksi ini, bahkan pada lingkungan x86-64. Sayangnya, GCC tidak dapat menghasilkan kode 16-bit dengan cara apa pun, jadi saya harus mengabaikan tujuan awal pembuatan game untuk 8086. Namun, ini tidak masalah, karena platform target DOSBox pada dasarnya adalah sebuah emulator 80386.

Secara teori, triknya juga harus bekerja di kompiler MinGW, tetapi ada kesalahan lama yang mencegahnya bekerja dengan benar ("tidak dapat melakukan operasi PE pada file keluaran non PE"). Namun, Anda dapat mengatasinya, dan saya melakukannya sendiri: Anda harus menghapus arahan OUTPUT_FORMAT dan menambahkan langkah objcopy tambahan ( objcopy -O binary ).

Halo Dunia di DOS


Untuk demonstrasi, kami akan membuat program DOS COM "Hello, World" menggunakan GCC di Linux.

Ada kendala utama dan signifikan dalam metode ini: tidak akan ada perpustakaan standar . Ini seperti menulis sistem operasi dari awal, dengan pengecualian beberapa layanan yang disediakan DOS. Itu berarti tidak ada printf() atau sejenisnya. Sebagai gantinya, kami meminta DOS untuk mencetak string ke konsol. Membuat permintaan DOS membutuhkan interupsi, yang berarti kode assembler inline!

DOS memiliki sembilan interupsi: 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x2F. Hal terpenting yang menarik minat kami adalah 0x21, fungsi 0x09 (cetak satu baris). Antara DOS dan BIOS, ada ribuan fungsi yang dinamai pola ini . Saya tidak akan mencoba menjelaskan assembler x86, tetapi singkatnya nomor fungsi macet di register ah - dan kebakaran interupsi 0x21. Fungsi 0x09 juga mengambil argumen - pointer ke garis untuk dicetak, yang dilewatkan dalam register dx dan ds .

Berikut adalah fungsi print() dari assembler inline GCC. Baris yang diteruskan ke fungsi ini harus diakhiri dengan $ karakter. Mengapa Karena DOS.

 static void print(char *string) { asm volatile ("mov $0x09, %%ah\n" "int $0x21\n" : /* no output */ : "d"(string) : "ah"); } 

Kode dinyatakan tidak volatile karena memiliki efek samping (cetak garis). Untuk GCC, kode assembler buram, dan pengoptimal bergantung pada batasan output / input / clobber (tiga baris terakhir). Untuk program DOS semacam itu, assembler bawaan apa pun akan memiliki efek samping. Ini karena ini ditulis bukan untuk optimasi, tetapi untuk akses ke sumber daya perangkat keras dan DOS - hal-hal yang tidak dapat diakses oleh C. sederhana

Anda juga harus menjaga pernyataan panggilan, karena GCC tidak tahu bahwa memori yang ditunjukkan oleh string pernah dibaca. Kemungkinan array yang mendukung string juga harus dinyatakan volatile . Semua ini menandakan hal yang tak terhindarkan: tindakan apa pun dalam lingkungan seperti itu berubah menjadi perjuangan tanpa akhir dengan pengoptimal. Tidak semua pertempuran ini bisa dimenangkan.

Sekarang ke fungsi utama. Namanya pada prinsipnya tidak penting, tetapi saya menghindari menyebutnya main() , karena MinGW memiliki ide lucu tentang bagaimana memproses karakter tersebut secara khusus, bahkan jika mereka memintanya untuk tidak melakukannya.

 int dosmain(void) { print("Hello, World!\n$"); return 0; } 

File COM terbatas pada ukuran 65279 byte. Ini karena segmen memori x86 adalah 64 KB, dan DOS hanya mengunduh file COM ke alamat segmen 0x0100 dan mengeksekusi. Tidak ada judul, hanya biner bersih. Karena program COM, pada prinsipnya, tidak dapat memiliki ukuran yang signifikan, maka tidak ada tata letak nyata (berdiri bebas) yang harus terjadi, semuanya dikompilasi sebagai unit terjemahan tunggal. Ini akan menjadi satu panggilan GCC dengan banyak parameter.

Opsi penyusun


Berikut adalah opsi kompiler utama.

-std=gnu99 -Os -nostdlib -m32 -march=i386 -ffreestanding

Karena pustaka standar tidak digunakan, satu-satunya perbedaan antara gnu99 dan c99 adalah trigraph yang terputus (sebagaimana mestinya), dan assembler built-in dapat ditulis sebagai asm bukan __asm__ . Ini bukan tempat sampah Newton. Proyek ini akan sangat erat kaitannya dengan GCC sehingga saya masih tidak peduli dengan ekstensi GCC.

Opsi -Os mengurangi hasil kompilasi sebanyak mungkin. Jadi program akan bekerja lebih cepat. Ini penting dengan memperhatikan DOSBox, karena emulator default berjalan lambat seperti mesin 80-an. Saya ingin menyesuaikan dengan batasan ini. Jika optimizer menyebabkan masalah, maka -O0 sementara -O0 untuk menentukan apakah kesalahan Anda atau optimizer ada di sini.

Seperti yang Anda lihat, pengoptimal tidak mengerti bahwa program akan bekerja dalam mode nyata dengan batasan pengalamatan yang sesuai. Ini melakukan segala macam optimasi tidak valid yang merusak program Anda yang benar-benar valid. Ini bukan bug GCC, karena kami sendiri melakukan hal-hal gila di sini. Saya harus mengulang kode beberapa kali untuk mencegah optimizer merusak program. Misalnya, kami harus menghindari mengembalikan struktur kompleks dari fungsi karena kadang-kadang dipenuhi dengan sampah. Bahaya sebenarnya adalah bahwa versi GCC di masa depan akan menjadi lebih pintar dan akan merusak lebih banyak kode. Ini teman Anda yang volatile .

Parameter berikutnya adalah -nostdlib , karena kita tidak akan dapat menautkan ke pustaka yang valid, bahkan secara statis.

Parameter -m32-march=i386 compiler untuk mengeluarkan kode 80386. Jika saya menulis bootloader untuk komputer modern, maka penglihatan pada 80686 juga akan normal, tetapi DOSBox adalah 80386.

Argumen -ffreestanding mensyaratkan GCC untuk tidak mengeluarkan kode yang mengakses fungsi pembantu perpustakaan standar bawaan. Terkadang, alih-alih kode yang benar-benar berfungsi, ini menghasilkan kode untuk menjalankan fungsi bawaan, terutama dengan operator matematika. Saya memiliki salah satu masalah utama dengan bcc, di mana perilaku ini tidak dapat dinonaktifkan. Opsi ini paling sering digunakan saat menulis boot loader dan kernel OS. Dan sekarang file dos dos .com.

Opsi Tautan


-Wl digunakan untuk meneruskan argumen ke linker ( ld ). Kami membutuhkan ini karena kami melakukan segalanya dalam satu panggilan ke GCC.

 -Wl,--nmagic,--script=com.ld 

--nmagic menonaktifkan perataan halaman bagian. Pertama, kita tidak membutuhkannya. Kedua, itu membuang-buang ruang yang berharga. Dalam pengujian saya, ini sepertinya bukan ukuran yang diperlukan, tetapi untuk berjaga-jaga, saya meninggalkan opsi ini.

Parameter --script menunjukkan bahwa kami ingin menggunakan skrip linker khusus. Ini memungkinkan Anda untuk secara akurat menempatkan bagian ( text , data , bss , rodata ) dari program kami. Ini skrip com.ld

 OUTPUT_FORMAT(binary) SECTIONS { . = 0x0100; .text : { *(.text); } .data : { *(.data); *(.bss); *(.rodata); } _heap = ALIGN(4); } 

OUTPUT_FORMAT(binary) memberi tahu Anda untuk tidak meletakkan ini dalam file ELF (atau PE, dll.). Linker hanya perlu mereset kode bersih. File COM hanyalah kode bersih, yaitu, kami memberikan perintah kepada linker untuk membuat file COM!

Saya mengatakan bahwa file COM diunggah ke 0x0100 . Baris keempat menggeser biner di sana. Byte pertama dari file COM masih byte pertama dari kode, tetapi akan diluncurkan dari memori ini diimbangi.

Kemudian semua bagian mengikuti: text (program), data (data statis), bss (data dengan inisialisasi nol), rodata (string). Akhirnya, saya menandai akhir biner dengan simbol _heap . Ini akan berguna nanti ketika menulis sbrk() ketika kita selesai dengan "Halo, Dunia". Saya mengindikasikan untuk menyelaraskan _heap dengan 4 byte.

Hampir selesai.

Peluncuran program


Tautan biasanya mengetahui titik masuk kami ( main ) dan mengaturnya untuk kami. Tapi karena kami meminta masalah "biner", kami harus mencari tahu sendiri. Jika fungsi print() adalah yang pertama dijalankan, maka program akan memulainya, yang mana salah. Program ini membutuhkan tajuk kecil untuk memulai.

Ada opsi STARTUP dalam skrip tautan untuk hal-hal seperti itu, tetapi untuk kesederhanaan kami akan menerapkannya langsung dalam program. Biasanya hal-hal seperti itu disebut crt0.o atau Boot.o , jika Anda tersandung di suatu tempat. Kode kita harus dimulai dengan assembler bawaan ini, sebelum ada inklusi dan sejenisnya. DOS akan melakukan sebagian besar instalasi untuk kita, kita hanya perlu pergi ke titik masuk.

 asm (".code16gcc\n" "call dosmain\n" "mov $0x4C, %ah\n" "int $0x21\n"); 

.code16gcc memberi tahu assembler bahwa kita akan bekerja dalam mode nyata, sehingga itu akan membuat konfigurasi yang benar. Meskipun namanya, itu tidak akan menghasilkan kode 16-bit! Pertama, fungsi dosmain , yang kami tulis sebelumnya, disebut. Dia kemudian memberi tahu DOS menggunakan fungsi 0x4C ("selesai dengan kode kembali") bahwa kita selesai dengan melewatkan kode keluar ke register al -byte 1-byte (sudah ditetapkan oleh dosmain ). Assembler bawaan ini volatile karena tidak memiliki input dan output.

Semuanya bersama


Inilah keseluruhan program dalam C.

 asm (".code16gcc\n" "call dosmain\n" "mov $0x4C,%ah\n" "int $0x21\n"); static void print(char *string) { asm volatile ("mov $0x09, %%ah\n" "int $0x21\n" : /* no output */ : "d"(string) : "ah"); } int dosmain(void) { print("Hello, World!\n$"); return 0; } 

Saya tidak akan mengulangi com.ld Inilah tantangan GCC.

 gcc -std=gnu99 -Os -nostdlib -m32 -march=i386 -ffreestanding \ -o hello.com -Wl,--nmagic,--script=com.ld hello.c 

Dan pengujiannya di DOSBox:



Kemudian jika Anda ingin grafik yang indah, satu-satunya pertanyaan adalah untuk memanggil interupsi dan menulis ke memori VGA . Jika Anda ingin suara, gunakan interupsi Speaker PC. Saya belum menemukan cara untuk memanggil Sound Blaster. Sejak saat itu, DOS Defender tumbuh.

Alokasi memori


Untuk membahas topik lain, ingat _heap ? Kita dapat menggunakannya untuk mengimplementasikan sbrk() dan mengalokasikan memori secara dinamis di bagian utama program. Ini adalah mode nyata dan tidak ada memori virtual, jadi kami dapat menulis ke memori apa pun yang dapat diakses kapan saja. Beberapa area dicadangkan (misalnya, memori bawah dan atas) untuk peralatan. Jadi tidak perlu menggunakan sbrk (), tetapi menarik untuk dicoba.

Seperti biasa pada x86, program dan partisi Anda berada di memori yang lebih rendah (0x0100 dalam kasus ini), dan tumpukan berada di memori atas (dalam kasus kami, di wilayah 0xffff). Pada sistem mirip Unix, memori yang dikembalikan oleh malloc() berasal dari dua tempat: sbrk() dan mmap() . Apa yang sbrk() lakukan adalah mengalokasikan memori tepat di atas segmen program / data, menambahnya "naik" ke arah tumpukan. Setiap panggilan ke sbrk() akan menambah ruang ini (atau membiarkannya tetap sama). Memori ini akan dikelola oleh malloc() dan sejenisnya.

Inilah cara menerapkan sbrk() dalam program COM. Harap perhatikan bahwa Anda perlu mendefinisikan size_t Anda sendiri, karena kami tidak memiliki perpustakaan standar.

 typedef unsigned short size_t; extern char _heap; static char *hbreak = &_heap; static void *sbrk(size_t size) { char *ptr = hbreak; hbreak += size; return ptr; } 

Ini hanya mengatur pointer ke _heap dan menambahkannya sesuai kebutuhan. sbrk() sedikit lebih sbrk() juga akan berhati-hati dengan perataan.

Suatu hal yang menarik terjadi pada saat pembuatan Defender DOS. Saya (salah) menganggap bahwa memori dari sbrk() reset. Jadi itu setelah pertandingan pertama. Namun, DOS tidak mengatur ulang memori ini antar program. Ketika saya memulai permainan lagi, itu melanjutkan persis di mana saya berhenti , karena struktur data yang sama dengan konten yang sama dimuat ke tempatnya. Kebetulan keren sekali! Ini adalah bagian dari apa yang membuat platform tertanam ini menyenangkan.

Source: https://habr.com/ru/post/id412867/


All Articles