用Qemu和GNU编译链研究ARM汇编(2)

研究两个汇编程序, 通过研究这两个程序,初步了解ARM汇编的知识:

  • 用于求数组和的程序
  • 用于计算字符串长度的程序

1. 数组求和

        .text
entry:  b start                 @ Skip over the data
arr:    .byte 10, 20, 25        @ Read-only array of bytes
eoa:                            @ Address of end of array + 1

        .align
start:
        ldr   r0, =eoa          @ r0 = &eoa
        ldr   r1, =arr          @ r1 = &arr
        mov   r3, #0            @ r3 = 0
loop:   ldrb  r2, [r1], #1      @ r2 = *r1++
        add   r3, r2, r3        @ r3 += r2
        cmp   r1, r0            @ if (r1 != r2)
        bne   loop              @    goto loop
stop:   b stop

.byte声明:

.byte声明的变量在内存中以连续的比特存在,.2byte和.4byte与之类似,分别用于存储16位值和32位值。联想到C语言中的内建数据结构定义,不难想象char/int/long int 预编译完是哪一种类型。

通用语法结构如下:

	.byte   exp1, exp2, ...
	.2byte  exp1, exp2, ...
	.4byte  exp1, exp2, ...

你可以指定数据的格式,二进制用前缀0b/0B修饰,八进制以前缀0修饰,十进制/十六进制以0x/0X开头。整数也可以用字符常量来表示,加上单引号即可,在这种情况下ASCII码会被用到。

也可以用C表达式,包含文字和其他符号的组合,如下例:

	pattern:  .byte 0b01010101, 0b00110011, 0b00001111
	npattern: .byte npattern - pattern
	halpha:   .byte 'A', 'B', 'C', 'D', 'E', 'F'
	dummy:    .4byte 0xDEADBEEF
	nalpha:   .byte 'Z' - 'A' + 1

.align声明:

ARM指令需要32位对齐,指令的起始内存地址需要是4的倍数,所以用.align指令来插入无用的byte,来确保下一条指令的起始地址是4的倍数。在代码中存在byte或是半字(half words)的时候,需要用到这条指令。

编译&运行:

	# assemble
	$ arm-none-eabi-as -o sum.o sum.s 
	# link to elf file
	$ arm-none-eabi-ld -Ttext=0x0 -o sum.elf sum.o
	# form bin file
	$ arm-none-eabi-objcopy -O binary sum.elf sum.bin
	# form flash image
	$ dd if=/dev/zero of=flash.bin bs=4k count=4k
	$ dd if=sum.bin of=flash.bin bs=4K conv=notrunc
	# emulate with flash image
	$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
	# examine the result
	R00=00000007 R01=00000007 R02=00000019 R03=00000037
	R04=00000000 R05=00000000 R06=00000000 R07=00000000
	R08=00000000 R09=00000000 R10=00000000 R11=00000000
	R12=00000000 R13=00000000 R14=00000000 R15=00000024
	PSR=600001d3 -ZC- A svc32
	FPSCR: 00000000

R03的值正是我们求和的结果: 0x37==55==10+20+25, 可以用一张流程图来表示程序的运行过程

2. 字符串长度

        .text
        b start

str:    .asciz "Hello World"

        .equ   nul, 0

        .align
start:  ldr   r0, =str          @ r0 = &str
        mov   r1, #0

loop:   ldrb  r2, [r0], #1      @ r2 = *(r0++)
        add   r1, r1, #1        @ r1 += 1
        cmp   r2, #nul          @ if (r1 != nul)
        bne   loop              @    goto loop

        sub   r1, r1, #1        @ r1 -= 1
stop:   b stop

运行结果:

	(qemu) info registers
	R00=00000010 R01=0000000b R02=00000000 R03=00000000
	R04=00000000 R05=00000000 R06=00000000 R07=00000000
	R08=00000000 R09=00000000 R10=00000000 R11=00000000
	R12=00000000 R13=00000000 R14=00000000 R15=0000002c
	PSR=600001d3 -ZC- A svc32
	FPSCR: 00000000

