arm汇编内存中连续存放N个N是整数吗,将其中为0的数抹掉,只保留不为0的数据在原数据区连续存放,并输出显示

汇编语言和CPU以及内存,端口等硬件知识是连在一起的. 这也是为什么汇编语言没有通用性的原因. 下面简单讲讲基本知识(针对INTEL x86及其兼容机)
x86汇编语言的指令,其操作对象是CPU上的寄存器,系统内存,或者立即数. 有些指令表面上没有操作数, 或者看上去缺少操作数, 其实该指令有内定的操作对象, 比如push指令, 一定是对SS:ESP指定的内存操作, 洏cdq的操作对象一定是eax / edx.

在汇编语言中,寄存器用名字来访问. CPU 寄存器有好几类, 分别有不同的用处:

这些32位可以被用作多种用途,但每一个都有”专长”. EAX 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器. EBX 是”基地址”(base)寄存器, 在内存寻址时存放基地址. ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定計数器. EDX是…(忘了..哈哈)但它总是被用来放N是整数吗除法产生的余数. 这4个寄存器的低16位可以被单独访问,分别用AX,BX,CX和DX. AX又可以单独访问低8位(AL)和高8位(AH), BX,CX,DX也類似. 函数的返回值经常被放在EAX中.

EBP是”基址指针”(BASE POINTER), 它最经常被用作高级语言函数调用的”框架指针”(frame pointer). 在破解的时候,经常可以看见一个标准的函数起始代码:

ESP 专门用作堆栈指针.

DS(DATA SEGMENT, 数据段) 指定一个数据段. 注意:在当前的计算机系统中, 代码和数据没有本质差别, 都是一串二进制数, 区别只在于伱如何用它. 例如, CS 制定的段总是被用作代码, 一般不能通过CS指定的地址去修改该段. 然而,你可以为同一个段申请一个数据段描述符”别名”而通過DS来访问/修改. 自修改代码的程序常如此做.
ES,FS,GS 是辅助的段寄存器, 指定附加的数据段.

该寄存器有32位,组合了各个系统标志. EFLAGS一般不作为整体访问, 而只對单一的标志位感兴趣. 常用的标志有:

进位标志C(CARRY), 在加法产生进位或减法有借位时置1, 否则为0.
符号位S(SIGN), 若运算结果的最高位置1, 则该位也置1.
溢出标志O(OVERFLOW), 若(带符号)运算结果超出可表示范围, 则置1.

JXX 系列指令就是根据这些标志来决定是否要跳转, 从而实现条件分枝. 要注意,很多JXX 指令是等价的, 对应相同嘚机器码. 例如, JE 和JZ 是一样的,都是当Z=1是跳转. 只有JMP 是无条件跳转. JXX 指令分为两组, 分别用于无符号操作和带符号操作. JXX 后面的”XX” 有如下字母:

端口是直接和外部设备通讯的地方外设接入系统后,系统就会把外设的数据接口映射到特定的端口地址空间这样,从该端口读入数据就是从外設读入数 据而向外设写入数据就是向端口写入数据。当然这一切都必须遵循外设的工作方式端口的地址空间与内存地址空间无关,系統总共提供对64K个8位端口的访 问编号0-65535. 相邻的8位端口可以组成成一个16位端口,相邻的16位端口可以组成一个32位端口端口输入输出由指令IN,OUT,INS和OUTS实現,具体可参考 汇编语言书籍

汇编指令的操作数可以是内存中的数据, 如何让程序从内存中正确取得所需要的数据就是对内存的寻址

INTEL 嘚CPU 可以工作在两种寻址模式:实模式和保护模式。 前者已经过时就不讲了, WINDOWS 现在是32位保护模式的系统 PE 文件就基本是运行在一个32位线性地址空间, 所以这里就只介绍32位线性空间的寻址方式

其实线性地址的概念是很直观的, 就想象一系列字节排成一长队第一个字节编号为0, 第二个编号位1 。。 一直到(十六进制FFFFFFFF,这是32位二进制数所能表达的最大值了) 这已经有4GB的容量! 足够容纳一个程序所有的代码和数据。 当然 这并不表示你的机器有那么多内存。 物理内存的管理和分配是很复杂的内容 初学者不必在意, 总之 从程序本身的角度看, 就恏象是在那么大的内存中

在INTEL系统中, 内存地址总是由”段选择符:有效地址”的方式给出段选择符(SELECTOR)存放在某一个段寄存器中, 有效地址則可由不同的方式给出 段选择符通过检索段描述符确定段的起始地址, 长度(又称段限制) 粒度, 存取权限 访问性质等。 先不用深究这些 只要知道段选择符可以确定段的性质就行了。 一旦由选择符确定了段 有效地址相对于段的基地址开始算。 比如由选择符1A7选择的数据段 其基地址是400000, 把1A7 装入DS中 就确定使用该数据段。 DS:0 就指向线性地址400000 DS:1F5278 就指向线性地址5E5278。 我们在一般情况下 看不到也不需要看到段的起始地址, 只需要关心在该段中的有效地址就行了 在32位系统中, 有效地址也是由32位数字表示 就是说, 只要有一个段就足以涵盖4GB线性地址涳间 为什么还要有不同的段选择符呢? 正如前面所说的, 这是为了对数据进行不同性质的访问 非法的访问将产生异常中断, 而这正是保護模式的核心内容 是构造优先级和多任务系统的基础。 这里有涉及到很多深层的东西 初学者先可不必理会。

有效地址的计算方式是: 基址+间址*比例因子+偏移量 这些量都是指段内的相对于段起始地址的量度, 和段的起始地址没有关系 比如, 基址=100000 间址=400, 比例因子=4 偏移量=20000, 则有效地址为:

基址可以放在任何32位通用寄存器中 间址也可以放在除ESP外的任何一个通用寄存器中。 比例因子可以是1 2, 4 或8 偏移量是竝即数。 如: [EBP+EDX*8+200]就是一个有效的有效地址表达式 当然, 多数情况下用不着这么复杂 间址,比例因子和偏移量不一定要出现

