Bir C++ fonksiyonunu yazıp Build dediğinde, aslında iki ayrı “dünya” ortaya çıkar: biri bizim okuduğumuz kaynak kod; diğeri CPU’nun çalıştırdığı bayt dizisi. Bu yazıda, Visual Studio 2026 akışını baz alarak, bir fonksiyonun assembly temsiline ve bellek yerleşimine “gerçekten anlaşılır” şekilde bakacağız.
- Bu yazıda ne öğreneceksin?
- Visual Studio 2026 ön koşulları
- Örnek fonksiyon: derleyiciyi görünür kılmak
- Derleme argümanları: Debug vs Release
- VS 2026: Disassembly penceresi ve .asm çıktısı
- Windows x64 calling convention (register düzeni)
- Assembly örnekleri: prolog/epilog neden var?
- .text, RVA/VA ve ASLR: memory layout’u anlamak
- dumpbin ile doğrulama
- “Benim çıktım neden farklı?”
- SSS
- Kaynaklar
Bu yazıda ne öğreneceksin?
- Bir C++ fonksiyonu derlenince .text içinde nasıl temsil edilir?
- Assembly komutları ile makine kodu (baytlar) arasındaki ilişki.
- Prolog/Epilog nedir, ne zaman görünür, ne zaman kaybolur?
- Debug ve Release arasındaki farkı derleme argümanları ile kontrol etmek.
- Visual Studio 2026’da Disassembly penceresi, assembly listing (.asm) ve temel doğrulama akışı.
Visual Studio 2026 ön koşulları
Bu makale “Visual Studio 2026 kullanıyorsun” varsayımıyla yazıldı. C++ tarafında en pratik kurulum,
Desktop development with C++ workload’ünün yüklü olmasıdır. Böylece MSVC toolset, Windows SDK,
dumpbin ve Developer Command Prompt/PowerShell gibi parçalar hazır gelir.
Bu yazı x64 odaklıdır. x86’da calling convention ve register/stack düzeni farklıdır.
Örnek fonksiyon: derleyiciyi görünür kılmak
Derleyiciler çok agresif optimizasyon yapabilir: fonksiyonu inline eder, bazı hesapları yok sayar,
hatta “gerekli değil” diye fonksiyonun kendisini bile görünmez hale getirir.
Bu yüzden iki küçük “kilit” kullanacağız:
__declspec(noinline) ve volatile.
#include <cstdint>
__declspec(noinline)
int add_and_scale(int a, int b)
{
volatile int sum = a + b;
int scaled = sum * 3;
return scaled - 5;
}
Derleme argümanları: Debug vs Release
“Aynı C++ kodu neden farklı assembly üretiyor?” sorusunun en büyük cevabı: derleme ayarları. Bu yüzden burada işi “derle-topla” mantığıyla netleştiriyoruz: Debug’da öğren, Release’de gerçeği gör.
| Hedef | Öneri | Beklenen sonuç |
|---|---|---|
| Debug (okunabilir) | /Od, /Zi, /Ob0, /FAs |
Daha uzun ama takip etmesi kolay disassembly; stack frame sık görünür. |
| Release (optimize) | /O2, /Ob2, (opsiyonel) /GL + /LTCG |
Daha kısa ve agresif kod; inline artar, bazı fonksiyonlar “kaybolur”. |
:: Debug odaklı: optimize kapalı, inline kapalı, asm listing açık
cl /std:c++20 /Od /Zi /Ob0 /FAs /Fa:out_debug.asm main.cpp
:: Release odaklı: optimize açık, inline agresif, asm listing açık
cl /std:c++20 /O2 /Ob2 /FAs /Fa:out_release.asm main.cpp
VS 2026: Disassembly penceresi ve .asm çıktısı
Visual Studio 2026’da iki pratik yol var:
-
Debugger ile anlık disassembly:
Debug → Windows → Disassembly -
Derleme ile .asm listing üretmek:
Project Properties altında Assembly Output / Listing seçenekleri veya doğrudan
/FAs,/Fa.
Önce Debug ayarlarıyla Disassembly penceresinde “hikâyeyi” oku. Sonra Release’e geçip “derleyici bunu nasıl kısaltmış?” diye kıyasla. Böyle yaptığında disassembly bir anda daha anlaşılır hale gelir.
Windows x64 calling convention (register düzeni)
x64 Windows tarafında temel kural basittir:
- 1. parametre:
RCX - 2. parametre:
RDX - 3. parametre:
R8 - 4. parametre:
R9 - Dönüş değeri:
RAX
Bizim fonksiyon için bu şu demek:
a çoğunlukla ECX, b çoğunlukla EDX,
sonuç EAX üzerinden döner.
Assembly örnekleri: prolog/epilog neden var?
Prolog/Epilog dediğimiz kısım, fonksiyonun stack’te yer ayırması ve işi bitince toparlamasıdır. Debug’da sık görürsün; Release’de (özellikle “leaf function” ise) bazen hiç görünmez.
add_and_scale:
lea eax, [rcx + rdx] ; eax = a + b
imul eax, eax, 3 ; eax *= 3
sub eax, 5 ; eax -= 5
ret
add_and_scale:
sub rsp, 40h ; stack alanı aç
mov dword ptr [rsp+20h], ecx ; sum = a
add dword ptr [rsp+20h], edx ; sum += b
mov eax, dword ptr [rsp+20h] ; eax = sum
imul eax, eax, 3
sub eax, 5
add rsp, 40h
ret
.text, RVA/VA ve ASLR: memory layout’u anlamak
“Memory layout” dediğimiz şeyin çok pratik bir özeti var: Fonksiyonun kodu PE içinde genellikle .text bölümündedir ve burada bir RVA ile temsil edilir. Çalışma anında ise Windows loader bu bölümü memory’ye map eder ve adres şöyle oluşur:
VA = ModuleBase + RVA
ASLR açıksa ModuleBase değişebilir; bu yüzden “debugger’daki adres” her çalıştırmada oynayabilir.
Ama aynı build için RVA çoğunlukla sabit kalır.
dumpbin ile doğrulama
Visual Studio 2026 tarafında “Developer Command Prompt/PowerShell” açıp dumpbin ile PE içini hızlıca kontrol edebilirsin.
Bu, “dosya dünyası”ndan bakmanın en kısa yoludur.
dumpbin /HEADERS your.exe
dumpbin /DISASM your.exe
“Benim çıktım neden farklı?”
Aynı C++ koduna bakıp “bende farklı çıktı” görmek çok normal. En sık sebepler:
- Optimizasyon:
/Odile/O2arasında uçurum var. - Inline:
/Ob2ile bazı fonksiyonlar çağrı olarak görünmez (içeri gömülür). - LTCG:
/GL+/LTCGlink aşamasında bile kodu değiştirir. - Toolset/derleyici sürümü: VS 2026’da toolset seçimi bile çıktıyı etkileyebilir.
- “Gözlemlenebilirlik”: Side-effect yoksa derleyici hesapları sadeleştirir.
SSS
1) Disassembly penceresinde fonksiyonumu bulamıyorum. Neden?
Büyük ihtimalle inline olmuştur veya optimizasyon sonucu “gömülmüştür”.
__declspec(noinline) kullanıp Debug’da /Ob0 ile tekrar dene.
2) Debug’da stack frame görüyorum, Release’de yok. Bu normal mi?
Evet. Release daha agresif optimize eder ve register kullanımını artırır. Özellikle “başka fonksiyon çağırmayan” küçük fonksiyonlar (leaf) stack kullanmadan bitebilir.
3) Adresler her çalıştırmada değişiyor. Bu bir problem mi?
Genelde ASLR yüzünden olur. VA değişebilir. Aynı build için RVA çoğunlukla sabit kalır. Bu yüzden “RVA mantığı” ile düşünmek daha sağlıklıdır.