Skip to content
Bloga Dönün
Eğitimler

Bayt sırası: aynı baytlar neden iki farklı sayı verir

Aynı 12 34 56 78 baytları okuyana göre 0x12345678 ya da 0x78563412 olur. JavaScript, Python, Go, PNG ve GZIP'te bayt sırasını çevrimiçi ölçümlerle görün.

13 dakika okuma

Bayt sırası: aynı baytlar neden iki farklı sayı verir

Bellekte dört bayt duruyor: 12 34 56 78. Bunları üç farklı JavaScript API’siyle okuyun, geriye iki farklı sayı gelir. Bayt sırası (endianness) dediğimiz şey budur. Sorunun tamamı aşağıdaki tabloda.

OkumaSonuç
new DataView(buf).getUint32(0)0x12345678
new DataView(buf).getUint32(0, true)0x78563412
new Uint32Array(buf)[0]0x78563412

Bozuk bir şey yok, hata da fırlatılmıyor. Her çağrı, çok baytlı bir sayının hangi ucunun önce geldiğine dair farklı bir kural uyguluyor.

Bayt sırası “makinem ne?” sorusu değil, “bu baytlar kimin kuralına göre yazıldı?” sorusudur. Diskinizdeki bir PNG big-endian’dır. Hemen yanındaki GZIP dosyası little-endian’dır. İşlemcinizin ikisinde de oy hakkı yok.

Aşağıdaki her şey macOS darwin arm64 üzerinde Node v26.7.0 ve Python 3.14.6 ile ölçüldü; bu makinede os.endianness() LE, sys.byteorder ise little döndürüyor.

1. Big-endian ve little-endian gerçekte ne demek

32 bitlik 0x12345678 değerini ele alalım. Dört bayttan oluşur: en anlamlısı 12, en anlamsızı 78. En düşük adrese bunlardan hangisinin düşeceğine bayt sırası karar verir.

YerleşimAdres 0Adres 1Adres 2Adres 3
Big-endian12345678
Little-endian78563412

Big-endian büyük ucu önce saklar; sayıyı kâğıda yazarken kullandığınız sıranın aynısı. Little-endian küçük ucu önce saklar. İkisi de baytın içindeki bitlere dokunmaz: 0x12 her iki yerleşimde de 0x12’dir. Yalnızca baytların tamamı yer değiştirir.

Hex’ten ikiliğe geçiş kısmı sizde biraz sallantılıysa, sayı tabanı dönüştürücü her baytı hex hâlinin yanında ikilik olarak açar; gösterimin kendisini de ikilik, hex ve sekizlik dönüşüm rehberi anlatıyor.

1.1 Neden iki tane var

Bölünme ilkesel değil, tarihsel. Big-endian insanların sayı yazma biçimiyle örtüşüyor ve ağ protokollerinin kuralı olarak yeterince erken yerleşti. İşlemci tarafında ise little-endian kazandı, çünkü x86 onu kullanıyor ve ARM varsayılan olarak ona ayarlı. Konuyla ilgili neredeyse her yazıda tekrarlanan verimlilik savı 8. bölümde ölçülüyor; sonuç kabaca %2 çıkıyor.

2. Bayt sırası platformun değil biçimin bir özelliğidir

Bayt sırasını, baytları okuyan makine değil, onları yazan taraf sabitler. Kanıt için bir dizüstü bilgisayar ve iki dosya yeter: aynı arm64 makinede, aynı süreç içinde bu ikisi zıt yönlerde çözülmek zorunda.

2.1 PNG big-endian’dır

RFC 2083, çok baytlı tam sayıların ağ bayt sırasında olmasını şart koşar; dolayısıyla bir PNG’deki her uzunluk, genişlik ve yükseklik big-endian’dır. Başlık yerleşimi sabittir: sekiz imza baytı, ardından dört baytlık parça uzunluğu, dört karakterlik parça türü, sonra genişlik ve yükseklik.

const fs = require('node:fs');
const png = fs.readFileSync('public/og/base-converter.png');

