本节必须掌握的知识点:
示例三源代码
代码分析
汇编解析
变量可以分为全局变量、局部变量、static静态变量和STL线程存储变量。在函数内定义的变量,叫做局部变量。在函数外定义的变量,叫做全局变量。本节先简单介绍局部变量的使用方法。我们将在第九章函数中详细讲解局部变量、全局变量和static静态变量。STL线程存储变量我们将在《Windows API每日一练》一书中详细讲解。
2.2.1 示例三
■变量和声明
示例三声明两个int类型的变量a和b。int为英文单词integer的缩写,为有符号数整型变量,可以是负整数,也可以是正整数。
鼠标选中VS左侧解决方案资源管理器“源文件”,点击鼠标右键,选择“添加”>“新建项(W)…”,新建项目2-2-1.c。编辑源代码如下所示:
示例代码3
/*
为两个变量赋整数值并显示
*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int a = 1;//声明int类型变量a,并赋初始值1。
int b = 2;//声明int类型变量b,并赋初始值2。
printf("a=%d\tb=%d\n", a, b);//将变量a和b以十进制整数输出;\t是制表符
a = b;//将b的值赋给a,此时a的值发生了变化。
printf("a=%d\tb=%d\n", a, b);//打印的a是发生变化后的值, b并没有变化。
b = a + b;//将b+a的值赋给b,此时b的值发生了变化。
a = b + a;//将b+a的值赋给a,此时a的值发生了变化。
printf("a=%d\tb=%d\n", a, b);//此时打印的是发生变化后变量a和b的值。
system("pause"); //在程序执行return 0; 之前暂停
return 0;
}
注意
1.要使用变量必须通过声明明确其类型和名称。
2.变量的使用必须遵循3个原则:一是有确定的地址;二是有已分配的存储空间;三是已完成初始化。
3.在实际工程中,变量的命令要有实际意义,而不是示例代码中的字母符号。
4.system函数是C语言工具库中的函数,在头文件stdlib.h中声明。参数"pause"为系统命令行参数,功能为暂停批处理文件的处理并显示消息。如果直接运行编译后的exe程序,控制台窗口会一闪而过,无法看到输出的结果。添加system函数调用可以在程序结束前暂停,方便观察控制台窗口输出的结果。
5.C语言表达式中的运算符前后添加空格,可以增强代码的可读性。
6.VS中int类型为32位有符号整数,取值范围是-231~231-1。但在其他编译器中,int类型有可能是16位有符号整型。在定义int类型变量时需要注意其取值范围。
●输出结果:
a=1 b=2
a=2 b=2
a=6 b=4
2.2.2 代码分析
■变量的定义
变量声明格式:
变量类型 变量名 = 初始值(或不赋初始值)。
示例三中的定义了两个变量,其中变量a赋初始值为1,变量b没有赋初始值。
例:
int a = 1; //赋初识值,已初始化
其真实的含义为:在偏移地址a处,以int类型为单位,分配4个字节空间,并存入初始值1。对照变量三原则,变量a可以直接在代码中使用。
int b; //未初始化
其真实含义为:在偏移地址b处,以int类型为单位,分配4个字节空间,未存入初始值。对照变量三原则,变量b未初始化前不可以在代码中直接使用。
上述语句也可以换一种写法:
int a,b; //定义int类型变量a和b
a= 1; //将变量a赋初始值1
■变量的命名规则
在C语言中,变量的命名有明确规则:
只能由字母、数字、下划线组成;
第一个字符必须是英文字母;
有效长度为255个字符;
不可以包含标点符号和类型说明符(%、&、!、#、@、$);
不可以是关键词。
●关键词:
由ANSI标准定义的C语言关键字共32个:auto、 double、 int、 struct、 break 、else、 long、 switch、case、 enum、 register 、typedef、 char 、extern 、return、 union 、const、 float 、short、 unsigned、 continue、 for、 signed 、void、default 、goto、 sizeof 、volatile、 do 、if 、while、 static。
上述32个关键字已经被C语言本身使用,不能作为其他用途使用,比如不能定义成变量名、函数名。
●常用的命名规则
1.驼峰命名法:
特点:多个单词组合,除第一个单词外每个单词的首字母大写(也称小驼峰)。
示例:iPadMini,mciSendSrting。
2.帕斯卡命名法:
特点:每一个单词的首字母大写,其余小写(也称大驼峰)。
示例:FirstName,OuGuang。
3.匈牙利命名法:
开头字母用变量类型的缩写,其余部分用变量的英文或者英文缩写,单词首字母大写。
示例:iMyAge,cMyName,fManHeight。
4.全大写:通常用于常量宏定义
示例:#define MAXSIZE 10。
5._t :一般是别名
示例:size_t,time_t。
举例
正确的变量命名:
int nName = 11;
int i_Age = 18;
错误的变量命名:
int 1Name = 0; 不能以数字开头!只能是字母、数字、下划线组成。
int case = 12; 不能以关键字作为变量名!只能是字母、数字、下划线组成。
int %age = 13; 不能用标点符号!只能是字母、数字、下划线组成。
int name age = 12; 不能用空格!只能是字母、数字、下划线组成。
具体命名形式请查看【附录C--代码规范】目前阶段只需要了解。
■变量的初始化
实验十:变量未初始化
●第一步:在VS中将示例三的变量声明修改为:
int a ;//未初始化
int b ;//未初始化
●第二步:按F7编译源代码,输出窗口显示信息如下:
1>d:\code\asm_to_c\myprojectone\chapter2\2-2\2-2-1.c(15): error C4700: 使用了未初始化的局部变量“b”
1>d:\code\asm_to_c\myprojectone\chapter2\2-2\2-2-1.c(15): error C4700: 使用了未初始化的局部变量“a”
实验十一:变量初始化
●第一步:在源代码“int a = 1;”处按F9下断点。
●第二步:按F5调试执行,查看反汇编窗口,反汇编代码如下:
int a = 1;//声明int类型变量a,并赋初始值1。
00BA4348 mov dword ptr [a],1
int b = 2;//声明int类型变量b,并赋初始值2。
00BA434F mov dword ptr [b],2
printf("a=%d\tb=%d\n", a, b);//将变量a和b以十进制整数打印出来;\t是制表符
●第三步:在监视1窗口内名称栏输入“&a”和“&b”,如图2-2所示:
图2-2 监视窗口获取局部变量a和b的地址和存储值
打开监视窗口方法:断下之后,点击VS菜单“调试”>“窗口”>“监视”。符号‘&’为地址符,“&a”意思是取变量a的地址,如图2-2所示,变量a的地址为0x0095fbfc,该地址处存储的值为{0xcccccccc}。同样,变量b的地址为0x0095fbf0,该地址处存储的值也是{0xcccccccc}。
【注意】:程序每次运行,系统分配的变量地址是不同的。
●第四步:按F10单步执行,如图2-3所示:
图2-3 F10单步执行
int a = 1;
对应的汇编语句为:
mov dword ptr [a] , 1
接下来将要执行的是语句:int b = 2;
此时观察监视1窗口,如图2-4所示:
图2-4 变量a赋值后的值
如图2-4所示,执行int a = 1;语句后,变量a地址处存储的值为0x00000001,变量b的初始值未变。
结论
1. 变量在未初始化的情况下,不可以在代码中直接使用。
2. 在函数内定义的变量称为局部变量,又称为自动变量。局部变量定义在函数的堆栈空间,未初始化之前的值为0xcc或者不确定。
不确定值:因为计算机存储介质并不是空白的,相反存放了很多以前运行的无用数据,当我们生成变量时,系统会分配内存空间,而分配的内存空间是之前遗留下来无用的数据的空间,有可能未被初始化为0。由于我们并没有给变量赋值,所以系统就随机分配无用数据,就造成了变量会被存入一个不确定的值,我们可以看作垃圾值。
0xcc:因为在main函数内声明的变量a和b是局部变量,其内存空间在堆栈内分配,在main函数初始化时,编译器自动给堆栈空间内分配了0D8H个字节的局部变量空间,并且将其全部初始化为0xcc(36h个0CCCCCCCCh)。下面是示例三的反汇编代码:
00BA4320 push ebp
00BA4321 mov ebp,esp ;建立堆栈框架
00BA4323 sub esp,0D8h ;分配0D8h个字节局部变量空间
00BA4329 push ebx ;保护寄存器
00BA432A push esi ;保护寄存器
00BA432B push edi ;保护寄存器
00BA432C lea edi,[ebp-0D8h];初始化局部变量堆栈空间
00BA4332 mov ecx,36h ;重复次数36H(0D8H/4=36H)
00BA4337 mov eax,0CCCCCCCCh ;初始化值,4个字节cch
00BA433C rep stos dword ptr es:[edi];重复36H次存储局部变量堆栈空间0CCCCCCCCh
3.变量在使用前必须符合变量三原则,有确定的地址,确定的存储空间、确定的初始值。
4.C语言编译器与汇编器有所不同,C语言编译器会检查变量的是否已初始化。而汇编语言中全局变量的初始值默认为0,局部变量是否填充0xcc取决于编译器,汇编器编译时不做检查。
5.除了局部变量之外,还有全局变量和static静态变量、TLS存储变量。我们将在后面的章节中详细介绍,此处不再赘述。
■变量赋值
示例代码三中,语句int a = 1;中的等号“=”表示把右边的常量值1赋给左边的变量a,可以通过“=”来改变变量的值。
如果用汇编语句来写:
mov sdword ptr a,1 ; sdword ptr表示32位有符号整数类型
准确的含义是指:将常量值1存入内存偏移地址a处的四个字节空间,并且存入的常量值可以是-231~231-1之间的任一整数值。
【注意】这里的等号和数学中的“x=1”含义是不同的。
■变量输出
示例三通过调用printf函数输出变量a和b的值:
printf("a=%d\tb=%d\n", a, b);//将变量a和b以十进制整数打印出来;\t是制表符
printf语句包含3个参数。第一个参数为格式化常量字符串,字符串中包含两个格式化说明符’%d’,分别对应第二个参数变量a和第三个参数变量b,表示按照有符号整数的格式分别输出变量a和变量b的值。
此外,格式化常量字符串中还包含两个转义字符,’\t’表示输出制表符,’\n’表示输出换行。
2.2.3 汇编解析
■汇编代码
;FileName:2-2-1.asm
;例3:示例代码2-1为两个变量赋整数值并显示
;by:bcdaren
;2023.08.27
;===============================
;C标准库头文件和导入库
include vcIO.inc
.data
a sdword 1 ;全局变量
b sdword 2 ;全局变量
szMsg db "a=%d",09h,"b=%d",0dh,0ah,0 ;制表符ASCII码为09h
.code
start:
push b
push a
push offset szMsg ;格式化常量字符串偏移地址入栈
call printf ;调用printf函数输出结果
;a = b;
mov eax,b
mov a,eax
invoke printf,offset szMsg,a,b;控制台窗口输出变量a和变量b的值
;b = a + b;
mov eax,b
add eax,a
mov b,eax
;a = b + a;
mov eax,b
add eax,a
mov a,eax
invoke printf,offset szMsg,a,b
;
invoke _getch ;等待输入单个字符
ret ;结束返回
end start
上述代码为2-2-1.c的汇编代码实现。vcIO.inc头文件在示例一中已经详细讲述,此处不再赘述。
.data数据段定义了格式化常量字符串szMsg。此外还定义了两个sdword类型的全局变量a和b。
注意
汇编语言变量的定义和C语言有区别:
1.汇编语言中定义的变量a和b是全局变量,数据类型为sdword类型,而C语言中定义的变量a和b是局部变量,数据类型是int类型。汇编器标准数据类型sdword等价于VS C\C++编译器标准数据类型int,都是表示32位有符号整数类型。汇编语言中,main函数内使用的变量通常定义为全局变量,但是全局变量有风险,其他函数调用时有可能无意中会改变全局变量的值,需要程序员时刻保持警惕。而C语言处于安全角度考虑,在变量定义时,就限定了局部变量的使用范围,仅在函数内有效,相对而言,对程序员的要求不再那么严格。
【注】如果汇编代码使用main函数,也可以将变量a和b定义为局部变量。后面的章节我们会实现。
2.C语言的赋值语句非常简单。
例如:a = b;
翻译为汇编语句:
mov eax,b
mov a,eax
需要借助于eax累加器完成变量间的赋值,这是由CPU指令规则决定的。
3.C语言的算术运算可以直接使用运算符实现。
例如:b = a + b;
翻译成汇编语句:
mov eax,b
add eax,a ;add加法指令
mov b,eax
同样需要借助于累加器eax实现。
4.C语言直接调用库函数printf输出变量a和b的值,函数名后的圆括号内为实参,以分号结束语句。实参按照从右往左的顺序入栈。
printf("a=%d\tb=%d\n", a, b);
翻译成汇编语句:
push b
push a
push offset szMsg ;格式化常量字符串偏移地址入栈
call printf ;调用printf函数输出结果
5.本书采用ANSI字符集C语言标准库函数,因此字符串中的字符均为ASCII字符。本例格式化字符串定义:
szMsg db "a=%d",09h,"b=%d",0dh,0ah,0 ;制表符ASCII码为09h
转义字符直接改为ASCII码字符,数据类型为db类型,相当于C语言中的char类型。转移字符’\t’的ASCII码值为09h,转义字符’\n’在Windows操作系统中对应的ASCII码字符为0dh,0ah,使用两个字节表示。而在Unix或Linux操作系统中,换行符为0ah。
■反汇编代码
int a = 1;//声明int类型变量a,并赋初始值1。
01274348 mov dword ptr [a],1
int b = 2;//声明int类型变量b,并赋初始值2。
0127434F mov dword ptr [b],2
printf("a=%d\tb=%d\n", a, b);//将变量a和b以十进制整数打印出来;\t是制表符
01274356 mov eax,dword ptr [b]
01274359 push eax
0127435A mov ecx,dword ptr [a]
0127435D push ecx
0127435E push offset string "a=%d\tb=%d\n" (01277B30h)
01274363 call _printf (0127104Bh)
01274368 add esp,0Ch
a = b;//将b的值赋给a,此时a的值发生了变化。
0127436B mov eax,dword ptr [b]
0127436E mov dword ptr [a],eax
printf("a=%d\tb=%d\n", a, b);//此时打印的a,是发生变化后的值,而b并没有发生变化。
01274371 mov eax,dword ptr [b]
01274374 push eax
01274375 mov ecx,dword ptr [a]
01274378 push ecx
01274379 push offset string "a=%d\tb=%d\n" (01277B30h)
0127437E call _printf (0127104Bh)
01274383 add esp,0Ch
b = a + b;//将b+a的值赋给b,此时b的值发生了变化。
01274386 mov eax,dword ptr [a]
01274389 add eax,dword ptr [b]
0127438C mov dword ptr [b],eax
a = b + a;//将b+a的值赋给a,此时a的值发生了变化。
0127438F mov eax,dword ptr [b]
01274392 add eax,dword ptr [a]
01274395 mov dword ptr [a],eax
printf("a=%d\tb=%d\n", a, b);//此时打印的是发生变化后变量a的值、变量b的值。
01274398 mov eax,dword ptr [b]
0127439B push eax
0127439C mov ecx,dword ptr [a]
0127439F push ecx
012743A0 push offset string "a=%d\tb=%d\n" (01277B30h)
012743A5 call _printf (0127104Bh)
printf("a=%d\tb=%d\n", a, b);//此时打印的是发生变化后变量a的值、变量b的值。
012743AA add esp,0Ch
system("pause"); //在程序执行return 0; 之前暂停
012743AD mov esi,esp
012743AF push offset string "pause" (01277B40h)
012743B4 call dword ptr [__imp__system (0127B174h)]
012743BA add esp,4
为了节约篇幅,上述反汇编代码没有包含堆栈框架。对比汇编代码,反汇编代码中没有使用高级汇编伪指令invoke,而是单条汇编指令组成,每条反汇编代码只有一个汇编指令。每条汇编指令对应一条机器指令,这才是计算机真实的执行过程。
我们可以把程序理解为:程序员编写的一段计算机控制指令,控制计算机各个部件的运行,已完成算术逻辑运算,实现数据的移动。
当我们深刻理解C语言语句执行的过程,才可以真正理解所编写的C语言代码的确切含义。当C语言程序运行出现错误时,也可以通过反汇编代码,逐条指令单步执行,以发现错误的具体原因。
C语言的语法非常灵活,为了便于掌握C语言各种不同语法的实现,下面我们做几个变量声明的实验。
实验十二:变量和声明
VS新建项目2-2-2.c:
/*
为两个变量赋整数值并显示
*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int x, y; //声明int型变量x,y
x = 11; //x变量初始化赋值
y = x + 10;
printf("x的值是%d。\n", x); //显示x的值
printf("y的值是%d。\n", y); //显示y的值
system("pause"); //在程序执行return 0; 之前暂停
return 0;
}
●输出结果:
x的值是11。
y的值是21。
请按任意键继续. . .
练习
- 请读者将2-2-2.c翻译成汇编语言实现。
- 请读者分析2-2-2.c的反汇编代码。
实验十三:变量初始化
VS新建项目2-2-3.c:
/*
为两个变量赋整数值并显示
*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int x = 11; //声明x变量,并初始化赋值
int y = 10; //声明y变量,并初始化赋值
printf("x的值是%d。\n", x); //显示x的值
printf("y的值是%d。\n", y); //显示y的值
system("pause"); //在程序执行return 0; 之前暂停
return 0;
}
●输出结果:
x的值是11。
y的值是10。
请按任意键继续. . .
练习
- 请读者将2-2-3.c翻译成汇编语言实现。
- 请读者分析2-2-3.c的反汇编代码。
实验十四:变量声明时初始化
VS新建项目2-2-4.c:
/*
为两个变量赋实数值并显示
只显示整数部分,小数部分丢失
*/
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
int x = 3.14;
int y = 5.7;
printf("x的值是%d。\n", x); //显示x的值
printf("y的值是%d。\n", y); //显示y的值
system("pause"); //在程序执行return 0; 之前暂停
return 0;
}
●输出结果:
x的值是3。
y的值是5。
请按任意键继续. . .
练习
- 请读者将2-2-4.c翻译成汇编语言实现。
- 请读者分析2-2-4.c的反汇编代码。
本文摘自编程达人系列教材《汇编的角度——C语言》。