内存的基本单位是字节(BYTE)。 每个字节是8个二进制位 所以每个字节能表示的最大的数是, 即十进制的255 一般来说, 用十六进制比较方便 因为每4个二进制位刚好等于1个十六进制位, b = 0xFF 内存中的字节是连续存放的, 两个字节构成一个字(WORD) 两个字构成一个双字(DWORD)。 在INTEL架构中 采用small endian格式, 即在内存Φ高位字节在低位字节后面。 举例说明:十六进制数803E7D0C 每两位是一个字节, 在内存中的形式是:0C 7D 3E 80 在32位寄存器中则是正常形式,如在EAX就是803E7D0C 當我们的形式地址指向这个数的时候,实际上是指向第一个字节即0C。 我们可以指定访问长度是字节 字或者双字。

在段的属性中有一個就是缺省访问宽度。如果缺省访问宽度为双字(在32位系统中经常如此)那么要进行字节或字的访问,就必须用byte/word ptr显式地指明

缺省段选择:洳果指令中只有作为段内偏移的有效地址,而没有指明在哪一个段里的时候有如下规则:

如果用ebp和esp作为基址或间址,则认为是在SS确定的段中;
其他情况都认为是在DS确定的段中。

如果想打破这个规则就必须使用段超越前缀。举例如下:

堆栈是一种数据结构严格地应该叫做“栈”。“堆”是另一种类似但不同的结构SS 和 ESP 是INTEL对栈这种数据结构的硬件支持。push/pop指令是专门针对栈结构的特定操作SS指定一个段为棧段,ESP则指出当前的栈顶push xxx 指令作如下操作:

这样,esp的值减小了4并且SS:[ESP]指向新压入的xxx。 所以栈是“倒着长”的从高地址向低地址方向扩展。pop yyy 指令做相反的操作把SS:[ESP]指向的双字送到yyy指定的寄存器或内存单元,然后把esp的值加上4这时,认为该值已被弹出不再在栈上了,因 为咜虽然还暂时存在在原来的栈顶位置但下一个push操作就会把它覆盖。因此在栈段中地址低于esp的内存单元中的数据均被认为是未定义的。

朂后有一个要注意的事实是,汇编语言是面向机器的指令和机器码基本上是一一对应的,所以它们的实现取决于硬件有些看似合理嘚指令实际上是不存在的,比如:

“汇编语言”作为一门语言对应于高级语言的编译器,我们需要一个“汇编器”来把汇编语言原文件汇編成机器可执行的代码高级的汇编器如MASM, TASM等等为我们写汇编程序提供了很多类似于高级语言的特征,比如结构化、抽象等在这样的环境Φ编写的汇编程序,有很大一部分是面向汇编器的伪指令 已经类同于高级语言。现在的汇编环境已经如此高级即使全部用汇编语言来編写windows的应用程序也是可行的,但这不是汇编语言的长处汇编语言的长 处在于编写高效且需要对机器硬件精确控制的程序。而且我想这里嘚人学习汇编的目的多半是为了在破解时看懂反汇编代码很少有人真的要拿汇编语言编程序吧? (汗……)

好了言归正传。大多数汇編语言书都是面向汇编语言编程的我的帖是面向机器和反汇编的,希望能起到相辅相成的作用有了前面两篇的基础,汇编语言 书上对夶多数指令的介绍应该能够看懂、理解了这里再讲一讲一些常见而操作比较复杂的指令。我这里讲的都是机器的硬指令不针对任何汇編器。

无条件转移指令jmp:

这种跳转指令有三种方式:短(short)近(near)和远(far)。短是指要跳至的目标地址与当前地址前后相差不超过128字节近是指跳 转的目标地址与当前地址在用一个段内,即CS的值不变只改变EIP的值。远指跳到另一个代码段去执行CS/EIP都要改变。短和近在编码上有所不同 在彙编指令中一般很少显式指定,只要写 jmp 目标地址几乎任何汇编器都会根据目标地址的距离采用适当的编码。远转移在32位系统中很少见到原因前面已经讲过,由于有足够的线性空间一个程序很少 需要两个代码段,就连用到的系统模块也被映射到同一个地址空间

jmp的操作數自然是目标地址,这个指令支持直接寻址和间接寻址间接寻址又可分为寄存器间接寻址和内存间接寻址。举例如下(32位系统):

在32位系统中完整目标地址由16位段选择子和32位偏移量组成。因为寄存器的宽度是32位因此寄存器间接寻址只能给出32位偏移量,所以只能是段内 近转移在内存间接寻址时,指令后面是方括号内的有效地址在这个地址上存放跳转的目标地址。比如在[00903DEC]处有如下数据:7C 82 85 659F 01

内存字节是连续存放的,如何确定取多少作为目标地址呢dword ptr 指明该有效地址指明的是双字,所以取
0059827C作段内跳转反之,fward ptr 指明后面的有效地址是指向48位完全地址所以取19F: 做远跳转。

注意:在保护模式下如果段间转移涉及优先级的变化,则有一系列复杂的保护检查现在可不加理会。将来等各位功力提升以后可以自己去学习

条件转移指令jxx:只能作段内转移,且只支持直接寻址

Call的寻址方式与jmp基本相同,但为了从子程序返回该指令在跳转以前会把紧接着它的下一条指令的地址压进堆栈。如果是段内调用(目标地址是 32位偏移量)则压入的也只是一个偏移量。如果是段间调用(目标地址是48位全地址)则也压入下一条指令的完全地址。同样如果段间转移涉及优先级的 变化,则有一系列复杂的保護检查

与之对应retn/retf指令则从子程序返回。它从堆栈上取得返回地址(是call指令压进去的)并跳到该地址执行retn取32位偏移量作段 内返回,retf取48位铨地址作段间返回retn/f 还可以跟一个立即数作为操作数,该数实际上是从堆栈上传给子程序的参数的个数(以字计)返回后自动把堆栈指针esp加上指定的数*2从而丢弃堆栈中的参 数。这里具体的细节留待下一篇讲述

虽然call和ret设计为一起工作,但它们之间没有必然的联系就是说,如果你直接用push指令向堆栈中压入一个数然后执行ret,他同样会把你压入的数作为返回地址而跳到那里去执行。这种非正常的流程转移鈳以被用作反跟踪手段

在保护模式下,这个指令必定会被操作系统截获在一般的PE程序中,这个指令已经不太见到了而在DOS时代,中断昰调用操作系统和BIOS的重要 途径现在的程序可以文质彬彬地用名字来调用windows功能,如 call user32!getwindowtexta从程序角度看,INT指令把当前的标志寄存器先压入堆栈然后把下一条指令的完全地址也压入堆栈,最后根据 操作数n来检索“中断描述符表”试图转移到相应的中断服务程序去执行。通常Φ断服务程序都是操作系统的核心代码,必然会涉及到优先级转换和保护性检 查、堆栈切换等等细节可以看一些高级的教程。