png.subarray(0, 8).toString('hex'); // '89504e470d0a1a0a' — PNG signature
png.readUInt32BE(8);                // 13   — IHDR chunk length
png.readUInt32BE(16);               // 1200 — image width
png.readUInt32BE(20);               // 630  — image height

png.readUInt32LE(16);               // the same four bytes, read the wrong way
AlanBaytlarBig-endianLittle-endian
IHDR uzunluğu00 00 00 0d13218,103,808
Görüntü genişliği00 00 04 b012002,953,052,160

2.2 GZIP little-endian’dır

RFC 1952 §2.3.1 bunu açıkça yazar: en anlamsız bayt önce. Bir gzip akışının son dört baytı ISIZE, yani sıkıştırılmamış boyuttur. 300 baytlık bir A dizisini sıkıştırıp bakalım:

python3 -c "import gzip,sys; sys.stdout.buffer.write(gzip.compress(b'A'*300))" > a.gz
const gz = fs.readFileSync('a.gz');

gz.subarray(-4).toString('hex');   // '2c010000'
gz.readUInt32LE(gz.length - 4);    // 300 — correct
gz.readUInt32BE(gz.length - 4);    // the same four bytes, read the wrong way
AlanBaytlarLittle-endianBig-endian
Kuyruktaki ISIZE2c 01 00 00300738,263,040

Aynı makine, aynı süreç, aynı dört baytlık okuma çağrısı. Çalışma kuralınız “makinem little-endian, o hâlde little-endian okurum” ise bu iki dosyadan biri çöpe dönüşerek çözülür. Her seferinde biçim karar verir.

3. JavaScript: iki API, iki zıt varsayılan

Bir ArrayBuffer içeriğini görüntülemenin iki yolu var ve varsayılanları birbirinin tersi. Tarayıcıda da Node’da da bayt sırası en çok burada karışıyor.

3.1 DataView bayt sırası: üçüncü argüman belirler

DataView metotları son argüman olarak isteğe bağlı bir littleEndian bayrağı alır. Yazmazsanız big-endian elde edersiniz. setUint32(0, x) ile setUint32(0, x, false) aynı çağrıdır.

const buf = new ArrayBuffer(4);
new Uint8Array(buf).set([0x12, 0x34, 0x56, 0x78]);
const dv = new DataView(buf);

dv.getUint32(0).toString(16);       // '12345678'  — big-endian, the default
dv.getUint32(0, true).toString(16); // '78563412'  — littleEndian: true

Yazma yönünde de aynı kural tersinden işler:

const hex = (b) => [...new Uint8Array(b)].map((x) => x.toString(16).padStart(2, '0')).join(' ');

dv.setUint32(0, 0x12345678);
hex(buf); // '12 34 56 78'

dv.setUint32(0, 0x12345678, true);
hex(buf); // '78 56 34 12'

3.2 TypedArray platformu izler ve bunu değiştiremezsiniz

Uint32Array, Int16Array, Float64Array ve diğerleri işlemci ne kullanıyorsa onu kullanır. Bunu değiştirebileceğiniz bir argüman ya da kurucuya verilecek bir bayrak yoktur. Bu arm64 makinede bu, little-endian demek; yani DataView varsayılanının tam tersi.

new Uint32Array(buf)[0] = 0x12345678;
hex(buf); // '78 56 34 12'

Yani tek bir ArrayBuffer, new DataView(buf).getUint32(0) üzerinden okunduğunda 0x12345678, new Uint32Array(buf)[0] üzerinden okunduğunda 0x78563412 verir. İkisi de doğru. Farklı sorulara cevap veriyorlar.

Tek baytlık görünümler bundan muaf, çünkü bayt sırası yalnızca bir bayttan geniş birimler için vardır. Uint8Array ile Int8Array hiçbir zaman bayrak istemez. Bir adım genişletin, sorun geri döner:

const two = new ArrayBuffer(2);
new Uint16Array(two)[0] = 0x00ff;
hex(two); // 'ff 00'

3.3 Node Buffer: sırayı metot adında belirtin

