PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır
Travis CI, GitHub'u kaynak kodu barındırma olarak kullanan yazılım oluşturmaya ve test etmeye yönelik dağıtılmış bir web hizmetidir. Yukarıdaki çalışma senaryolarına ek olarak, kapsamlı konfigürasyon seçenekleri sayesinde kendinizinkini de ekleyebilirsiniz. Bu yazıda Travis CI'yı PPSSPP kod örneğini kullanarak PVS-Studio ile çalışacak şekilde yapılandıracağız.

Giriş

Travis C.I. yazılım oluşturmaya ve test etmeye yönelik bir web hizmetidir. Genellikle sürekli entegrasyon uygulamalarıyla birlikte kullanılır.

PPSSPP — PSP oyun konsolu emülatörü. Program, Sony PSP'ye yönelik disk görüntülerinden herhangi bir oyunun başlatılmasını taklit edebilir. Program 1 Kasım 2012'de yayınlandı. PPSSPP, GPL v2 kapsamında lisanslanmıştır. Herkes iyileştirmeler yapabilir proje kaynak kodu.

PVS-Stüdyo — program kodundaki hataları ve olası güvenlik açıklarını aramaya yönelik bir statik kod analizörü. Bu yazıda bir değişiklik olarak PVS-Studio'yu yerel olarak geliştiricinin makinesinde değil bulutta başlatacağız ve PPSSPP'deki hataları arayacağız.

Travis CI'yi kurma

İhtiyacımız olan projenin bulunduğu GitHub'da bir depoya ve ayrıca PVS-Studio anahtarına ihtiyacımız olacak (alabilirsiniz) deneme anahtarı veya Açık Kaynak projeleri için ücretsiz).

Haydi siteye gidelim Travis C.I.. GitHub hesabınızı kullanarak yetkilendirme sonrasında depoların bir listesini göreceğiz:

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır
Test için PPSSPP'yi çatalladım.

Toplamak istediğimiz depoyu etkinleştiriyoruz:

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır
Şu anda Travis CI projemizi inşa edemiyor çünkü inşa etme talimatı yok. Yani konfigürasyon zamanı.

Analiz sırasında, bazı değişkenler bizim için yararlı olacaktır; örneğin, yapılandırma dosyasında belirtilmesi istenmeyen PVS-Studio anahtarı. Travis CI'daki derleme ayarlarını kullanarak ortam değişkenlerini ekleyelim:

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır
İhtiyacımız:

  • PVS_USERNAME - kullanıcı adı
  • PVS_KEY - anahtar
  • MAIL_USER - raporu göndermek için kullanılacak e-posta
  • MAIL_PASSWORD - e-posta şifresi

Son ikisi isteğe bağlıdır. Bunlar sonuçları postayla göndermek için kullanılacaktır. Raporu başka bir şekilde dağıtmak istiyorsanız bunları belirtmenize gerek yoktur.

Böylece ihtiyacımız olan ortam değişkenlerini ekledik:

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır
Şimdi bir dosya oluşturalım .travis.yml ve bunu projenin köküne yerleştirin. PPSSPP'nin Travis CI için zaten bir yapılandırma dosyası vardı, ancak bu çok büyüktü ve örnek için tamamen uygun değildi, bu yüzden onu büyük ölçüde basitleştirmek ve yalnızca temel öğeleri bırakmak zorunda kaldık.

Öncelikle sanal makinede kullanmak istediğimiz Ubuntu Linux sürümünü, dilini ve build için gerekli paketleri belirtelim:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

Listelenen tüm paketler yalnızca PPSSPP için gereklidir.

Şimdi montaj matrisini belirtiyoruz:

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PPSSPP_BUILD_TYPE=Linux PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"
      env: PPSSPP_BUILD_TYPE=Linux

