MISRA C ve Statik Kod Analizi
Gömülü ve kritik C yazılımında MISRA C, Cppcheck, Splint, Astrée ve TI Code Composer Studio üzerinden statik analizin garanti sınırlarını; lexical taramadan abstract interpretation'a uzanan analiz katmanlarıyla inceliyorum.
Statik kod analizi, kaynak kodu çalıştırmadan incelemekten ibaret değildir; hangi program davranışının hangi soyut model üzerinden güvenle çıkarılabildiğini belirleme işidir. Basit bir kural tarayıcı ile abstract interpretation kullanan bir analizör aynı başlık altında anılsa da sundukları garanti, maliyet ve hata sınıfları birbirinden çok farklıdır. Özellikle gömülü ve kritik C yazılımında asıl mesele "kaç uyarı bulunduğu" değil, analiz aracının hangi özellikleri kanıtlayabildiği, hangilerinde yalnız sezgisel bulgu ürettiği ve hangi varsayımlara bağlı olduğudur.
2015'te Baykar'ın İHA yazılım birimindeki stajım sırasında programlama standartları ve statik analiz üzerinde çalıştım. O dönemde MISRA C kurallarını inceledim; kendi C tabanlı denetimlerimi geliştirirken Cppcheck ve Splint çıktılarından da yararlandım. Astrée'yi abstract interpretation yaklaşımı açısından, Texas Instruments Code Composer Studio ve ilgili derleyici araçlarını ise gömülü hedefte derleme, tanılama ve hata ayıklama zincirinin parçası olarak inceledim. Daha sonra lisans bitirme projemde geliştirdiğim Mini İHA yazılımında da staj döneminde oluşturduğum kontrol yaklaşımını kullanmaya devam ettim. Bugün geriye baktığımda en değerli taraf tek tek kurallar değil, farklı analiz araçlarının aynı kaynak koda neden farklı şeyler söyleyebildiğini erken dönemde görmüş olmamdır.
MISRA C Bir Analiz Aracı Değildir
MISRA C'nin mühendislik değeri çoğu zaman araç ekranındaki "uyumlu/uyumsuz" sayacına indirgeniyor. Bu yaklaşım eksik. MISRA C, C dilinin kritik sistemlerde daha öngörülebilir kullanılmasını hedefleyen bir yönerge ailesidir. Kodun nasıl yazılacağına, hangi dil özelliklerinin sınırlandırılacağına ve sapmaların nasıl yönetileceğine ilişkin disiplin getirir; kendi başına statik analiz motoru değildir.
2015 döneminde doğal başvuru noktası MISRA C:2012 idi. Bugün MISRA C:2023 bulunuyor; eski bir çalışmayı yeni sürümün kurallarıyla yapılmış gibi anlatmak tarihsel olarak doğru olmaz. Daha önemlisi, "MISRA compliant" ifadesi tek bir aracın sıfır uyarı vermesiyle kurulamaz. MISRA Compliance:2020; hangi yönergelerin uygulandığının, bunların nasıl zorlandığının, sapmaların hangi süreçle yönetildiğinin ve dışarıdan gelen yazılım bileşenlerinin durumunun açıkça belirlenmesini ister. Dolayısıyla statik analiz raporu, uyum kanıtının bir girdisidir; uyumun kendisi değildir.
Bu ayrım pratikte önemlidir. Bir kural araç tarafından otomatik denetlenebilir, başka bir kural insan incelemesi veya tasarım kanıtı gerektirebilir. Bazı ihlaller kontrollü bir deviation ile kabul edilebilir. Böyle bir durumda hedef uyarıyı susturmak değil, sapmanın kapsamını ve gerekçesini denetlenebilir hale getirmektir.
Metin Taramasından Program Modeline
Staj döneminde geliştirdiğim ilk denetimlerin bir bölümü kaynak kodu karakter karakter işleyen durum makinelerine dayanıyordu. Yorum, string literal, character literal, preprocessor satırı ve normal kod birbirinden ayrılmadan yapılan düz metin araması kısa sürede yanlış sonuç üretir. // karakter dizisi bir string içinde bulunabilir; = işareti atama, karşılaştırma bağlamı veya makro içeriğiyle farklı anlamlar taşıyabilir. switch ve break gibi yapılar da yalnız kelime varlığına bakılarak güvenilir biçimde değerlendirilemez.
Bu tür bir tarayıcı belirli yerel kurallar için hala değerlidir. Kaynak uzunluğu N ise tek geçişli bir lexical/state-machine denetiminin temel maliyeti genellikle O(N) düzeyindedir. Ancak semantik iddialar için kaynak kodun daha zengin bir modele dönüştürülmesi gerekir:
kaynak -> token -> sözdizim ağacı -> sembol ve tip bilgisi -> kontrol akış grafiği -> veri akışı -> bulguAST üzerinde yerel bir kuralı denetlemek çoğu durumda düğüm sayısıyla doğrusal yürüyebilir. Kontrol akış grafiği oluşturulduğunda temel dolaşım maliyeti O(V + E) olarak düşünülebilir; burada V düğümleri, E akış kenarlarını gösterir. Veri akışı analizi ise düğümleri tek kez ziyaret etmek zorunda değildir. Bir fixed-point hesabı kabaca
OUT(n) = F_n(merge(OUT(p))), p ∈ pred(n)ilişkisini kararlı hale gelene kadar tekrar değerlendirir. Gerçek maliyet kullanılan soyut alanın yüksekliğine, widening/narrowing stratejisine, çağrı grafiğine, path sensitivity ve context sensitivity düzeyine bağlıdır. "Statik analiz O(n)'dir" veya "her zaman yavaştır" gibi tek cümlelik genellemeler bu yüzden yanıltıcıdır.
Cppcheck: Pratik Hata Bulma ve Düşük Sürtünme
Cppcheck'i yararlı yapan taraflardan biri, derleyicinin doğal olarak yakalamadığı bazı undefined behavior ve tehlikeli C/C++ örüntülerine odaklanmasıdır. Resmi dokümantasyon null pointer dereference, out-of-bounds erişim, sıfıra bölme, geçersiz shift, uninitialized value ve bellek yönetimi gibi sınıfları açıkça sayıyor. Gömülü projelerde sık görülen compiler extension ve standart dışı sözdizimlerini analiz edebilme hedefi de pratikte önemlidir.
Burada "düşük false positive" hedefi ile "sound analysis" kavramını karıştırmamak gerekir. Cppcheck'in kendi kılavuzu da kontrollerin kusursuz olmadığını ve bulunması gereken bazı hataların kaçabileceğini açıkça belirtiyor. Araç, mühendislik açısından çok yararlı bir hata bulucu olabilir; fakat sessiz kalması programın tüm yürütmelerde hatasız olduğunu kanıtlamaz.
Bugünkü açık kaynak Cppcheck dağıtımı MISRA C:2012 için misra.py eklentisi sağlıyor, fakat resmi kılavuz açık kaynak denetleyicilerin MISRA kurallarını yalnız kısmen kapsadığını belirtiyor. Bu güncel yeteneği 2015'te kullandığım sürüme geriye dönük olarak mal etmiyorum. Buradaki daha kalıcı ders şudur: bir aracın "MISRA desteği var" demesi, sürüm, kural kapsamı ve enforcement method incelenmeden yeterli bilgi değildir.
Stajda kendi denetimlerimin yanında Cppcheck çıktısını process üzerinden alıp ortak bir sonuç modeline dönüştürmem, bugün de yararlı bulduğum bir tasarım yaklaşımının erken örneğiydi. Farklı analiz motorlarının mesajlarını tek ekranda göstermek kolaydır; asıl iş, bulgunun kaynağını ve semantiğini kaybetmeden normalize etmektir. "Warning" kelimesini ortaklaştırmak, iki aracın aynı garantiye sahip olduğu anlamına gelmez.
Splint: Kaynak Koda Eklenen Sözleşmeler
Splint, klasik lint yaklaşımını annotation ile güçlendiren farklı bir çizgide durur. Aracın tarihsel dokümantasyonu use-before-definition, unreachable code, ignored return value, muhtemel sonsuz döngü ve fall-through gibi kontrollerin yanında, geliştiricinin kaynak koda eklediği açıklayıcı annotation'larla daha güçlü kontroller yapılabildiğini anlatır.
Bu fikir benim için özellikle öğreticiydi: analiz aracı program hakkında ne kadar az semantik bilgiye sahipse, niyet ile gerçek davranışı ayırması o kadar zorlaşır. Bir pointer'ın null olamayacağı, bir fonksiyonun hangi global durumu değiştirebileceği veya bir buffer'ın sahiplik ilişkisi yalnız C sözdiziminden her zaman çıkarılamaz. Annotation, programcı niyetini makine tarafından denetlenebilir bir sözleşmeye dönüştürür.
Bedeli de vardır. Annotation yanlışsa analiz de yanlış varsayımla ilerleyebilir; annotation bakımı kaynak kod bakımının bir parçasına dönüşür. Çok katı annotation rejimi legacy kodda adoption maliyetini yükseltebilir. Splint'in tarihsel değeri burada: güçlü analiz için bazen daha karmaşık motor yerine programa daha fazla açık bilgi vermek gerekir.
Astrée ve Abstract Interpretation
Astrée'yi diğer araçlardan ayıran şey, hedefinin yalnız "muhtemel bug bulmak" olmamasıdır. Resmi proje tanımında Astrée, C programlarında belirli run-time error sınıflarının yokluğunu kanıtlamayı hedefleyen, abstract interpretation temelli bir analizör olarak açıklanır. Özellikle gerçek zamanlı gömülü kontrol yazılımları için geliştirilmiş olması, onu kritik sistem bağlamında önemli bir referans yapar.
Abstract interpretation'ın temel fikri, programın bütün somut durumlarını tek tek yürütmek yerine bunları daha küçük bir soyut uzayda güvenli biçimde üstten yaklaşık hesaplamaktır. Örneğin bir değişkenin kesin değerini taşımak yerine bir aralık tutulabilir:
x ∈ [a, b]
y ∈ [c, d]ise toplama için güvenli soyut sonuç
x + y ∈ [a + c, b + d]olur. Bu sonuç her gerçek yürütmeyi kapsar, fakat gerçekte oluşamayacak değerleri de içerebilir. Sound over-approximation'ın doğası budur: hata kaçırmamak için bazı sahte yürütmeler de analiz uzayına girebilir.
Cousot ve Cousot'nun 1977'de ortaya koyduğu abstract interpretation çerçevesi, program özelliklerini fixpoint hesabı üzerinden güvenli soyutlamalarla incelemenin matematiksel temelini kurdu. Astrée bu teoriyi safety-critical C programlarına uyarlarken interval, octagon ve problem alanına özgü soyut alanlar gibi teknikleri birleştirir. 2003 tarihli PLDI çalışması, büyük safety-critical yazılımlarda run-time error sınıflarını az sayıda false alarm ile analiz etmenin mümkün olabileceğini gösteren önemli çalışmalardan biridir.
Burada iki sınır özellikle önemlidir. Birincisi, genel program doğrulama problemi karar verilebilir değildir; "her C programının her özelliğini otomatik kanıtlayan analizör" yoktur. İkincisi, Astrée'nin garantisi de desteklenen program sınıfı, dil semantiği, çevre varsayımları ve kanıtlanan property ile sınırlıdır. Resmi Astrée açıklaması, hedeflenen programlarda recursion ve dynamic memory allocation bulunmaması gibi sınırlar verir. Güçlü garanti, kapsamı açık tanımlandığı için anlamlıdır.
Code Composer Studio ve Hedefe Bağlı Gerçeklik
Code Composer Studio'yu statik analizör olarak sınıflandırmak doğru değildir. CCStudio; TI mikrodenetleyici ve işlemcileri için derleyici, debugger, profiler ve hedef donanım araçlarını bir araya getiren bir geliştirme ortamıdır. Statik tanılamanın bir bölümü IDE'den değil kullanılan compiler toolchain'den gelir. TI derleyicilerinin bazı sürüm ve ailelerinde MISRA denetim seçenekleri ve ayrıntılı diagnostics bulunmuştur; tam davranış hedef işlemciye ve compiler sürümüne göre değişir.
Bu ayrım gömülü C'de teorik değildir. Masaüstü analizörü bir ifadeyi C standardı açısından şüpheli bulabilirken hedef derleyici implementation-defined davranışı belirli bir şekilde tanımlayabilir. Tam tersi de mümkündür: derleyici kodu kabul eder fakat kritik yazılım yönergesi aynı yapıyı taşınabilirlik veya bakım açısından yasaklayabilir. volatile, integer width, bit-field yerleşimi, inline assembly, device register erişimi ve compiler intrinsic'leri bu sınırın sık görüldüğü alanlardır.
Staj döneminde TMS570 ve gömülü haberleşme üzerinde çalışırken derleyici/IDE tanıları ile bağımsız statik analiz bulgularını aynı şey olarak görmemeyi öğrendim. Kaynak seviyesindeki uyarı, hedefteki gerçek davranışın yalnız bir parçasıdır. Gömülü bir hata bazen kaynak kodda, bazen compiler seçeneğinde, bazen çevre birimi konfigürasyonunda, bazen de fiziksel sinyalde görünür. Bu nedenle CCStudio tarafındaki debug ve hedef gözlemi, statik analizin rakibi değil tamamlayıcısıdır.
False Positive, False Negative ve Garanti Dili
Bir statik analiz raporunu değerlendirirken önce aracın garanti dilini okumak gerekir.
Unsound bir bug finder pratikte çok faydalı olabilir; bazı gerçek hataları kaçırmayı kabul ederek daha az gürültü üretir. Sound bir analizör, tanımlı property ve varsayımlar altında olası hataları kaçırmamayı hedefler; buna karşılık over-approximation nedeniyle false positive üretebilir. Path-sensitive, context-sensitive ve relational analizler hassasiyeti artırabilir, fakat CPU ve bellek maliyetini de büyütür. Branch sayısı arttıkça naif path enumeration üstel patlamaya kadar gidebilir.
Bu nedenle "araç A daha çok hata buldu, demek ki daha iyi" karşılaştırması mühendislik açısından zayıftır. Araçların kontrol ettiği property seti, configuration, suppressions, modelled library semantics ve hedef platform bilgisi aynı değilse uyarı sayıları doğrudan karşılaştırılamaz.
Ben statik analiz sonuçlarını üç sınıfa ayırmayı daha sağlıklı buluyorum: doğrudan dil veya bellek güvenliği problemi, proje standardı ihlali ve ek kanıt gerektiren şüpheli davranış. Bu ayrım, gerçek kusuru biçimsel olmayan stil tercihinden ayırır ve ekiplerin "warning fatigue" nedeniyle önemli bulguları görmezden gelmesini azaltır.
Tek Araç Yerine Katmanlı Denetim
2015'te geliştirdiğim araç, bugünkü ölçütlerle tam bir C frontend veya formal verifier değildi. Belirli programlama standartlarını otomatikleştirmek için lexical durum takibi yapan kontrolleri, daha sonra masaüstü bir arayüz ve dış analiz motorlarının çıktılarıyla birleştirdim. Bu sınırlı çözümün değerini bugün daha net görüyorum: kapsamı dar fakat tekrar üretilebilirdi ve insan incelemesinde kolay unutulan kontrolleri otomatik hale getiriyordu.
Daha olgun bir kritik C doğrulama zincirinde katmanlar farklı sorulara cevap verir. Compiler diagnostics hedef toolchain'in gördüğü problemleri yakalar. MISRA denetimi yazım ve dil kullanım disiplinini kontrol eder. Cppcheck benzeri araçlar pratik bug sınıflarını arar. Annotation tabanlı yaklaşım program niyetini daha açık hale getirir. Abstract interpretation belirli run-time property'leri daha güçlü garanti diliyle inceleyebilir. Unit, integration, HIL ve sistem testleri ise yalnız statik modelden çıkarılamayan gerçek davranışı ölçer.
Bu katmanların hiçbirini "diğerini gereksiz kılan araç" olarak görmüyorum. Asıl mühendislik kararı, her aracın hangi riski azalttığını ve hangi kör noktayı bıraktığını bilmektir.
Statik analizde olgunluk, uyarı sayısını sıfırlamak değildir. Doğru hedef şudur: hangi kuralların otomatik denetlendiğini, hangi property'lerin gerçekten kanıtlandığını, hangi sapmaların bilinçli kabul edildiğini ve hangi davranışların ancak çalıştırma, ölçüm veya donanım gözlemiyle doğrulanabileceğini açık tutmak.
2015'te programlama standartlarını otomatikleştirmeye çalışırken başladığım düşünce bugün de değişmedi: tekrarlanabilir bir denetim insan hafızasına bırakılmamalıdır. Değişen şey, bir denetimin sonucuna ne kadar güvenilebileceğini artık aracın adından değil analiz modelinden, varsayımlarından ve kanıt kapsamından okumamdır.
Kaynakça
- MISRA. MISRA Compliance:2020 - Achieving compliance with MISRA Coding Guidelines. The MISRA Consortium, 2020. kaynak
- MISRA. MISRA C:2023 Addendum 2 - Coverage of ISO/IEC 17961. The MISRA Consortium, 2024. kaynak
- Cousot, Patrick; Cousot, Radhia. Abstract Interpretation: A Unified Lattice Model for Static Analysis of Programs by Construction or Approximation of Fixpoints. POPL, 1977. DOI: 10.1145/512950.512973.
- Blanchet, Bruno; Cousot, Patrick; Cousot, Radhia; Feret, Jérôme; Mauborgne, Laurent; Miné, Antoine; Monniaux, David; Rival, Xavier. A Static Analyzer for Large Safety-Critical Software. PLDI, 2003. DOI: 10.1145/781131.781153.
- École Normale Supérieure / CNRS / INRIA. The Astrée Static Analyzer. kaynak
- Cppcheck Project. Cppcheck - A Tool for Static C/C++ Code Analysis. kaynak
- Cppcheck Project. Cppcheck Manual. kaynak
- Splint Project. Splint Home Page. kaynak
- Evans, David. Splint User's Manual. Version 3.1.1, 2003. kaynak
- Texas Instruments. CCStudio Integrated Development Environment. kaynak