Buffer varsayılan diye bir şey bırakmaz, sırayı doğrudan metot adına koyar; Node kodunda bayt sırasını denetlemek genellikle bu yüzden en kolayıdır.

const b = Buffer.from([0x12, 0x34, 0x56, 0x78]);

b.readUInt32BE(0).toString(16); // '12345678'
b.readUInt32LE(0).toString(16); // '78563412'

b.swap32().toString('hex');     // '78563412' — mutates b in place

swap32() her dört baytlık grubu ters çevirir ve kopya yerine aynı arabelleği döndürür. Elinizde baştan sona yanlış sırayla yazılmış bir tam sayı dizisi varsa işinizi görür; arabelleğin paylaşıldığını unuttuysanız tehlikelidir.

4. Python struct: beş önek ve @ gerçekte neye mal olur

4.1 < > ! = @: beş struct pack bayt sırası öneki

0x12345678 değerini işaretsiz 32 bitlik tam sayı olarak paketleyelim; her önek için bir satır:

import struct

struct.pack('<I', 0x12345678).hex(' ')  # '78 56 34 12'  little-endian
struct.pack('>I', 0x12345678).hex(' ')  # '12 34 56 78'  big-endian
struct.pack('!I', 0x12345678).hex(' ')  # '12 34 56 78'  network order
struct.pack('=I', 0x12345678).hex(' ')  # '78 56 34 12'  native order, standard sizes
struct.pack('@I', 0x12345678).hex(' ')  # '78 56 34 12'  native order, native alignment

! ile > birebir aynı baytları üretir, çünkü ağ bayt sırası big-endian’dır. Açma yönü de simetriktir ve int.from_bytes aynı ikiliyi verir:

hex(struct.unpack('>I', b'\x12\x34\x56\x78')[0])    # '0x12345678'
hex(struct.unpack('<I', b'\x12\x34\x56\x78')[0])    # '0x78563412'
hex(int.from_bytes(b'\x12\x34\x56\x78', 'big'))     # '0x12345678'
hex(int.from_bytes(b'\x12\x34\x56\x78', 'little'))  # '0x78563412'

4.2 @ ile = bayt sırasında değil dolguda ayrışır

İkisi de platformu izler, dolayısıyla bu makinede ikisi de little-endian yazar. Fark hizalamada ve bu, struct’ınızın boyutunu değiştirir:

struct.calcsize('@ci')  # 8
struct.calcsize('=ci')  # 5
struct.calcsize('<ci')  # 5

Bir char ve ardından bir int, beş baytlık veri eder. Hiç önek yazmadığınızda devreye giren varsayılan @ altında Python üç dolgu baytı ekler; böylece int dört baytlık bir sınırda başlar. = altında ya da açıkça yazılmış herhangi bir bayt sırası öneki altında dolgu ortadan kalkar.

İmkânsız görünen bir hatanın arkasındaki mekanizma da bu: birisi bayt sırası sorununu düzeltmek için < ekliyor ve kayıt uzunluğu ayağının altından kayıyor. Olan bitenin bayt sırasıyla hiçbir ilgisi yok. @ önekinden vazgeçmek, yerel hizalamayı da sessizce kapatmış oluyor.

5. Ağ bayt sırası ve bunu diğer dillerin yazma biçimi

Ağ bayt sırası big-endian’dır. TCP, UDP ve IP başlıkları çok baytlı alanlarını bu şekilde taşır; kökeni de big-endian donanımın birinin taraf seçmesini gerektirecek kadar yaygın olduğu döneme dayanır. C bu dönüşümü htons, htonl, ntohs ve ntohl ile sunar: short ve long tipleri için host’tan ağa ve geri. Little-endian bir host’ta bunlar baytları takas eder; big-endian bir host’ta hiçbir şeye dokunmaz. Onları atlayan kodun farklı bir makineyle karşılaşana kadar sorunsuz çalışmasının sebebi de bu.

Go tam ters yolu seçer ve varsayılan diye bir şeye hiç izin vermez:

import "encoding/binary"

