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.
| Field | Ukuran | Offset | Keterangan |
|---|---|---|---|
id | 1 byte | 0 | mulai di awal struct |
| (padding) | 7 byte | 1 sampai 7 | menggeser waktu ke kelipatan 8 |
waktu | 8 byte | 8 | wajib pada offset kelipatan 8 |
kanal | 2 byte | 16 | sudah berada pada kelipatan 2 |
| (padding) | 6 byte | 18 sampai 23 | membulatkan 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 , 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.
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.
; 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 progbitsProgram 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.
# rakit dan tautkan program
nasm -f elf64 rekaman.asm -o rekaman.o
ld rekaman.o -o rekaman
# jalankan program
./rekaman
echo "exit=$?"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=0Baris 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.
/* 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;
}offsetof id = 0
offsetof waktu = 8
offsetof kanal = 16
sizeof rekaman = 24Hasil compiler C identik baris demi baris dengan keluaran program NASM, sehingga kesetaraan tata letaknya terbukti, bukan sekadar perkiraan.
| Pengukuran | NASM (equ manual) | C (gcc) |
|---|---|---|
offset id | 0 | 0 |
offset waktu | 8 | 8 |
offset kanal | 16 | 16 |
| ukuran struct | 24 | 24 |
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.
; 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# 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=$?"offsetof id = 0
offsetof waktu = 1
offsetof kanal = 9
sizeof rekaman = 11
exit=0Perbandingan berikut menegaskan perbedaannya.
| Pengukuran | struc NASM | C (gcc) |
|---|---|---|
offset id | 0 | 0 |
offset waktu | 1 | 8 |
offset kanal | 9 | 16 |
| ukuran struct | 11 | 24 |
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
equdan padding eksplisit berupatimes, lalu field diakses lewat[basis + offset]. - Keluaran
offsetofdansizeofdari program C identik dengan pengukuran alamat pada program NASM, sehingga kesetaraan tata letaknya terbukti. - Direktif
strucNASM menyusun field tanpa padding, sehingga offset dan ukurannya berbeda dari struct C dengan field yang sama.
Referensi#
- System V Application Binary Interface, AMD64 Architecture Processor Supplement — GitLab x86-psABIs — bagian tata letak struct menetapkan aturan penyelarasan field dan pembulatan ukuran struct.
- NASM 2.16.03 documentation — NASM Project
— bab direktif memuat
struc,endstruc,equ, dantimesyang dipakai membangun tata letak struct. - offsetof(3) — Linux man-pages — makro yang mengukur offset field struct dari sisi compiler C.