Binary ve Hexadecimal: Positional Notation, Bit, Byte ve Dönüşümler
Hex, her iki karakteri tam bir byte'a denk geldiği için binary'nin kısa yazımıdır: dört bit bir hex basamağıdır. Positional notation, elle dönüşüm, C# ve TypeScript literal'leri ve format'ları, renk kodu, GUID, hash ve magic number okumak, 500 GB'lık diskin Windows'ta neden 465 GB göründüğü.
Sürüm notu: Bu yazıdaki kodlar .NET 10 (C# 14), TypeScript 7.0 ve Node.js 24 ile derlenip denenmiştir; C# örneklerinin testleri xUnit v3 4.0.3 ile yazılmıştır.
Önce Tahmin Et
Bookstore'un tasarım ekibi marka rengini #3B82F6 olarak vermiş. Aynı değeri C#'ta int color = 0x3B_82_F6; diye yazıp konsola bastırıyorsun. Ekranda ne görürsün: 3B82F6 mı, 3882006 gibi bir şey mi, yoksa bambaşka bir sayı mı?
İkinci soru: mağazadan 500 GB yazan bir SSD (flash bellekli disk) aldın, Windows'a taktın. Dosya Gezgini kaç GB gösterir? Disk üreticisi yalan mı söylüyor, yoksa Windows mu yanlış hesaplıyor?
Üçüncü soru: bir kullanıcı profil fotoğrafı olarak avatar.png adında bir dosya yüklüyor. Uzantı PNG (kayıpsız sıkıştırma kullanan bir resim formatı), ama dosyanın ilk dört byte'ı 50 4B 03 04. Bu dosya gerçekte ne?
Bu yazının sonunda üç soruyu da hesap yaparak cevaplayabileceksin. Bu seri bilgisayarın her şeyi bit olarak nasıl tuttuğunu anlatıyor: önce sayı tabanları ve hex, sonra bitwise işlemler, integer ve floating point temsili, en son da text ve Unicode. Hepsinin temeli bu yazıdaki fikir: bir rakamın değeri, durduğu yerden gelir.
Positional Notation: Rakamın Değeri Durduğu Yerden Gelir
Günlük hayatta kullandığımız onluk sistemde 173 yazdığımızda aslında bir toplam yazıyoruz: 1 × 100 + 7 × 10 + 3 × 1. Her basamak bir öncekinin on katı değer taşıyor; 1, 10, 100, 1000. Buna positional notation (basamak değerli gösterim) denir ve "on" sayısının özel hiçbir yanı yoktur. Taban (radix) 2 seçersen basamaklar 1, 2, 4, 8, 16… olur ve yalnızca 0 ile 1 rakamlarını kullanırsın. Taban 16 seçersen basamaklar 1, 16, 256, 4096… olur ve on altı farklı rakama ihtiyaç duyarsın: 0–9 ve ardından A–F (A = 10, F = 15).
Aynı 173 dört tabanda şöyle yazılır:
| Taban | Yazım | Açılımı |
|---|---|---|
| 10 (decimal) | 173 | 1×100 + 7×10 + 3×1 |
| 2 (binary) | 10101101 | 128 + 32 + 8 + 4 + 1 |
| 8 (octal) | 255 | 2×64 + 5×8 + 5×1 |
| 16 (hexadecimal) | AD | 10×16 + 13×1 |
Bilgisayar binary çalışır çünkü donanımda iki durumu (gerilim var/yok) ayırt etmek güvenilir ve ucuzdur. Octal bugün neredeyse yalnızca Unix dosya izinlerinde (chmod 755) karşına çıkar. Hex ise her yerdedir: renk kodlarında, GUID'lerde (Globally Unique Identifier; dünya genelinde benzersiz olması amaçlanan 128 bitlik kimlik), hash çıktılarında, bellek adreslerinde, hata ayıklarken baktığın her byte dökümünde.
Elle Dönüşüm: Bölme ve Horner
Onluktan başka bir tabana geçmenin yolu tekrar tekrar bölmektir. 173'ü 2'ye böl, kalanı yaz, bölümle devam et: 173 → kalan 1, 86 → 0, 43 → 1, 21 → 1, 10 → 0, 5 → 1, 2 → 0, 1 → 1. Kalanlar sağdan sola dizilince 10101101 çıkar. İlk bulduğun kalan en sağdaki basamaktır, bu yüzden kod bir stack kullanıyor:
namespace Bookstore.Numbers;
public static class Radix
{
private const string Digits = "0123456789ABCDEF";
// Tekrarlı bölme: her kalan bir sonraki basamak, sağdan sola
public static string ToBase(uint value, int radix)
{
ArgumentOutOfRangeException.ThrowIfLessThan(radix, 2);
ArgumentOutOfRangeException.ThrowIfGreaterThan(radix, 16);
if (value == 0)
{
return "0";
}
var digits = new Stack<char>();
while (value > 0)
{
digits.Push(Digits[(int)(value % (uint)radix)]);
value /= (uint)radix;
}
return string.Concat(digits);
}
// Horner yöntemi: elindekini tabanla çarp, sıradaki basamağı ekle
public static uint FromBase(string text, int radix)
{
uint result = 0;
foreach (char c in text.ToUpperInvariant())
{
int digit = Digits.IndexOf(c);
if (digit < 0 || digit >= radix)
{
throw new FormatException($"'{c}' is not a base-{radix} digit.");
}
result = checked(result * (uint)radix + (uint)digit);
}
return result;
}
}
Ters yön için Horner yöntemi kullanılır: soldan başla, o ana kadar biriktirdiğin değeri tabanla çarp, yeni basamağı ekle. "AD" için: 0 × 16 + 10 = 10, sonra 10 × 16 + 13 = 173. Kuvvet hesaplamaya gerek kalmaz. checked burada önemli: "1FFFFFFFF" gibi 32 bite sığmayan bir girdi sessizce yanlış bir sayıya dönmek yerine OverflowException fırlatır. Testler hem bu davranışı hem de sonuçların framework'ün kendi dönüşümüyle aynı olduğunu doğruluyor:
[Theory]
[InlineData(173u, 2, "10101101")]
[InlineData(173u, 8, "255")]
[InlineData(173u, 16, "AD")]
[InlineData(0u, 2, "0")]
[InlineData(uint.MaxValue, 16, "FFFFFFFF")]
public void ToBase_matches_framework(uint value, int radix, string expected)
{
Assert.Equal(expected, Radix.ToBase(value, radix));
Assert.Equal(expected, Convert.ToString(value, radix).ToUpperInvariant());
}
Production'da bu sınıfı yazmayacaksın; framework hepsini yapıyor. Ama elle bir kez yapınca hex'e baktığında sayıyı görmeye başlarsın.
Bit, Nibble, Byte, Word: Hex Neden Byte'ın Kısa Yazımı
Bir bit tek bir binary basamaktır. Dört bit bir nibble'dır ve 0000 ile 1111 arasında, yani 0 ile 15 arasında değer alır. Bu aralık tam olarak tek bir hex basamağına denk gelir. Sekiz bit bir byte eder: iki nibble, yani iki hex basamağı, 0x00 ile 0xFF (0–255) arası.
Hex'in asıl gücü bu: binary'den hex'e geçmek için hiçbir hesap gerekmez, bitleri sağdan dörder dörder gruplayıp her grubu bir hex rakamına çevirirsin. 1010 1101 → A D. Ters yönde 0x3B82F6 → 0011 1011 1000 0010 1111 0110. 24 bitlik bir sayıyı binary yazmak 24 karakter tutarken hex 6 karakter tutar ve her iki karakter tam bir byte'ı gösterir. Decimal'de byte sınırları görünmez: 3900150 sayısına bakıp hangi byte'ın ne olduğunu söyleyemezsin.
Word ise mimariye göre değişen bir terim. Windows API'deki tip adları 16 bitlik dönemden kaldığı için orada WORD 16 bit, DWORD 32 bit, QWORD 64 bittir. Bir CPU'nun "doğal" word boyutu bugün çoğunlukla 64 bittir. Bir dokümanda "word" gördüğünde hangi anlamda kullanıldığına bak.
C#'ta Binary ve Hex: Literal, Format ve Dönüşüm
C# 7.0'dan beri sayıları kaynak kodda doğrudan binary yazabilir, uzun sayıları _ ile bölebilirsin. Ayırıcı yalnızca okunabilirlik içindir, değeri değiştirmez:
int flags = 0b1010_1101;
int color = 0x3B_82_F6;
long population = 8_100_000_000;
Console.WriteLine(flags); // 173
Console.WriteLine(color); // 3900150
Console.WriteLine(population); // 8100000000
Console.WriteLine(Convert.ToString(flags, 2)); // 10101101
Console.WriteLine(Convert.ToString(flags, 8)); // 255
Console.WriteLine(Convert.ToString(flags, 16)); // ad
Console.WriteLine(flags.ToString("X")); // AD
Console.WriteLine(flags.ToString("X8")); // 000000AD
Console.WriteLine(flags.ToString("B16")); // 0000000010101101
Console.WriteLine(Convert.ToString(-1, 2)); // 11111111111111111111111111111111
İlk sorunun cevabı burada: 0x3B_82_F6 bir sayı, ekrana decimal karşılığı 3900150 basılır. Kaynak kodda hangi tabanı kullandığın derleyiciye nasıl yazdığını söyler; bellekte hep aynı bitler durur.
Ayrıntılara dikkat: Convert.ToString(x, 16) küçük harf üretir, "X" format'ı büyük harf, "x" küçük harf. "X8" sayıyı en az 8 basamağa tamamlar; byte yazarken "X2" kullanmazsan 10 değeri "A", 255 değeri "FF" olur ve yan yana yazdığın byte'lar artık ayrıştırılamaz. "B" format'ı .NET 8 ile geldi ve binary yazımı Convert.ToString'den daha kısa hâle getirdi. Son satırdaki 32 tane 1 ise serinin üçüncü yazısının konusu: -1'in bit düzeyindeki karşılığı.
Byte dizileri için Convert.ToHexString ve Convert.FromHexString var (.NET 5+), küçük harfli versiyon Convert.ToHexStringLower ise .NET 9 ile geldi. Renk kodunu RGB'ye (Red, Green, Blue; kırmızı, yeşil ve mavi kanalların 0–255 arası değerleri) çevirmek bunlarla tek satır:
public readonly record struct Rgb(byte R, byte G, byte B)
{
public static Rgb FromHex(string hex)
{
ReadOnlySpan<char> digits = hex.AsSpan().TrimStart('#');
if (digits.Length != 6)
{
throw new FormatException("Expected 6 hex digits.");
}
byte[] bytes = Convert.FromHexString(digits);
return new Rgb(bytes[0], bytes[1], bytes[2]);
}
public string ToHex() => $"#{R:X2}{G:X2}{B:X2}";
}
Rgb blue = Rgb.FromHex("#3B82F6");
Console.WriteLine(blue); // Rgb { R = 59, G = 130, B = 246 }
Console.WriteLine(blue.ToHex()); // #3B82F6
3B = 3 × 16 + 11 = 59, 82 = 130, F6 = 246. CSS renk kodu üç byte'ın yan yana yazılmasından ibaret.
TypeScript'te Aynı İşler
JavaScript ve TypeScript'te de 0b, 0o, 0x prefix'leri ve _ ayırıcısı var. Farklar dönüşüm fonksiyonlarında:
const flags = 0b1010_1101;
const color = 0x3b_82_f6;
console.log(flags, color); // 173 3900150
console.log(flags.toString(2)); // 10101101
console.log(flags.toString(16)); // ad
console.log(flags.toString(16).toUpperCase().padStart(8, "0")); // 000000AD
console.log(parseInt("ad", 16), parseInt("10101101", 2)); // 173 173
console.log(Number("0xAD"), Number("0b10101101")); // 173 173
console.log(parseInt("0x1F", 16), parseInt("1F", 10), parseInt("1G", 16), Number("1G"));
// 31 1 1 NaN
console.log((-1).toString(2)); // -1
console.log((-1 >>> 0).toString(2)); // 11111111111111111111111111111111
function hexToRgb(hex: string): [number, number, number] {
const digits = hex.replace(/^#/, "");
return [0, 2, 4].map((i) => parseInt(digits.slice(i, i + 2), 16)) as [number, number, number];
}
console.log(hexToRgb("#3B82F6")); // [ 59, 130, 246 ]
İki satır dikkat istiyor. parseInt geçersiz bir karaktere gelince hata vermez, o ana kadar okuduğunu döndürür: parseInt("1G", 16) 1 verir, Number("1G") ise NaN. Kullanıcıdan gelen bir hex değeri doğrularken parseInt'e güvenme, önce /^[0-9a-f]+$/i gibi bir regex ile kontrol et. İkincisi, (-1).toString(2) C#'taki gibi 32 tane 1 değil -1 verir; JavaScript sayıyı işaretiyle yazar. Bit düzeyini görmek için >>> 0 hilesi gerekiyor, bunun neden çalıştığını sonraki yazıda göreceğiz.
Hex'i Okumak: GUID, Hash ve Magic Number
Hex okumayı öğrenince bazı değerler kendi yapılarını göstermeye başlar. Bookstore'da sipariş ID'leri için .NET 9 ile gelen Guid.CreateVersion7() kullanılıyor:
Guid orderId = Guid.CreateVersion7();
Console.WriteLine(orderId); // 01a1d759-f419-717a-bfda-090b4a0e002c
Console.WriteLine(orderId.Version); // 7
Console.WriteLine(orderId.ToString("N").Length); // 32
Console.WriteLine(orderId.ToByteArray().Length); // 16
32 hex karakter = 16 byte = 128 bit. Üçüncü grubun ilk karakteri (717a'daki 7) GUID'in version'ı. UUID (Universally Unique Identifier; GUID'in standartlardaki adı) değerlerini tanımlayan RFC'de (Request for Comments; internet standartlarının yayımlandığı doküman serisi), 9562 numaralı dokümanda, version 7'nin ilk 48 biti milisaniye cinsinden zaman damgasıdır, bu yüzden ardışık oluşturulan ID'ler sıralı gelir. Dördüncü grubun ilk karakteri (b = 1011) variant bitlerini taşır. Loglarda bir GUID'in version 4 mü 7 mi olduğunu tek bakışta görebilirsin.
Hash çıktıları da hex ile okunur. SHA (Secure Hash Algorithm; standartlaşmış hash fonksiyonu ailesi) ailesinden SHA-256 her zaman 32 byte üretir, hex'te 64 karakter:
byte[] hash = SHA256.HashData("Bookstore"u8);
Console.WriteLine(hash.Length); // 32
Console.WriteLine(Convert.ToHexString(hash));
// A77D2912A0F30BED2BED84F2EF923ECDFB7E3AF45FFD1928282C2E12DF5591FC
Console.WriteLine(Convert.ToHexStringLower(hash));
// a77d2912a0f30bed2bed84f2ef923ecdfb7e3af45ffd1928282c2e12df5591fc
Bir dosyanın türü de çoğu zaman ilk birkaç byte'ında yazar. Bu sabit başlangıçlara magic number (dosya imzası) denir. Git Bash'te veya Linux'ta xxd, PowerShell'de Format-Hex ile bakabilirsin:
$ xxd -l 16 cover.png
00000000: 8950 4e47 0d0a 1a0a 0000 000d 4948 4452 .PNG........IHDR
$ xxd -l 16 invoice.pdf
00000000: 2550 4446 2d31 2e34 0a25 d3eb e9e1 0a31 %PDF-1.4.%.....1
$ xxd -l 16 avatar.png
00000000: 504b 0304 0a00 0000 0000 6917 6e5d 8c39 PK........i.n].9
Soldaki sütun offset, ortası hex byte'lar, sağdaki sütun aynı byte'ların yazdırılabilir ASCII (İngilizce harfleri, rakamları ve temel işaretleri 0–127 arasına yerleştiren 7 bitlik karakter tablosu) karşılığı (yazdırılamayanlar nokta). PNG imzası bilerek seçilmiş: ilk byte 0x89 en üst biti 1 olan bir değer, ardından okunabilir PNG, sonra satır sonu karakterleri. Dosya bir yerde 7 bitlik bir kanaldan geçip bozulduysa ya da satır sonları çevrildiyse imza artık tutmaz. PDF düz metin olarak %PDF- ile başlar. 50 4B 03 04, yani PK, ZIP (yaygın bir arşiv ve sıkıştırma formatı) imzası; .docx, .xlsx ve .nupkg dosyaları da içte birer ZIP arşivi olduğu için aynı byte'larla başlar. Üçüncü sorunun cevabı: avatar.png aslında bir ZIP arşivi.
public static class FileSignature
{
private static readonly (string Kind, byte[] Magic)[] Known =
[
("PNG", [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]),
("PDF", "%PDF-"u8.ToArray()),
("ZIP", [0x50, 0x4B, 0x03, 0x04]),
];
public static string Detect(ReadOnlySpan<byte> header)
{
foreach ((string kind, byte[] magic) in Known)
{
if (header.StartsWith(magic))
{
return kind;
}
}
return "unknown";
}
}
Klasördeki dosyaların ilk sekiz byte'ını okuyup bu method'a verdiğimde çıktı:
avatar.png 504B03040A000000 ZIP
cover.png 89504E470D0A1A0A PNG
export.zip 504B03040A000000 ZIP
invoice.pdf 255044462D312E34 PDF
KB mi KiB mi: 500 GB'lık Disk Neden 465 GB Görünür
SI (Uluslararası Birimler Sistemi) önekleri ondalıktır: kilo = 1000, mega = 1000², giga = 1000³. Bilgisayarcılar ise bellek adreslemesi ikinin kuvvetleriyle çalıştığı için uzun süre "kilo" deyip 1024'ü kastettiler. Karışıklığı gidermek için IEC (International Electrotechnical Commission; elektrik ve elektronik standartlarını yayımlayan kuruluş) 1998 sonunda binary önekleri tanımladı: kibi (KiB = 1024), mebi (MiB = 1024²), gibi (GiB = 1024³). Bugün bu tanımlar IEC 80000-13 standardında duruyor.
long advertised = 500_000_000_000;
Console.WriteLine((advertised / 1e9).ToString("F2", CultureInfo.InvariantCulture)); // 500.00
Console.WriteLine((advertised / Math.Pow(1024, 3)).ToString("F2", CultureInfo.InvariantCulture)); // 465.66
Disk üreticisi 500 milyar byte satıyor ve doğru olarak 500 GB yazıyor. Windows Dosya Gezgini aynı sayıyı 1024³'e bölüyor ama birim olarak yine "GB" yazıyor; ekranda 465 GB görürsün. macOS 10.6'dan beri ondalık birim kullandığı için aynı diski 500 GB gösterir. Linux'ta df -h 1024'lük, df -H 1000'lik birim kullanır. Kaybolan byte yok, yalnızca iki farklı birim aynı adla gösteriliyor. Ağ hızları (Mbps) ise hep ondalık ve bit cinsindendir; 100 Mbps bir bağlantı saniyede en fazla 12,5 milyon byte taşır.
Üretimde Dikkat
Byte'ları loglarken tek bir biçimde karar kıl. Convert.ToHexString bitişik büyük harf, BitConverter.ToString ise A7-7D-29 gibi tireli bir biçim üretir. Aynı sistemin iki parçası farklı biçim yazarsa loglarda arama yaparken eşleşme bulamazsın.
Dosya yüklemelerinde türü uzantıdan veya istemcinin gönderdiği Content-Type'tan değil, ilk byte'lardan kontrol et. Ama magic number yalnızca bir ön kontrol: hem geçerli bir PNG hem başka bir format olarak yorumlanabilen dosyalar yapılabiliyor. Gerçek doğrulama, dosyayı amaçlanan kütüphaneyle açıp yeniden kaydetmektir; bu konu Web Güvenliği (OWASP Top 10) serisinin alanında.
Hash karşılaştırırken string değil byte karşılaştır. Linux'taki sha256sum küçük harf, PowerShell'in Get-FileHash'i büyük harf hex üretir; string olarak karşılaştırırsan aynı hash'e "farklı" dersin. Hex'i Convert.FromHexString ile byte'a çevirip CryptographicOperations.FixedTimeEquals kullan; bu method karşılaştırmayı her durumda aynı sürede bitirir, böylece süreden bilgi sızmaz:
string published = "a77d2912a0f30bed2bed84f2ef923ecdfb7e3af45ffd1928282c2e12df5591fc";
Console.WriteLine(Convert.ToHexString(hash) == published); // False
Console.WriteLine(Convert.ToHexString(hash).Equals(published, StringComparison.OrdinalIgnoreCase)); // True
Console.WriteLine(CryptographicOperations.FixedTimeEquals(hash, Convert.FromHexString(published))); // True
Kapasite gösterirken birimi açıkça yaz. Monitoring panelinde disk "465 GB" diye görünürken satın alma ekibinin elindeki fatura 500 GB diyorsa, birim yazmamış olmak gereksiz bir destek talebi demektir. Kendi formatlama kodunda 1024'e bölüyorsan etiketi GiB/MiB koy.
Sık Düşülen Tuzaklar
Hex string'leri string olarak karşılaştırmak. Configuration dosyasında "0A", kullanıcı girişinde "a" var; ikisi de 10 demek ama string olarak eşit değiller:
string fromConfig = "0A";
string fromUser = "a";
Console.WriteLine(fromConfig == fromUser); // False
Console.WriteLine(Convert.ToInt32(fromConfig, 16) == Convert.ToInt32(fromUser, 16)); // True
Ya iki tarafı da sayıya çevirip karşılaştır ya da tek bir kanonik biçime ("X2" gibi sabit uzunluk, sabit harf) getirdikten sonra sakla. Renk kodları, MAC (ağ kartının donanım adresi) değerleri ve hash'lerde bu hata sık görülür.
Byte'ları basamak sayısı sabitlemeden birleştirmek. $"{r:X}{g:X}{b:X}" RGB(10, 130, 246) için A82F6 üretir, beş karakter. Geri okunurken hangi byte'ın nerede bittiği belli değildir. Byte başına her zaman X2.
JavaScript'te başında sıfır olan sayılar. Eski JavaScript'te 010 octal kabul edilir ve 8 değerini taşır. Strict mode'da ve module olarak yüklenen dosyalarda bu yazım hata verir:
console.log(010);
// SyntaxError: Octal literals are not allowed in strict mode.
TypeScript de derleme sırasında yakalar: error TS1121: Octal literals are not allowed. Use the syntax '0o10'. Ama tarayıcıda çalışan eski, module olmayan bir script'te 010 hâlâ sessizce 8 olur. Posta kodu, telefon gibi başında sıfır olan değerleri sayı olarak değil string olarak tut; bunlar zaten aritmetik yapacağın sayılar değil.
Birim etiketine güvenmek. "GB" gördüğün bir değerin 1000³ mü 1024³ mü olduğunu kaynağına bakmadan bilemezsin. 1 TB'lık diskte fark yaklaşık 69 GB, yani yüzde 7.
Küçük Bir Deneme
★☆☆ 2026 sayısını elle binary'ye ve hex'e çevir: önce bölme yöntemiyle binary'yi bul, sonra dörder bit gruplayarak hex'e geç. Sonucu Convert.ToString(2026, 2) ve (2026).toString(16) ile kontrol et.
★★☆ FileSignature'a GIF (GIF87a veya GIF89a) ve JPEG (FF D8 FF) imzalarını ekle, her biri için bir test yaz. Elindeki bir JPEG'in ilk byte'larına xxd veya Format-Hex ile bakıp testi gerçek dosyayla doğrula.
★★★ C#'ta küçük bir xxd yaz: her satırda 16 byte, solda 8 basamaklı hex offset, ortada ikişer byte gruplanmış hex, sağda yazdırılabilir ASCII karakterler (0x20–0x7E arası; diğerleri nokta). Çıktını cover.png için gerçek xxd çıktısıyla satır satır karşılaştır.
Bu yazıda bitleri okumayı öğrendik. Serinin sonraki yazısı "Bitwise İşlemler" bitleri tek tek değiştirmeyi anlatıyor: Bookstore kullanıcı izinlerini tek bir integer içinde tutacak, BitOperations ile kaç izin olduğunu sayacak ve JavaScript'in büyük sayılarda bitwise operatörleri nasıl sessizce bozduğunu göreceğiz.