v := binary.BigEndian.Uint32(b)
binary.LittleEndian.PutUint32(b, v)

binary.BigEndian ile binary.LittleEndian, çağrı yerinde adını kendiniz yazdığınız değerlerdir. Platforma bağlı bir yol da yok, unutulacak isteğe bağlı bir bayrak da; bu yüzden Go’da bayt sırası incelemesi tanımlayıcıyı okumaktan ibarettir.

Aynı disiplin sabit nokta verileri için de geçerli. Bir Q15 ya da Q31 örneği, kodunuzdan çıktığı anda sıradan bir 16 veya 32 bitlik tam sayıdır; dolayısıyla her şeyle birlikte bayt sırası sorusunu da devralır. Q biçimi dönüştürücü kesrin arkasındaki tam sayıyı gösterir ve sıraya sokulan şey işte o tam sayıdır.

6. Kayan nokta sayılarının da bir bayt sırası vardır

Float özel bir tür değil. IEEE 754 bit desenini tanımlar; sonra o dört ya da sekiz bayt, biçim hangi sırayı istiyorsa o sırayla belleğe serilir.

struct.pack('>f', 1.0).hex(' ')  # '3f 80 00 00'
struct.pack('<f', 1.0).hex(' ')  # '00 00 80 3f'
struct.pack('>d', 0.1).hex(' ')  # '3f b9 99 99 99 99 99 9a'
struct.pack('<d', 0.1).hex(' ')  # '9a 99 99 99 99 99 b9 3f'

IEEE 754 dönüştürücü sorunun ilk yarısını yanıtlıyor: FP32 seçiliyken 3.14159 yazın, 0x40490FD0 çıkar. Bu yazı ikinci yarısını yanıtlıyor: o dört bayt dosyaya hangi sırayla ulaşır. Araç size değeri verir, bayt sırası ise dizilişi.

double 0.1 satırı aynı zamanda 0.1 + 0.2 ifadesinin neden yanlış davrandığını da açıklıyor. Tekrar eden o 99 baytları, hiç bitmeyen bir ikilik açılımın izidir; kayan nokta hassasiyeti rehberi bunu adım adım söküyor.

6.1 Float 1.0 değeri 3f 80 00 00 ya da 00 00 80 3f olur

FP32’de 1.0, bayt deseni çok dengesiz olduğu için işe yarar bir kanaryadır. Big-endian 3f 80 00 00 yazar, little-endian 00 00 80 3f. Tanımadığınız bir ikilik biçimin dökümünü alın, 1.0 olması gerektiğini bildiğiniz bir alan bulun; sondaki iki sıfır hangi uçta olduğunuzu söyler. Aynısı double için de geçerli; orada aynı değerin altı sıfır baytı tek tarafta kümelenir.

7. Metin kodlamalarında bayt sırası: BOM yalnızca bir bildirimdir

UTF-16 ve UTF-32 çok baytlı birimlerden oluşur, dolayısıyla tam da bu yazının anlattığı sorunla karşılaşır. Verdikleri cevap, dosyanın kendi sırasını bir byte order mark ile ilan etmesine izin vermek:

Buffer.from('\uFEFF', 'utf16le').toString('hex'); // 'fffe' — U+FEFF is the BOM
Buffer.from('A', 'utf16le').toString('hex');      // '4100'

Başta fffe görüyorsanız little-endian, feff görüyorsanız big-endian. Bu da BOM’u, bir biçimin bayt sırasını varsaymak yerine kendi ağzından bildirmesinin en yaygın örneği yapıyor. UTF-8’in neden BOM’a ihtiyaç duymadığı ve bu işaret davetsiz geldiğinde size neye mal olduğu dahil hikâyenin tamamı UTF-8, UTF-16 ve Unicode kodlama rehberi yazısında.

8. Little-endian daha mı hızlı? 40 milyon yineleme ne diyor

Little-endian’ın daha verimli olduğu iddiası arama motoru özetlerinde ve konuyla ilgili giriş niteliğindeki açıklamaların çoğunda karşınıza çıkıyor. Test edilebilir bir iddia. Bu arm64 makinede tek bir 32 bitlik okumanın kırk milyon yinelemesi:

