Struct, offset, dan penyelarasan

Tata letak field struct, padding penyelarasan, dan akses field secara manual dengan offset tetap.

Daftar isi

Struct pada bahasa C tidak menyimpan field-nya berdempetan tanpa sela, melainkan menyisipkan byte kosong agar setiap field berada pada alamat yang diselaraskan. Topik ini membahas aturan penyelarasan yang menentukan tata letak struct, pembuatan tata letak yang sama secara manual di NASM dengan offset tetap, dan verifikasi silang hasilnya dengan offsetof dari sisi C. Program struct diakhiri dengan pembacaan 24 byte memorinya, sehingga padding benar-benar terlihat sebagai byte bernilai nol di antara field.

Aturan penyelarasan field#

Compiler C menempatkan setiap field pada offset yang merupakan kelipatan perataan field tersebut, dan perataan skalar disamakan dengan ukurannya. Field 8 byte diletakkan pada offset kelipatan 8, field 2 byte pada kelipatan 2, dan seterusnya. Apabila ruang yang disisakan field sebelumnya tidak cukup, compiler menyisipkan padding, yaitu byte kosong tanpa nama yang hanya berfungsi sebagai penggeser posisi. Ukuran struct keseluruhan dibulatkan ke atas menjadi kelipatan perataan terbesar yang dimiliki field-nya, agar struct berikutnya dalam array tetap terselaraskan.

Struct contoh pada topik ini memuat tiga field dengan ukuran yang berbeda, yaitu id seluas 1 byte, waktu seluas 8 byte, dan kanal seluas 2 byte. Susunan tersebut membuat ketiga aturan di atas semuanya berlaku. Tabel berikut merangkum offset setiap field beserta padding yang menyertainya.

FieldUkuranOffsetKeterangan
id1 byte0mulai di awal struct
(padding)7 byte1 sampai 7menggeser waktu ke kelipatan 8
waktu8 byte8wajib pada offset kelipatan 8
kanal2 byte16sudah berada pada kelipatan 2
(padding)6 byte18 sampai 23membulatkan ukuran ke 24

Tanpa padding, jumlah ketiga field hanya 11 byte, tetapi ukuran struct menurut compiler adalah 24 byte karena pembulatan ke kelipatan 8, yaitu ⌈18/8⌉×8=24\lceil 18/8 \rceil \times 8 = 24, dengan 18 sebagai posisi tepat setelah field kanal. Diagram berikut menampilkan tata letak 24 byte struct tersebut, dengan area arsir menandai padding yang tidak menyimpan apa pun.

idpadding 7 bytewaktu, 8 bytekanalpadding 6 byte018161824offset byte
Tata letak struct rekaman berukuran 24 byte beserta padding penyelarasan

Akses field dengan offset tetap#

Tata letak yang sama dapat dibangun di NASM dengan offset tetap yang didefinisikan lewat equ. Konstanta ID_OFF, WAKTU_OFF, dan KANAL_OFF menggantikan peran offsetof, sedangkan padding ditulis eksplisit dengan times agar posisi setiap field tepat seperti pada tabel.

nasm rekaman.asm
; rekaman.asm, tata letak struct dengan offset tetap hasil equ manual
default rel

ID_OFF     equ 0                       ; offset field id
WAKTU_OFF  equ 8                       ; offset field waktu
KANAL_OFF  equ 16                      ; offset field kanal
UKURAN     equ 24                      ; ukuran struct setelah pembulatan

section .data
rekaman:
    db 7                          ; id, offset 0
    times 7 db 0                  ; padding penyelarasan untuk waktu
    dq 1000000                    ; waktu, offset 8
    dw 42                         ; kanal, offset 16
    times 6 db 0                  ; padding sampai ukuran struct

    label1  db "offsetof id    = "
    len1    equ $ - label1
    label2  db "offsetof waktu = "
    len2    equ $ - label2
    label3  db "offsetof kanal = "
    len3    equ $ - label3
    label4  db "sizeof rekaman = "
    len4    equ $ - label4

hexdigit db "0123456789abcdef"

section .bss
    digit   resb 25
    hexbuf  resb 72

section .text
global _start

_start:
    lea  rbx, [rekaman]           ; alamat dasar struct

    lea  rax, [rekaman + ID_OFF]
    sub  rax, rbx                 ; selisih alamat = offset id
    lea  rsi, [label1]
    mov  rdx, len1
    call cetak_baris

    lea  rax, [rekaman + WAKTU_OFF]
    sub  rax, rbx                 ; offset waktu
    lea  rsi, [label2]
    mov  rdx, len2
    call cetak_baris

    lea  rax, [rekaman + KANAL_OFF]
    sub  rax, rbx                 ; offset kanal
    lea  rsi, [label3]
    mov  rdx, len3
    call cetak_baris

    lea  rax, [rekaman + UKURAN]
    sub  rax, rbx                 ; ukuran struct
    lea  rsi, [label4]
    mov  rdx, len4
    call cetak_baris

    lea  rbx, [rekaman]           ; alamat untuk dump byte
    mov  r13, UKURAN              ; 24 byte
    call cetak_hex

    mov  rdi, 0
    mov  rax, 60
    syscall

