Arsitektur Memori x86: Analisis Mendalam Register Segmen dalam Real Mode

Oleh: Reza Ervani bin Asmanu

Dalam pengembangan sistem operasi dan pemrograman tingkat rendah (low-level programming) pada arsitektur Intel x86, pemahaman mengenai manajemen memori adalah fondasi yang tidak bisa ditawar. Pada mode operasi 16-bit (Real Mode), prosesor menggunakan mekanisme yang dikenal sebagai Segmentasi Memori untuk mengakses alamat memori fisik.

Artikel ini akan menguraikan secara rinci karakteristik, fungsi, dan aturan ketat penggunaan Register Segmen (Segment Registers) dalam bahasa Assembly.

Konsep Dasar: Segmentasi Memori

Prosesor Intel 8086/8088 asli memiliki bus data 16-bit namun memiliki bus alamat 20-bit. Ini menciptakan sebuah dilema: register internal hanya selebar 16-bit (nilai maksimum 65.535 atau 64KB), namun prosesor harus mampu mengalamati memori hingga 1 MB (1.048.576 byte).

Solusinya adalah penggunaan dua register untuk menentukan satu alamat fisik:

  1. Segment: Menentukan alamat basis (awal blok memori).
  2. Offset: Menentukan jarak dari alamat basis tersebut.

Rumus perhitungan alamat fisik adalah:

Alamat Fisik = (Segment × 16) + Offset

Atau dalam notasi heksadesimal, nilai segmen digeser (shifted) 4 bit ke kiri, lalu dijumlahkan dengan offset.

Contoh perhitungan konkret:

  • Jika CS = 0x1000 dan IP = 0x0050
  • Alamat fisik = (0x1000 × 16) + 0x0050 = 0x10000 + 0x0050 = 0x10050

Perlu dicatat bahwa mekanisme ini menciptakan fenomena yang disebut aliasing, di mana alamat fisik yang sama dapat direpresentasikan dengan kombinasi segment:offset yang berbeda. Misalnya, alamat fisik 0x12345 dapat diwakili oleh 0x1234:0x0005, 0x1230:0x0045, atau bahkan 0x1000:0x2345.

Klasifikasi Register Segmen

Terdapat empat register segmen utama pada arsitektur 8086 (CS, DS, SS, ES) dan dua tambahan pada arsitektur 80386 ke atas (FS, GS).

1. CS (Code Segment)

Register ini menyimpan lokasi basis dari kode program yang sedang dieksekusi (instruksi mesin).

Pasangan Offset: IP (Instruction Pointer).

Karakteristik:

  • Processor mengambil instruksi (fetch) dari alamat logika CS:IP.
  • Bersifat Read-Only dalam perspektif programmer aplikasi. Anda tidak dapat menulis data ke segmen ini secara sembarangan tanpa menimpa kode program.

Aturan Khusus:

  • Anda DILARANG mengubah nilai CS secara langsung menggunakan instruksi MOV (contoh: MOV CS, AX adalah instruksi ilegal).
  • Nilai CS hanya dapat diubah melalui instruksi kontrol alur seperti JMP (Far Jump), CALL (Far Call), RETF (Far Return), atau INT (Interrupt).

Contoh penggunaan Far Jump untuk mengubah CS:

jmp 0x1000:0x0000  ; Jump ke CS=0x1000, IP=0x0000

2. DS (Data Segment)

Register ini menunjuk ke segmen di mana variabel global dan data statis program disimpan.

Pasangan Offset: Secara default berpasangan dengan register umum seperti BX, SI, atau nilai immediate.

Karakteristik:

  • Hampir semua instruksi transfer data yang mengakses memori (seperti MOV AX, [BX]) secara implisit mengasumsikan data berada di segmen DS.

Best Practice:

  • Pada bootloader atau program bare-metal, nilai DS tidak dijamin nol oleh BIOS saat startup. Oleh karena itu, inisialisasi DS adalah langkah wajib pertama dalam kode.

Contoh inisialisasi yang benar:

mov ax, 0x07C0  ; Nilai segment untuk bootloader
mov ds, ax      ; Set DS

3. SS (Stack Segment)

Register ini menyimpan lokasi basis dari struktur data tumpukan (stack).

Pasangan Offset: SP (Stack Pointer) dan BP (Base Pointer).

Karakteristik:

  • Semua operasi PUSH, POP, CALL, dan RET menggunakan segmen ini.
  • Penggunaan register BP untuk mengakses variabel lokal (MOV AX, [BP+4]) secara otomatis akan mengacu pada segmen SS, bukan DS.

Peringatan Kritis:

  • Mengubah lokasi stack (SS dan SP) harus dilakukan dengan hati-hati. Disarankan untuk menonaktifkan interupsi (CLI) sebelum mengubah SS agar CPU tidak mencoba melakukan push data interupsi ke stack yang belum valid.

Stack pada arsitektur x86 tumbuh ke bawah (downward), artinya operasi PUSH akan mengurangi nilai SP, sedangkan POP akan menambahnya. Ini adalah perilaku yang harus dipahami dengan baik untuk menghindari stack overflow atau underflow.

4. ES (Extra Segment)

Register ini adalah segmen tambahan yang ditujukan untuk operasi penyalinan memori atau manipulasi string.

Pasangan Offset: DI (Destination Index).

Karakteristik:

  • Instruksi string primitif seperti MOVSB (Move String Byte) menyalin data dari DS:SI ke ES:DI.
  • Sangat berguna ketika programmer perlu mengakses dua area memori data yang berjauhan (lebih dari 64KB) secara bersamaan tanpa harus terus-menerus mengubah nilai DS.

Instruksi string lainnya yang menggunakan ES:DI sebagai tujuan termasuk STOSB (store byte), SCASB (scan byte), dan CMPSB (compare byte).

5. FS dan GS

Diperkenalkan pada prosesor 80386, register ini adalah segmen tambahan yang bersifat umum. Pada sistem operasi modern (seperti Windows atau Linux), FS/GS sering digunakan untuk menyimpan Thread Local Storage (TLS) atau struktur data kernel, namun pada Real Mode, mereka bebas digunakan sebagai segmen data tambahan.

Pada Windows 64-bit, GS digunakan untuk menunjuk ke struktur TEB (Thread Environment Block), sedangkan pada Linux 64-bit, FS digunakan untuk thread-local storage. Namun dalam konteks Real Mode, fungsi-fungsi ini tidak relevan dan register dapat digunakan secara bebas.

Aturan Teknis Penggunaan di Assembly (NASM)

Dalam penulisan kode Assembly, terdapat batasan keras (hard constraints) yang diterapkan oleh desain sirkuit prosesor terhadap register segmen.

1. Larangan Nilai Immediate (Immediate Value Restriction)

Prosesor x86 tidak memiliki jalur sirkuit untuk memindahkan nilai konstanta (angka langsung) ke dalam register segmen.

Salah (Illegal Instruction):

mov ds, 0x1234  ; Error: Invalid combination of opcode and operands

Benar:

Anda harus menggunakan General Purpose Register (biasanya AX) sebagai perantara.

mov ax, 0x1234
mov ds, ax      ; Valid

Atau menggunakan stack:

push 0x1234
pop ds          ; Valid

Metode kedua ini kadang lebih efisien dalam hal ukuran kode, terutama jika Anda perlu mengatur beberapa register segmen dengan nilai yang sama.

2. Override Segmen (Segment Override Prefix)

Meskipun setiap register offset memiliki pasangan default, programmer dapat memaksa CPU menggunakan segmen lain dengan teknik Override.

Contoh Default:

mov ax, [bp]  ; CPU membaca dari SS:BP

Contoh Override:

mov ax, [ds:bp]  ; CPU dipaksa membaca dari DS:BP

Prefix override ini dikodekan sebagai byte tambahan di depan instruksi. Terdapat prefix untuk setiap segment register: CS (0x2E), DS (0x3E), ES (0x26), SS (0x36), FS (0x64), dan GS (0x65). Assembler akan secara otomatis menyisipkan byte prefix ini ketika Anda menggunakan sintaks override.