Yolns/op
DataView.getUint32(4, true) (little-endian)4.4267
DataView.getUint32(4) (big-endian)4.5200
Buffer.readUInt32LE(4)1.5173
Buffer.readUInt32BE(4)5.0893
OranKatsayı
DataView big-endian / little-endian1.021×
Buffer big-endian / little-endian3.354×

Bu iki oran farklı şeyleri ölçüyor ve yalnızca ikincisini aktarmak, o yazıların yaptığı hatanın aynısı olurdu.

Bayt sırası maliyetinin dürüst ölçümü DataView çifti. İki çağrı da V8’de aynı satır içi yola derleniyor; big-endian olanı baytları takas etmek için fazladan tek bir ARM REV komutu taşıyor. 1.021× işte bu, kabaca %2. Küçük ama sıfır değil; bunu “bedava” diye yuvarlamayın.

Buffer çifti ise bambaşka bir şey ölçüyor. V8’de readUInt32LE için ayrılmış özel bir hızlı yol var, readUInt32BE bundan yararlanamıyor; yani 3.354×, tek bir runtime içindeki bir uygulama farkı, işlemcide bayt takas etmenin bedeli değil. Bunu big-endian’ın yavaş olduğunun kanıtı diye aktarmak yanlış olur. Runtime’ı değiştirin, sayı da onunla birlikte değişir.

Modern donanımda bayt sırası dönüşümü, bir biçim tasarımı tartışmasına girmeyecek kadar ucuz. Tarihsel verimlilik savı, özel takas komutlarının henüz var olmadığı bir dönemden kalma. Protokolünüzün ya da birlikte çalıştığınız sistemlerin hâlihazırda kullandığı sırayı seçin.

9. Bayt sırası hatasını sıradan bir hatadan nasıl ayırırsınız

“Makinem big-endian mı little-endian mı, nasıl bakarım” sorusunun karşılığı Node’da os.endianness(), Python’da sys.byteorder; ikisi de tek satır ve arama motorları cevabı zaten sonuçların üstünde basıyor. Üstelik gerçek hata ayıklamaların çoğunda yanlış soru: bir dosyayı ya da paketi ayrıştırırken kararı biçim verir, işlemcinizin işin içinde olduğu bile yoktur. İşe yarayan beceri, belirtiyi tanımaktır.

9.1 İki belirti: saçma sayı ve 256× sayı

Gürültülü olanı kolay. 2. bölümdeki PNG genişliğini yanlış yönde okuyun, 1200 piksellik bir görüntü için 2,953,052,160 çıkar. Mütevazı bir sayı olması gereken ve milyarlarla geri dönen her alan, aksi kanıtlanana kadar ters çevrilmiş 32 bitlik bir tam sayıdır.

Pahalıya patlayan ise sessiz olanı. 00 00 01 00 baytları big-endian’da 256, little-endian’da 65,536 okunur. İkisi de akla yatkın arabellek boyutları gibi görünür. Hata fırlatılmaz, hiçbir doğrulama tetiklenmez ve değer 256 kat yanlıştır. Bu tür hatalar kod incelemesinden sağ çıkar, çünkü ekrandaki sayı makul görünür; bu da onları JSON ayrıştırmasını bozan bir UTF-8 BOM ile aynı aileye koyar: görünmez, bayt düzeyinde bir ayrıntı ve tamamen yanıltıcı bir hata yüzeyi. Öbür ucunu UTF-8 BOM sorun giderme rehberi anlatıyor.

Başında üç sıfır baytı olan küçük değerler sessizce yer değiştirenlerdir, çünkü iki okuma da aralık içinde kalır. Bir de baytları elle ters çevirdiğinizde anlamlı bir sayı çıkıyorsa, hata ayıklayıcıya hiç dokunmadan cevabınızı almışsınız demektir.