r1是用来存储字符串长度的,计算结果为11。

.asciz声明: .asciz声明接受一个字符串作为参数,字符串以双引号修饰的字符表示。汇编器自动在字符串后面加nul字符(即\0字符)

.equ声明 汇编器维持一张符号表,符号表维持键->值的格式。当汇编器遇到一个标号时,汇编器将在符号表中自动建立一个条目。以后当汇编器遇到一个关于label的引用时,将自动替换为符号表中储存的label的地址。

使用汇编器指令.equ,我们可以手动在符号表中插入条目。

.equ通常这样定义: .equ name, expression

.equ不会分配任何内存,它们只是在符号表中插入条目罢了。

bne的意思是(!=) , b means bit. bit not equal. ble (<=), beq (==), bge (>=), bgt (>), and bne (!=).

用Qemu和GNU编译链研究ARM汇编(3)

在多个文件构成的程序中,源文件首先被编译成多个对象(object)文件(.o文件), 然后交由链接器生成最终的可执行文件,如下图所示:

{% img img /images/linker.png %}

在组建可执行文件时,链接器主要完成下列操作:

  • 解析符号
  • 重定位

1. 符号解析

在编译单个文件组成的程序时,所有标号的解析都可以由汇编器替代为对应的地址。而在多文件组成的程序中,如果有储存在其他文件中的符号引用,汇编器会将其标识为"unresolved”(未解析).当对象文件被传递给链接器时,链接器从这些文件中决定对应的值,并把code中的unresolved的值替代为正确的值。

我们用上一节的求和函数来演示链接器是如何进行符号解析的。 这两个文件汇编后,会在链接时被检查未被解析的引用。

main.s
        .text
        b start                 @ Skip over the data
arr:    .byte 10, 20, 25        @ Read-only array of bytes
eoa:                            @ Address of end of array + 1

        .align
start:
        ldr   r0, =arr          @ r0 = &arr
        ldr   r1, =eoa          @ r1 = &eoa

        bl    sum               @ Invoke the sum subroutine

stop:   b stop

more:

sum-sub.s
        @ Args
        @ r0: Start address of array
        @ r1: End address of array
        @
        @ Result
        @ r3: Sum of Array

        .global sum

sum:    mov   r3, #0            @ r3 = 0
loop:   ldrb  r2, [r0], #1      @ r2 = *r0++    ; Get array element
        add   r3, r2, r3        @ r3 += r2      ; Calculate sum
        cmp   r0, r1            @ if (r0 != r1) ; Check if hit end-of-array
        bne   loop              @    goto loop  ; Loop
        mov   pc, lr            @ pc = lr       ; Return when done

查看.o文件符号信息:

	$ arm-none-eabi-nm main.o
	00000004 t arr
	00000007 t eoa
	00000008 t start
	00000014 t stop
	         U sum
	$ arm-none-eabi-nm sum-sub.o 
	00000004 t loop
	00000000 T sum

t代表符号已经被定义了, 而u则代表符号未被定义。大写字母表示该符号是全局变量。

从上面的输出结果看,sum是被定义在sum-sub.o的全局变量,而该变量在main.o中未被解析到。当linker被调用时,符号引用将被解析到,对应的可执行文件将被生成。

总结: as程序负责把.s文件编译成object文件,而生成最终的可执行文件时,ld负责把未被定位的符号定位到实际的库函数所在的位置。

2. 重定位.

重定位用于改变已经分配给标号的地址。它包括将所有符号引用映射到新分配的内存地址。

合并段后的符号列表情况,可以对比于上面的main.o和sum-sub.o来看:

	$ arm-none-eabi-ld -Ttext=0x0 -o sum.elf main.o sum-sub.o
	arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 00000000
	$ arm-none-eabi-nm sum.elf
	00000004 t arr
	00008038 T __bss_end__
	00008038 T _bss_end__
	00008038 T __bss_start
	00008038 T __bss_start__
	00008038 T __data_start
	00008038 T _edata
	00008038 T _end
	00008038 T __end__
	00000007 t eoa
	00000024 t loop
	00080000 T _stack
	00000008 t start
	         U _start
	00000014 t stop
	00000020 T sum