cetak_baris:                  ; rsi = alamat label, rdx = panjang label, rax = nilai
    push rax
    call cetak_teks
    pop  rax
    call cetak_angka
    ret

cetak_teks:                   ; tulis rsi sepanjang rdx byte ke stdout
    mov  rax, 1
    mov  rdi, 1
    syscall
    ret

cetak_angka:                  ; tulis rax sebagai desimal tanpa tanda, memakai rbp dan r12
    mov  rbp, 10              ; pembagi
    lea  r12, [digit + 24]
    mov  byte [r12], 10       ; byte newline di ujung buffer
    lea  r12, [digit + 24]
.isi:
    xor  edx, edx
    div  rbp
    add  dl, 48               ; 48 adalah kode ASCII angka 0
    dec  r12
    mov  [r12], dl
    test rax, rax
    jnz  .isi
    lea  rdx, [digit + 25]
    sub  rdx, r12             ; panjang digit ditambah newline
    mov  rsi, r12
    mov  rax, 1
    mov  rdi, 1
    syscall
    ret

cetak_hex:                    ; rbx = alamat data, r13 = jumlah byte
    lea  r15, [hexbuf]
    lea  r9, [hexdigit]
    xor  rcx, rcx
.ulang:
    cmp  rcx, r13
    jae  .tulis
    movzx rax, byte [rbx + rcx]
    mov  dl, al               ; simpan byte asli di dl
    shr  al, 4                ; nibble tinggi
    mov  r8b, [r9 + rax]
    mov  [r15], r8b
    inc  r15
    mov  al, dl
    and  al, 0x0f             ; nibble rendah
    mov  r8b, [r9 + rax]
    mov  [r15], r8b
    inc  r15
    mov  byte [r15], 32       ; spasi antar byte
    inc  r15
    inc  rcx
    jmp  .ulang
.tulis:
    mov  byte [r15 - 1], 10   ; spasi terakhir diganti newline
    lea  rsi, [hexbuf]
    mov  rdx, r15
    sub  rdx, rsi
    mov  rax, 1
    mov  rdi, 1
    syscall
    ret

section .note.GNU-stack noalloc noexec nowrite progbits

Program ini tidak sekadar mencetak nilai konstanta equ, melainkan menghitung ulang setiap offset dari alamat sungguhan, yaitu selisih rekaman + offset terhadap rekaman. Akses field cukup menyalin pola itu, misalnya [rekaman + WAKTU_OFF] untuk membaca waktu.

bash
# rakit dan tautkan program
nasm -f elf64 rekaman.asm -o rekaman.o
ld rekaman.o -o rekaman

# jalankan program
./rekaman
echo "exit=$?"
text Keluaran
offsetof id    = 0
offsetof waktu = 8
offsetof kanal = 16
sizeof rekaman = 24
07 00 00 00 00 00 00 00 40 42 0f 00 00 00 00 00 2a 00 00 00 00 00 00 00
exit=0

Baris dump memperlihatkan 24 byte struct secara berurutan. Byte pertama bernilai 07 sebagai isi id, diikuti tujuh byte nol padding, lalu 8 byte waktu yang dibaca dari kanan ke kiri sebagai 40 42 0f atau 1000000 desimal. Setelah itu muncul 2a 00 sebagai kanal bernilai 42, dan enam byte nol penutup sebagai padding pembulatan ukuran.

Verifikasi silang dengan C#

Cara paling meyakinkan untuk membuktikan tata letak NASM sudah cocok dengan C adalah mengukur struct yang sama dari sisi compiler. Makro offsetof dari header stddef.h memberikan offset setiap field, sedangkan sizeof memberikan ukuran struct.

c layout.c
/* layout.c, ukuran dan offset field struct rekaman menurut compiler C */
#include <stddef.h>
#include <stdio.h>

struct rekaman {
    unsigned char id;      /* 1 byte  */
    long waktu;            /* 8 byte  */
    short kanal;           /* 2 byte  */
};

int main(void) {
    printf("offsetof id    = %zu\n", offsetof(struct rekaman, id));
    printf("offsetof waktu = %zu\n", offsetof(struct rekaman, waktu));
    printf("offsetof kanal = %zu\n", offsetof(struct rekaman, kanal));
    printf("sizeof rekaman = %zu\n", sizeof(struct rekaman));
    return 0;
}
text Keluaran
offsetof id    = 0
offsetof waktu = 8
offsetof kanal = 16
sizeof rekaman = 24

Hasil compiler C identik baris demi baris dengan keluaran program NASM, sehingga kesetaraan tata letaknya terbukti, bukan sekadar perkiraan.

PengukuranNASM (equ manual)C (gcc)
offset id00
offset waktu88
offset kanal1616
ukuran struct2424

Kesetaraan ini penting ketika data yang sama dibaca dua bahasa sekaligus, misalnya struct yang dikirim antara kode C dan rutin assembly, atau buffer yang ditulis program C lalu ditelusuri assembly. Selama offset dan ukurannya sama, kedua sisi menggunakan peta memori yang sama persis.

Varian struc NASM tanpa padding#

