汇编基础知识

汇编语言作为低级语言,直接描述/控制CPU的运行,学习汇编的意义就是为了理解CUP的运行情况已经高级代码的运行步骤

汇编语言

    来历
    最早的时候,编写程序就是手写二进制指令,然后通过各种开关输入计算机,比如要做加法,就按一下加法开关。后来发明了纸袋打孔法,通过在纸袋上打孔,将二进制指令自动输入计算机。
    为了解决二进制指令的可读性问题,工程师将那些指令写成了八进制。二进制转八进制是轻而易举的,但是八进制的可读性也不行。很自然地,最后还是用文字表达,加法指令写成 ADD。内存地址也不再直接引用,而是用标签表示。
    这样的话,就多出一个步骤,要把这些文字指令翻译成二进制,这个步骤就称为 assembling,完成这个步骤的程序就叫做 assembler。它处理的文本,自然就叫做 aseembly code。标准化以后,称为 assembly language,缩写为 asm,中文译为汇编语言。

    汇编语言的定位
    CPU只负责计算,不具备智能。输入一条指令(Instruction),它就会执行一次,然后停下来,等待下一条指令。而这些指令都是二进制编码,被称为操作码(opcode),比如指令00000011就表示加法指令。而编译器的作用,就是将高级语言写好的程序翻译成一条条操作码供CPU执行。
    可是对于人类来说,二进制程序不可读,所以无法理解机器的工作流程。因此为了解决可读性问题,以及偶尔的编辑需求,就诞生了汇编语言。
    汇编语言是二进制指令的文本形式,与指令是一一对应的关系,比如,加码至零00000011写成汇编语言就是ADD。只要还原成二进制,汇编语言就可以被CPU直接执行,所以它是最底层的低级语言。

寄存器

    学习汇编语言必须了解的知识点:寄存器和内存模型
    寄存器:CPU本身只负责运算,不负责存储数据。数据一般都存储在内存之中,CPU要用的时候就去内存中读写数据。但是CPU的运算速度远高于内存的读写速度,为了避免被拖慢,CPU都自带一级缓存和二级缓存。基本上,CPU缓存可以看作是读写速度较快的内存。
    但是,CPU缓存还是不够快,而且数据在缓存里面的地址是不固定的,CPU每次读写都要寻址也会拖慢速度。因此,除了缓存之外,CPU还自带了寄存器,用来存储常用的数据。也就是说,那些最频繁读写的数据(比如循环变量),都会放在寄存器里面,CPU优先读写寄存器,再由寄存器跟内存交换数据。寄存器不依靠地址区分数据,而依靠名称。每一个寄存器都有自己的名称,编程时告诉 CPU 去具体的哪一个寄存器拿数据,这样的速度是最快的。有人比喻寄存器是 CPU 的零级缓存。
CPU存储架构
    早期的x86 CPU只有8个寄存器,而且每个都有不同的用途(ESP 寄存器有特定用途,保存当前 Stack 的地址),现在的寄存器数量已经有100多个,都变成了通用寄存器。
    而ARM的32位CPU的寄存器数量是37个,一般是31个通用寄存器和6个状态寄存器。
    另外,32位CPU、64位CPU指的是寄存器的大小,32位的CPU的寄存器可以容纳4个字节的数据。

内存模型

    Heap堆
    寄存器只能存放少量的数据,大多数的时候,CPU要指挥寄存器,直接跟内存交换数据,所以必须要了解内存是怎么储存数据。
    程序运行的时候,操作系统会给它分配一段内存,用来存储程序和运行产生的数据。这段内存有起始地址和结束地址,比如从0x1000到0x8000,起始地址是较小的地址,结束地址是比较大的地址。
内存运行
    程序运行过程中,对于动态的内存占用请求(比如新建对象,或者使用malloc命令),系统就会从预先分配好的那段内存之中,划出一部分分给用户,具体规则是从起始地址开始划分(实际上,起始地址会有一段静态数据,这里忽略)。
    举例说明:用户要求得到10个字节的内存,那么从起始地址0x1000开始给它分配,一直分配到地址0x100A,如果再要求得到22个字节,那么就分配到0x1020。
