在看过了上面的几节之后,在潜意识中你想记住的东西肯定很多了。这个时候,你需要静下心来休息一下在沉淀一下。

"Now is a good point to take a break to let this information sink in."

下面,我们就看看C语言撰写的程序,在不同的CPU架构下,生成的汇编语言是怎么样的,各有什么特点,这和前面介绍的各种CPU架构的知识是如何联系的。如果你觉得还不够,也很高兴一起来探讨一下在不同的CPU架构下,函数的调用时如何实现的,以及各有什么特点。

反汇编文件

测试用的C源码如下,main.c文件:

#include <stdio.h>

int add(int a,int b)
{
    return a+b;
}


int main(int argc,char** argv)
{
    int a,b,c;

    a = 1;
    b = 2;

    c = fn(a,b);

    return c;
}

下面为编译以及反汇编的过程。当然,ARM和MIPS的编译和反汇编是使用的交叉编译工具链。例如在我的平台下,对应的命令就是ccarm,objdumparm和ccmips,objdumpmips。

#gcc -o main.o -c main.c
#objdump -d main.o>main.a

编译成x86下面的main.o文件,注意,没有链接。然后反汇编为main_x86.a如下所示。这里需要指出的是,下面的汇编语言并不是我们在《微机原理》课本上学习到的x86的汇编(我们称为intel汇编),而叫做AT&T汇编。那么,什么是AT&T汇编呢?

在将Unix移植到80386处理器上时,Unix圈内人士根据Unix领域的习惯和需要而定义了AT&T汇编。而GNU主要是在Unix领域活动,因此GNU开发的各种系统工具继承了AT&T的386汇编格式,这也是我们使用GNU工具进行反汇编的时候,看到的汇编语言。

main.o:     file format elf32-i386


Disassembly of section .text:

00000000 <add>:
   0:	55                   	push   %ebp
   1:	89 e5                	mov    %esp,%ebp
   3:	8b 55 0c             	mov    0xc(%ebp),%edx
   6:	8b 45 08             	mov    0x8(%ebp),%eax
   9:	01 d0                	add    %edx,%eax
   b:	5d                   	pop    %ebp
   c:	c3                   	ret    

0000000d <main>:
   d:	8d 4c 24 04          	lea    0x4(%esp),%ecx
  11:	83 e4 f0             	and    $0xfffffff0,%esp
  14:	ff 71 fc             	pushl  -0x4(%ecx)
  17:	55                   	push   %ebp
  18:	89 e5                	mov    %esp,%ebp
  1a:	51                   	push   %ecx
  1b:	83 ec 24             	sub    $0x24,%esp
  1e:	c7 45 f0 01 00 00 00 	movl   $0x1,-0x10(%ebp)
  25:	c7 45 f4 02 00 00 00 	movl   $0x2,-0xc(%ebp)
  2c:	8b 45 f4             	mov    -0xc(%ebp),%eax
  2f:	89 44 24 04          	mov    %eax,0x4(%esp)
  33:	8b 45 f0             	mov    -0x10(%ebp),%eax
  36:	89 04 24             	mov    %eax,(%esp)
  39:	e8 fc ff ff ff       	call   3a <main+0x2d>
  3e:	89 45 f8             	mov    %eax,-0x8(%ebp)
  41:	8b 45 f8             	mov    -0x8(%ebp),%eax
  44:	83 c4 24             	add    $0x24,%esp
  47:	59                   	pop    %ecx
  48:	5d                   	pop    %ebp
  49:	8d 61 fc             	lea    -0x4(%ecx),%esp
  4c:	c3                   	ret    

编译成mips下面的main.o文件,没有链接。然后反汇编为main_mips.a:


main.o:     file format elf32-bigmips

Disassembly of section .text:

0000000000000000 <add>:
   0:	27bdfff8 	addiu	$sp,$sp,-8
   4:	afbe0000 	sw	$s8,0($sp)
   8:	03a0f021 	move	$s8,$sp
   c:	afc40008 	sw	$a0,8($s8)
  10:	afc5000c 	sw	$a1,12($s8)
  14:	8fc20008 	lw	$v0,8($s8)
  18:	8fc3000c 	lw	$v1,12($s8)
  1c:	00000000 	nop
  20:	00431021 	addu	$v0,$v0,$v1
  24:	03c0e821 	move	$sp,$s8
  28:	8fbe0000 	lw	$s8,0($sp)
  2c:	03e00008 	jr	$ra
  30:	27bd0008 	addiu	$sp,$sp,8