NASM menyediakan pasangan direktif struc dan endstruc untuk mendefinisikan struct beserta simbol offset setiap fieldnya. Tata letak yang dihasilkannya berbeda dari C, karena struc menyusun field berdempetan tanpa menyisipkan padding penyelarasan apa pun.

nasm struc-tanpa-padding.asm
; struc-tanpa-padding.asm, offset struc NASM tanpa padding penyelarasan
default rel

struc rekaman_packed
    .id:    resb 1
    .waktu: resq 1
    .kanal: resw 1
endstruc

section .data
    label1  db "offsetof id    = "
    len1    equ $ - label1
    label2  db "offsetof waktu = "
    len2    equ $ - label2
    label3  db "offsetof kanal = "
    len3    equ $ - label3
    label4  db "sizeof rekaman = "
    len4    equ $ - label4

section .bss
    digit   resb 25

section .text
global _start

_start:
    mov  rax, rekaman_packed.id     ; konstanta offset hasil struc
    lea  rsi, [label1]
    mov  rdx, len1
    call cetak_baris

    mov  rax, rekaman_packed.waktu
    lea  rsi, [label2]
    mov  rdx, len2
    call cetak_baris

    mov  rax, rekaman_packed.kanal
    lea  rsi, [label3]
    mov  rdx, len3
    call cetak_baris

    mov  rax, rekaman_packed_size
    lea  rsi, [label4]
    mov  rdx, len4
    call cetak_baris

    mov  rdi, 0
    mov  rax, 60
    syscall

cetak_baris:                  ; rsi = alamat label, rdx = panjang label, rax = nilai
    push rax
    call cetak_teks
    pop  rax
    call cetak_angka
    ret

cetak_teks:                   ; tulis rsi sepanjang rdx byte ke stdout
    mov  rax, 1
    mov  rdi, 1
    syscall
    ret

cetak_angka:                  ; tulis rax sebagai desimal tanpa tanda, memakai rbp dan r12
    mov  rbp, 10              ; pembagi
    lea  r12, [digit + 24]
    mov  byte [r12], 10       ; byte newline di ujung buffer
    lea  r12, [digit + 24]
.isi:
    xor  edx, edx
    div  rbp
    add  dl, 48               ; 48 adalah kode ASCII angka 0
    dec  r12
    mov  [r12], dl
    test rax, rax
    jnz  .isi
    lea  rdx, [digit + 25]
    sub  rdx, r12             ; panjang digit ditambah newline
    mov  rsi, r12
    mov  rax, 1
    mov  rdi, 1
    syscall
    ret

section .note.GNU-stack noalloc noexec nowrite progbits
bash
# rakit dan jalankan program struktur tanpa padding
nasm -f elf64 struc-tanpa-padding.asm -o struc-tanpa-padding.o
ld struc-tanpa-padding.o -o struc-tanpa-padding
./struc-tanpa-padding
echo "exit=$?"
text Keluaran
offsetof id    = 0
offsetof waktu = 1
offsetof kanal = 9
sizeof rekaman = 11
exit=0

Perbandingan berikut menegaskan perbedaannya.

Pengukuranstruc NASMC (gcc)
offset id00
offset waktu18
offset kanal916
ukuran struct1124

Kesalahan umum#

Menjumlahkan ukuran field sebagai ukuran struct. Jumlah 1, 8, dan 2 byte memberi 11 byte, padahal sizeof menunjukkan 24 byte karena padding dan pembulatan. Mengalokasikan 11 byte untuk struct ini membuat field waktu pada elemen array berikutnya berada di alamat yang tidak terselaraskan dan seluruh posisi field setelahnya bergeser.

Menambah field tanpa menghitung ulang offset. Menyisipkan satu field baru di tengah struct menggeser semua offset setelahnya, sehingga konstanta WAKTU_OFF dan KANAL_OFF yang lama langsung salah tanpa pesan error dari assembler. Hitung ulang seluruh offset setiap kali definisi struct berubah, atau letakkan field baru di akhir struct agar offset lama tetap berlaku.

Membaca field dengan ukuran yang salah. Offset yang tepat tidak menolong bila jumlah byte yang dibaca tidak sesuai dengan fieldnya, misalnya memuat 8 byte pada [rekaman + KANAL_OFF] yang hanya menampung 2 byte. Dua byte ekstra itu berasal dari padding, sehingga nilainya tampak aneh tanpa pesan error apa pun. Pilih register seukuran field, misalnya movzx untuk field 1 atau 2 byte.

Ringkasan#

  • Compiler C menempatkan setiap field pada offset kelipatan perataannya, menyisipkan padding bila perlu, dan membulatkan ukuran struct ke kelipatan perataan terbesar.
  • Tata letak yang sama dibangun di NASM dengan offset tetap hasil equ dan padding eksplisit berupa times, lalu field diakses lewat [basis + offset].
  • Keluaran offsetof dan sizeof dari program C identik dengan pengukuran alamat pada program NASM, sehingga kesetaraan tata letaknya terbukti.
  • Direktif struc NASM menyusun field tanpa padding, sehingga offset dan ukurannya berbeda dari struct C dengan field yang sama.

Referensi#