C++核心检查中的新安全规则

锈和C++是两种流行的系统编程语言。 多年来,C++的焦点一直是性能方面的问题。 W 我们的听力越来越差 客户和安全研究人员的呼吁,C++应该具有更强的语言安全保证。 在编程安全方面,C++经常会生锈。 Visual Studio 2019版本16.7 包含 四条新规定 在C++核心检查中 将锈蚀的安全特性融入C++ .

null

更多详细信息 C++核心检查 ,请看 C++核心指南检查器引用 文档。 如果您刚刚开始使用本机代码分析工具,请看一下我们的介绍 C/C++代码分析的快速启动 .

RISE的模式匹配构造可以类似于C++使用。 switch 声明。然而,它们不同的一种方式是,锈当量要求程序员覆盖所有可能的p 模式匹配。这可以通过为每个对象编写显式处理程序来实现 图案或 为未明确涵盖的案例追加默认处理程序。

例如,如果默认处理程序 失踪 g。

// i32 == 32-bit signed integer 
fn printDiceRoll(roll: i32) { 
    match roll { 
        1 => println!("one!"), 
        2 => println!("two!"), 
        3 => println!("three!"), 
        4 => println!("four!"), 
        5 => println!("five!"), 
        6 => println!("six!"), 
        _ => println!("what kind of dice are you using?") // default handler 
    } 
}

这是一个整洁的小安全功能,因为它可以防止这种情况 非常容易 使,但不是那么容易抓住,progr 安明误差。

VisualStudio会在出现 enum 类型不包含在C++切换语句中。但是,对于其他类型(如整数)不存在此类警告,如上面的示例中所示。

本次发布 引入一个新的检查,以在switch语句切换到非- 枚举 类型(即。, char , int ,…)缺少 default 标签。 你可以在这张支票上找到详细的文件 在这里 . 要在VisualStudio中启用此规则,必须为项目选择规则集“C++核心检查样式规则”、“C++核心检查规则”或“Microsoft所有规则”,然后运行代码分析。

重写 g以上C++中的锈迹示例, 我们会的 得到像这样的东西 在下面。

void printDiceRoll(int roll) { 
    switch (roll) { 
        case 1: 
            std::cout << "one"; 
            break; 
        case 2: 
            std::cout << "two"; 
            break; 
        case 3: 
            std::cout << "three"; 
            break; 
        case 4: 
            std::cout << "four"; 
            break; 
        case 5: 
            std::cout << "five"; 
            break; 
        case 6: 
            std::cout << "six"; 
            break; 
        default: 
            std::cout << "what kind of dice are you using?"; 
            break; 
    } 
}

移除t default 处理程序现在会产生一个警告 .

void printDiceRoll(int roll) { 
    switch (roll) { // warning C26818: Switch statement does not cover all cases. Consider adding a 'default' label (es.79) 
        case 1: 
            std::cout << "one"; 
            break; 
        case 2: 
            std::cout << "two"; 
            break; 
        case 3:
            std::cout << "three"; 
            break; 
        case 4: 
            std::cout << "four"; 
            break; 
        case 5: 
            std::cout << "five"; 
            break; 
        case 6: 
            std::cout << "six"; 
            break; 
    } 
}

生锈的另一个限制 match 声明是它不支持 fallthrough 在案件之间。在C++中,另一方面,下面的代码是完全有效的 .

enum class Food { 
    BANANA, ORANGE, PIZZA, CAKE, KALE, CELERY 
}; 

void takeFromFridge(Food food) { 
    switch (food) { 
        case Food::BANANA: 
        case Food::ORANGE: 
            peel(food);   // implicit fallthrough 
        case Food::PIZZA: 
        case Food::CAKE: 
            eat(food); 
            break; 
        case Food::KALE: 
        case Food::CELERY: 
            throwOut(food);         
            break; 
    } 
}

虽然这个例子很好,但很含蓄 失败 两种情况之间可以很容易地是一个b ug公司。比如说,上面函数的程序员忘记了 break 通话后的声明 eat(food) . 代码将运行,但行为将完全不正确。使用更大更复杂的代码库,跟踪这个 错误的类型可以是 困难的 .

幸运的是,用C++ 17增加了 [[fallthrough]] 注释,其目的是标记 失败 在大小写标签之间,比如在上面的例子中,这样代码的维护者就可以确信 e 失败 行为是有意的。