可以看到stop后面的sum已经被定位好了(之前是main.o中的U标记),而loop则相应延后,被定位到了再往后的00000024。

地址的变更: loop原本地址为00000004, 现在是00000024, 而sum原本为0x00000000,现在为00000020, 这是因为sum-sub.o中的.text和main.o中的.text部分一起组成了sum.elf中的.text部分。

整体移动某个段到指定内存位置, 注意在-Ttext中我们增加的0x100的偏移量,使得地址对比于上面的结果整体上移了0x100:

	$ arm-none-eabi-ld -Ttext=0x100 -o sum100.elf main.o sum-sub.o
	arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 00000100
	$ arm-none-eabi-nm sum100.elf 
	00000104 t arr
	00008138 T __bss_end__
	00008138 T _bss_end__
	00008138 T __bss_start
	00008138 T __bss_start__
	00008138 T __data_start
	00008138 T _edata
	00008138 T _end
	00008138 T __end__
	00000107 t eoa
	00000124 t loop
	00080000 T _stack
	00000108 t start
	         U _start
	00000114 t stop
	00000120 T sum

3. 重定位.data到RAM中。

我们可以通过撰写链接脚本,将程序的.data段放置在RAM中。这也是通常嵌入式系统所谓bootloader干的活儿,从Flash中加载启动代码到RAM中而后执行。

例程从RAM中加载两个数值,将两者相加而后将结果写回RAM,两个值和结果都放置在.data部分。

代码:

        .data
val1:   .4byte 10               @ First number
val2:   .4byte 30               @ Second number
result: .4byte 0                @ 4 byte space for result

        .text
        .align
start:
        ldr   r0, =val1         @ r0 = &val1
        ldr   r1, =val2         @ r1 = &val2

        ldr   r2, [r0]          @ r2 = *r0
        ldr   r3, [r1]          @ r3 = *r1

        add   r4, r2, r3        @ r4 = r2 + r3

        ldr   r0, =result       @ r0 = &result
        str   r4, [r0]          @ *r0 = r4

stop:   b stop

链接脚本:

SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }

        . = 0xA0000000;
        .data : { * (.data); }
}

从connex的内存布局来看,内存地址为0xa000_0000到0xa400_0000,因而A0000000刚好在内存中。

查看链接后的内存符号地址:

	$ arm-none-eabi-as -o sum.o sum.s
	$ arm-none-eabi-ld -T sum_link.ld -o sum.elf sum.o 
	$ arm-none-eabi-nm -n sum.elf 
	00000000 t start
	0000001c t stop
	a0000000 d val1
	a0000004 d val2
	a0000008 d result

这样就完了?NO!!!!!!!!因为:RAM is Volatile! 内存是易变的!

RAM是易失性介质,怎可保证每次加电时就有代码洗干净PP在等着被运行?嵌入式系统里必然有非易失性存储,所有的代码和数据在加电前都需要放在这些非易失性存储介质中,例如在FLASH中。这样在加电后我们就可以利用一段启动代码把代码从FLASH搬到RAM中。

从这个设计思路出发,我们需要程序的.data有两个地址,一个是加载地址,另一个是运行地址。 这就是常说的:LMA(Load Memory Address) VS VMA(Virtual Memory Address)。

上面的代码需要做两个修改:

  1. 需要在.data中指定load地址和运行地址
  2. 需要写一段代码用于将数据从FLASH读取到RAM中, 从存储地址到运行地址。
SECTIONS {
        . = 0x00000000;
        .text : { * (.text); }
        etext = .;

        . = 0xA0000000;
        .data : AT (etext) { * (.data); }
}

etext包含了FLASH中放置完地址后的空白地址,记住这个地址以便在接下来将这个数值传送给.data部分,以便程序将.data部分从FLASH拷贝到RAM中。etext只是符号表中的一个,本身并不占据任何内存(可以回去翻上一篇日志)。

关于AT关键字: 它指定了.data部分的加载地址,一个地址或符号被传递给AT关键字,以便它从该地址拷贝数据。 在这里,我们传递etext符号给AT。