0000000000000034 <main>:
  34:	27bdffd8 	addiu	$sp,$sp,-40
  38:	afbf0024 	sw	$ra,36($sp)
  3c:	afbe0020 	sw	$s8,32($sp)
  40:	03a0f021 	move	$s8,$sp
  44:	afc40028 	sw	$a0,40($s8)
  48:	afc5002c 	sw	$a1,44($s8)
  4c:	24020001 	li	$v0,1
  50:	afc20010 	sw	$v0,16($s8)
  54:	24020002 	li	$v0,2
  58:	afc20014 	sw	$v0,20($s8)
  5c:	8fc40010 	lw	$a0,16($s8)
  60:	8fc50014 	lw	$a1,20($s8)
  64:	0c000000 	jal	0 <add>
  68:	00000000 	nop
  6c:	afc20018 	sw	$v0,24($s8)
  70:	8fc20018 	lw	$v0,24($s8)
  74:	03c0e821 	move	$sp,$s8
  78:	8fbf0024 	lw	$ra,36($sp)
  7c:	8fbe0020 	lw	$s8,32($sp)
  80:	03e00008 	jr	$ra
  84:	27bd0028 	addiu	$sp,$sp,40
	...

编译成arm下的main.o,没有链接。然后反汇编为main_arm.a:


main.o:     file format elf32-littlearm

Disassembly of section .text:

00000000 <add>:
   0:	e1a0c00d 	mov	r12, sp
   4:	e92dd800 	stmdb	sp!, {r11, r12, lr, pc}
   8:	e24cb004 	sub	r11, r12, #4	; 0x4
   c:	e24dd008 	sub	sp, sp, #8	; 0x8
  10:	e50b0010 	str	r0, [r11, -#16]
  14:	e50b1014 	str	r1, [r11, -#20]
  18:	e51b3010 	ldr	r3, [r11, -#16]
  1c:	e51b2014 	ldr	r2, [r11, -#20]
  20:	e0833002 	add	r3, r3, r2
  24:	e1a00003 	mov	r0, r3
  28:	ea000009 	b	2c <add+0x2c>
  2c:	e91ba800 	ldmdb	r11, {r11, sp, pc}

00000030 <main>:
  30:	e1a0c00d 	mov	r12, sp
  34:	e92dd800 	stmdb	sp!, {r11, r12, lr, pc}
  38:	e24cb004 	sub	r11, r12, #4	; 0x4
  3c:	e24dd014 	sub	sp, sp, #20	; 0x14
  40:	e50b0010 	str	r0, [r11, -#16]
  44:	e50b1014 	str	r1, [r11, -#20]
  48:	e3a03001 	mov	r3, #1	; 0x1
  4c:	e50b3018 	str	r3, [r11, -#24]
  50:	e3a03002 	mov	r3, #2	; 0x2
  54:	e50b301c 	str	r3, [r11, -#28]
  58:	e51b0018 	ldr	r0, [r11, -#24]
  5c:	e51b101c 	ldr	r1, [r11, -#28]
  60:	ebfffffe 	bl	0 <add>
  64:	e1a03000 	mov	r3, r0
  68:	e50b3020 	str	r3, [r11, -#32]
  6c:	e51b3020 	ldr	r3, [r11, -#32]
  70:	e1a00003 	mov	r0, r3
  74:	ea00001c 	b	78 <main+0x48>
  78:	e91ba800 	ldmdb	r11, {r11, sp, pc}

不同CPU架构下汇编语言的特点

代码长度

首先,我们看同一个C语言程序,在不同的处理器下,代码长度。代码长度为反汇编文件的第一列,可以看到,在x86下是0x4c+1个字节,在MIPS下是0x84+4个字节,在ARM下是0x78+4个字节。为什么这样呢?

首先看的是CISC和RISC的区别 CISC架构下,指令的长度不是固定的,而RISC为了实现流水线,每条指令的长度都是固定的。看到反汇编文件的第二列,x86的指令中,最短为一个字节,例如push,pop,ret等指令,最长为7个字节,例如movl指令。而对于ARM和MIPS,所有的指令长度都固定为4个字节。这也是ARM和MIPS的机器代码长度要明显比x86长的原因。

另外一个原因,就是CISC可能为某个特殊操作实现了一条指令,而RISC处理器则需要用多条指令组合来完成该操作。最明显的就是出入栈操作。

然后我们看MIPS和ARM的区别 ARM的代码长度比MIPS代码长度稍稍小,这也验证了人们常说的MIPS是纯粹的RISC架构,而ARM则在RISC的基础上吸收了CISC中的某些优点。我们还是要看看,ARM是因为吸收了哪些CISC的特点,才做到代码长度的减少的呢?最明显的是出入栈,在ARM中实现了多寄存器load/store指令,请注意main_arm.a文件的0x4,0x2c,0x34,0x78行的stmdb和ldmdb指令。那么,这和CISC有什么关系呢?仔细想想,这就是CISC中的为了某个特殊的操作而实现一条指令的思想。另外,多寄存器的load/store指令破坏了RISC中指令的执行周期必须是单周期的规定,这一点,也是人们指责ARM不是纯粹的RISC架构的证据之一。

另外还有就是ARM实现了条件标志,实现条件执行,这样可以减少分支指令的数目,提高代码的密度。不过在我们这个实例中没有这方面的应用。

出栈入栈

入栈指将CPU通用寄存器的值放入栈(存储器)中保存起来。出栈则是指将栈中的值恢复到CPU中相应通用寄存器。

在x86下面,有专门的出栈和入栈指令pop和push。出栈和入栈在栈中的位置是当前堆栈指针sp所指向的位置。在MIPS和ARM下面,没有专门的指令,所以入栈和出栈首先使用的是load/store指令将数据放入堆栈或者弹出堆栈,然后使用add/stub指令修改堆栈指针的值。比较特殊的是ARM有特殊的多寄存器load/store指令来快速的完成出入栈。由此可见,在MIPS和ARM下面,出入栈都是使用帧指针或者堆栈指针的相对位置,在执行之后,并不会影响堆栈指针sp的值。

栈的生长 栈的生长指堆栈指针sp的变化。例如,在函数调用的时候,需要一次性的分配被调用函数的栈空间大小。在x86,ARM,MIPS下都是使用的对堆栈指针寄存器直接加减的办法。而且,在MIPS和ARM下,也只有这种办法可以改变堆栈指针SP的值。而在x86下,除了直接加减的办法之外,出入栈操作指令pop和push还可以改变sp的值。

不同CPU架构下函数调用的特点

C语言函数的调用,请参考C语言-Stack的相关内容。涉及堆栈帧(stack frame),活动记录(active record),调用惯例(call convention)等相关概念。建议参考《程序员的自我修养-链接、装载与库》的第10.2节-栈和调用惯例。

毫无疑问,这里都是使用的C语言的默认调用惯例cdecl。cdecl调用管理的特点如下:

首先我们给出一个cdecl调用惯例的模型,不针对任何处理器架构,然后我们再看看不同的处理器架构下cdecl调用惯例有什么样的特点。下图为cdecl调用惯例的一般情况示意图。

下面分析cdecl调用惯例中,调用函数和被调用函数分别负责活动记录(堆帧栈)中的什么操作呢?首先是被调用函数,在函数的入口,需要做以下操作:

上面是被调用函数需要负责的事情,那么调用函数需要负责什么呢?主要有一下几点:

X86的函数调用

这个调用中没有调整堆栈指针的操作,因为不需要用到额外的堆栈空间。所以,只有上面提到的第一点和第二点。

push   %ebp
mov    %esp,%ebp
......
pop    %ebp
ret    

MIPS的函数调用

MIPS的堆栈,在被调用函数中有8bytes的升栈操作,下面的ARM中也是一样的。

addiu	$sp,$sp,-8
sw	$s8,0($sp)
move	$s8,$sp
......
move	$sp,$s8
lw	$s8,0($sp)
jr	$ra
addiu	$sp,$sp,8

ARM的函数调用

相比于MIPS架构,一条stmdb和ldmdb就可以完成多个寄存器的存储(store)和加载(load)。

mov	r12, sp
stmdb	sp!, {r11, r12, lr, pc}
sub	r11, r12, #4	; 0x4
sub	sp, sp, #8	; 0x8
......
b	2c <add+0x2c>
ldmdb	r11, {r11, sp, pc}

轻松一下

我们可以用《盗梦空间》来形容函数调用和堆栈操作。现实与梦,梦中有梦,层层深入。梦的层数太多,以至于不知道自己位于第几层梦境,以至于回到了现实还以为自己在做梦……

海报

海报-男主角

海报-最终没有回到现实的女主角