Bölüm hakkında biraz daha matris. Travis CI'da derleme seçenekleri oluşturmanın iki yolu vardır: birincisi, derleyicilerin, işletim sistemi türlerinin, ortam değişkenlerinin vb. listesini belirlemek ve ardından tüm olası kombinasyonların bir matrisini oluşturmaktır; ikincisi matrisin açık bir göstergesidir. Tabii ki, bu iki yaklaşımı birleştirebilir ve benzersiz bir durum ekleyebilir veya tam tersine, bölümü kullanarak onu hariç tutabilirsiniz. dışlamak. Bununla ilgili daha fazlasını şurada okuyabilirsiniz Travis CI belgeleri.

Geriye kalan tek şey projeye özel montaj talimatlarını sağlamaktır:

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Travis CI, bir sanal makinenin ömrünün çeşitli aşamaları için kendi komutlarınızı eklemenizi sağlar. Bölüm before_install paketleri kurmadan önce yürütülür. Daha sonra kurmaklisteden paketlerin kurulumunu takip eden addons.aptyukarıda da belirttiğimiz gibi. Montajın kendisi gerçekleşir senaryo. Her şey yolunda giderse, kendimizi içinde buluruz. başarı sonrası (Bu bölümde statik analiz yapacağız). Değiştirilebilecek adımların hepsi bunlar değil, daha fazlasına ihtiyacınız varsa o zaman şuraya bakmalısınız: Travis CI belgeleri.

Okumayı kolaylaştırmak için komutlar ayrı bir komut dosyasına yerleştirildi .travis.sh, proje köküne yerleştirilir.

Yani aşağıdaki dosyaya sahibiz .travis.yml:

language: cpp
dist: xenial

addons:
  apt:
    update: true
    packages:
      - ant
      - aria2
      - build-essential
      - cmake
      - libgl1-mesa-dev
      - libglu1-mesa-dev
      - libsdl2-dev
      - pv
      - sendemail
      - software-properties-common
    sources:
      - sourceline: 'ppa:ubuntu-toolchain-r/test'
      - sourceline: 'ppa:ubuntu-sdk-team/ppa'

matrix:
  include:
    - os: linux
      compiler: "gcc"
      env: PVS_ANALYZE=Yes
    - os: linux
      compiler: "clang"

before_install:
  - travis_retry bash .travis.sh travis_before_install

install:
  - travis_retry bash .travis.sh travis_install

script:
  - bash .travis.sh travis_script

after_success:
  - bash .travis.sh travis_after_success

Paketleri kurmadan önce alt modülleri güncelleyeceğiz. PPSSPP'yi oluşturmak için bu gereklidir. İlk fonksiyonu ekleyelim .travis.sh (uzantıya dikkat edin):

travis_before_install() {
  git submodule update --init --recursive
}

Şimdi doğrudan Travis CI'da PVS-Studio'nun otomatik başlatılmasını ayarlamaya geliyoruz. Öncelikle PVS-Studio paketini sisteme kurmamız gerekiyor:

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}

Fonksiyonun başında travis_install ihtiyaç duyduğumuz derleyicileri ortam değişkenlerini kullanarak kuruyoruz. O zaman eğer değişken $PVS_ANALYZE değeri saklar Evet (Bölümde belirttik) env derleme matrisi yapılandırması sırasında), paketi yükleriz pvs-studio. Buna ek olarak paketler de belirtilmiştir. libio-soket-ssl-perl и libnet-ssleay-perlancak sonuçların postalanması için gereklidirler, dolayısıyla raporunuzu iletmek için başka bir yöntem seçtiyseniz bunlar gerekli değildir.

Fonksiyon indirme_ekstresi belirtilen arşivi indirir ve paketini açar:

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

Projeyi bir araya getirmenin zamanı geldi. Bu bölümde oluyor senaryo:

travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}

Aslında bu, şu satırlar dışında basitleştirilmiş orijinal bir konfigürasyondur:

if [ "$PVS_ANALYZE" = "Yes" ]; then
  CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
fi

Kodun bu bölümünde ayarladığımız yapmak derleme komutlarını dışa aktarmak için bayrak. Bu, statik kod analizörü için gereklidir. Bununla ilgili daha fazla bilgiyi "PVS-Studio'yu Linux ve macOS'ta çalıştırma".