要把代码从FLASH拷贝到RAM中,下列信息需要被提供:

  1. Flash中数据地址(flash_sdata)
  2. RAM中数据地址(ram_sdata)
  3. .data部分大小(data_size)

拷贝代码:

        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

copy:
        ldrb  r4, [r0], #1
        strb  r4, [r1], #1
        subs  r2, r2, #1
        bne   copy

由此,我们需要在链接脚本中生成这三个数值:

SECTIONS {
        . = 0x00000000;
        .text : {
              * (.text);
        }
        flash_sdata = .;

        . = 0xA0000000;
        ram_sdata = .;
        .data : AT (flash_sdata) {* (.data); }
        ram_edata = .;
        data_size = ram_edata - ram_sdata;
}

ram_sdata为ram中数据开始地址,而ram_edata为结束地址,两者相减则为数据块大小。

改变后的带有copy数据的代码:

        .data
val1:   .4byte 10               @ First number
val2:   .4byte 30               @ Second number
result: .space 4                @ 1 byte space for result

        .text

        ;; Copy data to RAM.
start:
        ldr   r0, =flash_sdata
        ldr   r1, =ram_sdata
        ldr   r2, =data_size

copy:
        ldrb  r4, [r0], #1
        strb  r4, [r1], #1
        subs  r2, r2, #1
        bne   copy

        ;; Add and store result.
        ldr   r0, =val1         @ r0 = &val1
        ldr   r1, =val2         @ r1 = &val2

        ldr   r2, [r0]          @ r2 = *r0
        ldr   r3, [r1]          @ r3 = *r1

        add   r4, r2, r3        @ r4 = r2 + r3

        ldr   r0, =result       @ r0 = &result
        str   r4, [r0]          @ *r0 = r4

stop:   b stop

使用修改过的final_sum_ram.s和link脚本编译,并生成flash.bin后,就可以在qemu-system-arm中验证结果了。

$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
QEMU 1.4.2 monitor - type 'help' for more information
(qemu) info registers
R00=a0000008 R01=a0000004 R02=0000000a R03=0000001e
R04=00000028 R05=00000000 R06=00000000 R07=00000000
R08=00000000 R09=00000000 R10=00000000 R11=00000000
R12=00000000 R13=00000000 R14=00000000 R15=00000038
PSR=600001d3 -ZC- A svc32
FPSCR: 00000000

(qemu) xp /4dw 0xA0000000
00000000a0000000:         10         30         40          0

R04包含了我们相加后的结果, 为0x28=40, R02/R03则分别为操作数10/30. 而通过显示0xA0000000也显示了内存中的值分别为val1/val2/result的值。

接下来的章节中,我们将讲到C代码入口。

用Qemu和GNU编译链研究ARM汇编(1)

1. 汇编程序代码格式

汇编代码由一系列的声明所组成,每行一个。每条声明由下列格式组成:

	label(标签):	instruction	@comment(注释)

说明:

  • label: 标签的引入使得在内存中查询指令地址变得很方便,标签可以在任意一个内存地址使用, 例如分支指令中就可以用到标签, 标签可以包括字母、数字_和$符号。
  • 注释: 注释内容必须在@符号之后
  • 指令: 指令可以是ARM指令集或是汇编器指令,汇编器指令是需要传递给汇编器的命令,总是以.开头。

2. 一个简单的汇编语言文件:

        .text
start:                       @ Label, not really required
        mov   r0, #5         @ Load register r0 with the value 5
        mov   r1, #4         @ Load register r1 with the value 4
        add   r2, r1, r0     @ Add r0 and r1 and store in r2

stop:   b stop               @ Infinite loop to stop execution

上面代码的意思是,把立即数5载入到寄存器r0, 4载入到r1, 以r1和r0相加的结果填充r2.

.text是汇编器指令,用于告知汇编器需要把代码组装到code段,而不是.data段。有关section的概念在后面将被讲到。

3. 编译二进制文件

GNU的汇编器名字叫as, 用下列命令将源文件编译成.o文件

	$ arm-none-eabi-as -o add.o add.s