9.2 Hangi sırayla kontrol edilmeli

  1. Önce biçim belirtimine bakın. RFC 2083 PNG’nin big-endian olduğunu söyler; RFC 1952 §2.3.1 GZIP’in little-endian olduğunu. Makinenizin ne yaptığı ikisi için de konu dışıdır.
  2. İkinci olarak okuyucunuzun varsayılanına bakın. DataView.getUint32(0) big-endian, Uint32Array platform sırası, struct.pack('@I', ...) platform sırası, binary.BigEndian.Uint32 ise adında ne yazıyorsa o. Bayt sırası hatalarının çoğu eksik bir üçüncü argüman ya da eksik bir önektir, derin bir yanlış anlama değil.
  3. Platformdan en son şüphelenin. Uint32Array ya da @ ile bir dosya yazıp onu farklı bir mimariye gönderdiğinizde önem kazanır; bir bellek dökümünü belirtimle karşılaştırdığınızda da öyle. İyi tanımlanmış ve hangi sırayı kullandığını size zaten söyleyen bir biçimi okurken neredeyse hiç önemi yoktur.

Sıkça sorulan sorular

Big-endian mı yoksa little-endian mı daha iyi?

Ne big-endian ne de little-endian daha iyi. Biçim hangisini şart koşuyorsa o biçim için doğru olan odur ve seçme şansınız nadiren olur. Başarım tarafında, bu makinede big-endian bir DataView okuması little-endian olanının 1.021× katı ölçüldü; kabaca %2, yani herhangi bir tasarım kararını yönlendiremeyecek kadar küçük.

Makinemin big-endian mı little-endian mı olduğunu nasıl anlarım?

Node’da os.endianness() burada LE, Python’da sys.byteorder ise little döndürüyor. İkisi de tek satırlık. Yine de soru göründüğü kadar önemli değil: bir dosyayı ya da paketi ayrıştırdığınızda bayt sırasını biçim dikte eder, işlemcinizin söz hakkı olmaz.

DataView ile Uint32Array aynı arabellekten farklı sayılar veriyor, bu bir hata mı?

Hayır, bu belgelenmiş bir DataView davranışı. DataView.getUint32(0) varsayılan olarak big-endian, Uint32Array ise her zaman platformu izler; x86 ve Apple Silicon üzerinde bu, little-endian demek. Aynı baytlar, iki farklı kural. Üçüncü argüman olarak true geçin, DataView da aynı sonucu verir.

< eklediğimde struct düzenimin boyutu neden değişti?

Çünkü yerel hizalama için dolgu ekleyen varsayılan @ önekinden vazgeçtiniz. struct.calcsize('@ci') 8, buna karşılık struct.calcsize('=ci') ile struct.calcsize('<ci') 5. int öncesindeki üç dolgu baytı, yerel hizalamayla birlikte gitti.

Ağ bayt sırası big-endian mı little-endian mı?

Big-endian. TCP/IP başlıklarının kullandığı kural bu ve htons ile htonl işlevlerinin C’de var olma sebebi de bu. Python’da ! ve > önekleri birebir aynı baytları üretir: struct.pack('!I', 0x12345678) da struct.pack('>I', 0x12345678) da 12 34 56 78 verir.

Bayt sırası UTF-8’i etkiler mi?

Hayır. UTF-8 bir bayt akışıdır ve her kod noktası tek tek baytların sıralı dizisi olarak yazılır; dolayısıyla yeniden sıralanacak çok baytlı bir birim kalmaz. UTF-16 ile UTF-32 için bu sorun gerçekten var; tam da bu yüzden bir BOM taşırlar, 7. bölümde anlatıldığı gibi.

Tek baytlık diziler için bayt sırası işlemi gerekir mi?

Hayır. Bayt sırası yalnızca bir bayttan geniş birimler için vardır; bu yüzden Uint8Array, Int8Array ve Python bytes nesneleri bundan muaftır. Bir adım genişletin, sorun anında geri döner: new Uint16Array(two)[0] = 0x00ff bu makinede belleğe ff 00 olarak yerleşir.

Etiketler: endianness byte-order binary-data file-formats cross-platform