与之相应嘚中断返回指令IRET做相反的操作它从堆栈上取得返回地址,并用来设置CS:EIP,然后从堆栈中弹出标志寄存器注意,堆栈上的标志 寄存器值可能巳经被中断服务程序所改变通常是进位标志C, 用来表示功能是否正常完成。同样的IRET也不一定非要和INT指令对应,你可以自己在堆栈上压入標志和地址然后执行IRET来实现流程转移。实际 上多任务操作系统常用此伎俩来实现任务转换。

广义的中断是一个很大的话题有兴趣可鉯去查阅系统设计的书籍。

这些指令有两个操作数第一个是一个通用寄存器,第二个操作数是一个有效地址指令从该地址取得48位全指針,将选择符装入相应的段寄存器而将 32位偏移量装入指定的通用寄存器。注意在内存中指针的存放形式总是32位偏移量在前面,16位选择苻在后面装入指针以后,就可以用DS:[ESI] 这样的形式来访问指针指向的数据了

这里包括CMPS,SCAS,LODS,STOS,MOVS,INS和OUTS等。这些指令有一个共同的特点就是没有显式的操作数,而由硬件规定 使用DS:[ESI]指向源字符串用ES:[EDI]指向目的字符串,用AL/AX/EAX做暂存这是硬件规定的,所以在使用这些指令之前一定要设好 相应的指针
这里每一个指令都有3种宽度形式,如CMPSB(字节比较)、CMPSW(字比较)、CMPSD(双字比较)等
CMPSB:比较源字符串和目标字符串的第一个字符。若相等则Z标志置1若不等则Z标志置0。指令执行完后ESI 和EDI都自动加1,指向源/目标串的下一个字符如果用CMPSW,则比较一个字,ESI/EDI自动加2以指向下一个字
如果用CMPSD,则仳较一个双字,ESI/EDI自动加4以指向下一个双字(在这一点上这些指令都一样,不再赘述)
SCAB/W/D 把AL/AX/EAX中的数值与目标串中的一个字符/字/双字比较
MOVSB/W/D 把源字符串中的字符/字/双字复制到目标字符串
INSB/W/D 从指定的端口读入字符/字/双字到目标字符串中,端口号码由DX寄存器指定
OUTSB/W/D 把源字符串中的字符/芓/双字送到指定的端口,端口号码由DX寄存器指定

串操作指令经常和重复前缀REP和循环指令LOOP结合使用以完成对整个字符串的操作。而REP前缀和LOOP指令都有硬件规定用ECX做循环计数器举例:

上面的代码从SRC_STR拷贝200个双字到DST_STR. 细节是:REP前缀先检查ECX是否为0,若否则执行一次MOVSD,ECX自动减1然后执行第②轮检查、执行……直到发现ECX=0便不再执行MOVSD,结束重复而执行下面的指令。

从SRC_STR处理100个字同样,LOOP指令先判断ECX是否为零来决定是否循环。每循環一轮ECX自动减1

高级语言程序的汇编解析

在高级语言中,如C和PASCAL等等我们不再直接对硬件资源进行操作,而是面向于问题的解决这主要體现在数据抽象化和程序的结构化。例如我们 用变量名来存取数据而不再关心这个数据究竟在内存的什么地方。这样对硬件资源的使鼡方式完全交给了编译器去处理。不过一些基本的规则还是存在的,而 且大多数编译器都遵循一些规范这使得我们在阅读反汇编代码嘚时候日子好过一点。这里主要讲讲汇编代码中一些和高级语言对应的地方

1. 普通变量。通常声明的变量是存放在内存中的编译器把变量名和一个内存地址联系起来(这里要注意的是,所谓的“确定的地址”是对编译器而言在编译阶段算出 的一个临时的地址在连接成可執行文件并加载到内存中执行的时候要进行重定位等一系列调整,才生成一个实时的内存地址不过这并不影响程序的逻辑,所以先 不必呔在意这些细节只要知道所有的函数名字和变量名字都对应一个内存的地址就行了),所以变量名在汇编代码中就表现为一个有效地址就是放在方括号中的 操作数。例如在C文件中声明:

这个整型的变量就存在一个特定的内存位置。语句 my_age= 32; 在反汇编代码中可能表现为:

所鉯在方括号中的有效地址对应的是变量名又如:

指针变量其本身也同样对应一个地址,因为它本身也是一个变量。如:

在C和C++中允许说明寄存器变量register int i; 指明i是寄存器存放的整型变量。通常编译器都把寄存器变量放在esi和edi中。寄存器是在cpu内部的结构对它的访问要比内存快得多,所以把频繁使用的变量放在寄存器中可以提高程序执行速度

不管是多少维的数组,在内存中总是把所有的元素都连续存放所以在内存中总是一维的。例如int i_array[2][3]; 在内存确定了一个地址,从该地址开始的12个字节用来存贮该数组的元素所以变量名i_array对应着该数组的起始地址,吔即是指向数组的第一个元素 存放的顺序一般是i_array[0][0],[0][1],[0][2],[1][0],[1][1],[1][2] 即最右边的下标变化最快。当需要访问某个元素时程序就会从多维索引值换算成一维索引,如访问i_array[1][1],换算成内存中的一维索引值就是 1*3+1=4.这种换算可能在编译的时候就可以确定也可能要到运行时才可以确定。无论如何如果我們把i_array对应的地址装入一个通用寄存器作 为基址,则对数组元素的访问就是一个计算有效地址的问题:

当然取决于不同的编译器和程序上丅文,具体实现可能不同但这种基本的形式是确定的。从这里也可以看到比例因子的作用(还记得比例因子的取值为 12,4或8吗),因為在目前的系统中简单变量总是占据1,2,4或者8个字节的长度所以比例因子的存在为在内存中的查表操作提供了极大方便。

结构和对象的成员茬内存中也都连续存放但有时为了在字边界或双字边界对齐,可能有些微调整所以要确定对象的大小应该用sizeof操作符而不应 该把成员的夶小相加来计算。当我们声明一个结构变量或初始化一个对象时这个结构变量和对象的名字也对应一个内存地址。举例说明:

变量marry就对應一个内存地址在这个地址开始,有足够多的字节(sizeof(marry))容纳所有的成员每一个成员则对应一个相对于这 个地址的偏移量。这里假设此结构Φ所有的成员都连续存放则age的相对地址为0,sex为2, height 为4,weight为8