Visual Studio 2019版本16.7 ,警告C26819 当一个非空的开关箱掉入下一个箱而没有标记 失败 使用 [[fallthrough]] 注释。详细do 文件可以找到 在这里 . 在visualstudio中默认启用此规则 运行代码分析时。

void takeFromFridge(Food food) { 
    switch (food) { 
        case Food::BANANA: // empty case, fallthrough annotation not needed 
        case Food::ORANGE: 
            peel(food);    // warning C26819: Unannotated fallthrough between switch labels (es.78) 
        case Food::PIZZA:  // empty case, fallthrough annotation not needed 
        case Food::CAKE: 
            eat(food); 
            break; 
        case Food::KALE:  // empty case, fallthrough annotation not needed 
        case Food::CELERY: 
            throwOut(food);         
            break; 
    } 
}

若要修复此警告,请插入 [[fallthrough]] 陈述 .

void takeFromFridge(Food food) { 
    switch (food) { 
        case Food::BANANA: 
        case Food::ORANGE: 
            peel(food); 
            [[fallthrough]]; // the fallthrough is intended 
        case Food::PIZZA: 
        case Food::CAKE: 
            eat(food); 
            break; 
        case Food::KALE: 
        case Food::CELERY: 
            throwOut(food);         
            break; 
    } 
}

Rust和C++的主要区别是锈是M。 默认为ove而不是copy。

一些防锈代码:

struct MySequence {
    sequence: Vec<i32>
}

fn main() {
    let mut a = MySequence { sequence: vec![0, 1, 1, 2, 3, 5, 8, 13] };
    let mut _b = a;
    a.sequence.push(21); // error! `a` was moved into `b` and can no longer be used
}

这意味着在大多数情况下,无论何时需要复制,都必须使用显式的复制语义。

#[derive(Clone)]
struct MySequence {
    sequence: Vec<i32>
}

fn main() {
    let mut a = MySequence { sequence: vec![0, 1, 1, 2, 3, 5, 8, 13] };
    let mut _b = a.clone();
    a.sequence.push(21); // much better
}

另一方面,C++默认是复制。这不是一个问题 一般但是 有时可能是bug的来源。一种情况是 通常在语句的范围内发生。以下面的代码为例 .

struct Person { 
    std::string first_name; 
    std::string last_name; 
    std::string email_address; 
}; 

void emailEveryoneInCompany(const std::vector<Person>& employees) { 
    Email email; 
    for (Person p: employees) { // copy of type `Person` occurs on each iteration 
        email.addRecipient(p.email_address); 
    } 
    email.addBody("Sorry for the spam"); 
    email.send(); 
}

在上面的代码片段中,向量的每个元素都被复制到 p 在循环的每次迭代中。这一点并不明显,如果拷贝成本很高,这可能是导致效率低下的一个重要原因。为了弥补这个不必要的副本, 我们补充道 一个新的 C++核心检查规则 ,建议删除副本的方法 .

void emailEveryoneInCompany(const std::vector<Person>& employees) { 
    Email email; 
    for (Person p: employees) { // Warning C26817: Potentially expensive copy of variable 'p' in range-for loop. Consider making it a const reference (es.71) 
        email.addRecipient(p.email_address); 
    } 
    email.addBody("Sorry for the spam"); 
    email.send(); 
}

通过使用警告中的建议并更改变量的类型 p 从a循环 Person const Person& ,变量不再在每次迭代时接收昂贵的数据副本。

void emailEveryoneInCompany(const std::vector<Person>& employees) { 
    Email email; 
    for (const Person& p: employees) { // expensive copy no longer occurs 
        email.addRecipient(p.email_address); 
    } 
    email.addBody("Sorry for the spam"); 
    email.send(); 
}

为了决定什么是“昂贵的”拷贝,che使用以下启发式方法 ck公司:

如果类型的大小大于平台相关指针大小的两倍,并且类型不是智能指针或 gsl ::span , gsl :: string_span ,或 std:: string_view ,那么这个拷贝就被认为是昂贵的。这意味着对于小型 数据类型 如数字标量,则不会触发警告。对于较大的类型,例如 Person 在上面的例子中输入,副本被认为是昂贵的,并会发出警告。

最后一点要注意的是,如果t 这个变量在循环体中发生了变异 .

struct Person { 
    std::string first_name; 
    std::string last_name; 
    int hourlyrate; // dollars per hour 
}; 

void giveEveryoneARaise(const std::vector<Person>& employees) { 
    for (Person p: employees) { 
        p.hourlyrate += 10; // `p` can no longer be marked `const Person&`, so the copy is unavoidable 
    } 
}