3. Inisialisasi Atomik Stack

Saat mengubah lokasi stack, pastikan integritas SS dan SP. Pada prosesor Intel 80286 ke atas, interupsi ditahan selama satu instruksi setelah MOV SS, namun praktik terbaik untuk kompatibilitas penuh (terutama pada 8086/8088) adalah:

cli             ; 1. Disable Interrupts
mov ax, 0x9000
mov ss, ax      ; 2. Set Segment
mov sp, 0xFFFF  ; 3. Set Pointer
sti             ; 4. Enable Interrupts

Mekanisme “interrupt shadow” setelah MOV SS adalah fitur perlindungan hardware yang memastikan bahwa instruksi berikutnya (biasanya MOV SP) dapat dieksekusi tanpa interupsi, sehingga stack pointer tidak tertinggal dalam keadaan inkonsisten.

Studi Kasus: Implementasi Praktis

Kasus 1: Bootloader Minimalis

Berikut adalah contoh implementasi bootloader yang menunjukkan inisialisasi register segmen yang benar:

[BITS 16]
[ORG 0x7C00]

start:
    ; Langkah 1: Inisialisasi segment registers
    cli                 ; Disable interrupts selama setup
    xor ax, ax          ; ax = 0
    mov ds, ax          ; ds = 0
    mov es, ax          ; es = 0
    
    ; Langkah 2: Setup stack
    mov ss, ax          ; ss = 0
    mov sp, 0x7C00      ; sp = 0x7C00 (stack tumbuh ke bawah dari bootloader)
    sti                 ; Enable interrupts
    
    ; Langkah 3: Load kernel dari disk
    mov ah, 0x02        ; BIOS read sector function
    mov al, 1           ; Jumlah sector yang dibaca
    mov ch, 0           ; Cylinder 0
    mov cl, 2           ; Sector 2 (sector 1 adalah bootloader)
    mov dh, 0           ; Head 0
    mov bx, 0x1000      ; Offset untuk buffer
    int 0x13            ; BIOS disk interrupt
    
    ; Langkah 4: Jump ke kernel yang di-load
    jmp 0x0000:0x1000   ; Far jump ke kernel
    
times 510-($-$$) db 0   ; Padding hingga 510 bytes
dw 0xAA55               ; Boot signature

Perhatikan bahwa stack diinisialisasi di bawah bootloader (0x7C00) sehingga stack memiliki ruang untuk tumbuh ke bawah tanpa menimpa kode bootloader.

Kasus 2: Operasi String untuk Penyalinan Memori

Berikut adalah contoh penggunaan ES untuk operasi penyalinan memori blok besar:

copy_data:
    push ds
    push es
    
    ; Setup source segment (dari mana data akan disalin)
    mov ax, 0x1000
    mov ds, ax
    xor si, si          ; Offset source = 0
    
    ; Setup destination segment (ke mana data akan disalin)
    mov ax, 0x2000
    mov es, ax
    xor di, di          ; Offset destination = 0
    
    ; Copy 512 bytes
    mov cx, 256         ; 256 words = 512 bytes
    cld                 ; Clear direction flag (increment SI/DI)
    rep movsw           ; Repeat move word
    
    pop es
    pop ds
    ret

Instruksi REP MOVSW akan secara otomatis mengulangi operasi MOVSW sebanyak nilai dalam CX, dengan SI dan DI secara otomatis bertambah (atau berkurang jika direction flag di-set) setiap iterasi.

Kasus 3: Akses Data dengan Override

Terkadang Anda perlu mengakses data dari segmen yang berbeda tanpa mengubah nilai register segmen default:

read_remote_data:
    push es
    
    mov ax, 0x3000
    mov es, ax          ; ES menunjuk ke segmen remote
    
    mov bx, 0x0100      ; Offset di segmen remote
    mov al, [es:bx]     ; Baca byte dari ES:BX (0x3000:0x0100)
    
    ; Sementara itu, DS tetap menunjuk ke segmen data lokal
    mov bx, local_var
    mov [bx], al        ; Simpan ke DS:BX (segmen lokal)
    
    pop es
    ret

