常用的汇编指令都有哪些?MOV(MOVe) 传送指令
PUSH 入栈指令 JZ OPR //结果为零转移
汇编语言和CPU以及内存,端口等硬件知识是连在一起的. 这也是为什么汇编语言没有通用性的原因. 下面简单讲讲基本知识(针对INTEL x86及其兼容机) 在汇编语言中,寄存器用名字来访问. CPU 寄存器有好几类, 分别有不同的用处: 1. 通用寄存器: 这些32位可以被用作多种用途,但每一个都有”专长”. EAX 是”累加器”(accumulator), 它是很多加法乘法指令的缺省寄存器. EBX 是”基地址”(base)寄存器, 在内存寻址时存放基地址. ECX 是计数器(counter), 是重复(REP)前缀指令和LOOP指令的内定计数器. EDX是…(忘了..哈哈)但它总是被用来放整数除法产生的余数. 这4个寄存器的低16位可以被单独访问,分别用AX,BX,CX和DX. AX又可以单独访问低8位(AL)和高8位(AH), BX,CX,DX也类似. 函数的返回值经常被放在EAX中. ESI/EDI分别叫做”源/目标索引寄存器”(source/destination index),因为在很多字符串操作指令中, DS:ESI指向源串,而ES:EDI指向目标串. EBP是”基址指针”(BASE POINTER), 它最经常被用作高级语言函数调用的”框架指针”(frame pointer). 在破解的时候,经常可以看见一个标准的函数起始代码: push ebp ;保存当前ebp 这样一来,EBP 构成了该函数的一个框架, 在EBP上方分别是原来的EBP, 返回地址和参数. EBP下方则是临时变量. 函数返回时作 mov esp,ebp/pop ebp/ret 即可. ESP 专门用作堆栈指针. 2. 段寄存器: 3. 标志寄存器(EFLAGS): 该寄存器有32位,组合了各个系统标志. EFLAGS一般不作为整体访问, 而只对单一的标志位感兴趣. 常用的标志有: 进位标志C(CARRY), 在加法产生进位或减法有借位时置1, 否则为0. JXX 系列指令就是根据这些标志来决定是否要跳转, 从而实现条件分枝. 要注意,很多JXX 指令是等价的, 对应相同的机器码. 例如, JE 和JZ 是一样的,都是当Z=1是跳转. 只有JMP 是无条件跳转. JXX 指令分为两组, 分别用于无符号操作和带符号操作. JXX 后面的”XX” 有如下字母: 无符号操作: 带符号操作: 如果仔细想一想,就会发现 JA = JNBE, JAE = JNB, JBE = JNA, JG = JNLE, JGE= JNL, JL= JNGE, …. 4. 端口 端口是直接和外部设备通讯的地方。外设接入系统后,系统就会把外设的数据接口映射到特定的端口地址空间,这样,从该端口读入数据就是从外设读入数 据,而向外设写入数据就是向端口写入数据。当然这一切都必须遵循外设的工作方式。端口的地址空间与内存地址空间无关,系统总共提供对64K个8位端口的访 问,编号0-65535. 相邻的8位端口可以组成成一个16位端口,相邻的16位端口可以组成一个32位端口。端口输入输出由指令IN,OUT,INS和OUTS实现,具体可参考 汇编语言书籍。
汇编指令的操作数可以是内存中的数据, 如何让程序从内存中正确取得所需要的数据就是对内存的寻址。 INTEL 的CPU 可以工作在两种寻址模式:实模式和保护模式。 前者已经过时,就不讲了, WINDOWS 现在是32位保护模式的系统, PE 文件就基本是运行在一个32位线性地址空间, 所以这里就只介绍32位线性空间的寻址方式。 其实线性地址的概念是很直观的, 就想象一系列字节排成一长队,第一个字节编号为0, 第二个编号位1, 。。。。 一直到4294967295(十六进制FFFFFFFF,这是32位二进制数所能表达的最大值了)。 这已经有4GB的容量! 足够容纳一个程序所有的代码和数据。 当然, 这并不表示你的机器有那么多内存。 物理内存的管理和分配是很复杂的内容, 初学者不必在意, 总之, 从程序本身的角度看, 就好象是在那么大的内存中。 在INTEL系统中, 内存地址总是由”段选择符:有效地址”的方式给出。段选择符(SELECTOR)存放在某一个段寄存器中, 有效地址则可由不同的方式给出。 段选择符通过检索段描述符确定段的起始地址, 长度(又称段限制), 粒度, 存取权限, 访问性质等。 先不用深究这些, 只要知道段选择符可以确定段的性质就行了。 一旦由选择符确定了段, 有效地址相对于段的基地址开始算。 比如由选择符1A7选择的数据段, 其基地址是400000, 把1A7 装入DS中, 就确定使用该数据段。 DS:0 就指向线性地址400000。 DS:1F5278 就指向线性地址5E5278。 我们在一般情况下, 看不到也不需要看到段的起始地址, 只需要关心在该段中的有效地址就行了。 在32位系统中, 有效地址也是由32位数字表示, 就是说, 只要有一个段就足以涵盖4GB线性地址空间, 为什么还要有不同的段选择符呢? 正如前面所说的, 这是为了对数据进行不同性质的访问。 非法的访问将产生异常中断, 而这正是保护模式的核心内容, 是构造优先级和多任务系统的基础。 这里有涉及到很多深层的东西, 初学者先可不必理会。 有效地址的计算方式是: 基址+间址*比例因子+偏移量。 这些量都是指段内的相对于段起始地址的量度, 和段的起始地址没有关系。 比如, 基址=100000, 间址=400, 比例因子=4, 偏移量=20000, 则有效地址为: 100000+400*4+20000=100000+1000+20000=121000。 对应的线性地址是400000+121000=521000。 (注意, 都是十六进制数)。 基址可以放在任何32位通用寄存器中, 间址也可以放在除ESP外的任何一个通用寄存器中。 比例因子可以是1, 2, 4 或8。 偏移量是立即数。 如: [EBP+EDX*8+200]就是一个有效的有效地址表达式。 当然, 多数情况下用不着这么复杂, 间址,比例因子和偏移量不一定要出现。 内存的基本单位是字节(BYTE)。 每个字节是8个二进制位, 所以每个字节能表示的最大的数是11111111, 即十进制的255。 一般来说, 用十六进制比较方便, 因为每4个二进制位刚好等于1个十六进制位, 11111111b = 0xFF。 内存中的字节是连续存放的, 两个字节构成一个字(WORD), 两个字构成一个双字(DWORD)。 在INTEL架构中, 采用small endian格式, 即在内存中,高位字节在低位字节后面。 举例说明:十六进制数803E7D0C, 每两位是一个字节, 在内存中的形式是:0C 7D 3E 80。 在32位寄存器中则是正常形式,如在EAX就是803E7D0C。 当我们的形式地址指向这个数的时候,实际上是指向第一个字节,即0C。 我们可以指定访问长度是字节, 字或者双字。 假设DS:[EDX]指向第一个字节0C: mov AL, byte ptr DS:[EDX] ;把字节0C存入AL 在段的属性中,有一个就是缺省访问宽度。如果缺省访问宽度为双字(在32位系统中经常如此),那么要进行字节或字的访问,就必须用byte/word ptr显式地指明。 缺省段选择:如果指令中只有作为段内偏移的有效地址,而没有指明在哪一个段里的时候,有如下规则: 如果用ebp和esp作为基址或间址,则认为是在SS确定的段中; 如果想打破这个规则,就必须使用段超越前缀。举例如下: mov eax, dword ptr [edx] ;缺省使用DS,把DS:[EDX]指向的双字送入eax 堆栈: 堆栈是一种数据结构,严格地应该叫做“栈”。“堆”是另一种类似但不同的结构。SS 和 ESP 是INTEL对栈这种数据结构的硬件支持。push/pop指令是专门针对栈结构的特定操作。SS指定一个段为栈段,ESP则指出当前的栈顶。push xxx 指令作如下操作: 把ESP的值减去4; 这样,esp的值减小了4,并且SS:[ESP]指向新压入的xxx。 所以栈是“倒着长”的,从高地址向低地址方向扩展。pop yyy 指令做相反的操作,把SS:[ESP]指向的双字送到yyy指定的寄存器或内存单元,然后把esp的值加上4。这时,认为该值已被弹出,不再在栈上了,因 为它虽然还暂时存在在原来的栈顶位置,但下一个push操作就会把它覆盖。因此,在栈段中地址低于esp的内存单元中的数据均被认为是未定义的。 最后,有一个要注意的事实是,汇编语言是面向机器的,指令和机器码基本上是一一对应的,所以它们的实现取决于硬件。有些看似合理的指令实际上是不存在的,比如: mov DS:[edx], ds:[ecx] ;内存单元之间不能直接传送
“汇编语言”作为一门语言,对应于高级语言的编译器,我们需要一个“汇编器”来把汇编语言原文件汇编成机器可执行的代码。高级的汇编器如MASM, TASM等等为我们写汇编程序提供了很多类似于高级语言的特征,比如结构化、抽象等。在这样的环境中编写的汇编程序,有很大一部分是面向汇编器的伪指令, 已经类同于高级语言。现在的汇编环境已经如此高级,即使全部用汇编语言来编写windows的应用程序也是可行的,但这不是汇编语言的长处。汇编语言的长 处在于编写高效且需要对机器硬件精确控制的程序。而且我想这里的人学习汇编的目的多半是为了在破解时看懂反汇编代码,很少有人真的要拿汇编语言编程序吧? (汗……) 好了,言归正传。大多数汇编语言书都是面向汇编语言编程的,我的帖是面向机器和反汇编的,希望能起到相辅相成的作用。有了前面两篇的基础,汇编语言 书上对大多数指令的介绍应该能够看懂、理解了。这里再讲一讲一些常见而操作比较复杂的指令。我这里讲的都是机器的硬指令,不针对任何汇编器。 无条件转移指令jmp: 这种跳转指令有三种方式:短(short),近(near)和远(far)。短是指要跳至的目标地址与当前地址前后相差不超过128字节。近是指跳 转的目标地址与当前地址在用一个段内,即CS的值不变,只改变EIP的值。远指跳到另一个代码段去执行,CS/EIP都要改变。短和近在编码上有所不同, 在汇编指令中一般很少显式指定,只要写 jmp 目标地址,几乎任何汇编器都会根据目标地址的距离采用适当的编码。远转移在32位系统中很少见到,原因前面已经讲过,由于有足够的线性空间,一个程序很少 需要两个代码段,就连用到的系统模块也被映射到同一个地址空间。 jmp的操作数自然是目标地址,这个指令支持直接寻址和间接寻址。间接寻址又可分为寄存器间接寻址和内存间接寻址。举例如下(32位系统): jmp 8E347D60 ;直接寻址段内跳转 解释: 内存字节是连续存放的,如何确定取多少作为目标地址呢?dword ptr 指明该有效地址指明的是双字,所以取 注意:在保护模式下,如果段间转移涉及优先级的变化,则有一系列复杂的保护检查,现在可不加理会。将来等各位功力提升以后可以自己去学习。 条件转移指令jxx:只能作段内转移,且只支持直接寻址。 ========================================= Call的寻址方式与jmp基本相同,但为了从子程序返回,该指令在跳转以前会把紧接着它的下一条指令的地址压进堆栈。如果是段内调用(目标地址是 32位偏移量),则压入的也只是一个偏移量。如果是段间调用(目标地址是48位全地址),则也压入下一条指令的完全地址。同样,如果段间转移涉及优先级的 变化,则有一系列复杂的保护检查。 与之对应retn/retf指令则从子程序返回。它从堆栈上取得返回地址(是call指令压进去的)并跳到该地址执行。retn取32位偏移量作段 内返回,retf取48位全地址作段间返回。retn/f 还可以跟一个立即数作为操作数,该数实际上是从堆栈上传给子程序的参数的个数(以字计)返回后自动把堆栈指针esp加上指定的数*2,从而丢弃堆栈中的参 数。这里具体的细节留待下一篇讲述。 虽然call和ret设计为一起工作,但它们之间没有必然的联系。就是说,如果你直接用push指令向堆栈中压入一个数,然后执行ret,他同样会把你压入的数作为返回地址,而跳到那里去执行。这种非正常的流程转移可以被用作反跟踪手段。 中断指令INT n 在保护模式下,这个指令必定会被操作系统截获。在一般的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做暂存。这是硬件规定的,所以在使用这些指令之前一定要设好
相应的指针。 串操作指令经常和重复前缀REP和循环指令LOOP结合使用以完成对整个字符串的操作。而REP前缀和LOOP指令都有硬件规定用ECX做循环计数器。举例: LDS ESI,SRC_STR_PTR 上面的代码从SRC_STR拷贝200个双字到DST_STR. 细节是:REP前缀先检查ECX是否为0,若否则执行一次MOVSD,ECX自动减1,然后执行第二轮检查、执行……直到发现ECX=0便不再执行MOVSD,结束重复而执行下面的指令。 LDS ESI,SRC_STR_PTR LOOP LOOP1 从SRC_STR处理100个字。同样,LOOP指令先判断ECX是否为零,来决定是否循环。每循环一轮ECX自动减1。 REP和LOOP 都可以加上条件,变成REPZ/REPNZ 和 LOOPZ/LOOPNZ. 这是除了ECX外,还用检查零标志Z. REPZ 和LOOPZ在Z为1时继续循环,否则退出循环,即使ECX不为0。REPNZ/LOOPNZ则相反。 高级语言程序的汇编解析 在高级语言中,如C和PASCAL等等,我们不再直接对硬件资源进行操作,而是面向于问题的解决,这主要体现在数据抽象化和程序的结构化。例如我们 用变量名来存取数据,而不再关心这个数据究竟在内存的什么地方。这样,对硬件资源的使用方式完全交给了编译器去处理。不过,一些基本的规则还是存在的,而 且大多数编译器都遵循一些规范,这使得我们在阅读反汇编代码的时候日子好过一点。这里主要讲讲汇编代码中一些和高级语言对应的地方。 1. 普通变量。通常声明的变量是存放在内存中的。编译器把变量名和一个内存地址联系起来(这里要注意的是,所谓的“确定的地址”是对编译器而言在编译阶段算出 的一个临时的地址。在连接成可执行文件并加载到内存中执行的时候要进行重定位等一系列调整,才生成一个实时的内存地址,不过这并不影响程序的逻辑,所以先 不必太在意这些细节,只要知道所有的函数名字和变量名字都对应一个内存的地址就行了),所以变量名在汇编代码中就表现为一个有效地址,就是放在方括号中的 操作数。例如,在C文件中声明: int my_age; 这个整型的变量就存在一个特定的内存位置。语句 my_age= 32; 在反汇编代码中可能表现为: mov word ptr [007E85DA], 20 所以在方括号中的有效地址对应的是变量名。又如: char my_name[11] = “lianzi2000″; 这样的说明也确定了一个地址,对应于my_name. 假设地址是007E85DC,则内存中[007E85DC]=’l',[007E85DD]=’i', etc. 对my_name的访问也就是对这地址处的数据访问。 指针变量其本身也同样对应一个地址,因为它本身也是一个变量。如: char *your_name; 这时也确定变量”your_name”对应一个内存地址,假设为007E85F0. 语句your_name=my_name;很可能表现为: mov [007E85F0], 007E85DC ;your_name的内容是my_name的地址。 2. 寄存器变量 在C和C++中允许说明寄存器变量。register int i; 指明i是寄存器存放的整型变量。通常,编译器都把寄存器变量放在esi和edi中。寄存器是在cpu内部的结构,对它的访问要比内存快得多,所以把频繁使用的变量放在寄存器中可以提高程序执行速度。 3. 数组 不管是多少维的数组,在内存中总是把所有的元素都连续存放,所以在内存中总是一维的。例如,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对应的地址装入一个通用寄存器作 为基址,则对数组元素的访问就是一个计算有效地址的问题: ; i_array[1][1]=0×16 lea ebx,xxxxxxxx ;i_array 对应的地址装入ebx 当然,取决于不同的编译器和程序上下文,具体实现可能不同,但这种基本的形式是确定的。从这里也可以看到比例因子的作用(还记得比例因子的取值为 1,2,4或8吗?),因为在目前的系统中简单变量总是占据1,2,4或者8个字节的长度,所以比例因子的存在为在内存中的查表操作提供了极大方便。 4. 结构和对象 结构和对象的成员在内存中也都连续存放,但有时为了在字边界或双字边界对齐,可能有些微调整,所以要确定对象的大小应该用sizeof操作符而不应 该把成员的大小相加来计算。当我们声明一个结构变量或初始化一个对象时,这个结构变量和对象的名字也对应一个内存地址。举例说明: struct tag_info_struct 变量marry就对应一个内存地址。在这个地址开始,有足够多的字节(sizeof(marry))容纳所有的成员。每一个成员则对应一个相对于这 个地址的偏移量。这里假设此结构中所有的成员都连续存放,则age的相对地址为0,sex为2, height 为4,weight为8。 ; marry.sex=0; lea ebx,xxxxxxxx ;marry 对应的内存地址 对象的情况基本相同。注意成员函数具体的实现在代码段中,在对象中存放的是一个指向该函数的指针。 5. 函数调用 一个函数在被定义时,也确定一个内存地址对应于函数名字。如: long comb(int m, int n) return temp; 这样,函数comb就对应一个内存地址。对它的调用表现为: CALL xxxxxxxx ;comb对应的地址。这个函数需要两个整型参数,就通过堆栈来传递: ;lresult=comb(2,3); push 3 这里请注意两点。第一,在C语言中,参数的压栈顺序是和参数顺序相反的,即后面的参数先压栈,所以先执行push 3. 第二,在我们讨论的32位系统中,如果不指明参数类型,缺省的情况就是压入32位双字。因此,两个push指令总共压入了两个双字,即8个字节的数据。然 后执行call指令。call 指令又把返回地址,即下一条指令(mov dword ptr….)的32位地址压入,然后跳转到xxxxxxxx去执行。 在comb子程序入口处(xxxxxxxx),堆栈的状态是这样的: 03000000 (请回忆small endian 格式) 前面讲过,子程序的标准起始代码是这样的: push ebp ;保存原先的ebp 执行push ebp之后,堆栈如下: 03000000 执行mov ebp,esp之后,ebp 和esp 都指向原来的ebp. 然后sub esp, xxx 给临时变量留空间。这里,只有一个临时变量temp,是一个长整数,需要4个字节,所以xxx=4。这样就建立了这个子程序的框架: 03000000 所以子程序可以用[ebp+8]取得第一参数(m),用[ebp+C]来取得第二参数(n),以此类推。临时变量则都在ebp下面,如这里的temp就对应于[ebp-4]. 子程序执行到最后,要返回temp的值: mov eax,[ebp-04] mov esp,ebp ;这时esp 和ebp都指向old ebp,临时变量已经被撤销 这是esp指向返回地址。紧接的retn指令返回主程序: retn 4 该指令从堆栈弹出返回地址装入EIP,从而返回到主程序去执行call后面的指令。同时调整esp(esp=esp+4*2),从而撤销参数,使堆 栈恢复到调用子程序以前的状态,这就是堆栈的平衡。调用子程序前后总是应该维持堆栈的平衡。从这里也可以看到,临时变量temp已经随着子程序的返回而消 失,所以试图返回一个指向临时变量的指针是非法的。 为了更好地支持高级语言,INTEL还提供了指令Enter 和Leave 来自动完成框架的建立和撤销。Enter 接受两个操作数,第一个指明给临时变量预留的字节数,第二个是子程序嵌套调用层数,一般都为0。enter xxx,0 相当于: push ebp leave 则相当于: mov esp,ebp =============================================================
计算机汇编语言的一个突出优点就是利用符号(Symbol)来代替目标码,也即大量的二进制代码用符号来表示,使汇编语言源程序容易理解,便于记忆。 在宏汇编语言中所有变量名、标号名、记录名、指令助记符和寄存器名等统称符号.这些符号可通过汇编控制语句的伪操作命令重新命名,也可以通过指令给 它定义其它名字及新的类型属性,因而给程序设计带来很大的灵活性.符号是程序员在程序中用来代表某个存储单元、数据、表达式和名字等所定义的标识符,可分 为寄存器、标号、变量、数字、名字五类. 汇编语句形式: 标号 2 1 标号的属性 因标号表示的是指令地址,所以它有三个属性,即段属性、偏移属性和类型属性.段属性即段地址,标号的段必须在CS中.偏移属性是表示该标号到段首地 址的距离,单位是字节,是16位无符号整数.类型属性是距离属性,指标号和转移指令的距离,该标号在本段内引用,距离在-128~+127之间时称短标 号,距离属性为SHORT,当标号在本段,距离在-32768~+32767之间时称近标号,距离属性为NEAT,当引用标号的指令和标号不在同一段时称 远标号,距离属性为FAR. 2 2 标号的定义 标号的定义有三种方法: 变量 变量(Variable)代表存放在某些存储单元的数据,这些数据在程序运行期间可以随时被修改.变量是通过变量名在程序中引用,变量名实际上是存 储区中一个数据区的名字,以变量名数据的方式供程序员使用,作为指令或伪.指令的操作数,大大方便了程序设计者.由于变量是在逻辑段中定义.这就决定了变 量和标号一样具有段属性、偏移属性和类型属性,前两个和标号的属性相同,而类型属性是指出数据区的数据项的存取单位是字节(BYTE),字(WORD)或 数字(DWORD)等.可见变量和标号的主要区别在于变量指的是数据,而标号则对应的是指令。 .3 1 变量的定义 .3 1 1 用伪指令DB,DW,DD等来定义 格式:[变量名] 定义数据伪指令〈表达式〉 其中变量名可有可无,若没有名字则该变量为无名变量.表达式可以是常数、保留符号”?”、ASCII码字符串(只能用DB定义)、地址表达式(不能用DB定义)、预置数据表格和用DUP定义的重复值.变量名可在任一逻辑段中定义,其后边不紧跟冒号而是加一空格。 .例如:A DB 100;A为一个字节,值为100. 3 1 2 用伪指令LABEL定义变量 格式: 3 1 4 双重定义变量名利用隐含方式和显示方式的双重方式,可以对同一位置定义为双重变量. 格式 3 2 变量的访问 3 2 1 变量名作为存储单元的直接地址 变量名用直接寻址时,变量的类型必须与指令的要求相符合. 其中SEG和OFFSET用法和标号相同,分别表示取变量所在段的段地址和变量的偏移地址.而TYPE运算符,将回送该变量类型所表示的字节数. 3 2 5 取变量数据项个数运算符LENGTH对于变量定义时使用DUP的情况,汇编程序将回送DUP前的重复次数,即分配给该变量的单元数,若表达式有多个DUP,则取第一个DUP项,其它情况则回送1. 3 2 6 取变量数据项长度算符SIZE SIZE算符,汇编程序将回送分配给该变量的字节数,即 例如: 要注意:对字符串变量求其长度,使用SIZE不能达到目的. 3 2 7 变量名仅对应数据区第一个数据项 除标号和变量外,符号还可表示常量、段名、过程名、寄存器名和指令助记符等. (1)符号常数常数也常以符号形式出现,使之更具有通用性且便于修改.例:
汇编语言是各种计算机语言中与硬件关系最为密切、最直接的语言,是时空效率最高的语言,它能够利用计算机所有硬件特性并能直接控制硬件,所以在计算
机应用系统设计和过程控制中是必不可少的.目前教学中采用8086/8088汇编语言系统组织教学仍是最佳选择.其中子程序技术是一种解决重复性问题的重
要设计方法,采用子程序结构可以简化源程序书写、提高程序存储效率、减少出错率、增加程序的易读性和可维护性,并且有利用子程序资源的组织和使用.设计子
程序时,除了必需要考虑的程序调用、返回和完成特定功能的指令序列外,还必须注意解决子程序设计中带有的共性的一些问题,即:现场保护、参数传递、子程序
的嵌套与递归调用、编写子程序说明文档等.
ARM中C和汇编混合编程及示例 在嵌入式系统开发中,目前使用的主要编程语言是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关键字,用法如下:
_asm { instruction [; instruction] … [instruction] } asm(“instruction [; instruction]“);
下面通过一个例子来说明如何在C 中内嵌汇编语言,
#include <stdio.h> void my_strcpy(const char *src, char *dest) { char ch; _asm { loop: ldrb ch, [src], #1 strb ch, [dest], #1 cmp ch, #0 bne loop } } int main() { char *a = “forget it and move on!”; char b[64]; my_strcpy(a, b); printf(“original: %s”, a); printf(“copyed: %s”, b); return 0; }
在这里C 和汇编之间的值传递是用C 的指针来实现的,因为指针对应的是地址,所以汇编中也可以访问。
2. 在汇编中使用C定义的全局变量 内嵌汇编不用单独编辑汇编语言文件,比较简洁,但是有诸多限制,当汇编的代码较多时一般放在单独的汇编文件中。这时就需要在汇编和C 之间进行一些数据的传递,最简便的办法就是使用全局变量。
/* cfile.c * 定义全局变量,并作为主调程序 */ #include <stdio.h> int gVar_1 = 12; extern asmDouble(void); int main() { printf(“original value of gVar_1 is: %d”, gVar_1); asmDouble(); printf(” modified value of gVar_1 is: %d”, gVar_1); return 0; }
对应的汇编语言文件:
;called by main(in C),to double an integer, a global var defined in C is used. AREA asmfile, CODE, READONLY EXPORT asmDouble
IMPORT gVar_1 asmDouble ldr r0, =gVar_1 ldr r1, [r0] mov r2, #2 mul r3, r1, r2 str r3, [r0] mov pc, lr END
3. 在C 中调用汇编的函数 在C 中调用汇编文件中的函数,要做的主要工作有两个,一是在C 中声明函数原型,并加extern关键字;二是在汇编中用EXPORT 导出函数名,并用该函数名作为汇编代码段的标识,最后用mov pc, lr返回。然后,就可以在C 中使用该函数了。从C的角度,并不知道该函数的实现是用C还是汇编。更深的原因是因为C 的函数名起到表明函数代码起始地址的左右,这个和汇编的label是一致的。
/* cfile.c * in C,call an asm function, asm_strcpy * Sep 9, 2004 */ #include <stdio.h> extern void asm_strcpy(const char *src, char *dest); int main() { const char *s = “seasons in the sun”; char d[32]; asm_strcpy(s, d); printf(“source: %s”, s); printf(” destination: %s”,d); return 0; }
;asm function implementation AREA asmfile, CODE, READONLY EXPORT asm_strcpy asm_strcpy loop ldrb r4, [r0], #1 address increment after read cmp r4, #0 beq over strb r4, [r1], #1 b loop over mov pc, lr END
在这里,C 和汇编之间的参数传递是通过ATPCS(ARM Thumb Procedure Call Standard)的规定来进行的。简单的说就是如果函数有不多于四个参数,对应的用R0-R3来进行传递,多于4个时借助栈,函数的返回值通过R0来返 回。
4. 在汇编中调用C的函数 在汇编中调用C的函数,需要在汇编中IMPORT 对应的C函数名,然后将C 的代码放在一个独立的C 文件中进行编译,剩下的工作由连接器来处理。
;the details of parameters transfer comes from ATPCS ;if there are more than 4 args, stack will be used EXPORT asmfile AREA asmfile, CODE, READONLY IMPORT cFun ENTRY mov r0, #11 mov r1, #22 mov r2, #33 BL cFun END
/*C file, called by asmfile */ int cFun(int a, int b, int c) { return a + b + c; }
在汇编中调用C 的函数,参数的传递也是通过ATPCS来实现的。需要指出的是当函数的参数个数大于4时,要借助stack,具体见ATPCS规范。
ARM汇编指令的一些总结 ARM汇编指令很多,但是真正常用的不是很多,而且需要认真琢磨的又更少了。 比较有用的是MOV B BL LDR STR 还是通过具体汇编代码来学习吧。 @ disable watch dog timer mov r1, #0×53000000 //立即数寻址方式 mov r2, #0×0 str r2, [r1] MOV没有什么好说的,只要掌握几个寻址方式就可以了,而且ARM的寻址方式比386的简单很多。立即数寻址方式,立即数要求以“#”作前缀,对于十六进制的数,还要求在#后面加上0x或者&。0x大家很好理解。有一次我碰到了&ff这个数,现在才明白跟0xff是一样的。 STR是比较重要的指令了,跟它对应的是LDR。ARM指令集是加载/存储型的,也就是说它只处理在寄存器中的数据。那么对于系统存储器的访问就经常用到STR和LDR了。STR是把寄存器上的数据传输到指定地址的存储器上。它的格式我个人认为很特殊: STR(条件) 源寄存器,<存储器地址> 比如 STR R0, [R1] ,意思是R0-> [R1],它把源寄存器写在前面,跟MOV、LDR都相反。 LDR应该是非常常见了。LDR就是把数据从存储器传输到寄存器上。而且有个伪指令也是LDR,因此我有个百思不得其解的问题。看这段代码: mov r1, #GPIO_CTL_BASE add r1, r1, #oGPIO_F ldr r2,=0x55aa // 0x55aa是个立即数啊,前面加个=干什么? str r2, [r1, #oGPIO_CON] mov r2, #0xff str r2, [r1, #oGPIO_UP] mov r2, #0×00 str r2, [r1, #oGPIO_DAT] 对于当中的ldr 那句,我就不明白了,如果你把=去掉,是不能通过编译的。我查了一些资料,个人感觉知道了原因:这个=应该表示LDR不是ARM指令,而是伪指令。作为伪指令的时候,LDR的格式如下: LDR 寄存器, =数字常量/Label 它的作用是把一个32位的地址或者常量调入寄存器。嗬嗬,那大家可能会问, “MOV r2,#0x55aa”也可以啊。应该是这样的。不过,LDR是伪指令啊,也就是说编译时编译器会处理它的。怎么处理的呢?——规则如下:如果该数字常量 在MOV指令范围内,汇编器会把这个指令作为MOV。如果不在MOV范围中,汇编器把该常量放在程序后面,用LDR来读取,PC和该常量的偏移量不能超过 4KB。 这么一说,虽然似懂非懂,但是能够解释这个语句了。
然后说一下跳转指令。ARM有两种跳转方式。 (1) mov pc <跳转地址〉 这种向程序计数器PC直接写跳转地址,能在4GB连续空间内任意跳转。 (2)通过 B BL BLX BX 可以完成在当前指令向前或者向后32MB的地址空间的跳转(为什么是32MB呢?寄存器是32位的,此时的值是24位有符号数,所以32MB)。 B是最简单的跳转指令。要注意的是,跳转指令的实际值不是绝对地址,而是相对地址——是相对当前PC值的一个偏移量,它的值由汇编器计算得出。 BL非常常用。它在跳转之前会在寄存器LR(R14)中保存PC的当前内容。BL的经典用法如下: bl NEXT ; 跳转到NEXT …… NEXT …… mov pc, lr ; 从子程序返回。
最后提一下Thumb指令。ARM体系结构还支持16位的Thumb指令集。Thumb指令集是ARM指令集的子集,它保留了32位代码优势的同时 还大大节省了存储空间。由于Thumb指令集的长度只有16位,所以它的指令比较多。它和ARM各有自己的应用场合。对于系统性能有较高要求,应使用32 位存储系统和ARM指令集;对于系统成本和功耗有较高要求,应使用16位存储系统和ARM指令集。 对ARM异常(Exceptions)的理解 分类:技术笔记 毕设笔记 1.对ARM异常(Exceptions)的理解 所有的系统引导程序前面中会有一段类似的代码,如下:
从中我们可以看出,ARM支持7种异常。问题时发生了异常后ARM是如何响应的呢?第一个复位异常很好理解,它放在0×0的位置,一上电就执行它, 而且我们的程序总是从复位异常处理程序开始执行的,因此复位异常处理程序不需要返回。那么怎么会执行到后面几个异常处理函数呢? 看看书后,明白了ARM对异常的响应过程,于是就能够回答以前的这个疑问。 当一个异常出现以后,ARM会自动执行以下几个步骤: (1)把下一条指令的地址放到连接寄存器LR(通常是R14),这样就能够在处理异常返回时从正确的位置继续执行。 (2)将相应的CPSR(当前程序状态寄存器)复制到SPSR(备份的程序状态寄存器)中。从异常退出的时候,就可以由SPSR来恢复CPSR。 (3) 根据异常类型,强制设置CPSR的运行模式位。 (4)强制PC(程序计数器)从相关异常向量地址取出下一条指令执行,从而跳转到相应的异常处理程序中。 至于这些异常类型各代表什么,我也没有深究。因为平常就关心reset了,也没有必要弄清楚。 ARM规定了异常向量的地址: b reset ; 复位 0×0 ldr pc, _undefined_instruction ;未定义的指令异常 0×4 ldr pc, _software_interrupt ;软件中断异常 0×8 ldr pc, _prefetch_abort ;预取指令 0xc ldr pc, _data_abort ;数据 0×10 ldr pc, _not_used ;未使用 0×14 ldr pc, _irq ;慢速中断异常 0×18 ldr pc, _fiq ;快速中断异常 0x1c 这样理解这段代码就非常简单了。碰到异常时,PC会被强制设置为对应的异常向量,从而跳转到相应的处理程序,然后再返回到主程序继续执行。 这些引导程序的中断向量,是仅供引导程序自己使用的,一旦引导程序引导Linux内核完毕后,会使用自己的中断向量。 嗬嗬,这又有问题了。比如,ARM发生中断(irq)的时候,总是会跑到0×18上执行啊。那Linux内核又怎么能使用自己的中断向量呢?原因在于Linux内核采用页式存储管理。开通MMU的页面映射以后,CPU所发出的地址就是虚拟地址而不是物理地址。就Linux内核而言,虚拟地址0×18经过映射以后的物理地址就是0xc000 0018。所以Linux把中断向量放到0xc000 0018就可以了。 另外,说一下MMU。说句实话,还不是很明白这个MMU机理。参加Intel培训的时候,李眈说了MMU的两个主要作用: (1)安全性:规定访问权限 (2) 提供地址空间:把不连续的空间转换成连续的。 第2点是不是实现页式存储的意思? 2005年6月9日晚 补充一下: 05/06/14 .globl _start ;系统复位位置 …… _undefined_instruction : 也许有人会有疑问,同样是跳转指令,为什么第一句用的是 b reset; 为了理解这个问题,我们以未定义的指令异常为例。 当发生了这个异常后,CPU总是跳转到0×4,这个地址是虚拟地址,它映射到哪个物理地址 因此,之所以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模式下是一样的。其他指令不能使用条件执行。 2.5.1 ALU状态标志 CPSR寄存器包含下面的ALU状态标志: 2.5.2 执行条件 N,Z,C,V相关的条件码后缀如下表所列: 举例说明:
示例1: ADD r0, r1, r2 ; r0 = r1 + r2, 不更新标志位 ADDS r0, r1, r2 ; r0 = r1 + r2, 后缀S表示更新标志位 ADDCSS r0, r1, r2 ; If C 标志为1,则执行r0 = r1 + r2, 且更新标志, CMP r0, r1 ; CMP指令肯定会更新标志.
示例2:(请自行分析) gcd CMP r0, r1 BEQ end BLT less SUB r0, r0, r1 B gcd less SUB r1, r1, r0 B gcd end
在 ARM 汇编语言程序里,有一些特殊指令助记符,这些助记符与指令系统的助记符不同,没有相对应的操作码,通常称这些特殊指令助记符为伪指令,他们所完成的操作称 为伪操作。伪指令在源程序中的作用是为完成汇编程序作各种准备工作的,这些伪指令仅在汇编过程中起作用,一旦汇编结束,伪指令的使命就完成。
在 ARM 的汇编程序中,有如下几种伪指令:符号定义伪指令、数据定义伪指令、汇编控制伪指令、宏指令以及其他伪指令。
符号定义( Symbol Definition )伪指令 符号定义伪指令用于定义 ARM 汇编程序中的变量、对变量赋值以及定义寄存器的别名等操作。 常见的符号定义伪指令有如下几种: — 用于定义全局变量的 GBLA 、 GBLL 和 GBLS 。 — 用于定义局部变量的 LCLA 、 LCLL 和 LCLS 。 — 用于对变量赋值的 SETA 、 SETL 、 SETS 。 — 为通用寄存器列表定义名称的 RLIST 。 1、 GBLA、GBLL 和GBLS 语法格式: GBLA ( GBLL 或 GBLS ) 全局变量名 GBLA 、 GBLL 和 GBLS 伪指令用于定义一个 ARM 程序中的全局变量,并将其初始化。其中: GBLA 伪指令用于定义一个全局的数字变量,并初始化为 0 ; GBLL 伪指令用于定义一个全局的逻辑变量,并初始化为 F (假); GBLS 伪指令用于定义一个全局的字符串变量,并初始化为空; 由于以上三条伪指令用于定义全局变量,因此在整个程序范围内变量名必须唯一。 使用示例: GBLA Test1 ;定义一个全局的数字变量,变量名为 Test1 Test1 SETA 0xaa ;将该变量赋值为 0xaa GBLL Test2 ;定义一个全局的逻辑变量,变量名为 Test2 Test2 SETL {TRUE} ;将该变量赋值为真 GBLS Test3 ;定义一个全局的字符串变量,变量名为 Test3 Test3 SETS “ Testing ” ;将该变量赋值为 “ Testing ”
2、 LCLA、LCLL 和LCLS 语法格式: LCLA ( LCLL 或 LCLS ) 局部变量名 LCLA 、 LCLL 和 LCLS 伪指令用于定义一个 ARM 程序中的局部变量,并将其初始化。其中: LCLA 伪指令用于定义一个局部的数字变量,并初始化为 0 ; LCLL 伪指令用于定义一个局部的逻辑变量,并初始化为 F (假); LCLS 伪指令用于定义一个局部的字符串变量,并初始化为空; 以上三条伪指令用于声明局部变量,在其作用范围内变量名必须唯一。 使用示例: LCLA Test4 ;声明一个局部的数字变量,变量名为 Test4 Test3 SETA 0xaa ;将该变量赋值为 0xaa LCLL Test5 ;声明一个局部的逻辑变量,变量名为 Test5 Test4 SETL {TRUE} ;将该变量赋值为真 LCLS Test6 ;定义一个局部的字符串变量,变量名为 Test6 Test6 SETS “ Testing ” ;将该变量赋值为 “ Testing ”
3、 SETA、SETL 和SETS 语法格式: 变量名 SETA ( SETL 或 SETS ) 表达式 伪指令 SETA 、 SETL 、 SETS 用于给一个已经定义的全局变量或局部变量赋值。 SETA 伪指令用于给一个数学变量赋值; SETL 伪指令用于给一个逻辑变量赋值; SETS 伪指令用于给一个字符串变量赋值; 其中,变量名为已经定义过的全局变量或局部变量,表达式为将要赋给变量的值。 使用示例: LCLA Test3 ;声明一个局部的数字变量,变量名为 Test3 Test3 SETA 0xaa ;将该变量赋值为 0xaa LCLL Test4 ;声明一个局部的逻辑变量,变量名为 Test4 Test4 SETL {TRUE} ;将该变量赋值为真
4 、 RLIST 语法格式: 名称 RLIST { 寄存器列表 } RLIST 伪指令可用于对一个通用寄存器列表定义名称,使用该伪指令定义的名称可在 ARM 指令 LDM/STM 中使用。在 LDM/STM 指令中,列表中的寄存器访问次序为根据寄存器的编号由低到高,而与列表中的寄存器排列次序无关。 使用示例: RegList RLIST {R0-R5 , R8 , R10} ;将寄存器列表名称定义为 RegList ,可在 ARM 指令 LDM/STM中通过该名称访问寄存器列表。
数据定义( Data Definition )伪指令 数据定义伪指令一般用于为特定的数据分配存储单元,同时可完成已分配存储单元的初始化。 常见的数据定义伪指令有如下几种: — DCB 用于分配一片连续的字节存储单元并用指定的数据初始化。 — DCW ( DCWU ) 用于分配一片连续的半字存储单元并用指定的数据初始化。 — DCD ( DCDU ) 用于分配一片连续的字存储单元并用指定的数据初始化。 — DCFD ( DCFDU )用于为双精度的浮点数分配一片连续的字存储单元并用指定的数据初始 化。 — DCFS ( DCFSU ) 用于为单精度的浮点数分配一片连续的字存储单元并用指定的数据初 始化。 — DCQ ( DCQU ) 用于分配一片以 8 字节为单位的连续的存储单元并用指定的数据初始 化。 — SPACE 用于分配一片连续的存储单元 — MAP 用于定义一个结构化的内存表首地址 & |
|
来自: phoenixcyan > 《ARM汇编》