C和C++安全编码笔记:总结

《C和C++安全编码》(原书第2版)这本书是2013年出版的。

这里是基于之前所有笔记的简单总结,笔记列表如下:

字符串:https://blog.csdn.net/fengbingchun/article/details/105325508

指针诡计:https://blog.csdn.net/fengbingchun/article/details/105458861

动态内存管理:https://blog.csdn.net/fengbingchun/article/details/105921174

整数安全:https://blog.csdn.net/fengbingchun/article/details/106444980

格式化输出:https://blog.csdn.net/fengbingchun/article/details/106728792

并发:https://blog.csdn.net/fengbingchun/article/details/106962487

文件I/O:https://blog.csdn.net/fengbingchun/article/details/107138261

下面是对每章中关键语句的摘记:

1. 字符串

在获取一个数组的大小时,不要对一个指针应用sizeof运算符。

每个UTF-8字符由1~4个字节表示。

宽字符串字面值除了以字面L作为前缀外,其它的表示方式与字符串字面值相同。

不要试图修改字符串字面值。

不要指定一个用字符串字面值初始化的字符数组的界限。

在标准C++的string类中,其内部表示并不一定非得是以空字符结尾的,虽然所有常见的实现都是以空字符结尾的。

存储在unsigned char类型对象中的值,保证会当作一个纯粹的二进制表示法来表示属性值。

任何类型的非二进制位域(non-bit-field)的对象都可以复制到一个unsigned char数组中(例如,通过memcpy()),并每次1个字节地检查它们的表示形式。

保证字符串的存储空间具有容纳字符数据和空终结符的足够空间。

不要从一个无界源复制数据到定长数组。

不要使用废弃或过时的函数。

如果一个字符串没有以空字符结尾,程序可能会被欺骗,导致在数组边界之外读取或写入数据。

空终止字符之所以是必要的,是因为前面这些函数以及其它由C标准定义的字符串处理函数,都依赖于它的存在来标记字符串的结尾。

空字符结尾的字符串是用字符数组实现的。

C11附录K边界检查接口:设计目的主要是实现现有函数的更安全的替代品。例如,C11附录K定义了strcpy_s、strcat_s、strncpy_s和strncat_s函数,分别作为strcpy、strcat、strncpy和strncat的替代品,适用于源字符串长度未知的或保证小于已知目标缓冲区大小的情况。

basic_string类实现了”由被调用者分配,由被调用者释放”的内存管理策略。

注意std::string的下标成员std::string::operator[](不执行边界检查)不会抛出异常。

永远不要使用gets(),它不对缓冲区溢出进行任何检测。

如果字符数组不是正确地以空字符结尾的,strlen()函数可能会返回一个错误的超大的数值,使用它时,就可能会导致漏洞。

任何到达某个跨越信任边界的程序接口的数据都需要验证。

2. 指针诡计(pointer subterfuge):是通过修改指针值来利用程序漏洞的方法的统称。

atexit()是C标准定义的一个通用工具函数。atexit()可以注册无参函数,并在程序正常结束后调用该函数。atexit()通过向一个退出时将被调用的已有函数的数组中添加指定的函数完成工作。

防止指针诡计的最佳方式就是消除”允许内存被不正确地覆写”的漏洞。

3. 动态内存管理

free(void* p):如果p是一个空指针,则不执行任何操作。

对齐:完整的对象类型有对齐(alignment)要求,这种要求对可以分配该类型对象的地址施加限制。对齐是实现定义的整数值,它表示可以在一个连续的地址之间分配给指定的对象的字节数量。对象类型规定了每一个该类型对象的对齐要求。

每个有效对齐值都是2的一个非负整数幂。

不要假定内存分配函数初始化内存。

不要引用未初始化的内存。

内存分配函数的返回值表示分配失败或成功。如果请求的内存分配失败,那么aligned_alloc、calloc、malloc和realloc函数返回空指针。

空指针的解引用通常会导致段错误,但并非总是如此。许多嵌入式系统有映射到地址0处的寄存器,因此覆写它们会产生不可预知的后果。在某些情况下,解引用空指针会导致任意代码的执行。

不要执行零长度分配。

连续调用分配函数分配的存储的顺序、连续性、初始值都是不确定的。

C++在发起一个为零的请求时行为与C不同,它返回一个非空指针。

通常情况下,分配函数无法分配存储时抛出一个异常表示失败,这个异常将匹配类型为std::bad_alloc的异常处理器。

如果用std::nothrow参数调用new,当分配失败时,分配函数不会抛出一个异常。相反,它将返回一个空指针。

提供给一个释放函数的第一个参数的值可以是一个空指针值,如果是这样的话,并且如果释放函数是标准库提供的,那么该调用没有任何作用。

C++的内存分配和释放函数分配和释放内存的方式可能不同于C内存分配和释放函数分配和释放内存的方式。因此,在同一资源上混合调用C++内存分配和释放函数及C内存分配和释放函数是未定义的行为,并可能会产生灾难性的后果。

new和operator new():可以直接调用operator new()分配原始内存,但不调用构造函数。

释放函数必须避免抛出异常。

一个明显的可以减少C和C++程序中漏洞数量的技术就是在指针所引用的内存被释放后,将此指针设置为NULL。如果指针被设置为NULL,内存可以被”释放”多次而不会导致不良后果。

4. 整数安全