堆空间
    这种因为用户主动请求而划分出来的内存区域,叫作Heap(堆),它由起始地址开始,从低地址向高地址增长。Heap的一个重要特点就是不会自动消失,必须手动释放,或者由垃圾回收机制来回收。

    Stack栈
    除了堆以外,其他的内存占用叫作Stack(栈)。简单说,stack是由于函数运行而临时占用的内存区域。栈
    举例分析:系统开始执行main函数时,会在内存中建立一个帧(frame),所有的内部变量都保存在这个帧里面。main函数执行结束后,该帧就会被回收,释放所有的内部变量,不再占用空间。如果在main函数中调用了其它函数。也会为其他函数建立一个帧,用来存储它对应的内部变量,因此,调用栈有多少层,就有多少帧。
栈帧
    以上栈中等到add_a_and_b运行结束,它的帧就会被回收,系统会回到函数main刚才中断执行的地方,继续往下执行。通过这种机制,就实现了函数的层层调用,并且每一层都能使用自己的本地变量。
    所有的帧都存放在Stack,由于帧是一层层叠加的,所以Stack叫作栈。生成新的栈,就叫做“入栈”,英文是push;栈的回收就叫做“出栈”,英文名是pop。Stack的特点就是,最晚入栈的帧最早出栈(因为最内层的函数调用,最先结束运行),这就是后进先出的数据结构。每一次函数执行结束,就自动释放一个帧,所有函数执行结束,整个Stack就都释放了。
    Stack是由内存区域的结束地址开始,从高位地址向低位地址分配。比如,内存区域的结束抵制是0x8000,第一帧假定是16字节,那么下一次分配的地址就会从0x7FF0开始;第二帧假定需要64字节,那么地址就会移动到0x7FB0。因此,可以理解为堆是向上生长而栈是向下生长。

CPU指令

    以一个简答的加法程序来分析汇编语言如下:

// C语言源码
int add_a_and_b(int a, int b) {
   return a + b;
}

int main() {
   return add_a_and_b(2, 3);
}

// 转换成ASM汇编语言如下:
_add_a_and_b:
   push   %ebx
   mov    %eax, [%esp+8] 
   mov    %ebx, [%esp+12]
   add    %eax, %ebx 
   pop    %ebx 
   ret  

_main:
   push   3
   push   2
   call   _add_a_and_b 
   add    %esp, 8
   ret
  • push指令
        根据约定,程序从main函数开始执行,这时会在Stack上为main建立一个帧,并将Stack所执行的地址,写入ESP寄存器。后面如果有数据要写入main这个帧,就会写在ESP寄存器所保存的地址。
push 3	
			/*
			 开始执行第一行代码
             push指令用于将运算子放入Stack,这里就是将3写入main这个帧
             虽然看上去很简单,push指令其实有一个前置条件。它会先取出ESP寄存器里面的地址,将其减去4个字节,然后将新地址写入ESP寄存器。
             使用减法是因为Stack是从高到低位发展,4个字节则是因为3的类型是int,占用4个字节
             得到新地址后,3就会写入这个地址开始的4个字节
             */
 
push 2
			/*
			 第二行也是一样,push指令将2写入到main这个帧,位置紧贴着前面写入的3。这时,ESP寄存器会再减去4个字节(累计减去8)
			 */           

push操作

  • call指令
        call指令用来调用函数,上面的代码表示调用add_a_and_b函数。这时,程序就会去找_add_a_and_b标签,并为该函数建立一个新的帧。然后再开始执行_add_a_and_b的代码。
