Audit portabilitas adalah proses memeriksa kode C untuk menemukan asumsi yang hanya valid di satu platform atau compiler. Beberapa jebakan umum: mengasumsikan sizeof(int) == 4 (berbeda di platform 16-bit), menganggap char selalu signed (berbeda di ARM), melakukan signed overflow (undefined behavior), dan memakai int untuk ukuran buffer (overflow di objek >2GB). Bab ini mendemonstrasikan audit portabilitas dengan kode yang memeriksa sizeof berbagai tipe, menunjukkan perbedaan perilaku char signed vs unsigned, dan menerapkan pola aman untuk aritmetika yang dapat overflow. Setelah membaca bab ini, kita akan tahu bagian mana dari kode kita yang perlu diperbaiki agar portabel ke compiler dan arsitektur lain.

Audit sizeof dan Tipe Integer #

Tipe dasar C (int, long, short) tidak memiliki ukuran yang tetap—hanya batas minimum ditentukan oleh standar. Sebagai contoh, int minimal 2 byte (16-bit) tetapi bisa 4 byte (32-bit) atau 8 byte (64-bit) tergantung platform. long di Linux 64-bit adalah 8 byte, tetapi di Windows 64-bit tetap 4 byte (LLP64 model). Audit pertama yang perlu dilakukan adalah mencatat sizeof setiap tipe yang kita pakai:

c
// File: port_1.c - Audit portabilitas: sizeof, signed overflow, tipe integer
#include <stdio.h>
#include <stdint.h>
#include <limits.h>