Montaj başarılı olursa, o zaman başarı sonrasıStatik analiz yaptığımız yer:

travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}

Şimdi aşağıdaki satırlara daha yakından bakalım:

pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                -o PVS-Studio-${CC}.log 
                                --disableLicenseExpirationCheck
plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html

İlk satır, Travis CI ortam değişkenlerini ayarlarken en başta belirttiğimiz kullanıcı adı ve anahtardan bir lisans dosyası oluşturur.

İkinci satır analizi doğrudan başlatır. Bayrak -J analiz için iş parçacığı sayısını ayarlar, bayrak -ben lisansı, bayrağı belirtir günlüklerin çıktısı için dosyayı ve bayrağı tanımlar -disableLicenseExpirationCheck deneme sürümleri için gereklidir, çünkü varsayılan olarak pvs-studio-analizörü kullanıcıyı lisansın süresinin dolmak üzere olduğu konusunda uyaracaktır. Bunun olmasını önlemek için bu bayrağı belirtebilirsiniz.

Günlük dosyası, dönüştürme olmadan okunamayan ham çıktı içerdiğinden, öncelikle dosyayı okunabilir hale getirmelisiniz. Günlükleri aktaralım plog-dönüştürücüve çıktı bir html dosyasıdır.

Bu örnekte, şu komutu kullanarak raporları postayla göndermeye karar verdim: eposta gönder.

Sonuç olarak aşağıdaki dosyayı elde ettik .travis.sh:

#/bin/bash

travis_before_install() {
  git submodule update --init --recursive
}

download_extract() {
  aria2c -x 16 $1 -o $2
  tar -xf $2
}

travis_install() {
  if [ "$CXX" = "g++" ]; then
    sudo apt-get install -qq g++-4.8
  fi
  
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    wget -q -O - https://files.viva64.com/etc/pubkey.txt 
      | sudo apt-key add -
    sudo wget -O /etc/apt/sources.list.d/viva64.list 
      https://files.viva64.com/etc/viva64.list  
    
    sudo apt-get update -qq
    sudo apt-get install -qq pvs-studio 
                             libio-socket-ssl-perl 
                             libnet-ssleay-perl
  fi
    
  download_extract 
    "https://cmake.org/files/v3.6/cmake-3.6.2-Linux-x86_64.tar.gz" 
    cmake-3.6.2-Linux-x86_64.tar.gz
}
travis_script() {
  if [ -d cmake-3.6.2-Linux-x86_64 ]; then
    export PATH=$(pwd)/cmake-3.6.2-Linux-x86_64/bin:$PATH
  fi
  
  CMAKE_ARGS="-DHEADLESS=ON ${CMAKE_ARGS}"
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    CMAKE_ARGS="-DCMAKE_EXPORT_COMPILE_COMMANDS=On ${CMAKE_ARGS}"
  fi
  cmake $CMAKE_ARGS CMakeLists.txt
  make
}
travis_after_success() {
  if [ "$PVS_ANALYZE" = "Yes" ]; then
    pvs-studio-analyzer credentials $PVS_USERNAME $PVS_KEY -o PVS-Studio.lic
    pvs-studio-analyzer analyze -j2 -l PVS-Studio.lic 
                                    -o PVS-Studio-${CC}.log 
                                    --disableLicenseExpirationCheck
    
    plog-converter -t html PVS-Studio-${CC}.log -o PVS-Studio-${CC}.html
    sendemail -t [email protected] 
              -u "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -m "PVS-Studio $CC report, commit:$TRAVIS_COMMIT" 
              -s smtp.gmail.com:587 
              -xu $MAIL_USER 
              -xp $MAIL_PASSWORD 
              -o tls=yes 
              -f $MAIL_USER 
              -a PVS-Studio-${CC}.log PVS-Studio-${CC}.html
  fi
}
set -e
set -x

$1;