push %ebx
				//这一行表示将 EBX 寄存器里面的值,写入_add_a_and_b这个帧。
				//这是因为后面要用到这个寄存器,就先把里面的值取出来,用完后再写回去。
				//这时,push指令会再将 ESP 寄存器里面的地址减去4个字节(累计减去12)。
  • mov指令
        mov指令用于将一个值写入某个寄存器
 mov  %eax [%esp+8]	//先将 ESP 寄存器里面的地址加上8个字节,得到一个新的地址,然后按照这个地址在 Stack 取出数据。
 							//根据前面的步骤,可以推算出这里取出的是2,再将2写入 EAX 寄存器。
 mov  %ebx [%esp+12]	//将 ESP 寄存器的值加12个字节,再按照这个地址在 Stack 取出数据,这次取出的是3,将其写入 EBX 寄存器。
  • add命令
        add指令用于将两个运算子相加,并将结果写入第一个运算子
add %eax %ebx // 将 EAX 寄存器的值(即2)加上 EBX 寄存器的值(即3),得到结果5,再将这个结果写入第一个运算子 EAX 寄存器。
  • pop指令
        pop指令用于取出 Stack 最近一个写入的值(即最低位地址的值),并将这个值写入运算子指定的位置。
pop    %ebx
		//取出 Stack 最近写入的值(即 EBX 寄存器的原始值),再将这个值写回 EBX 寄存器(因为加法已经做完了,EBX 寄存器用不到了)。
		//注意,pop指令还会将 ESP 寄存器里面的地址加4,即回收4个字节。
  • ret指令
        ret指令用于终止当前函数的执行,将执行权交还给上层函数。也就是,当前函数的帧将被回收
ret
		// 该指令没有运算子
		// 随着add_a_and_b函数终止执行,系统就会回到刚才main函数中断的地方,继续往下执行
add %esp 8
		//将 ESP 寄存器里面的地址,手动加上8个字节,再写回 ESP 寄存器。
		//这是因为 ESP 寄存器的是 Stack 的写入开始地址,前面的pop操作已经回收了4个字节,这里再回收8个字节,等于全部回收。
ret
		//main函数运行结束,ret指令退出程序执行
ARM汇编与C语言编程

    ARM编程中使用的C语言是标准C语言,ARM的开发环境实际上就是嵌入了一个C语言的集成开发环境,只不过这个开发环境与ARM的硬件紧密相关。
    在使用C语言时,要用到和汇编语言的混合编程。若汇编代码较为简洁,则可使用直接内嵌汇编的方法;否则要将汇编程序以文件的形式加入到项目中,按照ATPCS(ARM/Thumb过程调用标准,ARM/Thumb Procedure Call Standard)的规定与C程序相互调用与访问。
    在C程序和ARM汇编程序之间相互调用时必须遵守ATPCS规则。ATPCS规定了一些子程序间调用的基本规则,哪寄存器的使用规则,堆栈的使用规则和参数的传递规则等。

寄存器使用规则
    子程序之间通过寄存器r0-r3来传递参数,当参数个数多于4个时,使用堆栈来传递参数。此时r0-r3可以记为A1~A4。
    在子程序中,使用寄存器r4-r11保存局部变量。因此当进行子程序调用时要注意对这些寄存器的保存和恢复。此时r4-r11可记作V1~V8。
    寄存器R12用于保存堆栈指针SP,当子程序返回时使用该寄存器出栈,记作IP。
    寄存器R13用作堆栈指针,记作SP。
    寄存器R14称为链接寄存器,记作LR。该寄存器用于保存在程序的返回地址。
    寄存器R15称为程序计数器,记作PC。

堆栈的使用规则
    ATPCS规定堆栈采用满递减类型(FD,Full Descending),即堆栈通过减小存储器地址而向下增长,堆栈指针指向内含有效数据项的最低地址。

参数的传递规则
    整数参数的前4个使用r0-r3传递,其他参数使用堆栈传递;浮点参数使用编号最小且能够满足需要的一组连续的FP寄存器传递参数。
    子程序的返回结果为一个32位整数时,通过r0返回;返回结果为一个64位整数时,通过r0和r1返回;依此类推。结果为浮点数时,通过浮点运算部件的寄存器F0、D0或者S0返回。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值