链接器的名字叫ld,用下列命令可以将二进制文件链接成elf文件

	$ arm-none-eabi-ld -Ttext=0x0 -o add.elf add.o

-Ttext指明需要分配给label的地址, 这条指令告诉链接器从地址0x0开始装载指令。我们可以用nm来查看具体的地址分配信息。

	$ arm-none-eabi-nm add.elf 
	00008010 T __bss_end__
	00008010 T _bss_end__
	00008010 T __bss_start
	00008010 T __bss_start__
	00008010 T __data_start
	00008010 T _edata
	00008010 T _end
	00008010 T __end__
	00080000 T _stack
	00000000 t start
	         U _start
	0000000c t stop

start和stop之间由0c个字节,因为stop是在start开始后三条指令, 每条指令的长度为4个Byte,3*4=12=0xc

更改链接的参数将得到不同的地址分配。

	$ arm-none-eabi-ld -Ttext=0x20000000 -o add.elf add.o
	arm-none-eabi-ld: warning: cannot find entry symbol _start; defaulting to 20000000
	$ arm-none-eabi-nm add.elf
	20008010 T __bss_end__
	20008010 T _bss_end__
	20008010 T __bss_start
	20008010 T __bss_start__
	20008010 T __data_start
	20008010 T _edata
	20008010 T _end
	20008010 T __end__
	00080000 T _stack
	20000000 t start
	         U _start
	2000000c t stop

ld得到的文件一般是ELF文件,在有操作系统的时候ELF可以工作的很好,但是我们将在裸机模式下(Bare Metal)运行此程序, 因此需要将文件类型转化为更简单的binary类型。

GNU编译链的objcopy可以完成不同可执行文件之间的转换:

	$ arm-none-eabi-objcopy -O binary add.elf add.bin
	$ file add.bin
	add.bin: Hitachi SH big-endian COFF

4. 在Qemu中执行二进制文件。

我们将使用connex开发板来模拟运行此程序,它把16MB的Flash放在地址0x0,而通常arm处理器重启时都会执行0x0处的代码。 因而我们需要把add.bin写入到16MB Flash文件的头部。

首先创建一个空的16MB Flash文件

	$ dd if=/dev/zero of=flash.bin bs=4k count=4k
	4096+0 records in
	4096+0 records out
	16777216 bytes (17 MB) copied, 0.0153106 s, 1.1 GB/s

而后,使用下列命令将add.bin放到Flash头部

	$ dd if=add.bin of=flash.bin bs=4K conv=notrunc
	0+1 records in
	0+1 records out
	16 bytes (16 B) copied, 0.00011154 s, 143 kB/s

add.bin大小刚好为16B, notrunc参数代表no truncated,意思是直接覆盖掉原有内容。

用下列命令执行此改动后的flash文件:

	$ qemu-system-arm -M connex -pflash flash.bin -nographic -serial /dev/null
	QEMU 1.4.2 monitor - type 'help' for more information

-M connex 指定connex机器, -pflash指定flash.bin代替flash闪存。

-pflash file use ‘file’ as a parallel flash image 并行flash镜像

-serial /dev/null 将connex的串口输出重定向到/dev/null

查看寄存器信息:

	(qemu) info registers
	R00=00000005 R01=00000004 R02=00000009 R03=00000000
	R04=00000000 R05=00000000 R06=00000000 R07=00000000
	R08=00000000 R09=00000000 R10=00000000 R11=00000000
	R12=00000000 R13=00000000 R14=00000000 R15=0000000c
	PSR=400001d3 -Z-- A svc32
	FPSCR: 00000000

R02的值正是计算后的结果4+5=9. R15=0000000c 猜测应该为指令寄存器,指向stop(0xc)

5. 更多的查看命令:

help	 List available commands
quit	 Quits the emulator
xp /fmt addr	 Physical memory dump from addr
system_reset	 Reset the system.
	(qemu) help xp
	xp /fmt addr -- physical memory dump starting at 'addr'
	(qemu) xp /4iw 0x0
	0x00000000:  e3a00005      mov	r0, #5	; 0x5
	0x00000004:  e3a01004      mov	r1, #4	; 0x4
	0x00000008:  e0812000      add	r2, r1, r0
	0x0000000c:  eafffffe      b	0xc