Şimdi değişiklikleri git deposuna aktarmanın zamanı geldi, ardından Travis CI derlemeyi otomatik olarak çalıştıracak. Yapı raporlarına gitmek için “ppsspp”e tıklayın:

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır
Mevcut yapıya genel bir bakış göreceğiz:

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır
Derleme başarıyla tamamlanırsa statik analizin sonuçlarını içeren bir e-posta alacağız. Elbette rapor almanın tek yolu posta göndermek değildir. Herhangi bir uygulama yöntemini seçebilirsiniz. Ancak derleme tamamlandıktan sonra sanal makine dosyalarına erişmenin imkansız olacağını unutmamak önemlidir.

Hata özeti

En zor kısmı başarıyla tamamladık. Şimdi tüm çabalarımızın buna değdiğinden emin olalım. Bana posta yoluyla gelen statik analiz raporundan bazı ilginç noktalara bakalım (bunu belirtmem boşuna değildi).

Tehlikeli optimizasyon

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  memset( &ctx, 0, sizeof( sha1_context ) );
}

PVS-Studio uyarısı: V597 Derleyici, 'toplam' arabelleğini temizlemek için kullanılan 'memset' işlev çağrısını silebilir. Özel verileri silmek için RtlSecureZeroMemory() işlevi kullanılmalıdır. sha1.cpp 325

Bu kod parçası güvenli karma modülünde bulunur ancak ciddi bir güvenlik açığı içerir (CWE-14). Debug sürümünü derlerken oluşturulan derleme listesine bakalım:

; Line 355
  mov r8d, 20
  xor edx, edx
  lea rcx, QWORD PTR sum$[rsp]
  call memset
; Line 356

Her şey yolunda ve işlev meme seti yürütülür, böylece RAM'deki önemli verilerin üzerine yazılır, ancak henüz sevinmeyin. Optimizasyonlu Sürüm versiyonunun montaj listesine bakalım:

; 354  :
; 355  :  memset( sum, 0, sizeof( sum ) );
; 356  :}

Listeden görülebileceği gibi derleyici çağrıyı görmezden geldi meme seti. Bunun nedeni, fonksiyonda sha1 aramadan sonra meme seti artık yapıya referans yok ctx. Bu nedenle derleyici, gelecekte kullanılmayan belleğin üzerine yazarak işlemci zamanını boşa harcamanın bir anlamı görmez. Bu işlevi kullanarak bunu düzeltebilirsiniz. RtlSecureZeroBellek veya benzer bir s.

düzeltin:

void sha1( unsigned char *input, int ilen, unsigned char output[20] )
{
  sha1_context ctx;

  sha1_starts( &ctx );
  sha1_update( &ctx, input, ilen );
  sha1_finish( &ctx, output );

  RtlSecureZeroMemory(&ctx, sizeof( sha1_context ) );
} 

Gereksiz karşılaştırma

static u32 sceAudioOutputPannedBlocking
             (u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    if (leftvol >= 0) {
      chans[chan].leftVolume = leftvol;
    }
    if (rightvol >= 0) {
      chans[chan].rightVolume = rightvol;
    }
    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

PVS-Studio uyarısı: V547 'Solvol >= 0' ifadesi her zaman doğrudur. sceAudio.cpp 120

İlk olarak else şubesine dikkat edin if. Kod yalnızca tüm koşullar sağlandığında yürütülecektir solvol > 0xFFFF || sağvol > 0xFFFF || solvol < 0 || sağvol < 0 sahte olduğu ortaya çıkacak. Bu nedenle else dalı için geçerli olan aşağıdaki ifadeleri elde ederiz: solvol <= 0xFFFF, sağvol <= 0xFFFF, solvol >= 0 и sağvol >= 0. Son iki ifadeye dikkat edin. Bu kod parçasının yürütülmesi için gerekli koşulun ne olduğunu kontrol etmek mantıklı mı?

Böylece bu koşullu ifadeleri güvenle kaldırabiliriz:

static u32 sceAudioOutputPannedBlocking
(u32 chan, int leftvol, int rightvol, u32 samplePtr) {
  int result = 0;
  // For some reason, this is the only one that checks for negative.
  if (leftvol > 0xFFFF || rightvol > 0xFFFF || leftvol < 0 || rightvol < 0) {
    ....
  } else {
    chans[chan].leftVolume = leftvol;
    chans[chan].rightVolume = rightvol;

    chans[chan].sampleAddress = samplePtr;
    result = __AudioEnqueue(chans[chan], chan, true);
  }
}

Başka bir senaryo. Bu gereksiz koşulların arkasında bir tür hata gizlidir. Belki de neyin gerekli olduğunu kontrol etmediler.

Ctrl+C Ctrl+V Geri Vurur

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfData) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

