VGA Metin Modu
Çevrilmiş İçerik: Bu, VGA Text Mode adlı gönderinin topluluk tarafından yapılmış bir çevirisidir. Eksik, güncel olmayan veya hata içeriyor olabilir. Lütfen herhangi bir sorunu bildirin!
Translation by @rhotav.
VGA metin modu, ekrana metin yazdırmanın basit bir yoludur. Bu yazıda, tüm güvensizliği (unsafety) ayrı bir modülde kapsülleyerek onun kullanımını güvenli ve basit hale getiren bir arayüz oluşturuyoruz. Ayrıca Rust’ın biçimlendirme makrolarına yönelik destek de uyguluyoruz.
Bu blog GitHub üzerinde açık biçimde geliştirilmektedir. Herhangi bir sorun veya sorunuz varsa lütfen orada bir issue açın. Ayrıca sayfanın en altına yorum bırakabilirsiniz. Bu yazının eksiksiz kaynak kodu post-03 dalında bulunabilir.
İçindekiler
🔗VGA Metin Arabelleği
VGA metin modunda ekrana bir karakter yazdırmak için, onun VGA donanımının metin arabelleğine yazılması gerekir. VGA metin arabelleği, doğrudan ekrana işlenen, tipik olarak 25 satır ve 80 sütundan oluşan iki boyutlu bir dizidir. Her dizi girdisi, tek bir ekran karakterini aşağıdaki biçim aracılığıyla tanımlar:
| Bit(ler) | Değer |
|---|---|
| 0-7 | ASCII kod noktası |
| 8-11 | Ön plan rengi |
| 12-14 | Arka plan rengi |
| 15 | Yanıp sönme |
İlk bayt, ASCII kodlamasıyla yazdırılması gereken karakteri temsil eder. Daha açık olmak gerekirse, bu tam olarak ASCII değildir; bazı ek karakterler ve küçük değişikliklerle code page 437 adlı bir karakter kümesidir. Basitlik için, bu yazıda ona ASCII karakter demeye devam edeceğiz.
İkinci bayt, karakterin nasıl görüntüleneceğini tanımlar. İlk dört bit ön plan rengini, sonraki üç bit arka plan rengini ve son bit ise karakterin yanıp sönüp sönmeyeceğini tanımlar. Aşağıdaki renkler kullanılabilir:
| Sayı | Renk | Sayı + Parlaklık Biti | Parlak Renk |
|---|---|---|---|
| 0x0 | Siyah | 0x8 | Koyu Gri |
| 0x1 | Mavi | 0x9 | Açık Mavi |
| 0x2 | Yeşil | 0xa | Açık Yeşil |
| 0x3 | Camgöbeği | 0xb | Açık Camgöbeği |
| 0x4 | Kırmızı | 0xc | Açık Kırmızı |
| 0x5 | Macenta | 0xd | Pembe |
| 0x6 | Kahverengi | 0xe | Sarı |
| 0x7 | Açık Gri | 0xf | Beyaz |
- bit, örneğin maviyi açık maviye dönüştüren parlaklık bitidir (bright bit). Arka plan rengi için bu bit, yanıp sönme biti olarak yeniden kullanılır.
VGA metin arabelleğine, 0xb8000 adresine belleğe eşlenmiş G/Ç (memory-mapped I/O) aracılığıyla erişilebilir. Bu, o adrese yapılan okuma ve yazmaların RAM’e erişmediği, doğrudan VGA donanımındaki metin arabelleğine eriştiği anlamına gelir. Yani onu, o adrese yapılan normal bellek işlemleri aracılığıyla okuyup yazabiliriz.
Belleğe eşlenmiş donanımın tüm normal RAM işlemlerini desteklemeyebileceğini unutmayın. Örneğin, bir cihaz yalnızca bayt bazında okumaları destekleyebilir ve bir u64 okunduğunda çöp değer döndürebilir. Neyse ki metin arabelleği normal okuma ve yazmaları destekler, bu yüzden onu özel bir şekilde ele almamıza gerek yok.
🔗Bir Rust Modülü
Artık VGA arabelleğinin nasıl çalıştığını bildiğimize göre, yazdırma işlemini yönetmek için bir Rust modülü oluşturabiliriz:
// src/main.rs içinde
mod vga_buffer;
Bu modülün içeriği için yeni bir src/vga_buffer.rs dosyası oluşturuyoruz. Aşağıdaki tüm kodlar (aksi belirtilmedikçe) yeni modülümüzün içine girer.
🔗Renkler
İlk olarak, farklı renkleri bir enum kullanarak temsil ediyoruz:
// src/vga_buffer.rs içinde
#[allow(dead_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum Color {
Black = 0,
Blue = 1,
Green = 2,
Cyan = 3,
Red = 4,
Magenta = 5,
Brown = 6,
LightGray = 7,
DarkGray = 8,
LightBlue = 9,
LightGreen = 10,
LightCyan = 11,
LightRed = 12,
Pink = 13,
Yellow = 14,
White = 15,
}
Her renk için sayıyı açıkça belirtmek amacıyla burada C benzeri bir enum kullanıyoruz. repr(u8) özniteliği sayesinde, her enum varyantı bir u8 olarak saklanır. Aslında 4 bit yeterli olurdu, ancak Rust’ta bir u4 tipi yok.
Normalde derleyici, kullanılmayan her varyant için bir uyarı verirdi. #[allow(dead_code)] özniteliğini kullanarak, Color enum’ı için bu uyarıları devre dışı bırakıyoruz.
Copy, Clone, Debug, PartialEq ve Eq trait’lerini türeterek (deriving), tip için copy semantiğini etkinleştiriyor ve onu yazdırılabilir ve karşılaştırılabilir hale getiriyoruz.
Ön plan ve arka plan rengini belirten tam bir renk kodunu temsil etmek için, u8’in üzerine bir newtype oluşturuyoruz:
// src/vga_buffer.rs içinde
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(transparent)]
struct ColorCode(u8);
impl ColorCode {
fn new(foreground: Color, background: Color) -> ColorCode {
ColorCode((background as u8) << 4 | (foreground as u8))
}
}
ColorCode struct’ı, ön plan ve arka plan rengini içeren tam renk baytını barındırır. Öncekine benzer şekilde, onun için Copy ve Debug trait’lerini türetiyoruz. ColorCode’un tam olarak bir u8 ile aynı veri yerleşimine sahip olmasını sağlamak için repr(transparent) özniteliğini kullanıyoruz.
🔗Metin Arabelleği
Artık bir ekran karakterini ve metin arabelleğini temsil edecek yapıları ekleyebiliriz:
// src/vga_buffer.rs içinde
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
struct ScreenChar {
ascii_character: u8,
color_code: ColorCode,
}
const BUFFER_HEIGHT: usize = 25;
const BUFFER_WIDTH: usize = 80;
#[repr(transparent)]
struct Buffer {
chars: [[ScreenChar; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
Rust’ta varsayılan struct’larda alan sıralaması tanımsız olduğundan, repr(C) özniteliğine ihtiyacımız var. Bu öznitelik, struct’ın alanlarının tıpkı bir C struct’ındaki gibi yerleştirilmesini garanti eder ve böylece doğru alan sıralamasını güvence altına alır. Buffer struct’ı için, onun tek alanıyla aynı bellek yerleşimine sahip olmasını sağlamak amacıyla yine repr(transparent) kullanıyoruz.
Gerçekten ekrana yazmak için, şimdi bir writer (yazıcı) tipi oluşturuyoruz:
// src/vga_buffer.rs içinde
pub struct Writer {
column_position: usize,
color_code: ColorCode,
buffer: &'static mut Buffer,
}
Writer her zaman son satıra yazacak ve bir satır dolduğunda (veya \n durumunda) satırları yukarı kaydıracak. column_position alanı, son satırdaki mevcut konumu takip eder. Mevcut ön plan ve arka plan renkleri color_code tarafından belirtilir ve VGA arabelleğine bir referans buffer içinde saklanır. Burada, referansın ne kadar süre geçerli olduğunu derleyiciye bildirmek için açık bir ömre (explicit lifetime) ihtiyacımız olduğunu unutmayın. 'static ömrü, referansın tüm program çalışma süresi boyunca geçerli olduğunu belirtir (VGA metin arabelleği için bu doğrudur).
🔗Yazdırma
Artık arabelleğin karakterlerini değiştirmek için Writer’ı kullanabiliriz. İlk olarak, tek bir ASCII baytı yazmak için bir metot oluşturuyoruz:
// src/vga_buffer.rs içinde
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
if self.column_position >= BUFFER_WIDTH {
self.new_line();
}
let row = BUFFER_HEIGHT - 1;
let col = self.column_position;
let color_code = self.color_code;
self.buffer.chars[row][col] = ScreenChar {
ascii_character: byte,
color_code,
};
self.column_position += 1;
}
}
}
fn new_line(&mut self) {/* TODO */}
}
Bayt, yeni satır (newline) baytı \n ise, writer hiçbir şey yazdırmaz. Bunun yerine, daha sonra uygulayacağımız bir new_line metodunu çağırır. Diğer baytlar ise ikinci match durumunda ekrana yazdırılır.
Bir bayt yazdırırken writer, mevcut satırın dolu olup olmadığını kontrol eder. Bu durumda, satırı kaydırmak için bir new_line çağrısı kullanılır. Ardından mevcut konuma arabelleğe yeni bir ScreenChar yazar. Son olarak, mevcut sütun konumu ilerletilir.
Tam dizeleri yazdırmak için, onları baytlara dönüştürüp tek tek yazdırabiliriz:
// src/vga_buffer.rs içinde
impl Writer {
pub fn write_string(&mut self, s: &str) {
for byte in s.bytes() {
match byte {
// yazdırılabilir ASCII baytı veya yeni satır
0x20..=0x7e | b'\n' => self.write_byte(byte),
// yazdırılabilir ASCII aralığının parçası değil
_ => self.write_byte(0xfe),
}
}
}
}
VGA metin arabelleği yalnızca ASCII’yi ve code page 437’nin ek baytlarını destekler. Rust dizeleri varsayılan olarak UTF-8’dir, bu yüzden VGA metin arabelleği tarafından desteklenmeyen baytlar içerebilirler. Yazdırılabilir ASCII baytlarını (bir yeni satır ya da boşluk karakteri ile ~ karakteri arasındaki herhangi bir şey) ve yazdırılamaz baytları ayırt etmek için bir match kullanıyoruz. Yazdırılamaz baytlar için, VGA donanımında 0xfe onaltılık koduna sahip olan bir ■ karakteri yazdırıyoruz.
🔗Deneyin!
Ekrana birkaç karakter yazmak için geçici bir fonksiyon oluşturabilirsiniz:
// src/vga_buffer.rs içinde
pub fn print_something() {
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello ");
writer.write_string("Wörld!");
}
Bu fonksiyon önce, 0xb8000 adresindeki VGA arabelleğine işaret eden yeni bir Writer oluşturur. Bunun söz dizimi biraz tuhaf görünebilir: İlk olarak, 0xb8000 tamsayısını değiştirilebilir bir ham işaretçiye (raw pointer) dönüştürüyoruz. Ardından onu (* aracılığıyla) dereference ederek ve hemen tekrar (&mut aracılığıyla) ödünç alarak değiştirilebilir bir referansa dönüştürüyoruz. Derleyici, ham işaretçinin geçerli olduğunu garanti edemediğinden, bu dönüştürme bir unsafe bloğu gerektirir.
Ardından ona b'H' baytını yazar. b öneki, bir ASCII karakterini temsil eden bir bayt değişmezi (byte literal) oluşturur. "ello " ve "Wörld!" dizelerini yazarak, write_string metodumuzu ve yazdırılamaz karakterlerin işlenmesini test ediyoruz. Çıktıyı görmek için, print_something fonksiyonunu _start fonksiyonumuzdan çağırmamız gerekir:
// src/main.rs içinde
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
vga_buffer::print_something();
loop {}
}
Projemizi şimdi çalıştırdığımızda, ekranın sol alt köşesinde sarı renkte bir Hello W■■rld! yazdırılmalı:

ö’nün iki ■ karakteri olarak yazdırıldığına dikkat edin. Bunun nedeni, ö’nün UTF-8’de iki baytla temsil edilmesi ve bu baytların ikisinin de yazdırılabilir ASCII aralığına girmemesidir. Aslında bu, UTF-8’in temel bir özelliğidir: çok baytlı değerlerin tek tek baytları asla geçerli ASCII değildir.
🔗Volatile
Mesajımızın doğru yazdırıldığını az önce gördük. Ancak bu, daha agresif optimizasyon yapan gelecekteki Rust derleyicileriyle çalışmayabilir.
Sorun şu ki, biz yalnızca Buffer’a yazıyor ve ondan bir daha hiç okumuyoruz. Derleyici, gerçekten (normal RAM yerine) VGA arabellek belleğine eriştiğimizi bilmiyor ve bazı karakterlerin ekranda görünmesi gibi bir yan etkiden haberi yok. Bu yüzden bu yazma işlemlerinin gereksiz olduğuna ve atlanabileceğine karar verebilir. Bu hatalı optimizasyonu önlemek için, bu yazma işlemlerini volatile olarak belirtmemiz gerekir. Bu, derleyiciye yazma işleminin yan etkileri olduğunu ve optimize edilerek kaldırılmaması gerektiğini bildirir.
VGA arabelleği için volatile yazmaları kullanmak amacıyla, volatile kütüphanesini kullanıyoruz. Bu crate (Rust dünyasında paketler bu şekilde adlandırılır), read ve write metotlarına sahip bir Volatile sarmalayıcı (wrapper) tipi sağlar. Bu metotlar dahili olarak core kütüphanesinin read_volatile ve write_volatile fonksiyonlarını kullanır ve böylece okuma/yazma işlemlerinin optimize edilerek kaldırılmamasını garanti eder.
volatile crate’ine bir bağımlılık eklemeyi, onu Cargo.toml dosyamızın dependencies bölümüne ekleyerek yapabiliriz:
# Cargo.toml içinde
[dependencies]
volatile = "0.2.6"
volatile’in 0.2.6 sürümünü belirttiğinizden emin olun. Crate’in daha yeni sürümleri bu yazıyla uyumlu değildir.
0.2.6, semantik sürüm numarasıdır. Daha fazla bilgi için cargo belgelerinin Bağımlılıkları Belirtme kılavuzuna bakın.
Onu, VGA arabelleğine yapılan yazma işlemlerini volatile hale getirmek için kullanalım. Buffer tipimizi aşağıdaki gibi güncelliyoruz:
// src/vga_buffer.rs içinde
use volatile::Volatile;
struct Buffer {
chars: [[Volatile<ScreenChar>; BUFFER_WIDTH]; BUFFER_HEIGHT],
}
Bir ScreenChar yerine, artık bir Volatile<ScreenChar> kullanıyoruz. (Volatile tipi generic’tir ve (neredeyse) herhangi bir tipi sarmalayabilir.) Bu, ona yanlışlıkla “normal” bir şekilde yazamayacağımızı garanti eder. Bunun yerine, artık write metodunu kullanmamız gerekir.
Bu, Writer::write_byte metodumuzu güncellememiz gerektiği anlamına gelir:
// src/vga_buffer.rs içinde
impl Writer {
pub fn write_byte(&mut self, byte: u8) {
match byte {
b'\n' => self.new_line(),
byte => {
...
self.buffer.chars[row][col].write(ScreenChar {
ascii_character: byte,
color_code,
});
...
}
}
}
...
}
= kullanan tipik bir atama yerine, artık write metodunu kullanıyoruz. Artık derleyicinin bu yazma işlemini asla optimize ederek kaldırmayacağını garanti edebiliriz.
🔗Biçimlendirme Makroları
Rust’ın biçimlendirme makrolarını da desteklemek güzel olurdu. Bu sayede, tamsayılar veya float’lar gibi farklı tipleri kolayca yazdırabiliriz. Onları desteklemek için, core::fmt::Write trait’ini uygulamamız gerekir. Bu trait’in gerekli tek metodu write_str’dir; bu metot, yalnızca fmt::Result dönüş tipiyle, write_string metodumuza oldukça benzer:
// src/vga_buffer.rs içinde
use core::fmt;
impl fmt::Write for Writer {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.write_string(s);
Ok(())
}
}
Ok(()), yalnızca () tipini içeren bir Ok Result’tur.
Artık Rust’ın yerleşik write!/writeln! biçimlendirme makrolarını kullanabiliriz:
// src/vga_buffer.rs içinde
pub fn print_something() {
use core::fmt::Write;
let mut writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
writer.write_byte(b'H');
writer.write_string("ello! ");
write!(writer, "The numbers are {} and {}", 42, 1.0/3.0).unwrap();
}
Şimdi ekranın altında bir Hello! The numbers are 42 and 0.3333333333333333 görmelisiniz. write! çağrısı, kullanılmadığında bir uyarıya neden olan bir Result döndürür, bu yüzden onun üzerinde, bir hata oluşursa panic’e neden olan unwrap fonksiyonunu çağırıyoruz. VGA arabelleğine yapılan yazmalar asla başarısız olmadığı için, bizim durumumuzda bu bir sorun değildir.
🔗Yeni Satırlar
Şu anda, yeni satırları ve artık satıra sığmayan karakterleri yalnızca yok sayıyoruz. Bunun yerine, her karakteri bir satır yukarı taşımak (en üst satır silinir) ve son satırın başından yeniden başlamak istiyoruz. Bunu yapmak için, Writer’ın new_line metodu için bir uygulama ekliyoruz:
// src/vga_buffer.rs içinde
impl Writer {
fn new_line(&mut self) {
for row in 1..BUFFER_HEIGHT {
for col in 0..BUFFER_WIDTH {
let character = self.buffer.chars[row][col].read();
self.buffer.chars[row - 1][col].write(character);
}
}
self.clear_row(BUFFER_HEIGHT - 1);
self.column_position = 0;
}
fn clear_row(&mut self, row: usize) {/* TODO */}
}
Tüm ekran karakterleri üzerinde iterasyon yapıyor ve her karakteri bir satır yukarı taşıyoruz. Aralık gösteriminin (..) üst sınırının dışlayıcı (exclusive) olduğuna dikkat edin. Ayrıca 0. satırı atlıyoruz (ilk aralık 1’den başlıyor), çünkü o, ekranın dışına kaydırılan satırdır.
Yeni satır kodunu tamamlamak için, clear_row metodunu ekliyoruz:
// src/vga_buffer.rs içinde
impl Writer {
fn clear_row(&mut self, row: usize) {
let blank = ScreenChar {
ascii_character: b' ',
color_code: self.color_code,
};
for col in 0..BUFFER_WIDTH {
self.buffer.chars[row][col].write(blank);
}
}
}
Bu metot, bir satırın tüm karakterlerinin üzerine bir boşluk karakteri yazarak onu temizler.
🔗Global Bir Arayüz
Bir Writer örneğini etrafta taşımadan diğer modüllerden bir arayüz olarak kullanılabilecek global bir writer sağlamak için, statik bir WRITER oluşturmayı deniyoruz:
// src/vga_buffer.rs içinde
pub static WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
Ancak onu şimdi derlemeye çalışırsak, aşağıdaki hatalar oluşur:
error[E0015]: calls in statics are limited to constant functions, tuple structs and tuple variants
--> src/vga_buffer.rs:7:17
|
7 | color_code: ColorCode::new(Color::Yellow, Color::Black),
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error[E0396]: raw pointers cannot be dereferenced in statics
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ dereference of raw pointer in constant
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:22
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
error[E0017]: references in statics may only refer to immutable values
--> src/vga_buffer.rs:8:13
|
8 | buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ statics require immutable values
Burada ne olduğunu anlamak için, statik değerlerin, çalışma zamanında başlatılan normal değişkenlerin aksine derleme zamanında başlatıldığını bilmemiz gerekir. Rust derleyicisinin bu tür başlatma ifadelerini değerlendiren bileşenine “const evaluator” denir. İşlevselliği hâlâ sınırlıdır, ancak onu genişletmek için, örneğin “Allow panicking in constants” RFC’sinde olduğu gibi devam eden çalışmalar vardır.
ColorCode::new ile ilgili sorun, const fonksiyonları kullanılarak çözülebilirdi; ancak buradaki temel sorun, Rust’ın const evaluator’ının ham işaretçileri derleme zamanında referanslara dönüştürememesidir. Belki bir gün çalışacak, ama o zamana kadar başka bir çözüm bulmamız gerekiyor.
🔗Lazy Statics
Statik değerlerin, const olmayan fonksiyonlarla bir kerelik başlatılması Rust’ta yaygın bir sorundur. Neyse ki, lazy_static adlı bir crate’te zaten iyi bir çözüm mevcuttur. Bu crate, tembelce (lazily) başlatılan bir static tanımlayan bir lazy_static! makrosu sağlar. Değerini derleme zamanında hesaplamak yerine, static ilk kez erişildiğinde kendisini tembelce başlatır. Böylece başlatma çalışma zamanında gerçekleşir, bu yüzden keyfi olarak karmaşık başlatma kodu mümkündür.
lazy_static crate’ini projemize ekleyelim:
# Cargo.toml içinde
[dependencies.lazy_static]
version = "1.0"
features = ["spin_no_std"]
Standart kütüphaneyi bağlamadığımız için spin_no_std özelliğine ihtiyacımız var.
lazy_static ile, statik WRITER’ımızı sorunsuzca tanımlayabiliriz:
// src/vga_buffer.rs içinde
use lazy_static::lazy_static;
lazy_static! {
pub static ref WRITER: Writer = Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
};
}
Ancak bu WRITER oldukça işe yaramaz, çünkü değiştirilemez (immutable). Bu, ona hiçbir şey yazamayacağımız anlamına gelir (çünkü tüm yazma metotları &mut self alır). Olası bir çözüm, bir değiştirilebilir statik (mutable static) kullanmak olurdu. Ama o zaman ona yapılan her okuma ve yazma unsafe olurdu, çünkü kolayca veri yarışlarına (data races) ve diğer kötü şeylere yol açabilirdi. static mut kullanmak kesinlikle önerilmez. Hatta onu kaldırma önerileri bile vardı. Peki alternatifler nelerdir? İç değiştirilebilirlik (interior mutability) sağlayan RefCell ya da hatta UnsafeCell gibi bir cell tipiyle değiştirilemez bir statik kullanmayı deneyebilirdik. Ancak bu tipler Sync değildir (haklı bir nedenle), bu yüzden onları statik değerlerde kullanamayız.
🔗Spinlock’lar
Senkronize iç değiştirilebilirlik elde etmek için, standart kütüphanenin kullanıcıları Mutex kullanabilir. Mutex, kaynak zaten kilitliyken thread’leri bloklayarak karşılıklı dışlama (mutual exclusion) sağlar. Ancak temel kernel’imizin herhangi bir bloklama desteği, hatta bir thread kavramı bile yok, bu yüzden onu da kullanamayız. Ancak bilgisayar biliminde, hiçbir işletim sistemi özelliği gerektirmeyen gerçekten temel bir mutex türü vardır: spinlock. Bloklamak yerine, thread’ler sıkı bir döngüde onu tekrar tekrar kilitlemeye çalışır ve böylece mutex tekrar serbest kalana kadar CPU zamanı harcar.
Dönen bir mutex (spinning mutex) kullanmak için, bağımlılık olarak spin crate’ini ekleyebiliriz:
# Cargo.toml içinde
[dependencies]
spin = "0.5.2"
Ardından, statik WRITER’ımıza güvenli iç değiştirilebilirlik eklemek için dönen mutex’i kullanabiliriz:
// src/vga_buffer.rs içinde
use spin::Mutex;
...
lazy_static! {
pub static ref WRITER: Mutex<Writer> = Mutex::new(Writer {
column_position: 0,
color_code: ColorCode::new(Color::Yellow, Color::Black),
buffer: unsafe { &mut *(0xb8000 as *mut Buffer) },
});
}
Artık print_something fonksiyonunu silebilir ve doğrudan _start fonksiyonumuzdan yazdırabiliriz:
// src/main.rs içinde
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
use core::fmt::Write;
vga_buffer::WRITER.lock().write_str("Hello again").unwrap();
write!(vga_buffer::WRITER.lock(), ", some numbers: {} {}", 42, 1.337).unwrap();
loop {}
}
Fonksiyonlarını kullanabilmek için fmt::Write trait’ini içe aktarmamız gerekir.
🔗Güvenlik
Kodumuzda yalnızca tek bir unsafe bloğumuz olduğuna dikkat edin; bu blok, 0xb8000’e işaret eden bir Buffer referansı oluşturmak için gereklidir. Sonrasında tüm işlemler güvenlidir. Rust, dizi erişimleri için varsayılan olarak sınır denetimi (bounds checking) kullanır, bu yüzden yanlışlıkla arabelleğin dışına yazamayız. Böylece, gerekli koşulları tip sistemine kodladık ve dışarıya güvenli bir arayüz sağlayabiliyoruz.
🔗Bir println Makrosu
Artık global bir writer’ımız olduğuna göre, kod tabanının her yerinden kullanılabilecek bir println makrosu ekleyebiliriz. Rust’ın makro söz dizimi biraz tuhaftır, bu yüzden sıfırdan bir makro yazmaya çalışmayacağız. Bunun yerine, standart kütüphanedeki println! makrosunun kaynağına bakıyoruz:
#[macro_export]
macro_rules! println {
() => (print!("\n"));
($($arg:tt)*) => (print!("{}\n", format_args!($($arg)*)));
}
Makrolar, match kollarına benzer şekilde, bir veya daha fazla kural aracılığıyla tanımlanır. println makrosunun iki kuralı vardır: İlk kural, argümansız çağrılar içindir; örneğin println!(), print!("\n") olarak genişletilir ve böylece yalnızca bir yeni satır yazdırır. İkinci kural, println!("Hello") veya println!("Number: {}", 4) gibi parametreli çağrılar içindir. O da, tüm argümanları ve sona eklenen bir yeni satır \n geçirerek print! makrosunun bir çağrısına genişletilir.
#[macro_export] özniteliği, makroyu tüm crate’e (yalnızca tanımlandığı modüle değil) ve dış crate’lere kullanılabilir kılar. Ayrıca makroyu crate kök dizinine yerleştirir; bu da makroyu std::macros::println yerine use std::println aracılığıyla içe aktarmamız gerektiği anlamına gelir.
print! makrosu şöyle tanımlanır:
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::io::_print(format_args!($($arg)*)));
}
Makro, io modülündeki _print fonksiyonunun bir çağrısına genişler. $crate değişkeni, diğer crate’lerde kullanıldığında std’ye genişleyerek makronun std crate’inin dışından da çalışmasını sağlar.
format_args makrosu, geçirilen argümanlardan bir fmt::Arguments tipi oluşturur ve bu, _print’e geçirilir. libstd’nin _print fonksiyonu, print_to’yu çağırır; bu da farklı Stdout cihazlarını desteklediği için oldukça karmaşıktır. Biz yalnızca VGA arabelleğine yazdırmak istediğimiz için o karmaşıklığa ihtiyacımız yok.
VGA arabelleğine yazdırmak için, println! ve print! makrolarını yalnızca kopyalıyor, ancak kendi _print fonksiyonumuzu kullanacak şekilde değiştiriyoruz:
// src/vga_buffer.rs içinde
#[macro_export]
macro_rules! print {
($($arg:tt)*) => ($crate::vga_buffer::_print(format_args!($($arg)*)));
}
#[macro_export]
macro_rules! println {
() => ($crate::print!("\n"));
($($arg:tt)*) => ($crate::print!("{}\n", format_args!($($arg)*)));
}
#[doc(hidden)]
pub fn _print(args: fmt::Arguments) {
use core::fmt::Write;
WRITER.lock().write_fmt(args).unwrap();
}
Orijinal println tanımından değiştirdiğimiz bir şey, print! makrosunun çağrılarının önüne de $crate koymamızdır. Bu, yalnızca println kullanmak istiyorsak print! makrosunu da içe aktarmamıza gerek olmamasını sağlar.
Standart kütüphanede olduğu gibi, her iki makroyu da crate’imizin her yerinde kullanılabilir kılmak için #[macro_export] özniteliğini ekliyoruz. Bunun makroları crate’in kök ad alanına (namespace) yerleştirdiğine dikkat edin, bu yüzden onları use crate::vga_buffer::println aracılığıyla içe aktarmak çalışmaz. Bunun yerine use crate::println yapmamız gerekir.
_print fonksiyonu, statik WRITER’ımızı kilitler ve onun üzerinde write_fmt metodunu çağırır. Bu metot, içe aktarmamız gereken Write trait’inden gelir. Sondaki ek unwrap(), yazdırma başarılı olmazsa panic’e neden olur. Ancak write_str’de her zaman Ok döndürdüğümüz için, bunun olmaması gerekir.
Makroların _print’i modülün dışından çağırabilmesi gerektiğinden, fonksiyonun public olması gerekir. Ancak bunu özel (private) bir uygulama detayı olarak kabul ettiğimiz için, onu üretilen belgelerden gizlemek amacıyla doc(hidden) özniteliğini ekliyoruz.
🔗println ile Hello World
Artık _start fonksiyonumuzda println kullanabiliriz:
// src/main.rs içinde
#[unsafe(no_mangle)]
pub extern "C" fn _start() -> ! {
println!("Hello World{}", "!");
loop {}
}
Makroyu main fonksiyonunda içe aktarmamıza gerek olmadığına dikkat edin, çünkü o zaten kök ad alanında bulunur.
Beklendiği gibi, artık ekranda bir “Hello World!” görüyoruz:

