2.1.虚拟机及开发系统平台介绍
VMware
Bochs
2.1.1.WMware的安装
VMware旗下的VMwareWorkstation和VMwarePlayer均可满足本书开发需求。只要能顺利安装一款Linux发行版操作系统,并支持动态挂载usb设备即可。
运行栏输入services.msc开启服务管理窗口
2.1.2.编译环境CentOS 6
VMware安装后,用该软件建立虚拟硬件平台,在虚拟平台上安装操作系统。
CentOS 6是本书编译环境选用的操作系统。
1.系统安装
只要是Linux发行版皆可
2.开发过程涉及的一些命令
- 编译器和编译工具
- gcc:gun c语言编译器
- as:gas汇编语言编译器
- ld:链接器,用于将编译文件链接成可执行文件
- nasm:nasm汇编语言编译器,用于编译intel格式的汇编语言
- make:编译工具,根据编译脚本文件记录的内容编译程序
- 系统工具与命令
- dd:复制指定大小的数据块,并在复制过程中转换数据格式
- mount:挂载命令,用于将u盘,光驱,软盘等存储设备挂载到指定路径上
- unmount:卸载命令,与mount命令功能相反
- cp:复制命令,复制指定文件或目录
- sync:数据同步命令,将已缓存的数据写回到存储设备上
- rm:删除命令,删除指定文件或目录
- objdump:反汇编命令,负责将可执行文件反编译成汇编语言
- objcopy:文件提取命令,将源文件的内容提取出来,再转存到目标文件中
以上命令和工具通常默认安装到linux发行版系统。如没有,用操作系统自带的软件更新工具(apt-get等),就能安装(或更新)最新版本的命令到系统中。
1.使用VMware软件创建虚拟平台时,不必为内存和硬盘分配过大的存储空间,且硬盘可配置成动态增长型。
2.本次开发不会使用swap分区,则系统没必要创建该分区。
2.1.3.Bochs虚拟机
1.Bochs环境安装
bochs-2.6.8,
自行下载和安装。
configure工具的配置信息:
./configure --with-x11 --with-wx --enable-debugger --enable-disasm
--enable-all-optimizations --enable-readline --enable-long-phy-address
--enable-ltdl-install --enable-idle-hack --enable-plugins --enable-a20-pin
--enable-x86-64 --enable-smp --enable-cpu-level=6 --enable-large-ramfile
--enable-repeat-speedups --enable-fast-function-calls --enable-handlers-chaining
--enable-trace-linking --enable-configurable-msrs --enable-show-ips --enable-cpp
--enable-debugger-gui --enable-iodebug --enable-logging --enable-assert-checks
--enable-fpu --enable-vmx=2 --enable-svm --enable-3dnow --enable-alignment-check
--enable-monitor-mwait --enable-avx --enable-evex --enable-x86-debugger
--enable-pci --enable-usb --enable-voodoo
因为不清楚调试内核需使用多少功能,索引就把它们都添加上去。编译时可能出现"文件不存在"错误,这时只需将后缀名为.cpp的文件克隆出一个名为.cc的副本即可通过编译。
cp misc/bximage.cpp misc/bximage.cc
cp iodev/hdimage/hdimage.cpp iodev/hdimage/hdimage.cc
cp iodev/hdimage/vmware3.cpp iodev/hdimage/vmware3.cc
cp iodev/hdimage/vmware4.cpp iodev/hdimage/vmware4.cc
cp iodev/hdimage/vpc-img.cpp iodev/hdimage/vpc-img.cc
cp iodev/hdimage/vbox.cpp iodev/hdimage/vbox.cc
2.Bochs运行环境配置
需要为即将实现的操作系统创建虚拟硬件环境。Bochs文件夹已有一个默认的系统环境配置文件.bochsec。
// 以下内容是本系统虚拟平台环境的配置信息
plugin_ctrl: unmapped=1,biosdev=1,speaker=1,extfpuirq=1,parallel=1,serial=1,iodebug=1
config_interface:textconfig
display_library:x
romimage:file="/usr/local/share/bochs/BIOS-bochs-latest"
vgaromimage:file="usr/local/share/bochs/VGABIOS-lgpl-latest"
boot:floppy
floppy_bootsig_check:disabled=0
floppya:type=1_44, 1_44="boot.img", status=inserted, write_protected=0
ata0:enabled=1,ioaddr1=0x1f0,ioaddr2=0x3f0,irq=14
ata0-master:type=none
atat0-slave:type=none
ata1:enable=1,ioaddr1=0x170, ioaddr2=0x370, irq=15
ata1-master:type=none
atat1-slave:type=none
ata2:enabled=0
ata3:enable=0
pci:enabled=1, chipset=i440fx
vga:extension=vbe, update_freq=5
cpu:count=1:1:1, ips=4000000,quantum=16,
model=corei7_haswell_4770,reset_on_triple_fault=1,cpuid_limit_winnt=0,
ignore_bad_msrs=1, mwait_is_nop=0, msrs="msrs.def"
...
特别说明以下几项
- boot:floppy:相当于设置BIOS的启动项,这里为软盘启动
- floppya:type=1_44, 1_44="boot.img", status=inserted, write_protected=0:设置插入软盘的类型为容量为1.44MB的软盘,软盘镜像文件的文件名为boot.img,状态是已经插入,写保护开关置于关闭状态
- cpu与cpuid:这两个选项描述了处理器的相关信息,可根据个人需求自行设定,在.bochsrc文件中也有详细说明可供参考
- megs:2048:设置虚拟平台的可用物理内存容量,以MB为单位。目前Bochs虚拟软件可用的内存上限是2048MB(2GB),如操作系统没足够内存,Bochs会运行失败,失败时的提示信息大致如下:
terminate called after throwing an instance of 'std::bad_alloc'
what(): std:bad_alloc
Aborted (core dumped)
3.Bochs相关的调试命令
Bochs虚拟机软件最大的优点是,在虚拟平台运行时,可通过命令对其进行调试,下表给出了经常使用的调试命令
b address 在某物理地址上设置断点 b 0x7c00
c 继续执行,直到遇到断点 c
s 单步执行 s
info cpu 查看寄存器信息 info cpu
r 察看寄存器信息 r
sreg 察看寄存器信息 sreg
creg 察看寄存器信息 creg
xp /nuf addr 察看内存物理地址内容 xp /10bx 0x100000
x /nuf addr 察看线性地址内容 x /40wd 0x90000
u start end 反汇编一段内存 u 0x100000 0x100010
n代表显示单元个数;u代表显示单元大小【b,h,w,g】;f代表显示格式【x:十六进制,d:十进制,t:二进制,c:字符】
先让程序在Bochs虚拟机里运行,待时机成熟再把它移植到物理平台。
2.2.汇编语言
书写格式,一种是AT&T,另一种是Intel格式。
Intel格式,支持它的编译器有MASM,NASM,YASM。AT&T,支持它的编译器是GNU的GAS编译器。
- BootLoader代码量大,用Intel格式,简单且便于阅读。
- 内核和应用程序,只有小部分用汇编,大部分用GNU C,为GNU C搭配上AT&T的汇编,便于相互调用。
2.2.1.AT&T汇编语言格式与Intel汇编语言格式
Intel汇编 | AT&T汇编 | |
书写格式 | 多数编译器要求关键字需大写 | 关键字需小写 |
赋值方向 | 从右到左。以ADD为例: ADD 目的操作数,源操作数 |
从左到右。 ADD 源操作数,目的操作数 |
操作数前缀 | 寄存器和立即数无前缀 | 寄存器前缀%,立即数前缀$,标识符直接用用的是数值,加前缀$用的是其地址 |
跳转和调用指令 | CALL FAR SECTION:OFFSET JMP FAR SECTION:OFFSET RET |
lcall $section:$offset ljmp $section:$offset lret |
内存间接寻址格式 | section:[base+index*scale+displacement],scale可取值是1,2,4,8 | section:displacement(base,index,scale) |
指令的后缀 | 使用内存操作数时应对操作数的位宽加以限制。BYTE PTR,WORD PTR,DWORD PTR。 MOV EAX, DWORD PTR[EBX] |
b,w,l,q movq %rax, %rbx 跳转指令的地址标识符可添加后缀表示跳转方向,f向前,b向后 jmp 1f 1: |
2.2.2.NASM编译器
1.符号[]
在NASM编译器中,如直接引用变量名或标识符,则编译器认为在引用变量的地址。如访问变量里的数据,需用[]。
2.符号$
代表当前行被编译后的地址
3.符号$$
一个节起始处被编译后的地址。
2.2.3.使用汇编语言调用C语言的函数
汇编语言调用c语言的过程涉及函数的调用约定,参数的传递方式,函数的调用方式等技术细节。
1.函数的调用方式
call/jmp/ret
2.函数的调用约定
函数的调用约定描述了执行函数时返回地址和参数的出入栈规律。
- stdcall调用约定
- 调用时,参数按从右到左顺序依次压入栈
- 函数的栈平衡操作由被调用函数完成。retn x。
- 函数的编译过程中,编译器会在函数名前用下划线修饰,其后用符号@修饰,并加上入栈的字节数,因此函数int function(int,int)最终被编译为_function@8
- cdecl调用约定
- 压栈顺序与stdcall相同
- 函数的栈平衡操作由调用函数完成。
- cdecl是GUN C编译器的默认调用约定。但GUN C在64位环境下,却使用寄存器作为函数参数传递方式。从左到右依次将前6个整型参数放在通用寄存器RDI, RSI, RDX, RCX, R8, R9;寄存器XMM0~XMM7用来保存浮点变量。RAX寄存器用于保存函数的返回值。
- ...
3.参数传递方式
2.3.c语言
下面对GUN C进行讲解,侧重于内嵌汇编语言和标准c语言扩展。
2.3.1.GUN C内嵌汇编语言
GUN C提供了关键字asm来声明代码是内嵌的汇编语句,如
#define nop() __asm__ __volatile__("nop \n\t")
- __asm__关键字:用于声明这行代码是一个内嵌汇编表达式,它是关键字asm的宏定义.
- __volatile__关键字:告诉编译器这行代码不能被优化。
1.内嵌汇编表达式
嵌入前的准备工作主要负责确定寄存器的分配情况,与c程序的融合情况等细节。
gun c语言的内嵌汇编表达式由4部分构成,它们之间使用":"号分隔,其完整格式为:
指令部分:输出部分:输入部分:损坏部分
如将内嵌汇编表达式当作函数,指令部分是函数的代码,输入部分用于向函数传入参数,输出部分则可以理解为函数的返回值。
- 指令部分是汇编代码本身,书写格式与AT&T汇编格式基本相同,存在些许不同。当指令表达式中存在多条汇编代码时,可全部书写在一对双引号中;亦可将汇编代码放在多对双引号中。如将所有指令写在同一双引号中,相邻两条指令间需用分号或换行符分隔。使用换行符,通常其后还会跟一个制表符。当汇编代码引用寄存器时,需在寄存器名前再添加一个%符。如"movl $0x10, %%eax"
- 输出部分紧接在指令部分之后。其格式为:"输出操作约束"(输出表达式),"输出操作约束"(输出表达式),......。输出操作约束和输出表达式成对出现,整个输出部分可含多条输出信息,每条信息间用逗号分隔开。
- 括号内的输出表达式部分主要负责保存指令部分的执行结果。通常,输出表达式是一个变量。
- 双引号内的部分。输出约束部分需用等号或加号修饰。等号意味着输出表达式是纯粹的输出操作,加号意味着输出表达式既用于输出,又用于输入。
- 输入部分记录着指令部分的输入信息,其格式为:"输入操作约束"(输入表达式),"输入操作约束"(输入表达式),......。
- 损坏部分描述了指令部分执行过程中,将被修改的寄存器,内存空间, 或标志寄存器,且这些修改部分并未在输出部分和输入部分出现过,格式为:"损坏描述","损坏描述",......。如需声明多个寄存器,需使用逗号分隔开。
- 寄存器修改通知。__asm__ __volatile__ ("movl %0, %%ecx"::"a"(__tmp):"cx");上述语句指令部分修改了寄存器ecx,却未被任何输入,输出部分记录,则需在损坏部分加以描述,一旦编译器发现后续代码还要用,会在内嵌汇编过程做好数据保存和恢复。注意,已在损坏部分声明的寄存器,不能作为输入,输出操作表达式的寄存器约束,不会被指派为q,r,g约束的寄存器。
- 内存修改通知。汇编语句的指令部分修改了内存数据,或内嵌汇编表达式出现的地方,内存数据可能发生改变,且被修改的内存未使用m约束。此时,应在损坏部分使用字符串memory,向编译器声明内存会发生改变。如损坏部分已经用memory对内存加以约束,则编译器保证汇编表达式执行后,重新寄存器装在已引用过的内存空间。
- 标志寄存器修改通知。内嵌汇编表达式中包含影响标志寄存器R|EFLAGS的指令时,需在损坏部分用cc向编译器声明
2.操作约束和修饰符
每个输入,输出表达式需指定自身的操作约束。类型可分为寄存器约束,内存约束,立即数约束。输出表达式中,还有限定寄存器操作的修饰符。
寄存器约束限定了表达式的载体是一个寄存器。可用寄存器全名或缩写。 __asm__ __volatile__("movl %0, %%cr0"::"eax"(cr0)); __asm__ __volatile__("movl %0, %%cr0"::"a"(cr0));
缩写 | 描述 | 缩写 | 描述 |
r | 任何输入,输出型寄存器 | d | 使用RDX/EDX/DX/DL寄存器 |
q | 从EAX/EBX/ECX/EDX指派一个寄存器 | D | 使用RDI/EDI/DI寄存器 |
g | 寄存器或内存空间 | S | 使用RSI/ESI/SI寄存器 |
m | 内存空间 | f | 选用浮点寄存器 |
a | 使用RAX/EAX/AX/AL寄存器 | i | 一个整数类型的立即数 |
b | 使用RBX/EBX/BX/BL寄存器 | F | 一个浮点类型的立即数 |
c | 使用RCX/ECX/CX/CL寄存器 |
- 内存约束。限定表达式载体是内存空间。 __asm__ __volatile__("sgdt %0":"=m"(__gdt_addr)::); __asm__ __volatile__("lgdt %0"::"m"(__gdt_addr));
- 立即数约束,只能用于输入部分,限定表达式的载体是一个数值。 __asm__ __volatile__("movl %0, %%ebx"::"i"(50)); 使用约束名i限定输入表达式是一个整数类型的立即数。希望是浮点类型的立即数,使用F。
- 修饰符之可用在输出部分,除了等号,加号,还有&。&只能写在输出约束部分第二个字符位置上,即只能位于=和+之后,告诉编译器不得为任何输入操作表达式分配该寄存器。编译器会在输入部分赋值前,先对&符号修饰的寄存器赋值,一旦后面的输入操作表达式向该寄存器赋值,会造成输入,输出数据混乱。
输入约束中用过模糊约束时(使用约束缩写),在输出约束中使用&修饰才有意义。如没有&,编译器先对输入部分赋值,指令部分结束后, 再对输出部分操作。
3.序号占位符
输入,输出操作约束的数值映射,每个内嵌汇编表达式最多只有10条输入,输出约束,这些约束按书写顺序依次被映射为序号0~9。%0对应第一个操作约束。编译时编译器会将每个占位符代表的表达式替换到相应的寄存器或内存。
指令部分在引用序号占位符时,可根据需要指定操作位宽是字节或字,也可指定操作的字节位置,即在%与序号占位符之间插入字母b表示操作最低字节,插入字母h表示操作次低字节。
2.3.2.GUN C语言对标准c语言的扩展
1.柔性数组成员(零长数组,变长数组)
struct s{int n; long d[0];};
int m =1;
struct s *p = malloc(sizeof(struct s) + sizeof(long [m]));
struct s结构体中的数组成员变量d在作用上与指针极为相似,
2.case关键字支持范围匹配
case 'a'...'z':
{...}
break;
3.typeof(x)获取变量x类型
4.可变参数宏
#define pr_debug(fmt, arg...) \
printk(fmt, ##arg)
可变参数arg被忽略或为空时,printk函数的##操作将迫使预处理器去掉它前面的那个逗号。
5.元素编号
6.当前函数名
__PRETTY__FUNCTION__保存着带语言特色的名字
__FUNCTION__保存着函数在源码中的名字
7.特殊属性声明
在声明处加入关键字__attribute__((ATTRIBUTE)),ATTRIBUTE是属性说明,如存在多个属性,必须使用逗号隔开。目前支持的属性说明有noreturn,noinline,always_inline,pure,const,nothrow,format,format_arg,no_instrument_function,section,constructor,destructor,used,unused,deprecated,weak,malloc,aliaswarn_unused_result nonnull等。
noreturn属性用来修饰函数,表示从不返回。
#define ATTRIB_NORET __attribute__((noreturn)) ....
asmlinkage NORET_TYPE void do_exit(long err_code) ATTRIB_NORET;d
packed属性的作用是取消结构在编译时的对齐优化,使其按实际占用字节对齐。
struct example_struct
{
char a;
int b;
long c;
} __attribute__((packed));
取消对齐优化下,上述类型对象占据13B.
regparm(n)用于指定寄存器传递参数的个数,只能用于函数定义和声明里。寄存器参数的上限值是3(顺序EAX,EDX,ECX),超过3个参数,剩余的用栈传递。
regparam只在x86体系下有效。x64下参考cdecl。