local_var db 0

Teknik ini sangat berguna dalam driver perangkat atau ketika bekerja dengan memory-mapped I/O.

Pitfall Umum dan Debugging

1. Lupa Inisialisasi DS

Kesalahan paling umum dalam pemrograman bootloader adalah asumsi bahwa DS bernilai 0 setelah BIOS melakukan boot. Faktanya, nilai DS tidak terdefinisi dan bisa berisi nilai apa saja. Selalu inisialisasi DS secara eksplisit.

Gejala: Program crash atau menampilkan data yang salah.

Solusi: Selalu tambahkan kode inisialisasi DS di awal program:

mov ax, 0x0000  ; atau nilai segment yang sesuai
mov ds, ax

2. Stack Overflow karena Posisi Stack yang Salah

Menempatkan stack terlalu dekat dengan kode atau data dapat menyebabkan stack overflow menimpa kode program.

Gejala: Program crash secara tidak terduga, terutama setelah beberapa pemanggilan fungsi atau interrupt.

Solusi: Tempatkan stack di area memori yang aman dengan jarak yang cukup dari kode dan data:

mov ax, 0x9000
mov ss, ax
mov sp, 0xFFFE  ; Top of segment, hampir 64KB ruang stack

3. Segment Wrap-around

Karena offset adalah 16-bit, nilai maksimumnya adalah 0xFFFF. Jika Anda mencoba mengakses offset yang lebih besar, akan terjadi wrap-around ke awal segment.

Contoh:

mov bx, 0xFFFF
inc bx          ; bx sekarang 0x0000, tidak 0x10000!
mov al, [bx]    ; Membaca dari awal segment, bukan offset 0x10000

Solusi: Jika data Anda lebih besar dari 64KB, gunakan multiple segments atau tingkatkan nilai segment:

; Untuk mengakses data di offset > 64KB
mov ax, ds
add ax, 0x1000  ; Pindah ke segment berikutnya (64KB lebih tinggi)
mov ds, ax
xor bx, bx      ; Mulai dari offset 0 di segment baru

4. Interrupt dengan Stack yang Tidak Valid

Jika interrupt terjadi saat SS:SP menunjuk ke lokasi yang tidak valid, sistem akan crash.

Solusi: Selalu gunakan CLI/STI saat mengubah stack:

cli
mov ss, ax
mov sp, new_sp
sti

5. Salah Perhitungan Alamat dengan ORG

Direktif ORG memberitahu assembler bahwa kode akan di-load pada offset tertentu, tetapi tidak mengubah nilai register segmen.

Salah:

[ORG 0x7C00]
start:
    mov si, message  ; SI = 0x7C?? (offset relatif)
    ; Tapi jika DS = 0x0000, alamat fisik = 0x00000 + 0x7C?? ✗

Benar:

[ORG 0x7C00]
start:
    xor ax, ax
    mov ds, ax       ; DS = 0x0000
    mov si, message  ; Sekarang DS:SI = 0x0000:0x7C?? ✓

Atau gunakan segment yang berbeda:

[ORG 0x0000]
start:
    mov ax, 0x07C0  ; 0x07C0 * 16 = 0x7C00
    mov ds, ax      ; DS = 0x07C0
    mov si, message ; DS:SI = 0x07C0:0x00?? (alamat fisik sama: 0x7C??)

Optimisasi dan Teknik Lanjutan

1. Menggunakan Stack untuk Transfer Cepat

Teknik PUSH/POP dapat digunakan untuk transfer nilai antar register segmen dengan overhead instruksi minimal:

; Transfer nilai dari DS ke ES
push ds
pop es          ; 2 instruksi, sangat efisien

2. Canonical Segment:Offset Pairs

Untuk setiap alamat fisik, terdapat kombinasi segment:offset “canonical” di mana offset adalah nilai terkecil yang mungkin. Ini didapat dengan membagi alamat fisik dengan 16:

Alamat fisik: 0x12345
Canonical form: 0x1234:0x0005

Menggunakan bentuk canonical memudahkan debugging dan membuat kode lebih mudah dipahami.

3. Huge Pointer vs Far Pointer

Dalam model memori C untuk Real Mode:

  • Near Pointer: Hanya menyimpan offset (16-bit), menggunakan DS sebagai segment.
  • Far Pointer: Menyimpan segment dan offset (32-bit total).
  • Huge Pointer: Seperti far pointer, tetapi kompiler melakukan normalisasi untuk pointer arithmetic yang benar melintasi boundary 64KB.

Assembly programmer perlu memahami konsep ini ketika berinteraksi dengan kode C.

Transisi ke Protected Mode

Register segmen memiliki peran yang sangat berbeda dalam Protected Mode:

  • Nilai dalam register segmen menjadi “selector” yang menunjuk ke entry dalam Global Descriptor Table (GDT) atau Local Descriptor Table (LDT).
  • Setiap segment descriptor dalam GDT berisi base address 32-bit, limit, dan atribut proteksi.
  • Perhitungan alamat menjadi: Alamat Linear = Base (dari descriptor) + Offset

Transisi dari Real Mode ke Protected Mode melibatkan:

  1. Setup GDT dengan descriptor yang valid
  2. Enable bit PE (Protection Enable) dalam register CR0
  3. Far jump untuk memuat CS dengan selector yang valid
  4. Reload semua register segmen dengan selector yang valid

Contoh kode transisi dasar:

; Load GDT
lgdt [gdt_descriptor]

; Enable Protected Mode
mov eax, cr0
or eax, 1
mov cr0, eax

; Far jump untuk flush pipeline dan load CS
jmp 0x08:protected_mode_start  ; 0x08 = code segment selector

[BITS 32]
protected_mode_start:
    ; Reload segment registers
    mov ax, 0x10    ; 0x10 = data segment selector
    mov ds, ax
    mov es, ax
    mov fs, ax
    mov gs, ax
    mov ss, ax

Kesimpulan

Register Segmen bukan sekadar wadah penyimpanan nilai, melainkan komponen vital yang mendefinisikan konteks dari eksekusi program. Kesalahan dalam mengelola register segmen (seperti lupa inisialisasi DS atau salah perhitungan SS) adalah penyebab utama undefined behavior dan crash pada pengembangan sistem operasi.

Pemahaman yang presisi tentang pasangan register (CS:IP, SS:SP, DS:BX) dan keterbatasan instruksi MOV pada segmen adalah kompetensi wajib bagi setiap system programmer. Lebih dari itu, memahami karakteristik unik setiap register segmen, aturan penggunaan override, dan pitfall yang umum terjadi akan secara signifikan meningkatkan kualitas dan keandalan kode low-level yang Anda tulis.

Meskipun Real Mode sudah jarang digunakan dalam aplikasi modern (kecuali pada tahap bootloader), prinsip-prinsip segmentasi memori ini tetap relevan untuk memahami arsitektur x86 secara menyeluruh dan menjadi fondasi untuk memahami mode operasi yang lebih kompleks seperti Protected Mode dan Long Mode.

Bagi para system programmer yang serius, investasi waktu untuk menguasai seluk-beluk register segmen akan terbayar dengan kemampuan untuk menulis kode bootloader, driver perangkat, dan bahkan sistem operasi sederhana dari nol. Ini adalah pengetahuan fundamental yang membedakan seorang programmer biasa dengan seorang system programmer sejati.


Referensi Tambahan:

  • Intel 80386 Programmer’s Reference Manual
  • AMD64 Architecture Programmer’s Manual
  • Dokumentasi NASM (Netwide Assembler)
  • OSDev Wiki untuk praktik terbaik dalam pengembangan OS

Tentang Penulis: Reza Ervani adalah system programmer dengan fokus pada arsitektur low-level dan pengembangan sistem operasi. Artikel ini merupakan bagian dari seri dokumentasi teknis mengenai arsitektur x86.