Catatan
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba masuk atau mengubah direktori.
Akses ke halaman ini memerlukan otorisasi. Anda dapat mencoba mengubah direktori.
Bahasa anotasi kode sumber Microsoft (SAL) menyediakan serangkaian anotasi yang dapat Anda gunakan untuk menjelaskan bagaimana fungsi menggunakan parameternya, asumsi yang dibuatnya tentang mereka, dan jaminan yang dibuatnya ketika selesai. Anotasi ditentukan dalam file <sal.h>
header . Analisis kode Visual Studio untuk C++ menggunakan anotasi SAL untuk memodifikasi analisis fungsinya. Untuk informasi selengkapnya tentang SAL 2.0 untuk pengembangan driver Windows, lihat Anotasi SAL 2.0 untuk Driver Windows.
Secara asli, C dan C++ hanya menyediakan cara terbatas bagi pengembang untuk secara konsisten mengekspresikan niat dan invariansi. Dengan menggunakan anotasi SAL, Anda dapat menjelaskan fungsi Anda secara lebih rinci sehingga pengembang yang mengonsumsinya dapat lebih memahami cara menggunakannya.
Apa itu SAL dan Mengapa Anda Harus Menggunakannya?
Secara sederhana, SAL adalah cara murah untuk membiarkan kompilator memeriksa kode Anda.
SAL Membuat Kode Lebih Berharga
SAL dapat membantu Anda membuat desain kode Anda lebih dapat dimengerti, baik untuk manusia maupun untuk alat analisis kode. Pertimbangkan contoh berikut yang menunjukkan fungsi runtime C memcpy
.
void * memcpy(
void *dest,
const void *src,
size_t count
);
Dapatkah Anda mengetahui fungsi ini? Ketika fungsi diimplementasikan atau dipanggil, properti tertentu harus dipertahankan untuk memastikan kebenaran program. Hanya dengan melihat deklarasi seperti yang ada dalam contoh, Anda tidak tahu apa itu. Tanpa anotasi SAL, Anda harus mengandalkan komentar dokumentasi atau kode. Berikut adalah apa yang dikatakan dokumentasi untuk memcpy
:
"
memcpy
salinan menghitung byte dari src ke dest;wmemcpy
salinan menghitung karakter lebar (dua byte). Jika sumber dan tujuan tumpang tindih, perilakumemcpy
tidak ditentukan. Gunakanmemmove
untuk menangani wilayah yang tumpang tindih.
Penting: Pastikan bahwa buffer tujuan berukuran sama atau lebih besar dari buffer sumber. Untuk informasi selengkapnya, lihat Menghindari Buffer Overruns."
Dokumentasi berisi beberapa bit informasi yang menunjukkan bahwa kode Anda harus mempertahankan properti tertentu untuk memastikan kebenaran program:
memcpy
count
menyalin byte dari buffer sumber ke buffer tujuan.Buffer tujuan harus setidaknya sebesar buffer sumber.
Namun, pengkompilasi tidak dapat membaca dokumentasi atau komentar informal. Ini tidak tahu bahwa terdapat hubungan antara kedua buffer dan count
, dan juga tidak dapat menebak secara efektif tentang adanya hubungan. SAL dapat memberikan lebih banyak kejelasan tentang properti dan implementasi fungsi, seperti yang ditunjukkan di sini:
void * memcpy(
_Out_writes_bytes_all_(count) void *dest,
_In_reads_bytes_(count) const void *src,
size_t count
);
Perhatikan bahwa anotasi ini menyerupai informasi dalam dokumentasi, tetapi lebih ringkas dan mengikuti pola semantik. Saat membaca kode ini, Anda dapat dengan cepat memahami atribut fungsi ini dan cara menghindari masalah keamanan kelebihan buffer. Lebih baik lagi, pola semantik yang disediakan SAL dapat meningkatkan efisiensi dan efektivitas alat analisis kode otomatis dalam penemuan awal potensi bug. Bayangkan seseorang menulis implementasi yang bermasalah dari wmemcpy
:
wchar_t * wmemcpy(
_Out_writes_all_(count) wchar_t *dest,
_In_reads_(count) const wchar_t *src,
size_t count)
{
size_t i;
for (i = 0; i <= count; i++) { // BUG: off-by-one error
dest[i] = src[i];
}
return dest;
}
Implementasi ini berisi kesalahan umum off-by-one. Untungnya, pembuat kode menyertakan anotasi ukuran buffer SAL—alat analisis kode dapat menangkap bug dengan menganalisis fungsi ini saja.
Dasar-Dasar SAL
SAL mendefinisikan empat jenis parameter dasar, yang dikategorikan berdasarkan pola penggunaan.
Kategori | Anotasi Parameter | Deskripsi |
---|---|---|
Input ke fungsi yang dipanggil | _In_ |
Data diteruskan ke fungsi yang disebut, dan diperlakukan sebagai baca-saja. |
Input ke fungsi yang dipanggil, dan output ke pemanggil | _Inout_ |
Data yang dapat digunakan diteruskan ke fungsi dan berpotensi dimodifikasi. |
Keluaran untuk pemanggil | _Out_ |
Pemanggil hanya menyediakan ruang bagi fungsi yang dipanggil untuk menulis. Fungsi yang disebut menulis data ke dalam ruang tersebut. |
Pengeluaran penunjuk ke pemanggil | _Outptr_ |
Seperti keluaran ke pemanggil. Nilai yang dikembalikan oleh fungsi yang dipanggil adalah pointer. |
Keempat anotasi dasar ini dapat dibuat lebih eksplisit dengan berbagai cara. Secara default, parameter penunjuk yang dianotasi diasumsikan diperlukan—parameter tersebut harus non-NULL agar fungsi berhasil. Variasi anotasi dasar yang paling umum digunakan menunjukkan bahwa parameter penunjuk bersifat opsional—jika NULL, fungsi masih dapat berhasil melakukan pekerjaannya.
Tabel ini memperlihatkan cara membedakan antara parameter yang diperlukan dan opsional:
Parameter diperlukan | Parameter bersifat opsional | |
---|---|---|
Input ke fungsi yang dipanggil | _In_ |
_In_opt_ |
Input ke fungsi yang dipanggil, dan output ke pemanggil | _Inout_ |
_Inout_opt_ |
Keluaran untuk pemanggil | _Out_ |
_Out_opt_ |
Pengeluaran penunjuk ke pemanggil | _Outptr_ |
_Outptr_opt_ |
Anotasi ini membantu mengidentifikasi kemungkinan nilai yang tidak diinisialisasi dan penggunaan penunjuk null yang tidak valid secara formal dan akurat. Meneruskan NULL ke parameter yang diperlukan dapat menyebabkan gangguan, atau dapat menyebabkan kode kesalahan "gagal" dikembalikan. Bagaimanapun, fungsi tidak dapat berhasil dalam melakukan pekerjaannya.
Contoh SAL
Bagian ini memperlihatkan contoh kode untuk anotasi SAL dasar.
Menggunakan alat analisis kode Visual Studio untuk menemukan cacat
Dalam contoh, alat analisis kode Visual Studio digunakan bersama dengan anotasi SAL untuk menemukan cacat kode. Berikut adalah cara untuk melakukan itu.
Untuk menggunakan alat analisis kode Visual Studio dan SAL
Di Visual Studio, buka proyek C++ yang berisi anotasi SAL.
Pada bilah menu, pilih Bangun, Jalankan Analisis Kode pada Solusi.
Pertimbangkan contoh _In_ di bagian ini. Jika Anda menjalankan analisis kode di atasnya, peringatan ini ditampilkan:
C6387 Nilai Parameter Tidak Valid 'pInt' dapat bernilai '0': ini tidak sesuai dengan spesifikasi untuk fungsi 'InCallee'.
Contoh: Anotasi _In_
Anotasi _In_
menunjukkan bahwa:
Parameter harus valid dan tidak akan dimodifikasi.
Fungsi ini hanya akan membaca dari buffer elemen tunggal.
Pemanggil harus menyediakan buffer dan menginisialisasinya.
_In_
menentukan "baca-saja". Kesalahan umum adalah menerapkan_In_
ke parameter yang seharusnya memiliki_Inout_
anotasi sebagai gantinya._In_
diizinkan tetapi diabaikan oleh penganalisis pada skalar non-pointer.
void InCallee(_In_ int *pInt)
{
int i = *pInt;
}
void GoodInCaller()
{
int *pInt = new int;
*pInt = 5;
InCallee(pInt);
delete pInt;
}
void BadInCaller()
{
int *pInt = NULL;
InCallee(pInt); // pInt should not be NULL
}
Jika Anda menggunakan analisis kode Visual Studio pada contoh ini, analisis tersebut memvalidasi bahwa penelepon meneruskan penunjuk non-Null ke buffer yang diinisialisasi untuk pInt
. Dalam hal ini, pInt
pointer tidak boleh NULL.
Contoh: Anotasi _In_opt_
_In_opt_
sama dengan _In_
, kecuali bahwa parameter input diizinkan untuk menjadi NULL dan, oleh karena itu, fungsi harus memeriksa ini.
void GoodInOptCallee(_In_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
}
}
void BadInOptCallee(_In_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer 'pInt'
}
void InOptCaller()
{
int *pInt = NULL;
GoodInOptCallee(pInt);
BadInOptCallee(pInt);
}
Analisis kode Visual Studio memvalidasi bahwa fungsi memeriksa NULL sebelum mengakses buffer.
Contoh: Anotasi _Out_
_Out_
mendukung skenario umum di mana penunjuk non-NULL yang mengarah ke buffer elemen diteruskan, dan fungsi tersebut menginisialisasi elemen. Pemanggil tidak harus menginisialisasi buffer sebelum melakukan panggilan; fungsi yang dipanggil berjanji akan menginisialisasi buffer tersebut sebelum mengembalikan hasil.
void GoodOutCallee(_Out_ int *pInt)
{
*pInt = 5;
}
void BadOutCallee(_Out_ int *pInt)
{
// Did not initialize pInt buffer before returning!
}
void OutCaller()
{
int *pInt = new int;
GoodOutCallee(pInt);
BadOutCallee(pInt);
delete pInt;
}
Analisis kode Visual Studio memvalidasi bahwa pemanggil meneruskan penunjuk non-NULL ke buffer untuk pInt
dan bahwa buffer diinisialisasi oleh fungsi sebelum fungsi tersebut mengembalikan.
Contoh: Anotasi _Out_opt_
_Out_opt_
sama dengan _Out_
, kecuali bahwa parameter diizinkan menjadi NULL dan, oleh karena itu, fungsi harus memeriksa ini.
void GoodOutOptCallee(_Out_opt_ int *pInt)
{
if (pInt != NULL) {
*pInt = 5;
}
}
void BadOutOptCallee(_Out_opt_ int *pInt)
{
*pInt = 5; // Dereferencing NULL pointer 'pInt'
}
void OutOptCaller()
{
int *pInt = NULL;
GoodOutOptCallee(pInt);
BadOutOptCallee(pInt);
}
Analisis kode Visual Studio memvalidasi bahwa fungsi ini memeriksa NULL sebelum pInt
didereferensikan, dan jika pInt
bukan NULL, bahwa buffer diinisialisasi oleh fungsi sebelum dikembalikan.
Contoh: Anotasi _Inout_
_Inout_
digunakan untuk membuat anotasi parameter penunjuk yang dapat diubah oleh fungsi. Penunjuk harus menunjuk ke data yang diinisialisasi yang valid sebelum panggilan, dan bahkan jika berubah, penunjuk harus tetap memiliki nilai yang valid saat dikembalikan. Anotasi menentukan bahwa fungsi dapat dengan bebas membaca dari dan menulis ke buffer satu elemen. Pemanggil harus menyediakan buffer dan menginisialisasinya.
Catatan
Seperti _Out_
, _Inout_
harus berlaku untuk nilai yang dapat dimodifikasi.
void InOutCallee(_Inout_ int *pInt)
{
int i = *pInt;
*pInt = 6;
}
void InOutCaller()
{
int *pInt = new int;
*pInt = 5;
InOutCallee(pInt);
delete pInt;
}
void BadInOutCaller()
{
int *pInt = NULL;
InOutCallee(pInt); // 'pInt' should not be NULL
}
Analisis kode Visual Studio memvalidasi bahwa pemanggil meneruskan penunjuk non-NULL ke buffer yang diinisialisasi untuk pInt
, dan bahwa, sebelum pengembalian, pInt
masih non-NULL dan buffer sudah diinisialisasi.
Contoh: Anotasi _Inout_opt_
_Inout_opt_
sama dengan _Inout_
, kecuali bahwa parameter input diizinkan untuk menjadi NULL dan, oleh karena itu, fungsi harus memeriksa ini.
void GoodInOutOptCallee(_Inout_opt_ int *pInt)
{
if(pInt != NULL) {
int i = *pInt;
*pInt = 6;
}
}
void BadInOutOptCallee(_Inout_opt_ int *pInt)
{
int i = *pInt; // Dereferencing NULL pointer 'pInt'
*pInt = 6;
}
void InOutOptCaller()
{
int *pInt = NULL;
GoodInOutOptCallee(pInt);
BadInOutOptCallee(pInt);
}
Analisis kode Visual Studio memvalidasi bahwa fungsi ini memeriksa NULL sebelum mengakses buffer, dan jika pInt
bukan NULL, bahwa buffer diinisialisasi oleh fungsi sebelum kembali.
Contoh: Anotasi _Outptr_
_Outptr_
digunakan untuk membuat anotasi parameter yang dimaksudkan untuk mengembalikan pointer. Parameter itu sendiri tidak boleh NULL, dan fungsi yang disebut mengembalikan pointer non-NULL di dalamnya dan pointer tersebut menunjuk ke data yang diinisialisasi.
void GoodOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 5;
*pInt = pInt2;
}
void BadOutPtrCallee(_Outptr_ int **pInt)
{
int *pInt2 = new int;
// Did not initialize pInt buffer before returning!
*pInt = pInt2;
}
void OutPtrCaller()
{
int *pInt = NULL;
GoodOutPtrCallee(&pInt);
BadOutPtrCallee(&pInt);
}
Analisis kode Visual Studio memvalidasi bahwa pemanggil melewati pointer non-NULL untuk *pInt
, dan bahwa buffer diinisialisasi oleh fungsi sebelum kembali.
Contoh: Anotasi _Outptr_opt_
_Outptr_opt_
sama dengan _Outptr_
, kecuali bahwa parameter bersifat opsional—pemanggil dapat meneruskan penunjuk NULL untuk parameter .
void GoodOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
if(pInt != NULL) {
*pInt = pInt2;
}
}
void BadOutPtrOptCallee(_Outptr_opt_ int **pInt)
{
int *pInt2 = new int;
*pInt2 = 6;
*pInt = pInt2; // Dereferencing NULL pointer 'pInt'
}
void OutPtrOptCaller()
{
int **ppInt = NULL;
GoodOutPtrOptCallee(ppInt);
BadOutPtrOptCallee(ppInt);
}
Analisis kode Visual Studio memvalidasi bahwa fungsi ini memeriksa NULL sebelum *pInt
didereferensikan, dan bahwa buffer diinisialisasi oleh fungsi sebelum kembali.
Contoh: Anotasi _Success_ dalam Kombinasi dengan _Out_
Anotasi dapat diterapkan ke sebagian besar objek. Khususnya, Anda dapat memberi anotasi seluruh fungsi. Salah satu karakteristik fungsi yang paling jelas adalah dapat berhasil atau gagal. Tetapi seperti hubungan antara buffer dan ukurannya, C/C++ tidak dapat mengekspresikan keberhasilan atau kegagalan fungsi. Dengan menggunakan _Success_
anotasi, Anda dapat mengatakan seperti apa keberhasilan untuk fungsi. Parameter pada anotasi _Success_
adalah ekspresi yang menunjukkan bahwa fungsi telah berhasil ketika ekspresi tersebut bernilai benar. Ekspresi bisa menjadi apa pun yang dapat ditangani pengurai anotasi. Efek anotasi setelah fungsi kembali hanya berlaku ketika fungsi berhasil. Contoh ini menunjukkan bagaimana _Success_
berinteraksi dengan _Out_
untuk melakukan hal yang benar. Anda dapat menggunakan kata kunci return
untuk mewakili nilai yang dikembalikan.
_Success_(return != false) // Can also be stated as _Success_(return)
bool GetValue(_Out_ int *pInt, bool flag)
{
if(flag) {
*pInt = 5;
return true;
} else {
return false;
}
}
Anotasi _Out_
membuat analisis kode Visual Studio memastikan bahwa pemanggil meneruskan pointer non-NULL ke buffer untuk pInt
, dan bahwa buffer tersebut diinisialisasi oleh fungsi sebelum proses tersebut selesai.
Praktik Terbaik SAL
Menambahkan Anotasi ke Kode yang Ada
SAL adalah teknologi canggih yang dapat membantu Anda meningkatkan keamanan dan keandalan kode Anda. Setelah belajar SAL, Anda dapat menerapkan keterampilan baru ke pekerjaan harian Anda. Dalam kode baru, Anda dapat menggunakan spesifikasi berbasis SAL berdasarkan desain di seluruh; dalam kode yang lebih lama, Anda dapat menambahkan anotasi secara bertahap dan dengan demikian meningkatkan manfaat setiap kali Anda memperbarui.
Header publik Microsoft sudah diberi anotasi. Oleh karena itu, kami menyarankan agar dalam proyek Anda, Anda terlebih dahulu menandai fungsi simpul daun dan fungsi yang memanggil API Win32, untuk mendapatkan manfaat maksimal.
Kapan saya Membuat Anotasi?
Berikut adalah beberapa panduan:
Buat anotasi semua parameter penunjuk.
Berikan anotasi rentang nilai sehingga Analisis Kode dapat menjamin keamanan buffer dan pointer.
Membuat anotasi aturan penguncian dan efek samping penguncian. Untuk informasi selengkapnya, lihat Anotasi Perilaku Penguncian.
Anotasi properti driver dan properti khusus domain lainnya.
Atau Anda dapat membuat anotasi semua parameter untuk memperjelas niat Anda secara keseluruhan dan memudahkan pemeriksaan apakah anotasi telah dilakukan.