如果容器 不是 const限定,则可以通过更改类型来避免复制 Person Perso n& .

void giveEveryoneARaise() { 
    std::vector<Person> employees = getEmployees(); 
    for (Person& p: employees) { // no more expensive copying, but any subsequent mutation will change the container! 
        p.hourlyrate += 10; 
    } 
}

但这种变化伴随着代码引入了新的副作用。因此,复制警告的范围仅建议 标记 循环变量为 const T& ,和 如果无法合法标记循环变量,则不会激发 const .

可以找到支票的完整文件 在这里 . 在visualstudio中默认启用此规则 运行代码分析时。

此版本中的最后一个新检查涉及昂贵的副本 发生 机智 h使用 auto 类型。

考虑下面的例子,在这个例子中,被指定引用的变量发生类型解析。

struct PasswordManager { 
    password: String 
} 

impl PasswordManager { 
    // member-access function returning an immutable reference to a member 
    fn getPassword(&self) -> &String { &self.password } 
    // Note: for the sake of an example dealing with expensive types, a &String is being returned. 
    // More realistically though, a string slice would be returned instead (similar to a string view in C++) 
} 

fn stealPassword(pm: &PasswordManager) { 
    let password = pm.getPassword(); // the type of `a` resolves to `&String`. No copy occurs. 
}

因为生锈的要求 这个 大多数 案例 复制必须是显式的,类型为 password 在本例中,当分配一个不可变引用时,自动解析为一个不可变引用,并且不执行昂贵的复制。

另一方面,考虑 C++代码 .

class PasswordManager { 
    std::string password; 
public: 
    const std::string& getPassword() const { return password; }  
}; 

void stealPassword(const PasswordManager& pm) {
    auto password = pm.getPassword(); // the type of `password` resolves to `std::string`. Copy occurs.
}

这里是 password 决心 std:: string ,即使 getPassword () 是一个 常量引用 一根绳子。其结果是 Pas swordManager :: password 复制到局部变量中 password .

将其与返回指针的函数进行比较:

class PasswordManager {
    std::string password;
public:
    const std::string* getPassword() const { return &password; }
};

void stealPassword(const PasswordManager& pm) {
    auto password = pm.getPassword(); // the type of `password` resolves to `const std::string*`. No copy occurs.
}

指定参照和点之间的行为差异 对标记为 auto 不明显,导致可能不需要和意外的复制。

为了防止这种行为引起的错误,检查器检查所有初始化实例,从对标记为的变量的引用开始 auto . 如果使用与要检查的范围相同的启发式方法,结果副本被认为是昂贵的,则检查器警告要标记变量 const auto& 相反。

class PasswordManager {
    std::string password;
public:
    const std::string& getPassword() const { return password; }
};

void stealPassword(const PasswordManager& pm) {
    auto password = pm.getPassword();  // Warning C26820: Assigning by value when a const-reference would suffice, use const auto&amp; instead (p.9)
}

A 与要检查的范围一样,当变量不能合法标记时,不会引发此警告 const .

std::string hashPassword(const PasswordManager& pm) {
    auto password = pm.getPassword(); // warning no longer gets raised because `password` is modified below
    password += "salt";
    return std::hash(password);
}

另一个不会引发警告的实例是,只要引用是从临时引用派生的。在这种情况下,使用 const a uto& 一旦临时文件被销毁,就会产生一个悬空引用。

class PasswordManager {
    std::string password;
public:
    PasswordManager(const std::string& password): password(password) {}
    const std::string& getPassword() const { return password; }
};

void stealPassword() {
    const auto& password = PasswordManager("123").getPassword(); // using `const auto&` instead of just `auto`
    use_password(password); // `password` is now referencing invalid memory!
}

可以找到支票的完整文件 在这里 . 在visualstudio中默认启用此规则 运行代码分析时。

查看这些新添加的规则和最近发布的 GSL 3.0级 图书馆 告诉我们他们 帮助 你写的更安全的C++。请继续关注我们的安全措施 规则 在VisualStudio的未来版本中。

下载 Visual Studio 2019版本16.7 今天就来试试。 我们会的 很高兴收到您的来信,帮助我们确定优先级并为您构建合适的功能。我们可以通过以下评论联系到您, 开发者社区 , 还有推特( @视觉 ). 提交bug或建议特性的最佳方法是通过开发者社区。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享