int main(void) {
    // 1. Asumsi sizeof yang sering salah
    printf("=== Audit sizeof ===\n");
    printf("sizeof(char)      = %zu (selalu 1, guaranteed)\n", sizeof(char));
    printf("sizeof(short)    = %zu (minimal 2 byte)\n", sizeof(short));
    printf("sizeof(int)      = %zu (minimal 2, biasanya 4)\n", sizeof(int));
    printf("sizeof(long)     = %zu (4 di Windows, 8 di Linux 64-bit)\n", sizeof(long));
    printf("sizeof(long long)= %zu (minimal 8 byte)\n", sizeof(long long));
    printf("sizeof(void*)    = %zu (4 atau 8 byte)\n", sizeof(void*));
    printf("sizeof(size_t)   = %zu (mengikuti pointer)\n", sizeof(size_t));

    // 2. Gunakan stdint.h untuk ukuran eksak
    printf("\n=== Tipe eksak dari stdint.h ===\n");
    printf("sizeof(int32_t)   = %zu (selalu 4 byte)\n", sizeof(int32_t));
    printf("sizeof(int64_t)   = %zu (selalu 8 byte)\n", sizeof(int64_t));
    printf("sizeof(uintptr_t) = %zu (cukup besar untuk pointer)\n", sizeof(uintptr_t));

    // 3. Batas tipe signed
    printf("\n=== Batas tipe signed ===\n");
    printf("INT_MAX  = %d\n", INT_MAX);
    printf("INT_MIN  = %d\n", INT_MIN);
    printf("INT32_MAX= %d\n", INT32_MAX);
    printf("INT64_MAX= %lld\n", (long long)INT64_MAX);

    // 4. Demonstrasikan signed overflow (UB!)
    printf("\n=== Signed overflow (UB!) ===\n");
    int a = INT_MAX;
    printf("a = INT_MAX = %d\n", a);
    printf("a + 1 (UB!) = %d (tidak terdefinisi, bisa crash/corrupt)\n", a + 1);

    // 5. Gunakan unsigned untuk arithmetic yang boleh overflow
    unsigned int b = UINT_MAX;
    printf("\nb = UINT_MAX = %u\n", b);
    printf("b + 1 (well-defined wrap) = %u\n", b + 1);

    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -pedantic -o port_1 port_1.c
./port_1

Output yang Dihasilkan (x86-64 Linux):

text
=== Audit sizeof ===
sizeof(char)      = 1 (selalu 1, guaranteed)
sizeof(short)    = 2 (minimal 2 byte)
sizeof(int)      = 4 (minimal 2, biasanya 4)
sizeof(long)     = 8 (4 di Windows, 8 di Linux 64-bit)
sizeof(long long)= 8 (minimal 8 byte)
sizeof(void*)    = 8 (4 atau 8 byte)
sizeof(size_t)   = 8 (mengikuti pointer)

=== Tipe eksak dari stdint.h ===
sizeof(int32_t)   = 4 (selalu 4 byte)
sizeof(int64_t)   = 8 (selalu 8 byte)
sizeof(uintptr_t) = 8 (cukup besar untuk pointer)

=== Batas tipe signed ===
INT_MAX  = 2147483647
INT_MIN  = -2147483648
INT32_MAX= 2147483647
INT64_MAX= 9223372036854775807

=== Signed overflow (UB!) ===
a = INT_MAX = 2147483647
a + 1 (UB!) = -2147483648 (tidak terdefinisi, bisa crash/corrupt)

b = UINT_MAX = 4294967295
b + 1 (well-defined wrap) = 0

Mari kita telaah baris-baris kode di atas:

  1. Baris 10-17 (printf("sizeof(char) = %zu\n", sizeof(char)); dst.): Audit sizeof setiap tipe dasar. sizeof(char) selalu 1 dijamin standar C; tipe lain hanya punya batas minimum. sizeof(long) adalah 8 di Linux 64-bit (LP64 model) tetapi 4 di Windows 64-bit (LLP64 model)—perbedaan ini sering menyebabkan bug saat kode Linux dipindah ke Windows.
  2. Baris 20-23 (sizeof(int32_t) = 4 dst.): Tipe dari stdint.h memiliki ukuran eksak. int32_t selalu 4 byte di platform apapun yang mendukung C99. int64_t selalu 8 byte. uintptr_t cukup besar untuk menyimpan pointer—di x86-32 adalah 4 byte, di x86-64 adalah 8 byte.
  3. Baris 26-30 (INT_MAX, INT_MIN, INT32_MAX, INT64_MAX): Konstanta batas dari <limits.h> dan <stdint.h>. Selalu pakai konstanta ini alih-alih meng-hardcode angka seperti 2147483647.
  4. Baris 33-35 (int a = INT_MAX; a + 1): Signed overflow adalah undefined behavior di C. Output di atas menunjukkan -2147483648 (wrap-around), tetapi compiler berhak mengoptimalkan kode dengan asumsi overflow tidak terjadi—hasil aktual bisa tidak terduga (mis. compiler menghapus cek bounds karena asumsi i + 1 > i selalu benar).
  5. Baris 38-40 (unsigned int b = UINT_MAX; b + 1): Unsigned overflow well-defined—selalu wrap-around modulo 2^N. Untuk arithmetic yang boleh wrap (mis. hash, checksum), gunakan tipe unsigned. Untuk arithmetic yang overflow-nya error, gunakan signed dengan explicit overflow check.

Jebakan Char Signed dan Pola size_t #

Tipe char di C bisa signed atau unsigned, tergantung implementasi compiler. GCC di x86 default ke signed char, tetapi ARM compiler sering default ke unsigned char. Kode yang menganggap char selalu signed (mis. membandingkan dengan nilai negatif) akan gagal di platform unsigned. Selain itu, penggunaan int untuk ukuran buffer bisa overflow di objek besar—selalu gunakan size_t:

c
// File: port_2.c - Audit: char signed/unsigned, NULL check, size_t
#include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <limits.h>

// Bahaya: char bisa signed atau unsigned tergantung platform
// x86: signed char (-128..127), ARM: unsigned char (0..255)
static int hitung_set_bit(unsigned char c) {
    int n = 0;
    while (c) {
        n += c & 1;
        c >>= 1;
    }
    return n;
}

// Fungsi yang menggunakan size_t untuk parameter ukuran (bukan int)
static size_t hitung_panjang_string(const char *s, size_t max_buf) {
    size_t len = 0;
    while (len < max_buf && s[len] != '\0') {
        ++len;
    }
    return len;
}

// Pola aman: cek overflow sebelum penambahan
static int aman_tambah(int a, int b, int *hasil) {
    // Cek overflow sebelum benar-benar menjumlahkan
    if ((b > 0 && a > INT_MAX - b) ||
        (b < 0 && a < INT_MIN - b)) {
        return -1;   // overflow
    }
    *hasil = a + b;
    return 0;
}

int main(void) {
    // 1. Char signness: kode lama sering salah menganggap char selalu signed
    unsigned char c = 200;
    unsigned char uc = 200;
    printf("unsigned char c=200 -> %d\n", c);
    printf("unsigned char uc=200 -> %d\n", uc);

    // 2. Set bit pada char
    unsigned char data = 0xAB;
    printf("\nJumlah bit 1 di 0xAB = %d\n", hitung_set_bit(data));

    // 3. size_t untuk ukuran buffer
    char buf[16] = "halo dunia";
    size_t panjang = hitung_panjang_string(buf, sizeof buf);
    printf("\nString: '%s'\n", buf);
    printf("Panjang: %zu (size_t, bukan int)\n", panjang);

    // 4. Overflow-safe addition
    int a = INT_MAX - 10;
    int b = 20;
    int hasil;
    if (aman_tambah(a, b, &hasil) == 0) {
        printf("\n%d + %d = %d\n", a, b, hasil);
    } else {
        printf("\n%d + %d = OVERFLOW (deteksi aman)\n", a, b);
    }

    // 5. NULL vs 0
    int *p = NULL;
    if (p == NULL) {
        printf("\nNULL masih sah di C89/C99/C11/C17/C23\n");
    }
    printf("NULL = %p\n", (void*)NULL);

    return 0;
}

Compile dan jalankan:

bash
gcc -std=c11 -Wall -Wextra -pedantic -o port_2 port_2.c
./port_2

Output yang Dihasilkan:

text
unsigned char c=200 -> 200
unsigned char uc=200 -> 200

Jumlah bit 1 di 0xAB = 5

String: 'halo dunia'
Panjang: 10 (size_t, bukan int)

2147483637 + 20 = OVERFLOW (deteksi aman)

NULL masih sah di C89/C99/C11/C17/C23
NULL = (nil)

Fungsi aman_tambah melakukan overflow check sebelum penjumlahan. Pola yang dipakai: jika b > 0 dan a > INT_MAX - b, maka a + b pasti overflow; jika b < 0 dan a < INT_MIN - b, pasti underflow. Cara ini memeriksa overflow tanpa benar-benar melakukannya, sehingga tidak melanggar undefined behavior. Fungsi hitung_panjang_string memakai size_t untuk parameter max_buf dan return value—size_t adalah tipe unsigned yang cukup besar untuk menyimpan ukuran objek apapun di memori (8 byte di x86-64), sehingga tidak overflow untuk file/buffer besar.

Checklist Audit Portabilitas #

Berikut checklist praktis untuk mengaudit kode C lama:

PeriksaMasalahSolusi
int untuk ukuran bufferOverflow jika objek >2GBGanti ke size_t
char untuk byte dataSignness berbeda antar platformGanti ke unsigned char atau uint8_t
long untuk pointer arithmeticUkuran berbeda LP64 vs LLP64Ganti ke intptr_t/uintptr_t
int untuk time_tOverflow di tahun 2038Ganti ke time_t atau int64_t
Cast struct* ke char* untuk inspeksi byteUB jika via unionPakai memcpy atau cast unsigned char* langsung
Asumsi sizeof(int) == 4Salah di platform 16-bitPakai sizeof(int32_t) == 4
Signed overflowUB, optimisasi compiler tak terdugaCek eksplisit sebelum operasi
Pengiriman struct langsung via send()Endianness dan padding berbedaSerialisasi byte-per-byte eksplisit
Magic number alamat/tipeTidak portabel antar platformPakai macro INT32_MAX, SIZE_MAX, PTRDIFF_MAX
NULL vs 0 untuk pointerAmbigu di C99Gunakan nullptr di C23 atau NULL untuk portabilitas