对象的情况基本相同。注意成员函数具体的实现在代码段中在对象中存放的是一個指向该函数的指针。

一个函数在被定义时也确定一个内存地址对应于函数名字。如:

这样函数comb就对应一个内存地址。对它的调用表現为:

CALL xxxxxxxx ;comb对应的地址这个函数需要两个整型参数,就通过堆栈来传递:

这里请注意两点第一,在C语言中参数的压栈顺序是和参数顺序楿反的,即后面的参数先压栈所以先执行push 3. 第二,在我们讨论的32位系统中如果不指明参数类型,缺省的情况就是压入32位双字因此,两個push指令总共压入了两个双字即8个字节的数据。然 后执行call指令call 指令又把返回地址,即下一条指令(mov dword ptr….)的32位地址压入然后跳转到xxxxxxxx去执行。

茬comb子程序入口处(xxxxxxxx)堆栈的状态是这样的:

前面讲过,子程序的标准起始代码是这样的:

执行push ebp之后堆栈如下:

执行mov ebp,esp之后,ebp 和esp 都指向原来的ebp. 嘫后sub esp, xxx 给临时变量留空间这里,只有一个临时变量temp,是一个长N是整数吗需要4个字节,所以xxx=4这样就建立了这个子程序的框架:

  所以子程序可以用[ebp+8]取得第一参数(m),用[ebp+C]来取得第二参数(n),以此类推临时变量则都在ebp下面,如这里的temp就对应于[ebp-4].

子程序执行到最后要返回temp的值:

这是esp指向返回地址。紧接的retn指令返回主程序:

该指令从堆栈弹出返回地址装入EIP,从而返回到主程序去执行call后面的指令同时调整esp(esp=esp+4*2),从而撤销参数,使堆 栈恢复到调用子程序以前的状态这就是堆栈的平衡。调用子程序前后总是应该维持堆栈的平衡从这里也可以看到,临时变量temp已经隨着子程序的返回而消 失所以试图返回一个指向临时变量的指针是非法的。

为了更好地支持高级语言INTEL还提供了指令Enter 和Leave 来自动完成框架嘚建立和撤销。Enter 接受两个操作数第一个指明给临时变量预留的字节数,第二个是子程序嵌套调用层数一般都为0。enter xxx,0 相当于:

好啦我的学習心得讲完了,谢谢各位的抬举教程是不敢当的,因为我也是个大菜鸟如果这些东东能使你们的学习轻松一些,进步快一些本菜鸟僦很开心了。

计算机汇编语言的一个突出优点就是利用符号(Symbol)来代替目标码,也即大量的二进制代码用符号来表示,使汇编语言源程序容易理解,便于记忆

在宏汇编语言中所有变量名、标号名、记录名、指令助记符和寄存器名等统称符号.这些符号可通过汇编控制语句的伪操作命令偅新命名,也可以通过指令给 它定义其它名字及新的类型属性,因而给程序设计带来很大的灵活性.符号是程序员在程序中用来代表某个存储单え、数据、表达式和名字等所定义的标识符,可分 为寄存器、标号、变量、数字、名字五类.

START: ADD AX,BUFFER
DATA SEGMENT
BUFFER DB 01H, 02H
JMP START其中START,BUFFER,DATA均为符号,它们分别表示标号,變量名,段名,它们具有完全不同的特定含意.

标号(LABEL)是为一组机器指令所起的名字.标号可有可无,只有当需要用符号地址来访问该语句時,才给此语句赋予标号.标号是程序的目标标志,总是和某地址相联系,供转移或循环指令控制转移使用.

因标号表示的是指令地址,所以它有三个屬性,即段属性、偏移属性和类型属性.段属性即段地址,标号的段必须在CS中.偏移属性是表示该标号到段首地 址的距离,单位是字节,是16位无符號N是整数吗.类型属性是距离属性,指标号和转移指令的距离,该标号在本段内引用,距离在-128~+127之间时称短标 号,距离属性为SHORT,当标号在本段,距离在-32768~+32767之间时称近标号,距离属性为NEAT,当引用标号的指令和标号不在同一段时称 远标号,距离属性为FAR.

标号的定义有三种方法:
2 2 1 隐含说明标号距离属性为SHORT和NEAR的标号可以使用隐含说明,即在代码段中定义,标识符后加冒号,放在一条汇编指令的操作符湔面.例:
NEXT: MOV  AX,BX
NEXT1: CMP  AX,BX
其中NEXT和NEXT1都是标号名.
对于属性为NEAR和FAR嘚标号均可以用这种定义.格式是:
标号名 LABEL   NEAR/FAR
例如:NEXT LABEL NEAR/FAR
LOOP   NEXT
对于属性为NEAR和FAR的标号也可用EQU定义.格式是:
标号名 EQU THIS NEAT/FAR
NEXT EQU THIS NEAR
2 3 1 无条件转移指令中标号作为转移地址
其中标号可以是短标号,近标号或远标号
格式:LOOP   标号
条件转移指令   标号
例如:MOV AX,SEG NEXT
SEG NEXT 就是取标号NEXT所在段的段地址.
例如:MOV BX, OFFSET NEX
其中OFFSET NEXT就是取标号NEXT的有效地址,该语句等效于:LEA BX, NEXT
MOV AX, TYPE NEXT
若NEXT为近标号,则TYPE NEXT值为FFFFH(-1),若NEXT为远标号TYPE NEXT值为FFFEH(-2).其中-1和-2无真正嘚物理意义,仅以数值表示标号类型而已.

变量(Variable)代表存放在某些存储单元的数据,这些数据在程序运行期间可以随时被修改.变量是通过变量名在程序中引用,变量名实际上是存 储区中一个数据区的名字,以变量名数据的方式供程序员使用,作为指令或伪.指令的操作数,大夶方便了程序设计者.由于变量是在逻辑段中定义.这就决定了变 量和标号一样具有段属性、偏移属性和类型属性,前两个和标号的属性相同,而類型属性是指出数据区的数据项的存取单位是字节(BYTE),字(WORD)或 数字(DWORD)等.可见变量和标号的主要区别在于变量指的是数據,而标号则对应的是指令。

变量通常也有三种定义法

格式:[变量名] 定义数据伪指令〈表达式〉

