Giriş
Uzay araçlarında veya otomotiv sistemlerinde kullanılanlar gibi güvenlik açısından kritik yazılımlar, son derece güvenilir kod gerektirir. Bu sorunu çözmek amacıyla NASA’nın Jet Propulsion Laboratory (JPL), 2006 yılında “
” “Power of 10” kodlama kurallarını oluşturdu. Bu özlü kılavuzlar, analiz edilmesi zor olan karmaşık C yapılarını ortadan kaldırarak kodun basit, doğrulanabilir ve güvenilir kalmasını sağlar.
Günümüzde, Aikido kod kalitesi gibi araçlar, her yeni çekme isteğinde on kuralın tamamının uygulanmasını sağlamak üzere özel denetimlerle yapılandırılabilir. Bu makalede, her bir kuralı ve bunun neden önemli olduğunu açıklıyor, hem yanlış hem de doğru yaklaşımları gösteren kod örnekleri sunuyoruz.
Bu kuralların önemi nedir?
NASA’nın kuralları, uzay aracı kontrolü ve uçuş yazılımı gibi görev açısından kritik uygulamalar için hayati önem taşıyan okunabilirlik, analiz edilebilirlik ve güvenilirlik unsurlarına odaklanmaktadır. Bu kılavuzlar, anlaşılması zor C yapılarını yasaklayarak ve koruyucu kontroller uygulayarak kodun incelenmesini ve doğruluğunun kanıtlanmasını kolaylaştırır. Statik analiz araçlarının sıklıkla gözden kaçırdığı kalıpları ele alarak MISRA C gibi standartları tamamlayıcı bir rol oynarlar. Örneğin, özyineleme ve dinamik bellek kullanımından kaçınmak kaynak kullanımını öngörülebilir tutarken, dönüş değeri denetimlerini zorunlu kılmak derleme aşamasında birçok hatanın yakalanmasına yardımcı olur.
Aslında, NASA’nın Toyota’nın elektronik gaz kelebeği yazılımı gibi kitlesel pazara yönelik bir gömülü sistem üzerinde yaptığı araştırmada yüzlerce kural ihlali tespit edildi. Bu durum, gerçek hayattaki projelerin sıklıkla bu kuralların önlemeye yönelik tasarlandığı aynı sorunlarla karşılaştığını göstermektedir. Listedeki her kural, yaygın bir hata türünü (kontrolsüz döngüler, null işaretçiye erişim, görünmez yan etkiler vb.) önler. Bu kuralları göz ardı etmek, zor fark edilen çalışma zamanı hatalarına, güvenlik açıklarına veya belirsiz davranışlara yol açabilir. Buna karşılık, on kuralın tümüne uymak, statik doğrulamayı çok daha yönetilebilir hale getirir.
Otomatikleştirilmiş araçlar önemlidir. Kod kalitesi platformları, yasaklanmış yapıları veya kalıpları tespit edecek şekilde yapılandırılabilir. Bu kurallar her pull isteğinde otomatik olarak çalışır ve kod birleştirilmeden önce sorunları tespit eder.
Bağlam ile kurallar arasında bağlantı kurma
Tek tek kurallara geçmeden önce, bağlamı anlamak önemlidir:
- Hedef Dil: NASA’nın “Power of 10” kuralları, kapsamlı araç desteğine (derleyiciler, analizörler, hata ayıklayıcılar) sahip olmakla birlikte tanımlanmamış davranışlarıyla da bilinen C dili için yazılmıştır. Bu kurallar, çöp toplama veya gelişmiş bellek yönetimi olmadığını varsayar. Yalnızca basit ve iyi yapılandırılmış C dili kullanılarak, statik analizden yararlanarak program özelliklerini kanıtlamak mümkündür.
- Statik Analiz: Otomatik kontrolleri kolaylaştırmak için pek çok kural mevcuttur. Örneğin, özyinelemeyi yasaklamak (Kural 1) ve döngü sınırlarını zorunlu kılmak (Kural 2), araçların herhangi bir fonksiyonun kaç yineleme yapabileceğini veya yığın kullanımının ne kadar olabileceğini kanıtlamasını sağlar. Benzer şekilde, karmaşık makroları yasaklamak ve işaretçileri sınırlamak (Kurallar 8–9), kod kalıplarını ön işleyici sihrinde veya çoklu dolaylı yönlendirmelerde gizli kalmak yerine açık hale getirir.
- Geliştirme İş Akışı: Modern DevSecOps iş akışlarında bu kurallar, sürekli entegrasyon (CI) kontrollerinin bir parçası haline gelir. Kod kalitesi araçları, GitHub, GitLab veya Bitbucket ile entegre olarak her bir çekme isteğini inceleyebilir ve hem basit sorunları hem de daha karmaşık kalıpları tespit edebilir. “Goto veya özyinelemeli işlev çağrılarının her kullanımını işaretle” veya “her döngünün sabit bir sınırı olduğundan emin ol” gibi her bir NASA kılavuzu için özel bir kural oluşturabilirsiniz. Bir kez yapılandırıldığında, bu kurallar gelecekteki her kod taramasında otomatik olarak uygulanır, ihlalleri erken aşamada tespit eder ve bunların nasıl düzeltileceğine dair rehberlik sağlar.
Kısacası, NASA’nın 10 kuralı , savunmacı ve analiz edilebilir C programlama anlayışını yansıtmaktadır. Aşağıda her bir kuralı sıralıyor, iyi ve kötü kod örneklerini gösteriyor ve kuralın neden var olduğunu ve hangi riskleri azalttığını açıklıyoruz.
NASA’nın 10 kodlama kuralı
1. Karmaşık kontrol akışından kaçının.
goto, setjmp veya longjmp komutlarını kullanmayın; kodun hiçbir bölümünde özyinelemeli işlevler yazmaktan kaçının.
❌ Kurallarauymayan örnek
// Non-compliant: recursive function call
int factorial(int n) {
if (n <= 1) return 1;
return n * factorial(n-1); // recursion (direct)
}✅ Kurallara uygun örnek (döngü kullanılır)
// Compliant: uses an explicit loop instead of recursion
int factorial(int n) {
int result = 1;
for (int i = n; i > 1; --i) {
result *= i;
}
return result;
}Bunun önemi: Özyineleme ve goto komutları, mantıksal olarak değerlendirilmesi zor olan doğrusal olmayan kontrol akışları oluşturur. Özyinelemeli çağrılar, çağrı grafiğini döngüsel hale getirir ve yığın derinliğini sınırsız kılar; goto komutu ise spagetti koduna yol açar. Basit döngüler ve doğrusal kod kullanıldığında, statik analiz aracı yığın kullanımını ve program yollarını kolayca doğrulayabilir. Bu kuralın ihlali, beklenmedik yığın taşmalarına veya manuel olarak incelenmesi zor mantık yollarına yol açabilir.
2. Döngülerin sabit üst sınırları olmalıdır.
Her döngünün derleme sırasında doğrulanabilir bir sınırı olmalıdır.
❌ Uygun olmayan örnek (sınırsız döngü):
// Non-compliant: loop with dynamic or unknown bound
int i = 0;
while (array[i] != 0) {
doSomething(array[i]);
i++;
}✅ Uygun örnek (sabit sınırlı döngü):
// Compliant: loop with explicit fixed upper bound and assert
#define MAX_LEN 100
for (int i = 0; i < MAX_LEN; i++) {
if (array[i] == 0) break;
doSomething(array[i]);
}Bunun önemi: Sınırsız döngüler sonsuza kadar çalışabilir veya kaynak sınırlarını aşabilir. Sabit bir sınır olduğunda, araçlar maksimum yineleme sayısını statik olarak kanıtlayabilir. Güvenlik açısından kritik sistemlerde, sınırın olmaması döngünün kontrolden çıkmasına neden olabilir. Açık bir sınır (veya statik bir dizi boyutu) uygulayarak, döngülerin öngörülebilir bir şekilde sonlanmasını sağlarız. Bu kural olmadan, döngü mantığındaki bir hata dağıtım aşamasına kadar fark edilmeyebilir (örneğin, sonsuz döngüye neden olan bir "off-by-one" hatası).
3. Başlatma işleminden sonra dinamik bellek kullanılmaz.
Çalışan kodda malloc/free veya herhangi bir yığın kullanımı yapmaktan kaçının; yalnızca sabit veya yığın tahsisi kullanın.
❌ Uygun olmayan örnek (malloc kullanıyor)
// Non-compliant: dynamic allocation inside the code
void storeData(int size) {
int *buffer = malloc(size * sizeof(int));
if (buffer == NULL) return;
// ... use buffer ...
free(buffer);
}✅ Uygun örnek (statik ayırma)
// Compliant: fixed-size array on stack or global
#define MAX_SIZE 256
void storeData() {
int buffer[MAX_SIZE];
// ... use buffer without dynamic alloc ...
}Bunun önemi: Çalışma zamanı sırasında dinamik bellek tahsisi, özellikle uzay araçları veya gömülü denetleyiciler gibi sınırlı kaynaklara sahip sistemlerde öngörülemeyen davranışlara, bellek parçalanmasına veya tahsis hatalarına yol açabilir. Görev sırasında malloc veya free işlevleri başarısız olursa, yazılım çökebilir veya öngörülemeyen şekilde davranabilir. Yalnızca sabit boyutlu veya yığın bellek tahsisi kullanmak, deterministik davranışı garanti eder, doğrulamayı basitleştirir ve çalışma zamanı bellek sızıntılarını önler.
4. İşlevler tek sayfaya sığar (~60 satır).
Her fonksiyonu kısa tutun (yaklaşık ≤ 60 satır).
❌ Kurallara uymayan örnek
// Non-compliant: hundreds of lines in one function (not shown)
void processAllData() {
// ... imagine 100+ lines of code doing many tasks ...
}✅ Uygun örnek (modüler işlevler)
// Compliant: break the task into clear sub-functions
void processAllData() {
preprocessData();
analyzeData();
postprocessData();
}
void preprocessData() { /* ... */ }
void analyzeData() { /* ... */ }
void postprocessData(){ /* ... */ }Bunun önemi: Aşırı uzun fonksiyonlar, bir birim olarak anlaşılması, test edilmesi ve doğrulanması zordur. Her fonksiyonu tek bir kavramsal görevle (ve bir basılı sayfa sınırları içinde) sınırlayarak, kod incelemeleri ve statik kontroller daha yönetilebilir hale gelir. Bir fonksiyon çok fazla satıra yayılırsa, mantıksal hatalar veya sınır koşulları gözden kaçabilir. Kodu daha küçük fonksiyonlara bölmek, netliği artırır ve diğer kuralların (fonksiyon başına doğrulama yoğunluğu ve dönüş kontrolleri gibi) uygulanmasını kolaylaştırır.
5. Her fonksiyonda en az iki assert ifadesi kullanın.
Her fonksiyon, koruyucu kontroller yapmalıdır.
❌ Uygun olmayan örnek (doğrulama yok):
int get_element(int *array, size_t size, size_t index) {
return array[index];
}✅ Uygun örnek (doğrulama ifadeleriyle birlikte):
int get_element(int *array, size_t size, size_t index) {
assert(array != NULL); // Assertion 1: pointer validity
assert(index < size); // Assertion 2: bounds check
if (array == NULL) return -1; // Recovery: return error
if (index >= size) return -1; // Recovery: return error
return array[index];
}Bunun önemi: Assert'ler, geçersiz koşullara karşı ilk savunma hattıdır. NASA, daha yüksek bir assert yoğunluğunun hataları yakalama olasılığını önemli ölçüde artırdığını tespit etmiştir. Her fonksiyonda en az iki assert (ön koşulları, sınırları ve değişmezleri kontrol eden) kullanıldığında, kod kendi varsayımlarını belgelendirir ve test sırasında anomalileri anında işaretler. Assert'ler olmadan, beklenmedik bir değer sessizce yayılabilir ve hatanın kaynağından çok uzak bir yerde arızaya neden olabilir.
6. Verileri mümkün olduğunca dar bir kapsamda tanımlayın.
Değişkenleri mümkün olduğunca yerel tutun; küresel değişkenlerden kaçının.
❌ Kurallara uymayan örnek (küresel veriler):
// Non-compliant: global variable visible everywhere
int statusFlag;
void setStatus(int f) {
statusFlag = f;
}✅ Uygun örnek (yerel kapsam):
// Compliant: local variable inside function
void setStatus(int f) {
int statusFlag = f;
// ... use statusFlag only here ...
}Bunun önemi: Kapsamı en aza indirmek, bağımlılığı ve istenmeyen etkileşimleri azaltır. Bir değişken yalnızca bir işlev içinde gerekliyse, onu global olarak tanımlamak, diğer kodların onu beklenmedik bir şekilde değiştirme riskini doğurur. Verileri yerel tutarak, her işlev daha bağımsız ve yan etkilerden arındırılmış hale gelir; bu da analiz ve test işlemlerini basitleştirir. Kurallara aykırı davranışlar (global durumun yeniden kullanılması gibi), takma ad kullanımı veya beklenmedik değişiklikler nedeniyle tespit edilmesi zor hatalara yol açabilir.
7. Tüm fonksiyonların dönüş değerlerini ve parametrelerini kontrol edin.
Çağıran taraf, geçersiz olmayan her dönüş değerini incelemelidir; her işlev, kendi giriş parametrelerini doğrulamalıdır.
❌ Uygun olmayan örnek (dönüş değerini göz ardı eder)
int bad_mission_control(int velocity, int time) {
int distance;
calculate_trajectory(velocity, time, &distance); // Didn't check!
return distance; // Could be garbage if calculation failed
}✅ Kurallara uygun örnek
int good_mission_control(int velocity, int time) {
int distance;
int status = calculate_trajectory(velocity, time, &distance);
if (status != 0) { // Checked the return value
return -1; // Propagate error to caller
}
return distance; // Safe to use
}Bunun önemi: Dönüş değerlerini veya geçersiz parametreleri göz ardı etmek, hataların başlıca kaynağıdır. Örneğin, malloc işlevini kontrol etmemek, null işaretçisinin dereferansına yol açabilir. Benzer şekilde, girdileri (örneğin dizi indeksleri veya biçim dizeleri) doğrulamamak, tampon taşmalarına veya çökmelere neden olabilir. NASA, her dönüş değerinin işlenmesini (veya niyeti belirtmek için açıkça void türüne dönüştürülmesini) ve her argümanın doğrulanmasını zorunlu kılar. Bu kapsamlı yaklaşım, hiçbir hatanın sessizce göz ardı edilmemesini sağlar.
8. Ön işleyiciyi include komutları ve basit makrolarla sınırlayın.
Karmaşık makrolar veya koşullu derleme hilelerinden kaçının.
❌ Kurallara uymayan örnek (karmaşık makro):
#tanımla DECLARE_FUNC(name) void func_##name(void)
DECLARE_FUNC(init); // Şu şekilde genişletilir: void func_init(void)✅ Uygun örnek (basit makrolar / satır içi):
// Compliant: use inline function or straightforward definitions
static inline int sqr(int x) { return x*x; }
#define MAX_BUFFER 256Bunun önemi: Karmaşık makrolar (özellikle çok satırlı veya fonksiyon benzeri makrolar) mantığı gizleyebilir, kontrol akışını karmaşıklaştırabilir ve statik analizi engelleyebilir. Ön işleyicinin görevlerini basit işlemlerle (örneğin sabitler ve başlıklar) sınırlamak, kodun açık ve anlaşılır kalmasını sağlar. Örneğin, makroları satır içi fonksiyonlarla değiştirmek, tip denetimini ve hata ayıklama kolaylığını artırır. Bu kural olmadan, makro genişletmeyle ilgili ince hatalar veya koşullu derleme hataları, incelemeler sırasında fark edilmeden gözden kaçabilir.
9. İşaretçi kullanımını sınırlayın.
Dolaylı erişimi tek bir düzeyle sınırlayın; int** ve işlev işaretçilerinden kaçının.
❌ Kurallara uymayan örnek (çoklu dolaylı atıf):
// Uygun değil: çift işaretçi ve işlev işaretçisi
int **doublePtr;
int (*funcPtr)(int) = someFunction;✅ Uygun örnek (tek işaretçi):
// Uyumlu: tek seviyeli işaretçi, işlev işaretçisi yok
int *singlePtr;
// İşlev işaretçisi yerine açık çağrı kullanın
int result = someFunction(5);Bunun önemi: Çok katmanlı işaretçiler ve işlev işaretçileri, veri akışını karmaşıklaştırır ve hangi bellek veya koda erişildiğini takip etmeyi zorlaştırır. Statik analiz araçları, her bir dolaylı erişimi çözümlemelidir; bu ise genel olarak kararsız bir durum olabilir. Tek işaretçili referanslarla sınırlandırıldığında, kod daha basit ve güvenli kalır. Bu kuralın ihlali, belirsiz takma adlara (bir işaretçinin verileri başka bir işaretçi aracılığıyla değiştirmesi) veya beklenmedik geri çağırma davranışlarına yol açabilir; her ikisi de güvenlik açısından kritik bağlamlarda risklidir.
10. Tüm uyarılar etkinleştirilmiş olarak derleyin ve bu uyarıları giderin.
Her türlü derleyici uyarısını etkinleştirin ve sürüm yayınlanmadan önce bunları giderin.
❌ Uygun olmayan örnek (uyarı içeren kod)
// Non-compliant: code that generates warnings (uninitialized, suspicious assignment)
int x;
if (x = 5) { // bug: should be '==' or initialize x
// ...
}
printf("%d\n", x); // warning: 'x' is used uninitialized✅ Uygun örnek (derleme hatasız)
// Compliant: initialize variables and use '==' in condition
int x = 0;
if (x == 5) {
// ...
}
printf("%d\n", x);Bunun önemi: Derleyici uyarıları genellikle gerçek hataları (başlatılmamış değişkenler, tür uyuşmazlıkları veya istenmeyen atamalar gibi) işaret eder. NASA’nın kuralı, hiçbir uyarının göz ardı edilmemesini zorunlu kılar. Herhangi bir sürüm yayınlanmadan önce, kod maksimum ayrıntı düzeyinde derlenmeli ve uyarı vermemelidir. Bu uygulama, birçok önemsiz hatayı erken aşamada yakalar. Bir uyarı giderilemiyorsa, kod yeniden yapılandırılmalı veya belgelenmelidir; böylece uyarı baştan ortaya çıkmaz.
Bu kuralların her biri, gizli hataların bir türünü ortadan kaldırır. Birlikte uygulandığında ise C kodunu çok daha öngörülebilir ve doğrulanabilir hale getirirler.
Sonuç
NASA’nın 10 kuralı (“Power of 10”), kritik öneme sahip C yazılımları için net ve etkili bir kodlama standardı sunar. Karmaşık yapıları önleyip denetimleri zorunlu kılarak, gizli hataların ortaya çıkma olasılığını azaltır ve statik analizi uygulanabilir hale getirir. Modern yazılım geliştirme süreçlerinde bu kılavuzlar, kod kalitesi araçları ile otomatikleştirilebilir. NASA kılavuzlarına aykırı durumları işaretlemek üzere özel kurallar tanımlanabilir ve bu kurallar her pull isteğinde çalıştırılarak geliştiricilere anında geri bildirim sağlanabilir.
Bu denetimleri erken aşamada uygulamak, daha güvenli, daha kaliteli ve bakımı daha kolay bir kod elde edilmesini sağlar. Havacılık ve uzay sektörü dışında bile bu ilkeler geçerlidir: küçük ve anlaşılır işlevler, açık döngüler, savunmacı programlama ve tüyler ürpertici işaretçi akrobasileri yapmamak. Bir kod kalitesi aracıyla bu kurallara uymak ve bunları otomatikleştirmek, ekibinizin hataları erken aşamada tespit etmesine ve daha güvenilir yazılımlar sunmasına yardımcı olur.

