0103、安全开发篇(一):基础语法
返回:安全开发篇(一)汇总 · 上一节:环境与框架 · 安全开发总目录
这一篇从 2.0 开始,主要整理 C 语言里最基础的一组表达方式:怎么命名、怎么定义变量、怎么表示常量、怎么输出、怎么接收输入。
这些内容看起来都很普通,但后面学指针、内存、结构体、文件读写和 Windows API 时,所有问题都会落回这些基础细节:类型是否匹配、缓冲区是否够用、格式化字符串是否正确、输入边界是否检查。
2.0 关键字以及命名规范
C 语言里的关键字是语言已经占用的词,不能再拿来当变量名、函数名或自定义标识符。
| 类别 | 常见关键字 | 作用 |
|---|---|---|
| 类型相关 | int、char、float、double、void |
描述数据类型或函数返回值 |
| 控制流程 | if、else、switch、case、for、while、do |
控制程序执行顺序 |
| 跳转控制 | break、continue、return、goto |
改变当前执行流程 |
| 存储相关 | auto、static、extern、register |
描述变量的存储方式或链接属性 |
| 类型限定 | const、volatile、signed、unsigned、short、long |
补充类型的取值范围或访问规则 |
| 自定义类型 | struct、union、enum、typedef |
组织更复杂的数据结构 |
| 大小计算 | sizeof |
计算类型或对象占用的字节数 |
标识符规则
变量名、函数名、结构体名都属于标识符。基本规则先记住这几条:
- 只能由字母、数字和下划线组成。
- 不能以数字开头。
- 不能使用 C 语言关键字。
- 区分大小写,
value和Value是两个不同名字。 - 尽量不要用中文、空格和奇怪符号,后续跨平台、命令行和脚本处理都会麻烦。
1 | int age = 20; |
命名习惯
命名不是为了“显得规范”,而是为了让几个月后的自己还能看懂这段代码。
| 场景 | 示例 | 说明 |
|---|---|---|
| 普通变量 | score、user_age |
直接表达变量含义 |
| 函数名 | print_menu、calc_sum |
尽量用动词或动宾结构 |
| 常量 | MAX_SIZE、BUFFER_LEN |
常量可以用全大写加下划线 |
| 临时变量 | i、j、tmp |
只适合很短的局部范围 |
| 布尔含义 | is_valid、has_error |
让条件判断更自然 |
2.1 变量定义及初始化
变量可以理解成一块有名字的内存空间。类型决定这块空间怎么解释,变量名方便我们在代码里使用它。
1 | int age = 20; |
| 概念 | 示例 | 说明 |
|---|---|---|
| 定义变量 | int age; |
创建一个变量 |
| 初始化 | int age = 20; |
创建变量时给初始值 |
| 赋值 | age = 21; |
变量已经存在,再修改它的值 |
| 读取变量 | printf("%d", age); |
使用变量当前保存的值 |
初始化很重要
没有初始化的局部变量,它里面可能是内存中原本残留的数据。初学阶段不要依赖这种“碰巧能跑”的结果。
1 |
|
作用域和生命周期
| 变量位置 | 作用范围 | 初学理解 |
|---|---|---|
| 函数内部 | 从定义处到所在代码块结束 | 局部变量,用完就结束 |
| 函数外部 | 当前文件或多个文件中可见 | 全局变量,谨慎使用 |
| 代码块内部 | {} 内部 |
只在这一小段逻辑里可见 |
1 |
|
2.2 类型限定符
类型限定符用来补充说明一个基础类型的范围、符号或访问方式。
| 写法 | 含义 | 例子 |
|---|---|---|
signed |
有符号,可以表示正负数 | signed int a = -10; |
unsigned |
无符号,只表示非负数 | unsigned int size = 100; |
short |
较短整数类型 | short s = 10; |
long |
较长整数类型 | long value = 1000L; |
long long |
更长整数类型 | long long big = 100000LL; |
const |
值不应该被修改 | const int max = 100; |
volatile |
值可能被外部因素改变 | 常见于硬件寄存器、多线程或系统编程场景 |
signed 与 unsigned
unsigned 很常见,比如数组长度、缓冲区大小、文件大小等。但它也容易带来比较问题。
1 |
|
安全开发里经常会遇到长度、索引和偏移。只要涉及 unsigned,就要特别小心减法、比较和边界判断,避免把一个负数转换成非常大的无符号数。
用 sizeof 验证类型大小
不同平台和编译器上,部分类型大小可能不同。不要只靠背,最好用 sizeof 验证。
1 |
|
2.3 常量特性及表现形式
常量是不希望在程序运行过程中被随意修改的数据。C 语言里常见常量可以分成字面量、宏常量、const 常量和枚举常量。
| 形式 | 示例 | 说明 |
|---|---|---|
| 整型字面量 | 10、0x10、010 |
十进制、十六进制、八进制 |
| 浮点字面量 | 3.14、1.0f |
小数常量 |
| 字符常量 | 'A'、'\n' |
单个字符 |
| 字符串常量 | "hello" |
一串字符,结尾包含 \0 |
| 宏常量 | #define MAX_SIZE 100 |
预处理阶段替换 |
const 常量 |
const int max = 100; |
有类型检查,更推荐 |
| 枚举常量 | enum Color { RED, BLUE }; |
表示一组具名整数值 |
不同进制
1 |
|
#define 和 const
1 |
|
| 写法 | 优点 | 注意 |
|---|---|---|
#define |
简单直接,常用于宏替换 | 没有类型检查,调试时不一定直观 |
const |
有类型,更像普通变量 | 在 C 里不一定能用于所有编译期常量场景 |
enum |
适合一组相关常量 | 本质上常作为整数处理 |
2.4 拓展:突破编译器限制
这里的重点不是鼓励写“绕过规则”的代码,而是理解:编译器提供的是一层类型和语法约束,真正运行时,数据最终还是落在内存字节上。
比如一个 int 在内存里就是若干个字节。我们可以通过字符指针观察它的底层字节表示。
1 |
|
这段代码适合用来观察大小端和内存布局,但不要把强制类型转换当成万能工具。越是接近内存底层,越要清楚自己在改哪块内存、按什么类型解释、边界在哪里。
const、类型转换、指针重解释这些内容后面会反复出现。学习时可以用小程序观察现象,但不要在正式代码里靠“强行转换”绕过类型约束,这类写法很容易变成未定义行为。
2.5 拓展:内存保护属性
程序运行时,内存不是一整块随便读写的空间。操作系统会按页管理内存,并给不同区域设置不同的访问属性。
| 区域 | 常见内容 | 常见属性理解 |
|---|---|---|
| 代码区 | 程序指令 | 可执行,通常不希望随意写 |
| 只读数据区 | 字符串常量、只读数据 | 可读,不可写 |
| 栈 | 局部变量、函数调用信息 | 可读写 |
| 堆 | 动态申请的内存 | 可读写 |
| 映射区域 | DLL、文件映射、系统库 | 属性取决于映射方式 |
从安全开发角度看,内存保护属性很重要。很多漏洞利用、防护机制和崩溃原因,本质上都和“这块内存能不能读、能不能写、能不能执行”有关。
常见访问异常
| 行为 | 可能结果 |
|---|---|
| 写入只读内存 | 访问冲突 |
| 执行不可执行内存 | 触发数据执行保护相关异常 |
| 读取无效地址 | 访问冲突 |
| 访问已经释放的堆内存 | 结果不确定,可能崩溃,也可能暂时看不出来 |
2.6 拓展:物理页属性探测
这里更准确地说,是在用户态观察“虚拟内存页”的信息。普通应用程序通常不能直接操作真实物理页,但可以查询自己进程里某个地址所在内存区域的状态、大小和保护属性。
在 Windows 下可以用 VirtualQuery 观察一块地址对应的内存信息。
1 |
|
这类代码暂时只当作“观察工具”来理解。后面学 Windows、PE、进程内存和调试时,再展开保护属性、内存区域、模块加载和权限变化。
这一节先记住一句话:地址不是孤立的数字,它属于某个内存区域,而这个区域有大小、状态和访问权限。
2.7 转义字符
转义字符用来表示一些不能直接写出来,或者写出来会产生歧义的字符。
| 转义字符 | 含义 |
|---|---|
\n |
换行 |
\t |
水平制表符 |
\\ |
反斜杠 |
\" |
双引号 |
\' |
单引号 |
\0 |
字符串结束符 |
\r |
回车 |
\b |
退格 |
1 |
|
\0 很重要
C 字符串以 \0 作为结束标志。后面学字符数组和字符串处理时,很多越界和乱码问题都和这个结束符有关。
1 |
|
text[3] 是字符串结尾的 \0,作为整数输出通常是 0。
2.8 数据输出与格式化字符串
printf 通过格式化字符串控制输出内容。
1 |
|
常见格式占位符
| 占位符 | 含义 | 示例 |
|---|---|---|
%d |
有符号十进制整数 | printf("%d", num); |
%u |
无符号十进制整数 | printf("%u", size); |
%x / %X |
十六进制整数 | printf("%X", value); |
%c |
字符 | printf("%c", ch); |
%s |
字符串 | printf("%s", text); |
%f |
浮点数 | printf("%f", score); |
%p |
地址 | printf("%p", ptr); |
%zu |
size_t 类型 |
printf("%zu", sizeof(int)); |
宽度和精度
1 |
|
| 写法 | 效果 |
|---|---|
%5d |
宽度为 5,右对齐 |
%-5d |
宽度为 5,左对齐 |
%.2f |
保留两位小数 |
%08d |
宽度为 8,不足补 0 |
格式化字符串安全
格式化字符串本身也是安全开发里很重要的点。不要把用户输入直接当作格式化字符串。
1 |
|
以后写日志、命令行工具或网络服务时,要养成 printf("%s", user_input) 这种习惯。格式化字符串错误虽然看起来像“小语法问题”,但在安全领域可以变成真正的漏洞入口。
2.9 数据交互传递
输入输出是程序和外界交换数据的第一步。初学阶段最常见的是控制台输入,后面会扩展到文件、网络、命令行参数和 GUI。
scanf 与 scanf_s
Visual Studio 中常提示使用 scanf_s。这是 MSVC 提供的安全版本,对部分输入需要额外传入缓冲区大小。
1 |
|
字符串输入要限制长度
1 |
|
%31s 的意思是最多读取 31 个字符,留 1 个位置给字符串结束符 \0。
用 fgets 读取一整行
如果输入里可能包含空格,可以用 fgets。
1 |
|
本章最小复盘代码
这段代码把变量、常量、格式化输出和基础输入放在一起。复盘时重点看:变量是否初始化、输入是否限制长度、格式占位符是否匹配。
1 |
|
常见错误整理
| 错误 | 示例 | 问题 |
|---|---|---|
| 变量未初始化 | int a; printf("%d", a); |
输出结果不可靠 |
| 格式不匹配 | printf("%d", 3.14); |
输出异常或产生未定义行为 |
| 忘记取地址 | scanf("%d", age); |
应该传 &age |
| 字符串输入不限制长度 | scanf("%s", name); |
可能写爆数组 |
| 混用有符号和无符号 | int 与 unsigned int 比较 |
结果可能不符合直觉 |
| 使用中文符号 | printf("hi"); |
编译器无法识别 |
| 误用八进制 | int x = 010; |
实际是十进制 8 |
| 直接输出用户输入 | printf(input); |
可能产生格式化字符串风险 |
学习检查清单
| 检查项 | 状态 |
|---|---|
| 能说出关键字和普通变量名的区别 | 待复盘 |
| 能按规则写出清晰的变量名和函数名 | 待复盘 |
| 能定义变量,并在使用前完成初始化 | 待复盘 |
| 能区分赋值、初始化和读取变量 | 待复盘 |
能解释 signed、unsigned、short、long 的基本含义 |
待复盘 |
能用 sizeof 观察不同类型的字节大小 |
待复盘 |
能区分字面量、宏常量、const 常量和枚举常量 |
待复盘 |
能解释 \n、\t、\\、\0 等常见转义字符 |
待复盘 |
能正确使用 %d、%u、%x、%c、%s、%f、%p、%zu |
待复盘 |
能说明为什么不能直接 printf(user_input) |
待复盘 |
能用 scanf_s、scanf 或 fgets 完成基础输入 |
待复盘 |
| 能给字符串输入设置长度边界 | 待复盘 |
| 能大致理解内存页有读、写、执行等访问属性 | 待复盘 |
这一篇的目标不是把语法背成表格,而是形成一种写 C 程序的基本警觉:变量要初始化,类型要匹配,输出要写对格式,输入要限制边界。