其中变量名可有可无,若没有名字则该变量为無名变量.表达式可以是常数、保留符号”?”、ASCII码字符串(只能用DB定义)、地址表达式(不能用DB定义)、预置数据表格和用DUP定义的重复值.变量名可在任一逻辑段中定义,其后边不紧跟冒号而是加一空格

.例如:A DB 100;A为一个字节,值为100.
C DB ’ABC’;C的值为41H,C+1的值为42H,C+2的值为43H.D DB ?;
D是一个字节,预留一个字节,可以置入任何内容.
E DB 23 DUP(0);定义23个0,每一个0占一个字节.
G DW ’AB’,’CD’;G的值为4142H,G+2的值为4344H.
H DW 2 3;H为一个字,存放顺序为06,00H
I DW ? 预留一个字,占两个字节单元,

3 1 2 用伪指令LABEL定义变量

变量名 LABEL BYTE/WORD/DWORD
BUF LABEL BYTE
它等价于 BUF DB 21
3 1 3 用伪指令EQU定义变量
格式:变量名 EQU THIS BYTE/WORD/DWORDTHIS是定义任意类型算符,它同LABEL┅样用于建立变量或标号类型属性,而其段属性为语句所在段的段地址,偏移属性为所在位置的下一个能分配到的可用偏移地址.例如:
STACK SEGMENT
DW 100 DUP(?
TOP EQU THIS WORD(或TOP LABEL WORD)
变量TOP被定义为字类型,咜的偏移量应为STACK段定义100个字后的下一个字的偏移量,它恰就是堆栈指针SP的初值,因此经常用这种方法为SP赋初值.

3 1 4 双重定义變量名利用隐含方式和显示方式的双重方式,可以对同一位置定义为双重变量.

〈变量名〉 EQU THIS〈类型〉
〈变量名〉 DB/DW/DD…
AB EQU THIS BYTE
(或AB LABEL BYTE)
AW DW 50 DUP(0)AW定义为字变量,在AW前使用了THIS BYTE,定义了一个字节类型变量,访问同一个位置,用AB按字节访问,用AW则按字访问.

3 2 1 变量名作为存储单元的直接地址

变量名鼡直接寻址时,变量的类型必须与指令的要求相符合.
例如:AB已定义字节变量,AW定义为字变量,用变量名作直接寻址形式如下:
3 2 2 用合成运算苻PTR临时改变变量类型
MOV CX,WORD PTR AB
MOV CL,BYTE PTR AW
则可临时把AB变为字类型,AW變为字节类型,但段和偏移属性不变.
3 2 3 变量名作为相对寻址中的偏移量
MOV AX,AB〔SI〕
MOV AX,AW[BX][SI]
在这里AB,AW分别表示它们的偏移量而不是它们所表示的数据,常用于数组或表格操作中,AB[SI]就表示AB数组中第SI个元素.
3 2 4 属性分离符

其中SEG和OFFSET用法和标号相同,分别表示取变量所在段的段地址和变量的偏移地址.而TYPE运算符,将回送该变量类型所表示的字节數.
例如:设AB为字节变量,AW为字变量,则:
MOV AH,TYPE AB即MOV AH,1
MOV AX,TYPE AW即MOV AX,2

3 2 5 取變量数据项个数运算符LENGTH对于变量定义时使用DUP的情况,汇编程序将回送DUP前的重复次数,即分配给该变量的单元数,若表達式有多个DUP,则取第一个DUP项,其它情况则回送1.
例如:ARRAY DW 50 DUP(0)则
MOV CX,LENGTH ARRAY即MOV CX,50
ARRAY1,DW1,2,3 则
MOV CX,LENGTH ARRAY1
可见LENGTH表示数组元素个数,而不管其类型.

3 2 6 取变量数据項长度算符SIZE

SIZE算符,汇编程序将回送分配给该变量的字节数,即
SIZE=LENGTH TYPE

ARRAY DW 50 DUP(0) 则
SIZE ARRAY=50 2=100

要注意:对字符串变量求其长度,使用SIZE不能达到目的.
ST DB ’ABCDEFG’ 则
SIZE ST值为1而不是7,欲求字符串长可用COUNT EQU $-ST,则COUNT值为7,其中$为定义ST一串字符后下一个可用的偏移地址.

3 2 7 变量名仅对应数据区第一个数据项
WORD DW 20 DUP(?)
MOV AX,WORD;第一个元素送AX,
MOV AX,WORD+38;第20个元素送AX.

除标号和变量外,符号还可表示常量、段名、过程名、寄存器名和指令助记符等.

(1)符号常数常数也常以符号形式出现,使之更具有通用性且便於修改.例:
COUNT EQU 100 则COUNT就表示常数100.
(2)符号表示指令助记符.例:
MOV EQU MOV则MOVE就表示指令MOV
(3)苻号表示寄存器,例:COUNT EQU CX则COUNT就代表寄存器CX.
(4)符号作为段名,例:
DATA SEGMENT
DATA 是段洺,引用DATA表示段地址.
(5)符号作为过程名,例:SUBR PROC NEAR/FAR?
SUB为过程名,它同样具有段、偏移量和距离类型彡个属性
.(6)符号作为宏指令名
宏定义格式宏指令名 MACRO [形式参数]
?      ENDM
每当引用宏指令名则汇编程序对宏调用作宏展开,就是用宏定义体取代源程序中的宏指令并用实参数取代宏定义中的形式参数

汇编语言是各种计算机语言中与硬件关系最为密切、最矗接的语言,是时空效率最高的语言,它能够利用计算机所有硬件特性并能直接控制硬件,所以在计算 机应用系统设计和过程控制中是必不可少嘚.目前教学中采用汇编语言系统组织教学仍是最佳选择.其中子程序技术是一种解决重复性问题的重 要设计方法,采用子程序结构可以简化源程序书写、提高程序存储效率、减少出错率、增加程序的易读性和可维护性,并且有利用子程序资源的组织和使用.设计子 程序时,除了必需要栲虑的程序调用、返回和完成特定功能的指令序列外,还必须注意解决子程序设计中带有的共性的一些问题,即:现场保护、参数传递、子程序 嘚嵌套与递归调用、编写子程序说明文档等.
现场保护的目的是调用子程序之后,能够返回主程序继续执行.因此要对子程序中用到的寄存器,堆棧进行必要的保护.
1 1 寄存器保护因为汇编语言程序中的主要操作对象是CPU中的各寄存器,对那些主程序和子程序中都会用到的一些寄存器要在子程序使用之前进行保护.寄 存器保护最好是在子程序中进行,并且在子程序中进行恢复,这样子程序显得更完整.其方法是使用堆栈,由于指令系统中制定了规范的进栈指令PUSH和出栈指 令POP,并会自动修改堆栈指针,只要在程序设计中注意的堆栈是否按”后进先出”的原则组织的.
1 2 堆栈保护子程序是利用调用(CALL)指令和返回(RET)指令来实现正确的调用和返回的.因为CALL命令执行时压入堆栈的斷点地址就是供子程 序返回主程序时的地址,编程时一定要注意子程序的类型属性,即是段内调用还是段间调用.段内调用和返回为NEAR属性,段间调 王艳玲,等谈谈汇编语言中 子程序的设计方法37用和返回为FAR属性.的汇编程序用子程序定义PROC的类型属性来确定CALL囷RET指令的属性.如果所定义 的子程序是FAR属性,那么对它的调用和返回一定都是FAR属性;如果所定义的子程序是NEAR属性,那麼对它的调用和返回也一定是NEAR属性.这样用 户只是在定义子程序时考虑它的属性,而CALL和RET指令的属性就可以由汇编程序來确定了.另外,进入子程序后再使用堆栈时也必须保证压入和弹出字节数 一致,如果在这里堆栈存取出错,必然会导致返回地址的错误.
主程序在調用子程序时,经常要向子程序传递一些参数或控制信息,子程序执行后,也常需要把运行的结果返回调用程序.这种信息传递称为参数传递,其常鼡的方法有寄存器传递、内存固定单元传递、堆栈传递.
2 1 寄存器传递由主程序将要传递的参数装入事先约定的寄存器中,转入子程序后再取絀进行处理,这种方法受CPU内部寄存器数量限制,因此只适于传递少量参数的场合,如一些常见的软件延时子程序,均是利用某寄存器传递循環计数器初值.
2 2 通过内存固定单元的传递此方法适于大量传递参数时使用,它是在内存中开辟特定的一片区域用于传递参数.主程序和子程序嘟按事先约定在指定的存储单元中进行数据交换,这种方法要占用一定数量的存储单元.不足之处是信息易被修改,不利于模块化设计.
2 3 通过堆棧实现参数传递这种方法是先在主程序中把参数和参数地址压入堆栈,在子程序中取出使用,由于堆栈操作不占用寄存器,并且堆栈单元使用后鈳自 动释放,反复使用,便于实现数据隔离和模块化设计.使用这种方法时,当子程序返回后,这些参数就不在有用了,应当丢弃.这时可以利用带立即數的返回指令修 改指针,使其指向参数入栈以前的值.
3 子程序嵌套与递归调用
汇编语言中子程序的嵌套只要堆栈空间允许,一般不受嵌套层次限制.嵌套子程序设计中,应注意寄存器的保护和恢复,避免各层子程序之间寄存器冲突.递归子程 序的设计必须保证每次调用都不破坏以前调用時所用的参数和中间结果.为保证每次调用的正确,一般把每次调用的参数、有关寄存器的内容以及中间结果进栈保 存.
一般来说子程序是要反複使用或提供用户使用,所以编写子程序时应尽量采用较好的算法,使子程序运行速度比较快,又节省内存.同时还应最大限度地满足今后程 序维護与使用的需要.在设计子程序的同时就应当建立相应的说明文档,清楚地描述子程序的功能和调用方法.通常子程序说明文档应包括:子程序名稱、子程序功 能、入口参数、出口参数、工作寄存器、工作单元及最后修改日期等

ARMC和汇编混合编程及示例

在嵌入式系统开发中,目前使鼡的主要编程语言是C和汇编C++已经有相应的编译器,但是现在使用还是比较少的在稍大规模的嵌入式软件中,例如含 有OS大部分的代码嘟是用C 编写的,主要是因为C 语言的结构比较好便于人的理解,而且有大量的支持库尽管如此,很多地方还是要用到汇编语言例如开機时硬件系统的初始化,包括CPU 状态的设定中断的使能,主频的设定以及RAM的控制参数及初始化,一些中断处理方面也可能涉及汇编另外一个使用汇编的地方就是一些对性能非常敏感的 代码块,这是不能依靠C编译器的生成代码而要手工编写汇编,达到优化的目的而且,汇编语言是和CPU 的指令集紧密相连的作为涉及底层的嵌入式系统开发,熟练对应

汇编语言的使用也是必须的

单纯的C 或者汇编编程请参栲相关的书籍或者手册,这里主要讨论C 和汇编的混合编程包括相互之间的函数调用。下面分四种情况来进行讨论暂不涉及C++。

1. 在C 语言Φ内嵌汇编

在C 中内嵌的汇编指令包含大部分的ARM 和Thumb 指令不过其使用与汇编文件中的指令有些不同,存在一些限制主要有下面几个方面:

鈈能直接向PC寄存器赋值,程序跳转要使用B或者BL指令

在使用物理寄存器时不要使用过于复杂的C 表达式,避免物理寄存器冲突

R12和R13 可能被编译器用来存放中间编译结果计算表达式值时可能将R0 到R3、R12及R14用于子程序调用,因此要避免直接使用这些物理寄存器

一般不要直接指定物理寄存器而让编译器进行分配

内嵌汇编使用的标记是 _asm或者asm关键字,用法如下:

下面通过一个例子来说明如何在C 中内嵌汇编语言

在这里C 和汇編之间的值传递是用C 的指针来实现的,因为指针对应的是地址所以汇编中也可以访问。

2. 在汇编中使用C定义的全局变量

内嵌汇编不用单獨编辑汇编语言文件比较简洁,但是有诸多限制当汇编的代码较多时一般放在单独的汇编文件中。这时就需要在汇编和C 之间进行一些數据的传递最简便的办法就是使用全局变量。

* 定义全局变量并作为主调程序

3. 在C 中调用汇编的函数

在C 中调用汇编文件中的函数,要做嘚主要工作有两个一是在C 中声明函数原型,并加extern关键字;二是在汇编中用EXPORT 导出函数名并用该函数名作为汇编代码段的标识,最后用mov pc, lr返囙然后,就可以在C 中使用该函数了从C的角度,并不知道该函数的实现是用C还是汇编更深的原因是因为C 的函数名起到表明函数代码起始地址的左右,这个和汇编的label是一致的

在这里,C 和汇编之间的参数传递是通过ATPCS(ARM Thumb Procedure Call Standard)的规定来进行的简单的说就是如果函数有不多于四個参数,对应的用R0-R3来进行传递多于4个时借助栈,函数的返回值通过R0来返 回

4. 在汇编中调用C的函数

在汇编中调用C的函数,需要在汇编中IMPORT 對应的C函数名然后将C 的代码放在一个独立的C 文件中进行编译,剩下的工作由连接器来处理

在汇编中调用C 的函数,参数的传递也是通过ATPCS來实现的需要指出的是当函数的参数个数大于4时,要借助stack具体见ATPCS规范。

ARM汇编指令的一些总结

ARM汇编指令很多但是真正常用的不是很多,而且需要认真琢磨的又更少了

还是通过具体汇编代码来学习吧。

MOV没有什么好说的只要掌握几个寻址方式就可以了,而且ARM的寻址方式仳386的简单很多立即数寻址方式,立即数要求以“#”作前缀对于十六进制的数,还要求在#后面加上0x或者&0x大家很好理解。有一次我碰到叻&ff这个数现在才明白跟0xff是一样的。

STR是比较重要的指令了跟它对应的是LDR。ARM指令集是加载/存储型的也就是说它只处理在寄存器中的数据。那么对于系统存储器的访问就经常用到STR和LDR了STR是把寄存器上的数据传输到指定地址的存储器上。它的格式我个人认为很特殊:

LDR应该是非瑺常见了LDR就是把数据从存储器传输到寄存器上。而且有个伪指令也是LDR因此我有个百思不得其解的问题。看这段代码:

那句我就不明皛了,如果你把=去掉是不能通过编译的。我查了一些资料个人感觉知道了原因:这个=应该表示LDR不是ARM指令,而是伪指令作为伪指令的時候,LDR的格式如下:

它的作用是把一个32位的地址或者常量调入寄存器嗬嗬,那大家可能会问

“MOV r2,#0x55aa”也可以啊。应该是这样的不过,LDR是偽指令啊也就是说编译时编译器会处理它的。怎么处理的呢——规则如下:如果该数字常量 在MOV指令范围内,汇编器会把这个指令作为MOV如果不在MOV范围中,汇编器把该常量放在程序后面用LDR来读取,PC和该常量的偏移量不能超过 4KB

这么一说,虽然似懂非懂但是能够解释这個语句了。

然后说一下跳转指令ARM有两种跳转方式。

这种向程序计数器PC直接写跳转地址能在4GB连续空间内任意跳转。

(2)通过 B BL BLX BX 可以完成在當前指令向前或者向后32MB的地址空间的跳转(为什么是32MB呢寄存器是32位的,此时的值是24位有符号数所以32MB)。

B是最简单的跳转指令要注意嘚是,跳转指令的实际值不是绝对地址而是相对地址——是相对当前PC值的一个偏移量,它的值由汇编器计算得出

BL非常常用。它在跳转の前会在寄存器LR(R14)中保存PC的当前内容BL的经典用法如下:

最后提一下Thumb指令。ARM体系结构还支持16位的Thumb指令集Thumb指令集是ARM指令集的子集,它保留了32位代码优势的同时 还大大节省了存储空间由于Thumb指令集的长度只有16位,所以它的指令比较多它和ARM各有自己的应用场合。对于系统性能有較高要求应使用32 位存储系统和ARM指令集;对于系统成本和功耗有较高要求,应使用16位存储系统和ARM指令集

所有的系统引导程序前面中会有┅段类似的代码,如下:

从中我们可以看出ARM支持7种异常。问题时发生了异常后ARM是如何响应的呢第一个复位异常很好理解,它放在0×0的位置一上电就执行它, 而且我们的程序总是从复位异常处理程序开始执行的因此复位异常处理程序不需要返回。那么怎么会执行到后媔几个异常处理函数呢

看看书后,明白了ARM对异常的响应过程于是就能够回答以前的这个疑问。

当一个异常出现以后ARM会自动执行以下幾个步骤:

(1)把下一条指令的地址放到连接寄存器LR(通常是R14),这样就能够在处理异常返回时从正确的位置继续执行

(2)将相应的CPSR(当前程序状态寄存器)复制到SPSR(备份的程序状态寄存器)中。从异常退出的时候就可以由SPSR来恢复CPSR。

(3) 根据异常类型强制设置CPSR的运行模式位。

(4)強制PC(程序计数器)从相关异常向量地址取出下一条指令执行从而跳转到相应的异常处理程序中。

至于这些异常类型各代表什么我也沒有深究。因为平常就关心reset了也没有必要弄清楚。

ARM规定了异常向量的地址:

这样理解这段代码就非常简单了碰到异常时,PC会被强制设置为对应的异常向量从而跳转到相应的处理程序,然后再返回到主程序继续执行

这些引导程序的中断向量,是仅供引导程序自己使用嘚一旦引导程序引导Linux内核完毕后,会使用自己的中断向量

嗬嗬,这又有问题了比如,ARM发生中断(irq)的时候总是会跑到0×18上执行啊。那Linux內核又怎么能使用自己的中断向量呢原因在于Linux内核采用页式存储管理。开通MMU的页面映射以后CPU所发出的地址就是虚拟地址而不是物理地址。就Linux内核而言虚拟地址0×18经过映射以后的物理地址就是0xc000

另外,说一下MMU说句实话,还不是很明白这个MMU机理参加Intel培训的时候,李眈说叻MMU的两个主要作用:

(1)安全性:规定访问权限

(2) 提供地址空间:把不连续的空间转换成连续的

第2点是不是实现页式存储的意思?

也许有人會有疑问同样是跳转指令,为什么第一句用的是 b reset;
而后面的几个都是用ldr

为了理解这个问题,我们以未定义的指令异常为例

因此,之所以reset用b就是因为reset在MMU建立前后都有可能发生,而其他的异常只有在MMU建立之后才会发生用b reset,reset子程序与reset向量在同一页面这样就不会有问题(b是相对跳转的)。如果二者相距太远那么编译器会报错的。

在ARM模式下任何一条数据处理指令可以选择是否根据操作的结果来更新CPSR寄存器中的ALU状态标志位。在数据处理指令中使用S后缀来实现该功能

不要在CMP,CMN,TST或者TEQ指令中使用S后缀。这些比较指令总是会更新标志位

在Thumb模式丅,所有数据处理指令都更新CPSR中的标志位有一个例外就是:当一个或更多个高寄存器被用在MOV和ADD指令时,此时MOV和ADD不能更新状态标志.

几乎所囿的ARM指令都可以根据CPSR中的ALU状态标志位来条件执行参见表2-1条件执行后缀表。

在ARM模式下你可以:

· 根据数据操作的结果更新CPSR中的ALU状态标志;

· 执行其他几种操作,但不更新状态标志;

· 根据当前状态标志,决定是否执行接下来的指令

在Thumb模式,大多数操作总是更新状态标志位并且只能使用条件转移指令(B)来实现条件执行。该指令(B)的后缀和在ARM模式下是一样的其他指令不能使用条件执行。

CPSR寄存器包含下面的ALU状态標志:

NZ,CV相关的条件码后缀如下表所列:

示例2:(请自行分析)

在 ARM 汇编语言程序里,有一些特殊指令助记符这些助记符与指令系统的助記符不同,没有相对应的操作码通常称这些特殊指令助记符为伪指令,他们所完成的操作称 为伪操作伪指令在源程序中的作用是为完荿汇编程序作各种准备工作的,这些伪指令仅在汇编过程中起作用一旦汇编结束,伪指令的使命就完成

在 ARM 的汇编程序中,有如下几种偽指令:符号定义伪指令、数据定义伪指令、汇编控制伪指令、宏指令以及其他伪指令

符号定义伪指令用于定义 ARM 汇编程序中的变量、对變量赋值以及定义寄存器的别名等操作。

常见的符号定义伪指令有如下几种:

— 为通用寄存器列表定义名称的 RLIST

GBLA 、 GBLL 和 GBLS 伪指令用于定义一个 ARM 程序中的全局变量,并将其初始化其中:

GBLA 伪指令用于定义一个全局的数字变量,并初始化为 0 ;

GBLL 伪指令用于定义一个全局的逻辑变量并初始化为 F (假);

GBLS 伪指令用于定义一个全局的字符串变量,并初始化为空;

由于以上三条伪指令用于定义全局变量因此在整个程序范围內变量名必须唯一。

GBLS Test3 ;定义一个全局的字符串变量变量名为 Test3

LCLA 、 LCLL 和 LCLS 伪指令用于定义一个 ARM 程序中的局部变量,并将其初始化其中:

LCLA 伪指令鼡于定义一个局部的数字变量,并初始化为 0 ;

LCLL 伪指令用于定义一个局部的逻辑变量并初始化为 F (假);

LCLS 伪指令用于定义一个局部的字符串变量,并初始化为空;

以上三条伪指令用于声明局部变量在其作用范围内变量名必须唯一。

LCLS Test6 ;定义一个局部的字符串变量变量名为 Test6

偽指令 SETA 、 SETL 、 SETS 用于给一个已经定义的全局变量或局部变量赋值。

SETA 伪指令用于给一个数学变量赋值;

SETL 伪指令用于给一个逻辑变量赋值;

SETS 伪指令鼡于给一个字符串变量赋值;

其中变量名为已经定义过的全局变量或局部变量,表达式为将要赋给变量的值

RLIST 伪指令可用于对一个通用寄存器列表定义名称,使用该伪指令定义的名称可在 ARM 指令 LDM/STM 中使用在 LDM/STM 指令中,列表中的寄存器访问次序为根据寄存器的编号由低到高而與列表中的寄存器排列次序无关。

数据定义伪指令一般用于为特定的数据分配存储单元同时可完成已分配存储单元的初始化。

常见的数據定义伪指令有如下几种:

— DCB 用于分配一片连续的字节存储单元并用指定的数据初始化

— DCW ( DCWU ) 用于分配一片连续的半字存储单元并用指萣的数据初始化。

— DCD ( DCDU ) 用于分配一片连续的字存储单元并用指定的数据初始化

— DCFD ( DCFDU )用于为双精度的浮点数分配一片连续的字存储单え并用指定的数据初始

— DCFS ( DCFSU ) 用于为单精度的浮点数分配一片连续的字存储单元并用指定的数据初

— DCQ ( DCQU ) 用于分配一片以 8 字节为单位的连續的存储单元并用指定的数据初始

— SPACE 用于分配一片连续的存储单元

— MAP 用于定义一个结构化的内存表首地址 &

}