4: 4 个条目被显示, i表示打印出指令,即内建的反汇编, w表明条目的大小为32个bit,即一个全字。

在Qemu上运行Raspberry PI镜像

1. 下载和准备镜像文件

	$ wget http://downloads.raspberrypi.org/images/raspbian/2013-05-25-wheezy-raspbian/2013-05-25-wheezy-raspbian.zip
	$ unzip 2013-05-25-wheezy-raspbian.zip

2. 查看镜像文件分区信息

	$ fdisk -l 2013-05-25-wheezy-raspbian.img 
	Disk 2013-05-25-wheezy-raspbian.img: 1939 MB, 1939865600 bytes, 3788800 sectors
	Units = sectors of 1 * 512 = 512 bytes
	Sector size (logical/physical): 512 bytes / 512 bytes
	I/O size (minimum/optimal): 512 bytes / 512 bytes
	Disk label type: dos
	Disk identifier: 0x000c7b31
	
	                         Device Boot      Start         End      Blocks   Id
	System
	2013-05-25-wheezy-raspbian.img1            8192      122879       57344    c 	W95 FAT32 (LBA)
	2013-05-25-wheezy-raspbian.img2          122880     3788799     1832960   83	Linux

从上面可以看到,根文件分区的地址偏移为512*122880=62914560

3. 更改根分区文件里preload信息:

	$ sudo mount ./2013-05-25-wheezy-raspbian.img -o offset=62914560 /mnt3
	$ sudo vim /mnt3/etc/ld.so.preload 
	#注释掉这一行,否则在qemu启动完系统后将自动提示配置rpi而造成系统无法登陆
	#/usr/lib/arm-linux-gnueabihf/libcofi_rpi.so
	$ sudo umount /mnt3

4. 用qemu-system-arm启动raspberrypi镜像

	$ qemu-system-arm -kernel kernel-qemu -cpu arm1176 -m 256 -M versatilepb \
	-no-reboot -serial stdio -append "root=/dev/sda2 panic=1" -hda \
	./2013-05-25-wheezy-raspbian.img 

系统将启动到一个root登陆的无需密码的shell中,运行下列命令以修复文件系统:

	$ fsck /dev/sda2
	$ shutdown -r now

再次启动完毕后的登陆用户名和密码如下,接下来就等同于原机操作了。

	Login as pi
	Password raspberry

5. ArchLinux on RaspberryPI

基本步骤也是一样,挂在第2块分区后,需要更改etc/fstab做下列修改:

	# <file system>        <dir>         <type>    <options>          <dump> <pass>
	/dev/sda1	  /boot           vfat    defaults        0       0
	/dev/sda2	  /		auto    defaults        0       0

之后挂载命令一样。

用Qemu模拟ARM(1)

前面已经安装并配置了编译链和qemu,现在可以用qemu来模拟arm平台了。

1. Hello, Qemu!

输入下面的代码:

#include<stdio.h>
int main()
{
    printf("Hello, Qemu!\n");
    return 0;
}

编译并运行:

	$ arm-none-linux-gnueabi-gcc -o hello hello.c -static
	$ qemu-arm ./hello
	$ file hello
	hello: ELF 32-bit LSB  executable, ARM, EABI5 version 1 (SYSV), \
	 statically linked, for GNU/Linux 2.6.16, not stripped

不加-static变量的话,运行时则需要使用-L选项链接到相应的运行库

	$ qemu-arm -L /home/Trusty/CodeSourcery/\
	Sourcery_CodeBench_Lite_for_ARM_GNU_Linux/\
	arm-none-linux-gnueabi/libc/  ./hello_1 
	Hello, Qemu!
	$ file hello_1
	hello_1: ELF 32-bit LSB  executable, ARM, EABI5 version 1 (SYSV),\
	 dynamically linked (uses shared libs), for GNU/Linux 2.6.16, not stripped