V501 '||' ifadesinin solunda ve sağında aynı '!Memory::IsValidAddress(psmfData)' alt ifadeleri vardır. Şebeke. scePsmf.cpp 703

İçerideki çeke dikkat edin if. Adresin geçerli olup olmadığını kontrol etmemiz sizce de tuhaf değil mi? psmfVerileri, iki katı kadar mı? Yani bu bana tuhaf geliyor... Aslında bu elbette bir yazım hatası ve amaç her iki giriş parametresini de kontrol etmekti.

Doğru seçenek:

static u32 scePsmfSetPsmf(u32 psmfStruct, u32 psmfData) {
  if (!Memory::IsValidAddress(psmfStruct) ||
      !Memory::IsValidAddress(psmfData)) {
    return hleReportError(ME, SCE_KERNEL_ERROR_ILLEGAL_ADDRESS, "bad address");
  }
  ....
}

Unutulan değişken

extern void ud_translate_att(
  int size = 0;
  ....
  if (size == 8) {
    ud_asmprintf(u, "b");
  } else if (size == 16) {
    ud_asmprintf(u, "w");
  } else if (size == 64) {
    ud_asmprintf(u, "q");
  }
  ....
}

PVS-Studio uyarısı: V547 'Boyut == 8' ifadesi her zaman yanlıştır. syn-att.c 195

Bu hata klasörde bulunur extyani projeyle pek alakalı değil ama hata ben farkına varmadan bulundu, bu yüzden onu bırakmaya karar verdim. Sonuçta bu makale hataların gözden geçirilmesiyle ilgili değil, Travis CI ile entegrasyonla ilgili ve analizörün herhangi bir yapılandırması yapılmadı.

değişken boyut bir sabit tarafından başlatılır, ancak operatöre kadar kodda hiç kullanılmaz ifki bu da elbette şunu sağlıyor yanlış koşulları kontrol ederken, çünkü hatırladığımız gibi, boyut sıfıra eşittir. Daha sonraki kontrollerin de hiçbir anlamı yok.

Görünüşe göre kod parçasının yazarı değişkenin üzerine yazmayı unutmuş boyut bundan önce.

dur

Muhtemelen hatalarla son bulacağımız yer burasıdır. Bu makalenin amacı, projeyi mümkün olduğunca ayrıntılı bir şekilde analiz etmek değil, PVS-Studio'nun Travis CI ile birlikte çalışmasını göstermektir. Daha büyük ve daha güzel hatalar istiyorsanız onlara her zaman hayran olabilirsiniz burada :).

Sonuç

Artımlı analiz uygulamasıyla birlikte projeler oluşturmak için web hizmetlerini kullanmak, kodu birleştirdikten hemen sonra birçok sorunu bulmanızı sağlar. Ancak tek bir derleme yeterli olmayabilir, bu nedenle testi statik analizle birlikte ayarlamak kodun kalitesini önemli ölçüde artıracaktır.

Faydalı linkler

PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl yapılandırılır

Bu makaleyi İngilizce konuşan bir kitleyle paylaşmak istiyorsanız lütfen çeviri bağlantısını kullanın: Maxim Zvyagintsev. PSP oyun konsolu emülatörü örneğini kullanarak Travis CI'da PVS-Studio nasıl kurulur.

Kaynak: habr.com

Yorum ekle