🔗Panic Mesajlarını Yazdırma
Artık bir println makromuz olduğuna göre, onu panic fonksiyonumuzda panic mesajını ve panic’in konumunu yazdırmak için kullanabiliriz:
// main.rs içinde
/// Bu fonksiyon panic anında çağrılır.
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
println!("{}", info);
loop {}
}
Şimdi _start fonksiyonumuza panic!("Some panic message"); eklediğimizde, aşağıdaki çıktıyı alıyoruz:

Böylece yalnızca bir panic oluştuğunu değil, aynı zamanda panic mesajını ve kodun neresinde gerçekleştiğini de biliyoruz.
🔗Özet
Bu yazıda, VGA metin arabelleğinin yapısını ve ona 0xb8000 adresindeki bellek eşlemesi aracılığıyla nasıl yazılabileceğini öğrendik. Bu belleğe eşlenmiş arabelleğe yazmanın güvensizliğini kapsülleyen ve dışarıya güvenli ve kullanışlı bir arayüz sunan bir Rust modülü oluşturduk.
Cargo sayesinde, üçüncü taraf kütüphanelere bağımlılık eklemenin ne kadar kolay olduğunu da gördük. Eklediğimiz iki bağımlılık, lazy_static ve spin, OS geliştirmede çok kullanışlıdır ve onları gelecekteki yazılarda daha fazla yerde kullanacağız.
🔗Sırada ne var?
Bir sonraki yazı, Rust’ın yerleşik birim test (unit test) çerçevesinin nasıl kurulacağını açıklar. Ardından, bu yazıdaki VGA arabellek modülü için bazı temel birim testleri oluşturacağız.
Yorumlar
Bir sorunuz mu var, geri bildirim paylaşmak veya fikirlerinizi tartışmak mı istiyorsunuz? Buraya yorum bırakmaktan çekinmeyin! Lütfen İngilizce kullanın ve Rust'ın davranış kurallarına uyun. Bu yorum dizisi doğrudan GitHub'daki bir tartışmaya bağlıdır, dolayısıyla isterseniz oraya da yorum yapabilirsiniz.
Instead of authenticating the giscus application, you can also comment directly on GitHub.
Mümkünse yorumlarınızı İngilizce bırakınız.