Jul 9, 2013
Technology1. 汇编程序代码格式
汇编代码由一系列的声明所组成,每行一个。每条声明由下列格式组成:
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,即一个全字。
Jul 8, 2013
Technology1. 下载和准备镜像文件
$ 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
之后挂载命令一样。
Jul 8, 2013
Technology前面已经安装并配置了编译链和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的输出。
Jul 8, 2013
Technology1. 关于Bootloader:
(引导程序)位于电脑或其他计算机应用上,是指引导操作系统启动的程序。引导程序启动方式和程序视应用机型种类而不同。例如在普通的个人电脑上,引导程序通常分为两部分:第一阶段引导程序位于主引导记录(MBR),用以引导位于某个分区上的第二阶段引导程序,如NTLDR、GNU
GRUB等。
嵌入式系统中常见的Bootloader主要有以下几种:
- Das U-Boot
是一个主要用于嵌入式系统的开机载入程序,可以支持多种不同的计算机系统结构,包括PPC、ARM、AVR32、MIPS、x86、68k、Nios与MicroBlaze。
- vivi是由mizi公司设计为ARM处理器系列设计的一个bootloader.
- Redboot (Red Hat Embedded Debug and Bootstrap)是Red
Hat公司开发的一个独立运行在嵌入式系统上的BootLoader程序,是目前比较流行的一个功能、可移植性好的BootLoader。
2. 关于“裸机编程(Bare-Metal)”:
微控制器开发人员很熟悉这个概念,
Bare-Metal是指的你的程序和处理器之间没有任何东西—-你写的程序将直接运行在处理器上,
换言之,开发人员是在直接操控硬件。在裸机编程的场景中,需要由开发人员检查并排除任何一个可以导致系统崩溃的风险。
“Bare-Metal"要求开发人员了解关于硬件的细节,所以接下来我们将对编译链和qemu本身进行分析。
3. 下载qemu源码包并查询相关硬件信息:
ArchLinux采用ABS(Arch Build
System)来管理源码包,下面的步骤将qemu源码包下载到本地,更详细的关于ABS的操作可以在ArchLinux的Wiki中找到
$ pacman -S abs
$ pacman -Ss qemu
extra/qemu 1.4.2-2 [installed]
$ abs extra/qemu
$ cp -r /var/abs/extra/qemu/ ~/abs
$ cd ~/abs && makepkg -s --asroot -o
得到versatilepb开发板的CPU型号, 可以看到"arm926"是我们要的结果。
$ grep "arm" src/qemu-1.4.2/hw/versatilepb.c
#include "arm-misc.h"
static struct arm_boot_info versatile_binfo;
args->cpu_model = "arm926";
cpu = cpu_arm_init(args->cpu_model);
cpu_pic = arm_pic_init_cpu(cpu);
arm_load_kernel(cpu, &versatile_binfo);
得到versatilepb开发板的串口寄存器硬件信息:
$ grep "UART*" src/qemu-1.4.2/hw/versatilepb.c
/* 0x10009000 UART3. */
/* 0x101f1000 UART0. */
/* 0x101f2000 UART1. */
/* 0x101f3000 UART2. */
所以说开源是王道嘛,很快就查到了每一个需要了解的细节。UART0在内存中map到的地址是0x101f1000,
我们直接往这个地址写数据,就可以在终端上看到数据输出了。
4. 查看编译链支持的平台:
$ cat ~/CodeSourcery/Sourcery_CodeBench_Lite_for_ARM_EABI/share/doc/arm-arm-none-eabi/info/gcc.info | grep arm926
`arm926ej-s', `arm940t', `arm9tdmi', `arm10tdmi', `arm1020t',
arm926ej-s是被支持的,因此我们可以用这套编译链来生成需要的裸机调试代码。
5. 启动应用程序init.c的编写:
首先创建应用程序init.c:
volatile unsigned char * const UART0_PTR = (unsigned char *)0x0101f1000;
void display(const char *string){
while(*string != '\0'){
*UART0_PTR = *string;
string++;
}
}
int my_init(){
display("Hello Open World\n");
}
init.c中,我们首先声明一个volatile变UART0_PTR,volatile关键字用于告知编译器此变量是用于直接访问内存映像设备的,即串口0内存地址
display()函数则是用于将字符串中的字符按顺序输出到串口0, 直到遇到字符串结尾。
my_init()调用了display(), 接下来我们将把它作为C入口函数.
预编译init.c:
$ arm-none-eabi-gcc -c -mcpu=arm926ej-s init.c -o init.o
6. 启动代码start.s编写:
.global _Start
_Start:
LDR sp, = sp_top
BL my_init
B .
处理器加电后,将跳转到指定的内存地址,从此地址开始读入并执行代码。
_Start被声明为全局函数,_Start的实现中,首先将栈地址指向sp_top, LDR(load),
sp是栈地址寄存器(stack pointer),
BL则是跳转指令,跳转到my_init函数,事实上你可以跳转到任何一个你想跳转的函数,临时写一个their_init()跳转过去也行。Debug时常更改这里以调试不同的子系统功能。
“B.“可以理解为汇编里的while(1)或for(;;)循环,处理器空转,什么也不做。如果不调用它,系统就会崩溃。所谓嵌入式编程的一个基本理念就是,代码无限循环。
预编译汇编文件start.s:
$ arm-none-eabi-as -mcpu=arm926ej-s startup.s -o startup.o
7. 接下来我们需要用一个可以被编译器识别的链接脚本链接两文件, linker.ld:
ENTRY(_Start)
SECTIONS
{
. = 0x10000;
startup : { startup.o(.text)}
.data : {*(.data)}
.bss : {*(.bss)}
. = . + 0x500;
sp_top = .;
}
ENTRY(_Start)用于告知链接器程序的入口点(entry point)是_Start(start.s中定义).
Qemu模拟器如果加上-kernel选项时,将自动从0x10000开始执行,所以我们必须将代码放到这个地址。所以第四行我们指定”.
= 0x10000”. SECTIONS就是用于定义程序的不同部分的。
startup.o组成了代码的text部分,然后是data部分和bss部分,最后一步则定义了栈指针(sp,
stack pointer)地址. 栈通常是向下增长的,所以最好给它一个比较安全的地址, . =
.+0x500就是用于避免栈被改写的。sp_top用于存储栈顶地址。
有关程序结构:
- BSS段: 在采用段式内存管理的架构中,BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS是英文Block Started by Symbol的简称。BSS段属于静态内存分配。.bss section的空间结构类似于stack, 主要用于存储静态变量、未显式初始化、在变量使用前由运行时初始化为零。
- 数据段(data segment): 通常是指用来存放程序中已初始化且不为0的全局变量的一块内存区域。数据段属于静态内存分配。
- 代码段(code segment/text segment): 通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许程序自修改。在代码段中,也有可能包含一些只读的常数变量,例如字符串常量等。
编译:
$ arm-none-eabi-ld -T linker.ld init.o startup.o -o output.elf
$ file output.elf
output.elf: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV),statically linked, not stripped
$ arm-none-eabi-objcopy -O binary output.elf output.bin
$ file output.bin
output.bin: data
8. 使用qemu-system-arm运行output.bin:
$ qemu-system-arm --help | grep nographic
-nographic disable graphical output and redirect serial I/Os to console.
$ qemu-system-arm -M versatilepb -nographic -kernel output.bin
Hello Open World
9. Play more tricks:
改动init.c里的串口输出地址为串口1:
volatile unsigned char * const UART0_PTR = (unsigned char *)0x0101f2000;
// 0x101f1000 --> 0x101f2000
按照步骤3~7里重新编译,并运行以查看结果:
# 没有反应!
$ qemu-system-arm -M versatilepb -nographic -kernel output.bin
# 终端有输出字符。
$ qemu-system-arm -M versatilepb -kernel output.bin -serial vc:800x600 -serial stdio
Hello Open World
同样你也可以把字符输出到第三个串口,只不过前两个-serial的重定向需要指定到别的设备而已。
Jul 8, 2013
Technology1. 下载并交叉编译u-boot。
新版本的u-boot我加载后总有问题,2009.11版则可以顺利通过编译和测试。
$ wget ftp://ftp.denx.de/pub/u-boot/u-boot-2009.11.tar.bz2
$ tar xjvf u-boot-2009.11.tar.bz2
$ cd u-boot-2009.11
$ make versatilepb_config arch=ARM CROSS_COMPILE=arm-none-eabi-
$ make all arch=ARM CROSS_COMPILE=arm-none-eabi-
编译完成后会在目录下生成u-boot.bin和u-boot文件。
2. 运行u-boot.bin:
$ qemu-system-arm -M versatilepb -kernel u-boot.bin -nographic
如果采用-nographic来运行qemu-system-arm,终端将无法再响应任何系统输入譬如Ctrl+c/ctrl+d_,要终止qemu-system-arm就只能查到进程号再kill。所以我一般不带-nographic选项,启动后ctrl+alt+2去看serial0输出,保留在终端窗口直接ctrl+c杀死qemu-sytem-arm进程的权力。
3. 用u-boot引导镜像文件:
改动上一篇文章里用于构建启动镜像的linker.ld文件,因为u-boot.bin文件大小的缘故,我们需要把启动镜像的起始地址整体上移.
$ ls -l -h u-boot.bin
-rwxr-xr-x 1 Trusty root 85K Jul 8 15:57 u-boot.bin
linker.ld文件里, 0x100000,这个大小相比于85K显然已经足够。
ENTRY(_Start)
SECTIONS
{
. = 0x100000;
startup : { startup.o(.text)}
.data : {*(.data)}
.bss : {*(.bss)}
. = . + 0x500;
sp_top = .;
}
按上一章的编译方法生成output.bin,不再重述。
使用mkimage工具创建u-boot可识别的image文件:
$ mkimage -A arm -C none -O linux -T kernel -d output.bin -a 0x00100000 -e 0x00100000 output.uimg
Image Name:
Created: Mon Jul 8 16:04:11 2013
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 152 Bytes = 0.15 kB = 0.00 MB
Load Address: 00100000
Entry Point: 00100000
$ file *.uimg
output.uimg: u-boot legacy uImage, , Linux/ARM, OS Kernel Image (Not \
compressed), 152 bytes, Mon Jul 8 16:04:11 2013, Load Address: 0x00100000,\
Entry Point: 0x00100000, Header CRC: 0x3C62F575, Data CRC: 0x69CE9647
将u-boot.bin和output.uimg打包为一个文件:
$ cat u-boot.bin output.uimg >flash.bin
下面这条命令用于计算output.img在使用u-boot加载完flash.bin后在内存中的地址,-kernel选项告诉qemu从0x100000开始加载镜像,即65536。
65536+u-boot.bin后的大小,即output.img在内存中的地址。printf则是用16进制的格式打印出来,以便加载.
$ printf "0x%X" $(expr $(stat -c%s u-boot.bin) + 65536)
0x2525C
启动qemu-system-arm并运行自定义镜像:
$ qemu-system-arm -M versatilepb -nographic -kernel flash.bin
# iminfo 0x2525c
## Checking Image at 0002525c ...
Legacy image found
Image Name:
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 152 Bytes = 0.1 kB
Load Address: 00100000
Entry Point: 00100000
Verifying Checksum ... OK
VersatilePB # bootm 0x2525c
## Booting kernel from Legacy Image at 0002525c ...
Image Name:
Image Type: ARM Linux Kernel Image (uncompressed)
Data Size: 152 Bytes = 0.1 kB
Load Address: 00100000
Entry Point: 00100000
Loading Kernel Image ... OK
OK
Starting kernel ...
Hello Open World
u-boot可以支持的选项还有很多,包括使用NFS/TFTP启动等等,留待以后慢慢研究。