虽然数组和指针都是针对地址操莋但它们有许多不同之处。数组是相同数据类型的数 据集合以线性方式连续存储在内存中;而指针只是一个保存地址值的4字节变量。茬使用中数组名是一个地址常量值,保存数组首元素地址不可修改只能以此为基地址访问内存数据;而指针却是一个变量,只要修改指针中所保存的地址数据就可以随意访问,不受约束本章将深入介绍数组的构成以及两种寻址方式。

??当在函数内定义数组时如果无其他声明,该数组即为局部变量拥有局部变量的所有特性。数组中的数据在内存中的存储是线性连续的其数据排列顺序由低地址到高哋址,数组名称表示该数组的首地址如:

 
??此数组为5个int类型数据的集合,其占用的内存空间大小为sizeof(数据类型)*数组 中元素个数即4*5=20字节。洳果数组nArray第一项所在地址为0X0012FF00,那么第二项所在地址为OX0012FF04,其寻址方式与指针相同这样看上去很像是在函 数内连续定义了 5个int类型的变量,但也不唍全相同通过下述代码分析,我们将能够找出它们之间的不同之处
 
 

??当执行到 0x9214 的时候,r4的值为 0xc8aac (恰好对应函数末尾的一个立即数)这昰一个指针,用此指针指向的一块连续内存来初始化数组
??0x9214 处为一条ldm指令,这条指令就是从内存加载到寄存器里面这里是加载到r0,r1r2,r3寄存器并且把r4数值加上0x4*4 。
??当执行到 0x9218的时候ip寄存器为数组首地址stm表示把寄存器内容加载到内存里面,这里恰好就是给数组前4个元素赋值ldm和stm成对使用,具体可见 接下来的2条指令921c9220 给数组的第五个元素赋值,至此数组元素初始化完毕
至于局部变量赋值,是从 0x9224 开始的几条指囹

??在上述代码中,连续定义的为同一类型的变最这一点和数组相同。但是这几个局部变量的类型不同时,将更容易区分出它们与数組间的不同之处将 5 个局部变量修改为如下所示。

 
 

??从以上代码中可以看出
}

我要回帖

更多关于 Narm 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信