特定于编译器和平台的整数极值记录在头文件中。牢记这些值都是特定于平台的。出于可移植性的考虑,在代码中应该使用具名常量而不是实际的值。

C标准允许的负数表示方法有三种,分别是原码表示法(sign and magnitude)、反码表示法(one’s complement)和补码表示法(two’s complement)。若要取一个原码的相反数,只要改变符号位。若要取一个反码的相反数,需要改变每一位(包括符号位)。若要取一个补码的相反数,首先构造反码的相反数,然后再加1(在需要时进位)。

用补码表示的一个给定类型最小负值的相反数不能以那种类型表示。

在把char型用于数值时仅使用明确的signed char或unsigned char型。

整数提升保留值,其中包括符号。如果在所有的原始值中,较小的类型可以被表示为一个int,那么:原始值较小的类型会被转换成int;否则,它被转换成unsigned int。

之所以需要整数类型提升,主要是为了防止运算过程中中间结果发生溢出而导致算术错误,也为了在该架构中以自然的大小执行操作。

当从一个无符号类型转换为有符号类型时,应验证范围。

当从一个有符号类型转换到精度较低的有符号类型时,应验证范围。从较高精度的有符号类型转换为较低精度的有符号类型需要同时对上限和下限进行检查。

从有符号类型转换为无符号类型时,应验证取值范围。

唯一的对所有数据值和所有符号标准的实现都保证安全的整数类型转换是转换为符号相同而宽度更宽的类型。

在C中有符号溢出是未定义的行为,允许实现默默地回绕(最常见的行为)、陷阱、饱和(固定在最大值/最小值中),或执行实现选择的其它任何行为。

使用静态断言static_assert来测试一个常数表达式的值。

不要移动一个负的位数或移动比操作数中存在的位数更多的位。

移位运算符和其它位运算符应仅用于无符号整数操作数。

应使用无符号整数表示不可能是负数的整数值,而且应使用有符号整数值表示可以为负的值。在一般情况下,应该使用完全可以代表任何特定变量可能值的范围的最小的有符号或无符号类型,以节省内存。

5. 格式化输出

格式化输出函数是由一个格式字符串和可变数目的参数构成的。

变参函数是通过使用一个部分参数列表后跟一个省略号进行声明的。省略号必须出现在参数列表的最后。参数列表的终止条件是函数的实现者和使用者之间的一个契约。

格式字符串:是由普通字符(ordinary character)(包括%)和转换规范(conversion specification)构成的字符序列。当参数多于转换规范时,多余的将被忽略,而当参数不足时,则结果是未定义的。

为了消除格式字符串漏洞,推荐在可能的情况下使用iostream代替stdio,在没有条件的情况下则要尽量使用静态格式字符串。

6. 并发

并发是一种系统属性,它是指系统中几个计算同时执行,并可能彼此交互。一个并发程序通常使用顺序线程和(或)进程的一些组合来执行计算,其中每个线程和进程执行可以在逻辑上并行执行的计算。

多线程不一定是并发的。

所有的并行程序都是并发的,但不是所有的并发程序都是并行的。这意味着并发程序既可以用交错、时间分片的方式执行又可以并行执行。

对不能缓存的数据使用volatile。当一个变量被声明为volatile时,就会禁止编译器对该内存位置的读取和写入顺序进行重新排列。

在重组程序方面,编译器具有非常大的自由度。

C和C++都支持几种不同类型的同步原语,包括互斥变量(mutex variable)、条件变量(condition variable)和锁变量(lock variable)。

锁机制导致一个或多个线程等待,直到另一个线程退出临界区。

一个线程锁定一个互斥量后,任何后续试图锁定该互斥量的线程都将被阻止,直到此互斥量被解锁为止。

互斥量可以包装在临界区,以使它们序列化,从而使程序是线程安全的。互斥量不与任何其它数据关联。它们只是作为锁对象。

在用C++编程时,如果发生临界区抛出异常,或退出时没有明确地对互斥量解锁,我们建议使用锁卫士缓解这些问题。

原子操作是不可分割的。也就是说,一个原子操作不能被任何其它的操作中断,当正在执行原子操作时,它访问的内存,也不可以被任何其它机制改变。

原子对象不存在数据竞争,虽然它们仍然可能会受到竞争条件的影响。

为了使一个函数成为线程安全的,它必须同步访问共享资源。

7. 文件I/O

特殊文件:包括目录、符号链接、命名管道、套接字和设备文件。

文本流stdin、stdout和stderr是FILE指针类型的表达式。

文件描述符是每一个进程为了文件访问的目的,用来识别一个打开的文件的唯一的非负整数。文件描述符只是一个标识符或句柄,它实际上并没有描述什么。

超级UID(root)拥有一个为0的UID,并可以访问任何文件。

文件权限一般都用八进制值的向量表示。

权限字符串的第一个字符表示文件类型:普通-、目录d、符号链接l、设备b/c、套接字s或FIFO f/p。

目录内的特殊文件名”.”指的是目录本身,”..”指的是目录的父目录。作为一种特例,在根目录中,”..”可能指的是根目录本身。

符号链接是特殊的文件,其中包含了实际文件的路径名。

对同步原语的使用要求我们小心翼翼地将临界区的大小减到最小。

当宣告一个函数为线程安全的时候,就意味着作者相信这个函数可以被并发线程调用,同时该函数不会导致任何竞争条件问题。

GitHub:https://github.com/fengbingchun/Messy_Test

你可能感兴趣的:(C/C++/C++11)