动态编译和静态编译生成的文件大小差别:

	$ ls -l -h
	total 656K
	-rwxr-xr-x 1 Trusty root 640K Jul  7 18:46 hello
	-rwxr-xr-x 1 Trusty root 6.6K Jul  7 18:48 hello_1

###小插曲1:

系统里安装了两套编译链arm-none-eabi-和arm-none-linux-eabi-,很容易让人混淆,可参考编译链的命名规则:

arch(架构)-vendor(厂商名)-(os(操作系统名)-)abi(Application Binary Interface,应用程序二进制接口)

举例说明:

  • x86_64-w64-mingw32 = x86_64 “arch"字段 (=AMD64), w64 (=mingw-w64 是"vendor"字段), mingw32 (=GCC所见的win32 API)
  • i686-unknown-linux-gnu = 32位 GNU/linux编译链
  • arm-none-linux-gnueabi = ARM 架构, 无vendor字段, linux 系统, gnueabi ABI.
  • arm-none-eabi = ARM架构, 无厂商, eabi ABI(embedded abi)

两种编译链的主要区别在于库的差别,前者没有后者的库多,后者主要用于在有操作系统的时候编译APP用的。前者不包括标准输入输出库在内的很多C标准库,适合于做面向硬件的类似单片机那样的开发。因而如果采用arm-none-eabi-gcc来编译hello.c会出现链接错误。

###小插曲2:

qemu-arm和qemu-system-arm的区别:

  • qemu-arm是用户模式的模拟器(更精确的表述应该是系统调用模拟器),而qemu-system-arm则是系统模拟器,它可以模拟出整个机器并运行操作系统
  • qemu-arm仅可用来运行二进制文件,因此你可以交叉编译完例如hello world之类的程序然后交给qemu-arm来运行,简单而高效。而qemu-system-arm则需要你把hello world程序下载到客户机操作系统能访问到的硬盘里才能运行。

2. 使用qemu-system-arm运行Linux内核

从www.kernel.org下载最新内核,而后解压

	$ tar xJf linux-3.10.tar.xz
	$ cd linux-3.10
	$ make ARCH=arm versatile_defconfig
	$ make menuconfig ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi-

上面的命令指定内核架构为arm,交叉编译链为arm-none-linux-gnueabi, 需要在make menuconfig弹出的窗口中选择到 “Kernel Features”, 激活“Use the ARM EABI to compile the kernel”, 如果不激活这个选项的话,内核将无法加载接下来要制作的initramfs。

如果需要在u-boot上加载内核,就要编译为uImage的格式,uImage通过mkimage程序来压缩的,ArchLinux的yaourt仓库里可以找到这个包:

	$ yaourt -S mkimage

安装好mkimage后,开始编译内核,因为CPU有4核,所以开启了-j8选项以加速编译:

	$ make ARCH=arm CROSS_COMPILE=arm-none-linux-gnueabi- all -j8 uImage 

接下来我们可以在qemu-system-arm中测试我们的内核了

	$ qemu-system-arm -M versatilepb -m 128M -kernel ./arch/arm/boot/uImage

在弹出的窗口中可以内核运行到了kernel panic状态,这是因为内核无法加载root镜像的缘故,我们将制作一个最简单的hello world的文件系统,告知kernel运行之。

#include <stdio.h>
 
void main() {
	  printf("Hello World!\n");
	  while(1);
}

编译并制作启动镜像:

	$ arm-none-linux-gnueabi-gcc -o init init.c -static
	$ echo init |cpio -o --format=newc > initramfs
	1280 blocks
	$ file initramfs 
	initramfs: ASCII cpio archive (SVR4 with no CRC)

接下来我们回到编译目录下执行:

	$ qemu-system-arm -M versatilepb -kernel ./arch/arm/boot/uImage  -initrd
	../initramfs -serial stdio -append "console=tty1"

这时候可以看到,kernel运行并在Qemu自带的终端里打印出"Hello World!"。

如果我们改变console变量为ttyAMA0, 将在启动qemu-system-arm的本终端上打印出qemu的输出。