Program kami memproses paket jaringan, khususnya, tajuk TCP / IP / etc. Di dalamnya, nilai numerik - offset, penghitung, alamat - disajikan dalam urutan byte jaringan (big-endian); kami sedang mengerjakan x86 (little-endian). Dalam struktur standar yang menggambarkan header, bidang ini diwakili oleh tipe integer sederhana (
uint16_t
,
uint16_t
). Setelah beberapa bug karena kami lupa mengubah urutan byte, kami memutuskan untuk mengganti tipe bidang dengan kelas yang melarang konversi implisit dan operasi atipikal. Di bawah cut adalah kode utilitarian dan contoh spesifik kesalahan yang pengetikan ketat terungkap.
Pesanan byte
Likbez bagi mereka yang tidak mengetahui urutan byte (endianness, byte order). Secara lebih rinci
sudah ada di "Habré" .
Dalam notasi angka yang biasa, mereka bergerak dari yang tertua (kiri) ke yang termuda (kanan) di sebelah kiri: 432
10 = 4 × 10
2 + 3 × 10
1 + 2 × 10
0 . Tipe data integer memiliki ukuran tetap, misalnya, 16 bit (angka dari 0 hingga 65535). Mereka disimpan dalam memori sebagai dua byte, misalnya, 432
10 = 01b0
16 , yaitu, byte 01 dan b0.
Cetak byte dari nomor ini:
#include <cstdio> // printf() #include <cstdint> // uint8_t, uint16_t int main() { uint16_t value = 0x01b0; printf("%04x\n", value); const auto bytes = reinterpret_cast<const uint8_t*>(&value); for (auto i = 0; i < sizeof(value); i++) { printf("%02x ", bytes[i]); } }
Pada prosesor Intel atau AMD (x86) biasa, kami mendapatkan yang berikut:
01b0 b0 01
Bytes dalam memori terletak dari yang termuda ke yang tertua, dan tidak seperti saat menulis angka. Pesanan ini disebut
little-endian (LE). Hal yang sama berlaku untuk angka 4-byte. Urutan byte ditentukan oleh arsitektur prosesor. Urutan "asli" untuk prosesor juga disebut
urutan CPU atau host (urutan byte CPU / host). Dalam kasus kami, urutan byte host adalah little-endian.
Namun, Internet tidak dilahirkan pada x86, dan di sana urutan byte berbeda -
dari yang tertua ke yang termuda (big-endian, BE). Mereka mulai menggunakannya di header protokol jaringan (IP, TCP, UDP), sehingga big-endian juga disebut
urutan byte jaringan.Contoh: port 443 (1bb
16 ), yang menggunakan HTTPS, ditulis dalam header TCP bytes bb 01, yang bila dibaca akan menghasilkan bb01
16 = 47873.
Urutan byte angka dapat dikonversi. Sebagai contoh, untuk
uint16_t
terdapat fungsi standar
htons()
(pertama-tama
dan selanjutnya untuk
s hort integer - dari pesanan host ke pesanan jaringan untuk bilangan bulat pendek) dan sebaliknya
ntohs()
. Demikian pula, untuk
uint32_t
ada
htonl()
dan
ntohl()
(panjang adalah bilangan bulat panjang).
Sayangnya, kompiler tidak tahu dari mana nilai tertentu dari variabel tipe
uint32_t
berasal, dan tidak memperingatkan jika Anda mencampur nilai dengan pesanan byte yang berbeda dan mendapatkan hasil yang salah.
Mengetik dengan kuat
Risiko membingungkan pesanan byte jelas, bagaimana menghadapinya?
- Ulasan kode. Ini adalah prosedur wajib dalam proyek kami. Sayangnya, penguji setidaknya ingin mempelajari kode yang memanipulasi byte: "Saya melihat
htons()
- mungkin, penulis memikirkan segalanya". - Disiplin, aturan seperti: BE hanya dalam paket, semua variabel di LE. Ini tidak selalu masuk akal, misalnya, jika Anda perlu memeriksa port terhadap tabel hash, lebih efisien untuk menyimpannya dalam urutan byte jaringan dan mencari "apa adanya".
- Tes. Seperti yang Anda tahu, mereka tidak menjamin tidak adanya kesalahan. Data dapat dicocokkan dengan buruk (1.1.1.1 tidak berubah saat mengubah urutan byte) atau disesuaikan dengan hasilnya.
Ketika bekerja dengan jaringan, Anda tidak dapat mengabaikan urutan byte, jadi saya ingin membuatnya tidak mungkin untuk mengabaikannya saat menulis kode. Selain itu, kami tidak hanya memiliki nomor di BE - itu adalah nomor port, alamat IP, nomor urut TCP, checksum. Satu tidak dapat ditugaskan ke yang lain, bahkan jika jumlah bit cocok.
Solusinya diketahui - pengetikan ketat, yaitu, tipe terpisah untuk port, alamat, angka. Selain itu, jenis ini harus mendukung konversi BE / LE.
Boost.Endian tidak cocok untuk kita, karena tidak ada Boost dalam proyek ini.
Ukuran proyek sekitar 40 ribu baris dalam C ++ 17. Jika Anda membuat tipe pembungkus yang aman dan menimpa struktur header pada mereka, semua tempat di mana ada pekerjaan dengan BE akan secara otomatis berhenti mengkompilasi. Anda harus melalui semuanya sekali, tetapi kode baru hanya akan aman.
Nomor kelas big-endian #include <cstdint> #include <iosfwd> #define PACKED __attribute__((packed)) constexpr auto bswap(uint16_t value) noexcept { return __builtin_bswap16(value); } constexpr auto bswap(uint32_t value) noexcept { return __builtin_bswap32(value); } template<typename T> struct Raw { T value; }; template<typename T> Raw(T) -> Raw<T>; template<typename T> struct BigEndian { using Underlying = T; using Native = T; constexpr BigEndian() noexcept = default; constexpr explicit BigEndian(Native value) noexcept : _value{bswap(value)} {} constexpr BigEndian(Raw<Underlying> raw) noexcept : _value{raw.value} {} constexpr Underlying raw() const { return _value; } constexpr Native native() const { return bswap(_value); } explicit operator bool() const { return static_cast<bool>(_value); } bool operator==(const BigEndian& other) const { return raw() == other.raw(); } bool operator!=(const BigEndian& other) const { return raw() != other.raw(); } friend std::ostream& operator<<(std::ostream& out, const BigEndian& value) { return out << value.native(); } private: Underlying _value{}; } PACKED;
- File header dengan tipe ini akan disertakan di mana-mana, jadi alih-alih
<iostream>
yang ringan <iosfwd>
. - Alih-alih
htons()
, dll. - intrinsik kompiler cepat. Secara khusus, mereka constexpr
propagasi konstan, oleh karena itu konstruktor constexpr
. - Terkadang sudah ada nilai
uint16_t
/ uint16_t
terletak di BE. Struktur Raw<T>
dengan panduan deduksi memungkinkan Anda dengan mudah membuat BigEndian<T>
darinya.
Poin kontroversial di sini adalah
PACKED
: struktur paket dianggap kurang optimal. Satu-satunya jawaban adalah mengukur. Tolok ukur kode kami tidak mengungkapkan adanya pelambatan. Selain itu, dalam hal paket jaringan, posisi bidang di header masih tetap.
Dalam kebanyakan kasus, BE tidak memerlukan operasi selain perbandingan. Nomor urut harus dilipat dengan benar dengan LE:
using BE16 = BigEndian<uint16_t>; using BE32 = BigEndian<uint32_t>; struct Seqnum : BE32 { using BE32::BE32; template<typename Integral> Seqnum operator+(Integral increment) const { static_assert(std::is_integral_v<Integral>); return Seqnum{static_cast<uint32_t>(native() + increment)}; } } PACKED; struct IP : BE32 { using BE32::BE32; } PACKED; struct L4Port : BE16 { using BE16::BE16; } PACKED;
Struktur TCP Header Aman enum TCPFlag : uint8_t { TH_FIN = 0x01, TH_SYN = 0x02, TH_RST = 0x04, TH_PUSH = 0x08, TH_ACK = 0x10, TH_URG = 0x20, TH_ECE = 0x40, TH_CWR = 0x80, }; using TCPFlags = std::underlying_type_t<TCPFlag>; struct TCPHeader { L4Port th_sport; L4Port th_dport; Seqnum th_seq; Seqnum th_ack; uint32_t th_flags2 : 4; uint32_t th_off : 4; TCPFlags th_flags; BE16 th_win; uint16_t th_sum; BE16 th_urp; uint16_t header_length() const { return th_off << 2; } void set_header_length(uint16_t len) { th_off = len >> 2; } uint8_t* payload() { return reinterpret_cast<uint8_t*>(this) + header_length(); } const uint8_t* payload() const { return reinterpret_cast<const uint8_t*>(this) + header_length(); } }; static_assert(sizeof(TCPHeader) == 20);
TCPFlag
dapat dibuat enum class
, tetapi dalam praktiknya hanya dua operasi yang dilakukan pada flag: memeriksa entri ( &
) atau mengganti flag dengan kombinasi ( |
) - tidak ada kebingungan.- Bidang bit dibiarkan primitif, tetapi metode akses yang aman dibuat.
- Nama bidang dibiarkan klasik.
Hasil
Sebagian besar pengeditan adalah hal sepele. Kode lebih bersih:
auto tcp = packet->tcp_header(); - return make_response(packet, - cookie_make(packet, rte_be_to_cpu_32(tcp->th_seq)), - rte_cpu_to_be_32(rte_be_to_cpu_32(tcp->th_seq) + 1), - TH_SYN | TH_ACK); + return make_response(packet, cookie_make(packet, tcp->th_seq.native()), + tcp->th_seq + 1, TH_SYN | TH_ACK); }
Pada bagian, tipe mendokumentasikan kode:
- void check_packet(int64_t, int64_t, uint8_t, bool); + void check_packet(std::optional<Seqnum>, std::optional<Seqnum>, TCPFlags, bool);
Tiba-tiba, ternyata Anda dapat salah membaca ukuran jendela TCP, sementara tes unit akan berlalu dan bahkan lalu lintas akan dikejar:
// window size auto wscale_ratio = options().wscale_dst - options().wscale_src; if (wscale_ratio < 0) { - auto window_size = header.window_size() / (1 << (-wscale_ratio)); + auto window_size = header.window_size().native() / (1 << (-wscale_ratio)); if (header.window_size() && window_size < 1) { window_size = WINDOW_SIZE_MIN; } header_out.window_size(window_size); } else { - auto window_size = header.window_size() * (1 << (wscale_ratio)); + auto window_size = header.window_size().native() * (1 << (wscale_ratio)); if (window_size > WINDOW_SIZE_MAX) { window_size = WINDOW_SIZE_MAX; }
Contoh kesalahan logis: pengembang kode asli berpikir bahwa fungsi menerima BE, meskipun sebenarnya tidak. Saat mencoba menggunakan
Raw{}
alih-alih
0
program tidak kompilasi (untungnya, ini hanya tes unit). Segera kami melihat pilihan data yang gagal: kesalahan akan ditemukan lebih cepat jika tidak 0 telah digunakan, yang sama dalam urutan byte apa pun.
- auto cookie = cookie_make_inner(tuple, rte_be_to_cpu_32(0)); + auto cookie = cookie_make_inner(tuple, 0);
Contoh serupa: pertama, kompiler menunjukkan ketidakcocokan antara
def_seq
dan jenis
cookie
, kemudian menjadi jelas mengapa tes lulus sebelumnya - konstanta seperti itu.
- const uint32_t def_seq = 0xA7A7A7A7; - const uint32_t def_ack = 0xA8A8A8A8; + const Seqnum def_seq{0x12345678}; + const Seqnum def_ack{0x90abcdef}; ... - auto cookie = rte_be_to_cpu_32(_tcph->th_ack); + auto cookie = _tcph->th_ack; ASSERT_NE(def_seq, cookie);
Ringkasan
Intinya adalah:
- Ditemukan satu bug dan beberapa kesalahan logis dalam unit test.
- Refactoring membuat saya memilah-milah tempat meragukan, keterbacaan meningkat.
- Performanya telah dipertahankan, tetapi bisa saja menurun - diperlukan tolok ukur.
Ketiga poin itu penting bagi kami, jadi kami pikir refactoring sepadan.
Apakah Anda mengasuransikan diri terhadap kesalahan dengan tipe ketat?