这一章会给大家介绍编写本操作系统所需的基础知识、系统环境及环境搭建方法,大家不必在这方面耗费太多心血,本着够用就好的原则就行。 我们可以在实践中慢慢摸索,不断完善和丰富这些知识。相信聪明的你应该早已具备了一定的开发能力, 如果感觉自己这方面能力不足,网上有很多这方面的学习资料供大家学习,所以这里就不浪费篇幅详细讲解了。

对于开发使用的系统环境,目前作者使用的是windows系统,开发操作系统的编译环境是Linux的开源发行版——CentOS6。因此,作者选用了VMware虚拟机来搭载CentOS6操作系统。虽然我们打算将我们的操作系统运行在物理平台上。但是,在刚开始写操作系统的时候,直接使用物理平台会让我们调试代码变得很艰难。选用Bochs虚拟机来调试系统环境是一个不错的选择。

对于基础知识,不管你是精通C和汇编语言,能够写出高效的让人看不懂代码的大神,还是刚开始学习编程语言抱着《谭浩强C语言》乱啃的菜鸟。都请你们静下心来,平静的看完这一章。让心沉淀,再踏上征途。

这章可以作为复习章,或者作为提高自己知识技能的学习章。总而言之,这一章的知识很重要,不然接下来的内容你会看的很吃力,俗话说:不是一番寒彻骨,怎得梅花扑鼻香。我们今天止步不前,是为了明天大踏步的前进。菜鸟们,你们不必太担心,只要你有决心,就一定能成功。我们都是地才!

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.2 汇编语言

这一节主要介绍的是AT&T汇编语言和Intel汇编语言的基本格式以及一些明显的区别,困惑的你一定会想问,为什么要介绍这些呢?

因为咱们的操作系统最开始引导部分的代码用的是Intel汇编语言, NASM编译器不知道读者是否使用过,这个编译器用的就是Intel汇编语言,NASM的代码格式简洁,给人感觉使用起来会很舒服。还有YASM编译器,如果你习惯使用YASM编译器也没有问题,毕竟启动阶段到内核程序的跳转是在内存里完成的,这两个阶段是独立编译的不会在编译的过程中产生依赖关系,您可以放心大胆的使用。这两种编译器都可以在CentOS上安装使用,并且是开源免费的,省得让微软、IBM、Google等大叔们找我们的麻烦。

然后当引导程序完成进入内核后,内核会使用GNU C编译器(GCC),并在内核启动部分代码中嵌入了AT&T汇编语言。。。。。没办法GNU的汇编编译器(AS)用的就是AT&T汇编语言,它在寄存器前面非要加“%”,书写起来有些别扭,就算是在GNU C里面嵌入汇编语言的时候也要保持这种风格,看来只能学着习惯了。

所以,就有了下文书介绍的这些内容。但由于每个人的基础不一样,没有办法面面俱到,只能介绍一些以后会用到的知识。现在是不是觉得书到用时方恨少了呢?没关系,我会尽量把汇编代码解释的详细些。同时还希望读者们课下多多努力呦~!!~!

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.2.1 AT&T 汇编 与 Intel 汇编

AT&T汇编与Intel汇编在功能上没有什么太大的区别,但是,在语法格式,赋值方向,前缀等地方却各自有各自的特点。下面我们就开始介绍:

语法格式

  • Intel 汇编

    mov ax, 0x10  
    

注意:在Intel格式中指令需要使用大写字母,但是我们使用的nasm却不需要使用大写字母,有可能是编译器的原因,但是我没有具体验证过,其他的编译器可能会有这方面要求

  • AT&T 汇编

    mov $0x10, %ax  
    

赋值方向

  • Intel 汇编

在Intel语法中,第一个是目的操作数,第二个是源操作数,赋值方向从右向左。
例如:add指令

这是Intel技术文档内对add指令的一部分描述:

Adds the destination operand (first operand) and the source operand (second operand) and then stores the result in the destination operand.

翻译:add指令的目的操作数是第一个操作数,源操作数是第二个操作数,执行的结果会保存到目的操作数里。

  • AT&T 汇编

在AT&T语法中,恰恰与Intel相反,第一个是源操作数,第二个是目的操作数,赋值方向从左向右。

呵呵,是不是有点意思。。~我们的操作系统都用到了这两种汇编语言,同学们要好好学,不要偏科呀。我有时候也容易把这两个语言的赋值方向搞混,所以,你们要格外注意了!!!!

操作数前缀

  • Intel 汇编

在Intel语法中,寄存器和立即数不需要添加前缀。例如:

mov cx, 12
  • AT&T 汇编

在AT&T语法中,如果要使用寄存器的话,需要在前面加前缀“%”;如果要使用立即数的话,需要在前面加“$”。 例如:

mov $12, %cx  

对于符号常数我们可以直接引用,不需要加前缀。例如:

values: .long 0x5a5a5a5a  
movl values, %eax  

在这里,符号values是一个符号常数,执行的结果是将常数0x5a5a5a5a装入寄存器eax中。

如果在符号前面加前缀$,表示引用的是该符号的地址。 例如:

values: .long 0x5a5a5a5a 
movl $values, %ebx  

这句汇编的意思是将values的地址装入ebx寄存器中。

锁总线前缀“lock”,这个lock前缀一般是用在多核CPU上的,它的目的是:锁住系统前端总线,防止其他CPU通过前端总线访问内存或其他系统硬件资源。像Linux的spinlock功能,就有lock的身影。

跳转和调用指令

  • Intel 汇编

远程跳转指令使用的是“jmp”后面跟的是段地址和段内偏移。远程调用指令使用的是“call”后面同样跟的是段地址和段内偏移,远程返回指令使用的是“ret”。

    call far section:offset
    jmp far section:offset  
    ret
  • AT&T 汇编

对于远程跳转指令和远程调用过程的指令码需要使用前缀“l”,分别为“ljmp”和“lcall”,与“lcall”相对应的返回指令时“lret”。例如:

    lcall $section:$offset  
    ljmp $section:$offset  
    lret

内存间接寻址格式

  • Intel 汇编

Intel使用“[”、“]”来表示间接寻址,格式如下:

    section:[base+index*scale+displacement]

其中scale可以取值1,2,4,其默认值为1。section可以指定任意段寄存器作为段前缀,不同情况下的默认段寄存器是不同的。

  • AT&T 汇编

AT&T使用“(”、“)”来表示间接寻址,格式如下:

    section:displacement(base,index,scale)  

这里的section,base,index,scale,displacement与Intel的使用规则相同。

指令的后缀

  • Intel 汇编

Intel中处理内存操作数时需要区分操作数大小,字节:BYTE PTR、字:WORD PTR、双字:DWORD PTR。例如:

mov al, bl
mov ax, bx
mov eax, dword ptr [ebx]
  • AT&T 汇编

AT&T 语法中大部分指令处理内存操作数时需要区分操作数大小,“b”表示byte(一个字节);“w”表示word(2 个字节);“l”表示long(4 个字节)。例如:

movb %bl, %al  
movw %bx, %ax  
movl (%ecx), %eax  

另外,AT&T的跳转指令标号后的后缀表示跳转方向,“f”表示向前(forward),“b”表示向后(back)。例如:

jmp 1f  
1:  
jmp 1f  
1:

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.2.2 NASM编译器

很多人在学习汇编的时候都可能是从I386学起的,使用的编译器是MASM(MASM 是Microsoft Macro Assembler 的缩写),其实NASM的格式与MASM总体上是差不多的。

值得说明的有如下几点:

1. [ ]的使用

在NASM中,如果你引用标签或者变量名, 都被认为引用的是该名字的地址,如果想访问他们里面的内容,必须使用 [ ]。这么理解如果不太容易记忆的话,那么你可以把他想象成C语言里的数组,数组名字代表的是它的地址,加上[ ]就代表的是它里面的内容。是不是一下子就明白了?其实,说不定C的编译器就是这么做的呢,毕竟C编译器会把代码编译成汇编代码,然后再编译成二进制文件的,对吧~!

2. $

$表示当前地址——当前行被编译后的地址。好像不太容易理解对吧,不要紧,请看下面的代码:

jmp $  

这句汇编的意思就是死循环,转化成机器码是E9 FD FF,其中E9的意思是jmp,FD FF是个地址,但是在x86里面是小端排列的,所以要将数值转换为地址:FFFD,其实就是-3,这个jmp是相对跳转,跳转的地址就是执行完这条命令后,指令寄存器-3的地址,正好这条指令的长度就是3个字节,所以,又回到了这条指令重新执行。上述过程中,$指的就是地址E9啦。

3. $$

明白了$,那么,$$是什么意思呢?

它表示的是一个节(section)的开始处被编译后的地址(就是这个节的起始地址)。我们一般写汇编程序的时候,使用一个section就够了,只有在写复杂程序的时候,才会用到多个section。section既可以是数据段,也可以是代码段。所以,如果把section比喻成函数,还是不太恰当。

提示:

在写程序的过程中,$-$$会经常被用到,它表示本行程序距离节(section)开始处的相对距离。如果只有一个节(section)的话,那么他就表示本行程序距离程序头的距离。在以后我们会把它与times联合使用,如:

times 512 - ( $ - $$) db 0  

它的意思将在第三章给大家做详细解释,同时也希望有兴趣的同学,自己学习一下,就当做是一个课后作业吧,嘿嘿。~

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.2.3 汇编调用C函数

讲解这节的主要目的是在以后的操作系统开发过程中会用到这些内容。

比如:从系统引导过程中的汇编程序跳转到系统主函数中,或者在中断处理的汇编代码中跳转到中断处理函数(传说中的中断上部), 这些过程都是从汇编程序跳转到C程序的,其中不可缺少的有:调用约定,参数传递方式,函数调用方式等。因为这些过程都是在系统内核中,所以,我们讲解的是GNU C语言和AT&T汇编语言。话不多说,下面让我们逐一介绍。

函数的调用方式

函数的调用方式其实没那么复杂,基本上就是jmp、call、ret或者他们的变种而已。让我们先看下面的程序。

int test()
{
    int i = 0;
    i =  1 + 2;
    return i;
}

int main()
{
    test();
    return 0;
}  

这段程序基本上没有什么难点,很简单,对吧?唯一要注意的地方是main函数的返回值,这里个人建议大家要使用int类型作为主函数的返回值,而不要使用void,或者其他类型。虽然,在主函数执行到return 0之后就跟我们没有什么关系了。但是,有的编译器要求主函数要有个返回值,或者,在某些场合里,系统环境会用到主函数的返回值。考虑到上述原因,要使用int类型作为主函数的返回值,如果处于某个特殊的或者可预测的环境下,那就无所谓了。

说了这么多,反汇编一下这段代码,看看汇编语言是怎么调用test函数的。工具objdump,用于反汇编二进制程序,它有很多参数,可以反汇编出各类想要的信息。

objdump工具命令:

objdump -d test

下面是反汇编后的部分代码,把相关的系统运行库等一些与上面C程序不相关的代码忽略掉。经过删减后的反汇编代码如下:

0000000000400474 <test>:
  400474:    55                       push   %rbp
  400475:    48 89 e5                 mov    %rsp,%rbp
  400478:    c7 45 fc 00 00 00 00     movl   $0x0,-0x4(%rbp)
  40047f:    c7 45 fc 03 00 00 00     movl   $0x3,-0x4(%rbp)
  400486:    8b 45 fc                 mov    -0x4(%rbp),%eax
  400489:    c9                       leaveq 
  40048a:    c3                       retq   

000000000040048b <main>:
  40048b:    55                       push   %rbp
  40048c:    48 89 e5                 mov    %rsp,%rbp
  40048f:    b8 00 00 00 00           mov    $0x0,%eax
  400494:    e8 db ff ff ff           callq  400474 <test>
  400499:    b8 00 00 00 00           mov    $0x0,%eax
  40049e:    c9                       leaveq 
  40049f:    c3                       retq   

大家先看000000000040048b :这一行,这里就是主函数,前面的000000000040048b其实是函数main的地址。一共16个数,16 * 4 = 64,对!这就是64位地址宽度啦。

乍一看,有好多个“%”符号,还记得2.2.1节里讲的AT&T汇编语法吗?这就是那里面说——引用寄存器的时候要在前面加“%”符号。

还有一些汇编指令的后缀,如:“l”、“q”。“l”的意思是双字(long型),“q”的意思是四字(64位寄存器的后缀就是这个)。

如果您仔细观察,是不是会发现有些寄存器rbp,rsp等,感觉会跟ebp和esp有关系呢?答对了,esp寄存器是32位寄存器,而rsp寄存器是64位寄存器。这是Intel对寄存器的一种向下继承性,从最开始一字节的al,ah,到两字节的ax(16位),四字节的eax(32位),再到八字节的rax(64位),寄存器的长度在不断的扩展,对于相关指令的使用,也从“b”、“l”,“q”,也是不断的向下继承或扩展。

这里有一条指令leaveq,它等效于 movq %rbp, %rsp; popq %rbp;

callq 400474 这句的意思就是跳转到test函数里执行。其实汇编调用C函数就这么简单,如果把这条callq指令改成jmpq指令也是可以的。这要从call和jmp的区别上说起,call会把在其之后的那条指令的地址压入栈,在上面反汇编后的代码中,就是0000000000400499,然后再跳转到test函数里执行。而jmpq就不会把地址0000000000400499压入栈中。当函数执行完毕,调用retq指令返回的时候,会把栈中的返回地址弹出到rip寄存器中,这样就返回到main函数中继续执行了。

实现jmpq代替callq的伪代码如下所示:

pushq    $0x0000000000400499  
jmpq     400474 <test>  

对于callq 400474 这条指令也可以使用retq来实现。它的实现原理是:指令retq会将栈中的返回地址弹出,并放入到rip寄存器中,然后处理器从rip寄存器所指的地址内取指令后继续执行。根据这个原理,可以先将返回地址0000000000400499压入栈中。然后再将test函数的入口地址0000000000400474压入栈中,接着使用retq指令,以调用返回的形式,从main函数“返回”到test函数中。

实现retq代替callq的伪代码如下所示:

pushq $0x0000000000400499
pushq $0x0000000000400474
retq  

这些看起来是不是没有想象的那么难?其实把汇编的原理掌握清楚了,这些都是可以灵活运用的,希望这段内容能启发读者的灵感~!

调用约定

对于不同的公司,不同的语言以及不同的需求,都是用各自不同的调用约定,而且他们往往差异很大。在IBM兼容机对市场进行洗牌后,微软操作系统和编程工具占据了统治地位,除了微软之外,还有零星的一些公司,以及开源项目GCC,都各自维护着自己的标准。下面是比较流行的几款调用标准,咱们写的大多数程序都出自这个标准之一。

  • stdcall

    1.在进行函数调用的时候,函数的参数是从右向左依次放入栈中的。

    如:

    int function(int first,int second)  
    

    这个函数的参数入栈顺序,首先是参数second,然后是参数first。

    2.函数的栈平衡操作是由被调用函数执行的,使用的指令是 retn X,X表示参数占用的字节数,CPU在ret之后自动弹出X个字节的堆栈空间。例如上面的function函数,当我们把function的函数参数压入栈中后,当function函数执行完毕后,由function函数负责将传递给它的参数first和second从栈中弹出来。

    3.在函数名的前面用下划线修饰,在函数名的后面由@来修饰,并加上栈需要的字节数。如上面的function函数,会被编译器转换为_function@8。

  • cdecl

    1.在进行函数调用的时候,和stdcall一样,函数的参数是从右向左依次放入栈中的。

    2.函数的栈平衡操作是由调用函数执行的,这点是与stdcall不同之处。stdcall使用retn X平衡栈,cdecl则使用leave、pop、增加栈指针寄存器的数据等方法平衡栈。

    3.每一个调用它的函数都包含有清空栈的代码,所以编译产生的可执行文件会比调用stdcall约定产生的文件大。

cdecl是GCC的默认调用约定。但是,GCC在x64位系统环境下,使用寄存器作为函数调用的参数。按照从左向右的顺序,头六个整型参数放在寄存器RDI, RSI, RDX, RCX, R8和R9上,同时XMM0到XMM7用来放置浮点变元,返回值保存在RAX中,并且由调用者负责平衡栈。

  • fastcall

    1.函数调用约定规定,函数的参数在可能的情况下使用寄存器传递参数,通常是前两个 DWORD类型的参数或较小的参数使用ECX和EDX寄存器传递,其余参数按照从右向左的顺序入栈。

    2.函数的栈平衡操作是由被调用函数在返回之前负责清除栈中的参数。

还有很多调用规则,如:thiscall、naked call、pascal等,有兴趣的读者可以自己去研究一下。

参数传递方式

函数参数的传递方式无外乎两种,一种是通过寄存器传递,另一种是通过内存传递。这两种传递方式在我们平时的开发中并不会被关注,因为不在特殊情况下,这两种传递方式,都可以满足要求。但是,我们要写的是操作系统,在操作系统里面有很多苛刻的环境要求,这使得我们不得不了解这些参数传递方式,来解决这些问题。

  • 寄存器传递

寄存器传递就是将函数的参数放到寄存器里传递,而不是放到栈里传递。这样的好处主要是执行速度快,编译后生成的代码量少。但只有少部分调用规定默认是通过寄存器传递参数,大部分编译器是需要特殊指定使用寄存器传递参数的。

在X86体系结构下,系统调用一般会使用寄存器传递,由于作者看过的内核种类有限,也不能确定所有的内核都是这么处理的,但是Linux内核肯定是这么做的。因为应用程序的执行空间和系统内核的执行空间是不一样的,如果想从应用层把参数传递到内核层的话,最方便快捷的方法是通过寄存器传递参数,否则需要使用很大的周折才能把数据传递过去,原因会在以后的章节中详细讲述。

  • 内存传递

内存传递参数很好理解,在大多数情况下参数传递都是通过内存入栈的形式实现的。

在X86体系结构下的Linux内核中,中断或异常的处理会使用内存传递参数。因为,在中断产生后,到中断处理的上半部,中间的过渡代码是用汇编实现的。汇编跳转到C语言的过程中,C语言是用堆栈保存参数的,为了无缝衔接,汇编就需要把参数压入栈中,然后再跳转到C语言实现的中断处理程序中。

以上这些都是在X86体系结构下的参数传递方式,在X64体系结构下,大部分编译器都使用的是寄存器传递参数。因此,内存传递和寄存器传递的区别就不太重要了。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.3 C语言

我想绝大部分读者对C语言并不陌生,但是包括作者在内,没有人敢说自己对C语言熟练掌握或者精通的。因为它的灵活性仅次于变换末次的汇编语言。考虑到个人能力有限,在这里就以本书操作系统的主要开发语言——GNU C为介绍内容。

这一节涉及到两方面内容:

  • GNU C内嵌汇编语言

  • GNU C语言对标准C语言的扩展

以上这些是以后写操作系统时候会用到的内容,希望大家能够熟练掌握。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.3.1 GNU C内嵌汇编语言

由于在很多情况下,C语言无法完全代替汇编语言,比如:操作某些特殊的CPU寄存器,操作主板上的某些IO端口或者性能达不到要求等情况下,我们必须在C语言里面嵌入汇编语言,以达到我们的需求。

当需要在C语言里嵌入汇编语言段的时候,对于GNU C我们可以使用它提供的关键词“asm”来实现。先看下面一段代码:

#define nop()         __asm__ __volatile__ ("nop    \n\t")

这段内嵌汇编语句是什么意思呢?根据函数名,读者们大概也能猜到了。这个正是nop函数的实现,而且这个nop函数也是本操作系统内核里面的库函数。让我们从这行代码入手,开始学习GNU C内嵌汇编语言。

首先,介绍__ asm __ 和 __ volatile __ 这两个关键字。

__ asm __ 修饰这个是一段汇编语言,它是GNU C定义的关键字asm的宏定义(#define __ asm __ asm),它用来声明一个内嵌汇编表达式。所以,任何一个内嵌汇编表达式都以它开头,它是必不可少的;如果要编写符合ANSI C标准的代码(即:与ANSI C兼容),那就要使用__ asm __;

__ volatile __ 修饰这段代码不被编译器优化,保持代码原样。这个volatile正是我们需要的,如果经过编译器优化,很有可能将我们写的程序修改,并达不到预期的执行效果了。如果要编写符合ANSI C标准的代码(即:与ANSI C兼容),那就要使用__ volatile __;

然后,该介绍内嵌汇编的语言了。 一般而言,C语言里嵌入汇编代码片段都要比纯汇编语言写的代码复杂得多。因为这里有个怎样分配寄存器、怎样与C代码中的变量融合的问题。为了这个目的,必须要对所用的汇编语言做更多的扩充,增加对汇编语言的明确指示。

C语言里的内嵌汇编代码可分为四部分,以“:”号进行分隔,其一般形式为:

指令部分输出部分输入部分损坏部分

指令部分

第一部分就是汇编语言的语句本身,其格式与在汇编语言程序中使用的格式基本相同,但也有不同之处。这一部分被称为“指令部分”说明他是必须有的,而其它各部分则视具体情况而定,如果不需要的话是可以忽略的,所以在最简单的情况下就与常规的汇编语句基本相同。

指令部分的编写规则:

当指令列表里面有多条指令时,可以在一对双引号中全部写出,也可将一条或多条指令放在一对双引号中,所有指令放在多对双引号中;

  • 如果是将所有指令写在一对双引号中,那么,相邻两条指令之间必须用分号”;“或换行符(\n)隔开,如果使用换行符(\n),通常\n后面还要跟一个\t;或者是相邻两条指令分别单独写在两行中;
  • 如果将指令放在多对双引号中,除了最后一对双引号之外,前面的所有双引号里的最后一条指令后面都要有一个分号(;)或(\n)或(\n\t);

在涉及到具体的寄存器时就要在寄存器名前面加上两个"%"号,以免混淆。

输出部分

第二部分,紧接在指令部分后面的是“输出部分”,用来指定当前内嵌汇编语句的输出表达式。
格式为:“操作约束”(输出表达式)

用括号括起来的部分,它用于保存当前内嵌汇编语句的一个输出值。在输出表达式内需要用(=)或(+)来进行修饰。 等号(=)与加号(+)是有区别的:等号(=)表示当前表达式是一个纯粹的输出操作,而加号(+)则表示当前表达式不仅仅是一个输出操作,还是一个输入操作;不管是等号(=)还是加号(+),所表示的都是可写,只能用于输出,只能出现在输出部分,而不能出现在输入部分;在输出部分可以出现多个输出操作表达式,多个输出操作表达式之间必须用逗号(,)隔开;

用双引号括起来的部分,被称作是:”输出操作约束“,也可以称为”输出约束“;关于约束部分将在后面一起进行讲解。

输入部分

第三部分用来指定当前内嵌汇编语句的输入;称为输入表达式;
格式为:”操作约束“(输入表达式)

输入部分同样也由两部分组成:由双引号括起来的部分和由圆括号括起来的部分;这两个部分对于当前内嵌汇编语句的输入来说,是必不可少的;用于约束当前内嵌汇编语句中的当前输入;这个部分也成为”输入操作约束“,也可以成为是”输入约束“;与输出表达式中的操作约束不同的是,输入表达式中的操作约束不允许指定等号(=)和加号(+)约束,也就是说,它只能是只读的;

操作约束

每一个输入和输出表达式都必须指定自己的操作约束;约束的类型有:寄存器约束、内存约束、立即数约束;

  • 寄存器约束

当输入或输出需要借助于一个寄存器时,需要为其指定一个寄存器约束;

可以直接指定一个寄存器名字;比如:

__asm__ __volatile__("movl %0,%%cr0"::"eax"(cr0));

也可以指定寄存器的缩写名称;比如:

__asm__ __volatile__("movl %0,%%cr0"::"a"(cr0));  

如果指定的是寄存器的缩写名称,比如:字母a;那么,GNU C将会根据当前操作表达式的宽度来决定使用%rax、%eax、%ax还是%al;

常用的寄存器约束的缩写:

r:I/O,表示任何寄存器;
q:I/O,表示使用一个通用寄存器,由GCC在%rax/%eax/%ax/%al、%rbx/%ebx/%bx/%bl、%rcx/%ecx/%cx/%cl或%rdx/%edx/%dx/%dl中选取一个GNU C认为是合适的;
g:I/O,表示使用寄存器或内存地址;
m、v、o:I/O,表示使用内存地址;
a、b、c、d:I/O,分别表示使用%rax/%eax/%ax/%al、%rbx/%ebx/%bx/%bl、%rcx/%ecx/%cx/%cl或%rdx/%edx/%dx/%dl;
D、S:I/O,表示使用%rdi/%edi/%di或%rsi/%esi/%si;
f:I/O,表示使用浮点寄存器;
i:I/O,表示使用一个整数类型的立即数;
F:I/O,表示使用一个浮点类型的立即数;

  • 内存约束

如果一个输入/输出操作表达式,表现为一个内存地址(指针变量),不想借助于任何寄存器,则可以使用内存约束;

例如:

__asm__ __volatile__ ("sgdt %0":"=m"(__gdt_addr)::);  
__asm__ __volatile__ ("lgdt %0"::"m"(__gdt_addr));

内存约束使用约束名“m”,表示的是使用系统支持的任何一种内存方式,不需要借助于寄存器;

  • 立即数约束

如果一个输入操作表达式是一个数字常数,不想借助于任何寄存器或内存,则可以使用立即数约束;立即数在表达式中只能作为右值使用,对于使用立即数约束的表达式而言,只能放在输入部分;

比如:

__asm__ __volatile__("movl %0,%%ebx"::"i"(50));  

使用约束名“i”表示输入表达式是一个整数类型的立即数,不需要借助于任何寄存器,只能用于输入部分;使用约束名“F”表示输入表达式是一个浮点数类型的立即数,不需要借助于任何寄存器,只能用于输入部分;

  • 修饰符

等号(=)和加号(+)作为修饰符,已经在输出部分讲解过了,这里主要讲解“&”符。

符号“&”也只能写在输出表达式的约束部分,用于约束寄存器的分配,但是只能写在约束部分的第二个字符的位置上。因为,第一个字符的位置我们要写(=)或(+)。

用符号“&”进行修饰,就表示不得为任何输入操作表达式分配与此输出操作表达式相同的寄存器;其原因是,GNU C会先使用输出值对被修饰符“&”修饰的输出操作表达式进行赋值,然后,才对输入操作表达式进行赋值。这样的话,如果不使用修饰符“&”对输出操作表达式进行修饰,一旦后面的输入操作表达式使用了与输出操作表达式相同的寄存器,就会产生输入和输出数据混乱的情况;

值得注意的是:如果输出操作表达式的寄存器约束被指定为某个寄存器,而在输入操作表达式的寄存器约束中至少存在一个可选约束(意思是GNU C可以从多个寄存器中选取一个,或使用非寄存器方式)时,比如“q”、“r”或“g”时,此输出操作表达式使用符号“&”修饰才有意义!如果为所有的输入操作表达式指定了固定的寄存器,或使用内存/立即数约束时,则此输出操作表达式使用符号“&”修饰没有任何意义;

如果没有使用修饰符“&”修饰输出操作表达式会是什么样子呢?那就意味着GNU C会先把输入操作表达式的值输入到选定的寄存器中,然后经过处理,最后才用输出值填充对应的输出操作表达式;

序号占位符

对于序号占位符,在内嵌汇编语句中最多只能有10个输入/输出操作表达式,这些操作表达式按照他们被列出来的顺序,依次被赋予编号0至9;对于占位符中的数字而言,与这些编号是一一对应的;比如:占位符%0对应编号为0的操作表达式,占位符%1对应编号为1的操作表达式,依次类推;

因为在占位符前面必须要有一个百分号“%”,为了与寄存器区别开来,必须要在指令列表里列出的寄存器名称前面使用两个百分号(%%)修饰。GNU C对占位符进行编译的时候,会将每一个占位符替换为对应的输入/输出操作表达式所指定的寄存器/内存/立即数;

扩展:对于操作表达式,根据需要也可以指定为字节操作或者字操作。对操作表达式进行的字节操作,默认为对其最低字节进行操作。字操作也是一样。不过,在一些特殊的操作中,对操作表达式进行字节操作时也允许明确指出是对哪一个字节操作,此时在“%”与序号占位符之间插入一个“b”表示最低字节,插入一个“h”表示次低字节。

损坏部分

有的时候,当您想通知GNU C当前内嵌汇编语句可能会对某些寄存器或内存进行修改,希望GNU C在编译时能够将这一点考虑进去;那么您就可以在损坏部分声明这些寄存器或内存;

  • 寄存器修改通知

这种情况一般发生在一个寄存器出现在指令列表中,但又不是输入/输出操作表达式所指定的,也不是在一些输入/输出操作表达式中使用“r”或“g”约束时由GNU C选择的。同时,此寄存器被指令列表中的指令所修改,而这个寄存器只供当前内嵌汇编语句使用;比如:

__asm__("movl %0,%%ecx"::"a"(__tmp):"cx");  

这个内嵌汇编语句中,%ecx出现在指令列表中,并且被指令修改了,但是却未被任何输入/输出操作表达式所指定。所以,必须要在损坏部分指定“cx”。

在损坏部分声明这些寄存器的方法很简单,只需要将寄存器的名字用双引号括起来就可以了;如果要声明多个寄存器,则相邻两个寄存器名字之间用逗号隔开,这点和输入/输出部分是一样的。

注意:如果在输入/输出操作表达式中指定寄存器;或为一些输入/输出操作表达式使用“q”/“r”/“g”约束,让GNU C为你选择一个寄存器时;GNU C对这些寄存器的状态是非常清楚的,它知道这些寄存器是被修改的,根本不需要在损坏部分声明它们;但除此之外,GNU C对剩下的寄存器中哪些会被当前内嵌汇编语句所修改却一无所知;所以,如果真的在当前内嵌汇编指令中修改了它们,最好在损坏部分声明它们,让GNU C针对这些寄存器做相应的处理;否则,有可能会造成寄存器不一致,从而造成程序执行错误;

寄存器名称串:
“al”/“ax”/“eax”/“rax”:代表寄存器%rax
“bl”/“bx”/“ebx”/“rbx”:代表寄存器%rbx
“cl”/“cx”/“ecx”/“rcx”:代表寄存器%rcx
“dl”/“dx”/“edx”/“rdx”:代表寄存器%rdx
“si”/“esi”/“rsi”:代表寄存器%rsi
“di”/“edi”/“rdi”:代表寄存器%rdi

如果要使用寄存器名称串,只需要使用“ax”,“bx”,“cx”,“dx”,“si”,“di”就可以了。

如果在一个内嵌汇编语句的损坏部分向GNU C声明了某个寄存器会发生改变。那么,在GNU C编译时,如果发现这个被声明的寄存器的内容在此内嵌汇编之后还要继续使用,GNU C会首先将此寄存器的内容保存起来,然后在此内嵌汇编语句的相关代码生成之后,再将其内容恢复。

另外需要注意的是:如果在损坏部分声明了一个寄存器,那么这个寄存器将不能再被用作当前内嵌汇编语句的输入/输出操作表达式的寄存器约束,如果输入/输出操作表达式的寄存器约束被指定为“q”/“r”/“g”,GNU C也不会选择已经被声明在损坏部分中的寄存器;

  • 内存修改通知

除了寄存器的内容会被修改之外,内存的内容同样也会被修改。如果一个内嵌汇编语句的指令列表中的指令对内存进行了修改,或者在此内嵌汇编出现的地方,内存内容可能发生改变,而且被改变的内存地址没有在其输出操作表达式中使用“m”约束。这种情况下,您需要在损坏部分使用字符串“memory”向GNU C声明。

如果一个内嵌汇编语句的损坏部分存在“memory”,那么GNU C会保证在此内嵌汇编之前,如果某个内存的内容被装入了寄存器,那么,在这个内嵌汇编之后,如果需要使用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷贝;因为这个时候寄存器中的拷贝很可能已经和内存处的内容不一致了。

  • 标志寄存器修改通知

当一个内嵌汇编中包含影响标志寄存器r|eflags的条件,那么也需要在损坏部分中使用“cc”来向GNU C声明这一点。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.3.2 GNU C对标准C语言的扩展

为了方便使用,GNU C在标准C语言的基础上进行了部分方便开发的扩展。这里讲解一些开发中可能会用到的,或者使用频率比较高的内容。

  1. 零长度数组和变量长度数组

    GNU C 允许使用零长度数组,比如:

    char data[0];  
    

    GNU C 允许使用一个变量定义数组的长度如:

    int n=0;
    scanf("%d",&n);
    int array[n];  
    
  2. case 范围

    GNU C支持 case x...y这样的语法,[x,y]之间数均满足条件。

    case 'a'...'z':  /*from 'a' to 'z'*/
    break;  
    
  3. 语句表达式
    GNU C 把包含在括号中的复合语句看作是一个表达式,称为语句表达式。

     #define min_t(type,x,y)\
             ({type __x=(x); type __y=(y);__x<__y?__x:__y;})
    

    这种写法可以避免

     #define min_t(x,y) ((x)<(y)?(x):(y))  
    

    在min_t(x++,++y)中出现的副作用

  4. typeof 关键字

    typeof(x)可以获得x的类型借助typeof关键字我们可以重新定义min_t:

    #define min_t(x,y)\
        ({typeof(x) __x=(x); typeof(y) __y=(y);__x<__y?__x:__y;})  
    
  5. 可变参数宏

    GNU C中宏也支持可变参数

    #define pr_debug(fmt,arg...) \
            printk(fmt,##arg)  
    

    这里,如果可变参数被忽略或为空,“##”操作将使预处理器去掉它前面的那个逗号。如果你在宏调用时,确实提供了一些可变参数,GNU C也会工作正常,它会把这些可变参数放到逗号的后面。

  6. 标号元素

    标准C要求数组或结构体的初始化值必须以固定的顺序出现,在GNU C中,通过指定索引或结构体成员名,允许初始化以任意顺序出现。

    unsigned char data[MAX] =
    {
             [0]=10,
             [10]=100,
    };
    
    
    struct file_operations ext2_file_operations=
    {
            open:ext2_open,
            close:ext2_close,
    };
    

    在linux 2.6中推荐如下方式:

    struct file_operations ext2_file_operations=
    {
         .read=ext2_read,
         .write=ext2_write,
    };  
    
  7. 当前函数名

    GNU C中预定义两个标志符保存当前函数的名字,__ FUNCTION __ 保存函数在源码中的名字, __ PRETTY__ FUNCTION __保存带语言特色的名字。在C函数中这两个名字是相同的.

    void func_example()
    {
         printf("the function name is %s",__FUNCTION__);
    }
    

    在C99中支持__ func __ 宏,因此建议使用 __ func __ 替代 __ FUNCTION __ 。

  8. 特殊属性声明

    GNU C 允许声明函数、变量和类型的特殊属性,以便进行手工的代码优化和定制。如果要指定一个属性声明,只需要在声明后添加__ attribute __((ATTRIBUTE))。其中ATTRIBUTE为属性说明,如果存在多个属性,则以逗号分隔。GNU C 支持noreturn,noinline, always_inline, pure, const, nothrow, format, format_arg, no_instrument_function, section, constructor, destructor, used, unused, deprecated, weak, malloc, alias warn_unused_result nonnull等十个属性。

    noreturn属性作用于函数,表示该函数从不返回。这会让编译器优化代码并消除不必要的警告信息。例如:

    #define ATTRIB_NORET __attribute__((noreturn)) ....
    asmlinkage NORET_TYPE void do_exit(long error_code) ATTRIB_NORET;  
    

    packed属性作用于变量和类型,用于变量或结构域时,表示使用最小可能的对齐,用于枚举、结构或联合类型时表示该类型使用最小的内存。如对于结构体,就是它告诉编译器取消结构在编译过程中的优化对齐,按照实际占用字节数进行对齐。例如:

    struct example_struct
    {
             char a;
             int b;
             long c;
    } __attribute__((packed));    
    

    regparm属性用于指定最多可以使用n个寄存器(eax, edx, ecx)传递参数,n的范围是0~3,超过n时则将参数压入栈中(n=0表示不用寄存器传递参数)。

    注意:以上这些属性都是在X86处理器体系结构下的,在X64体系结构下,大部分内容都是同样有效的。但是,这里要注意regparm属性,由于在X64体系结构下,GUN C的默认调用约定使用寄存器传递参数。所以,如果regparm属性里使用的寄存器个数超过3个,也仍然会使用其他寄存器来传递参数。这一点要遵循X64体系结构的调用约定。

    下面可以看一个例子。

    int q = 0x5a;
    int t1 = 1;
    int t2 = 2;
    int t3 = 3;
    int t4 = 4;
    #define REGPARM3 __attribute((regparm(3)))
    #define REGPARM0 __attribute((regparm(0)))
    void REGPARM0 p1(int a)
    {
         q = a + 1;
    }
    
    
    void REGPARM3 p2(int a, int b, int c, int d)
    {
         q = a + b + c + d + 1;
    }
    
    
    int main()
    {
        p1(t1);
        p2(t1,t2,t3,t4);
        return 0;
    }  
    

    使用objdump命令反汇编,相关命令如下:

    objdump -D 可执行程序  
    

    其中-D选项用于反汇编所有的程序段,包括:代码段、数据段、只读数据段以及一些系统段等等。而-d命令只反汇编代码段的内容。

    反汇编后的关键代码如下:

    Disassembly of section .text:
    0000000000400474 <p1>:
      400474:    55                       push   %rbp
      400475:    48 89 e5                 mov    %rsp,%rbp
      400478:    89 7d fc                 mov    %edi,-0x4(%rbp)
      40047b:    8b 45 fc                 mov    -0x4(%rbp),%eax
      40047e:    83 c0 01                 add    $0x1,%eax
      400481:    89 05 3d 04 20 00        mov    %eax,0x20043d(%rip)        # 6008c4 <q>
      400487:    c9                       leaveq 
      400488:    c3                       retq   
    
    
    0000000000400489 <p2>:
      400489:    55                       push   %rbp
      40048a:    48 89 e5                 mov    %rsp,%rbp
      40048d:    89 7d fc                 mov    %edi,-0x4(%rbp)
      400490:    89 75 f8                 mov    %esi,-0x8(%rbp)
      400493:    89 55 f4                 mov    %edx,-0xc(%rbp)
      400496:    89 4d f0                 mov    %ecx,-0x10(%rbp)
      400499:    8b 45 f8                 mov    -0x8(%rbp),%eax
      40049c:    8b 55 fc                 mov    -0x4(%rbp),%edx
      40049f:    8d 04 02                 lea    (%rdx,%rax,1),%eax
      4004a2:    03 45 f4                 add    -0xc(%rbp),%eax
      4004a5:    03 45 f0                 add    -0x10(%rbp),%eax
      4004a8:    83 c0 01                 add    $0x1,%eax
      4004ab:    89 05 13 04 20 00        mov    %eax,0x200413(%rip)        # 6008c4 <q>
      4004b1:    c9                       leaveq 
      4004b2:    c3                       retq   
    
    
    00000000004004b3 <main>:
      4004b3:    55                       push   %rbp
      4004b4:    48 89 e5                 mov    %rsp,%rbp
      4004b7:    53                       push   %rbx
      4004b8:    8b 05 0a 04 20 00        mov    0x20040a(%rip),%eax        # 6008c8 <t1>
      4004be:    89 c7                    mov    %eax,%edi
      4004c0:    e8 af ff ff ff           callq  400474 <p1>
      4004c5:    8b 0d 09 04 20 00        mov    0x200409(%rip),%ecx        # 6008d4 <t4>
      4004cb:    8b 15 ff 03 20 00        mov    0x2003ff(%rip),%edx        # 6008d0 <t3>
      4004d1:    8b 1d f5 03 20 00        mov    0x2003f5(%rip),%ebx        # 6008cc <t2>
      4004d7:    8b 05 eb 03 20 00        mov    0x2003eb(%rip),%eax        # 6008c8 <t1>
      4004dd:    89 de                    mov    %ebx,%esi
      4004df:    89 c7                    mov    %eax,%edi
      4004e1:    e8 a3 ff ff ff           callq  400489 <p2>
      4004e6:    b8 00 00 00 00           mov    $0x0,%eax
      4004eb:    5b                       pop    %rbx
      4004ec:    c9                       leaveq 
      4004ed:    c3                       retq   
      4004ee:    90                       nop
      4004ef:    90                       nop
    
    
    Disassembly of section .data:
    00000000006008c0 <__data_start>:
      6008c0:    00 00                    add    %al,(%rax)
        ...
    
    
    00000000006008c4 <q>:
      6008c4:    5a                       pop    %rdx
      6008c5:    00 00                    add    %al,(%rax)
        ...
    
    
    00000000006008c8 <t1>:
      6008c8:    01 00                    add    %eax,(%rax)
        ...
    
    
    00000000006008cc <t2>:
      6008cc:    02 00                    add    (%rax),%al
        ...
    
    
    00000000006008d0 <t3>:
      6008d0:    03 00                    add    (%rax),%eax
        ...
    
    
    00000000006008d4 <t4>:
      6008d4:    04 00                    add    $0x0,%al
        ...
    

    如果读者还记得2.2.3节中,关于GCC基于X64体系结构的调用约定的话,那就很容易可以看出,函数p1和p2都使用寄存器传递参数,顺序就是RDI, RSI, RDX, RCX,这些细节已经跟regparm的规定完全不一致了。所以,在这里作者觉得,regparm已经不起作用了。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.1 虚拟机及开发系统平台介绍

任何软件开发都需要有开发环境,操作系统同样也需要有开发环境。

因为开发操作系统使用的语言是汇编和C语言再加上一些灵活多变的设计思想,导致开发操作系统不会像开发应用程序那样有很多的调试软件和开发库的支持。一切都需要我们从零做起,这也是他的魅力之一。

为了解决版权问题和一些收费软件的麻烦,并随着开源免费软件大军逐渐壮大。Linux家族的操作系统是首当其冲的选择。如果您的系统平台是Windows或Mac OS的话,VMware虚拟机以它的稳定、方便、灵活、功能强大,深受开发者们喜爱。我们不会使用它的复杂功能,以方便自己的使用为原则,任何虚拟机都是可以接受的。

对于Bochs的选择也不是绝对的,如果您手头有其他的可调式虚拟机,只要能设置断点、查看内存、查看寄存器状态、反汇编内存代码等基本功能就可以。

希望读者们能够根据自己的喜好,搭建出一个顺手的开发环境。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.1.1 VMware的安装

VMware这款虚拟机大家并不陌生,基本上是属于开发必备软件了。

如果您正在使用的是Linux的某个发行版,请跳过这一节,下面内容是针对Windows用户进行的介绍。

在这里作者使用的操作系统是Win7 SP1,但是编译环境选择的是Linux的某个发行版。所以,需要一款虚拟机来虚拟硬件平台。VMware是当下的主流选择,目前的最新版是VMware Workstation 10或VMware Player 7。这些版本对于开发操作系统已经足够用了,如果使用的是老版本,影响也不会太大,只要能顺利安装上一款Linux发行版系统,并且可以挂载USB设备就可以了。假如调皮的你想换一款虚拟机也是没有问题的,只要能满足上述需求,那么这款虚拟机就足够了,一切随您心意。

注意事项:

1.在安装完VMware后,使用优化软件对电脑进行清理和优化时要特别注意。在优化过程中,优化软件可能会关闭VMware的一些自动开启的系统服务,所以有的时候无法连接网络和挂载USB设备,这点读者要注意一下。

解决办法是:在运行栏内输入services.msc把相关的VMware服务打开,如果不知道该打开那个的话,那就索性把VMware的相关服务全部打开。

2.对于win7系统来说,运行VMware的时候尽量以管理员权限运行,否则容易报错。

以上两点仅供参考——纸上得来终觉浅,欲知此事要躬行。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.1.2 系统平台——CentOS 6

系统安装

对于操作系统,只要是Linux的发行版就可以,关键还是用着要顺手,舒服。

如果想问为什么要选择CentOS作为操作系统而不选择Ubuntu这种更新的操作系统?
其实,它们没有什么更高明之处,主要是工作习惯使用CentOS了。虽然大部分软件不是最新的,但是对于企业来说,稳定更重要。而且,CentOS是redhat的免费版,提供的维护和更新时间更长。操作界面简单、方便, 对于其他Linux发行版系统日渐复杂绚丽的操作界面和风格。CentOS对于初学者上手会比较快,消耗的系统资源低。

一些开发涉及的相关命令

对于开发操作系统主要使用的工具或命令屈指可数:

  • 编译器或编译工具:

gcc:GUN C语言编译器,支持C99标准并且有扩展;
as:GAS汇编语言编译器,用于编译AT&T格式的汇编语言;
ld:链接器,用于将代码程序编译后的中间文件,链接成可执行文件;
nasm:NASM编译器,用于编译Intel格式的汇编语言;
make:编译工具,根据编译脚本文件的内容,编译程序。

  • 系统工具或命令:

dd:用指定大小的块拷贝一个文件,并在拷贝的同时进行指定的转换;
mount:挂载命令,用于将U盘,光驱,软盘等存储设备,挂载到指定路径上。
umount:卸载命令,与mount命令相反。
cp:拷贝命令,用于将文件或目录拷贝到指定目录下。
sync:同步数据命令,用于将文件同步回写到存储设备上。
rm:删除命令,删除指定文件或目录。
objdump:反汇编命令,用于将可执行文件反编译成汇编语言。
objcopy:文件提取命令,用于将源文件中的内容,提取到目标文件中。

这些工具基本上默认安装的Linux发行版系统里面都是默认就会安装的。如果您的电脑里没有相关的命令,您也不需要担心,根据相关的Linux发行版系统提供的更新软件工具(yum、apt-get等)就能更新到最新版本。对于这些工具的版本要求几乎没有。因为我们使用的是最原始的功能——将代码编译成二级制文件,就连常用的软件库都不需要。我们要从零做起!

注意事项:

  • 对于VMware分配的内存和硬盘空间不用太多,硬盘可以配置成动态增长的,这样会节省很大空间。

  • 对于Linux发行版系统的SWAP分区,这个对我们来说可有可无。swap分区是对少于4G物理内存的系统环境,有大内存开销的时候才起作用(内存使用量低于内存管理单元的临界值)。

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充

2.1.3 Bochs——一个可调试的虚拟机

这是一个开源的可调式虚拟机,在开发操作系统初期阶段,可以将我们的程序运行在bochs虚拟机上面。该虚拟机可以设置内存断点,查看寄存器状态或数据,将内存地址范围内的内容反编译成汇编语言,查看内存区的数据等功能。

Bochs环境安装

对于bochs的版本还是相对新一些比较好,因为这款软件还在完善中,新版本会解决不少bug,对于开发系统内核级软件来说,可能会比较重要。因此,作者选择的是最新的bochs-2.6.6。详细的下载和编译细节在这里就不多讲解了,这个问题就交给读者自己解决了,O(∩_∩)O哈!

注意:

在这里分享一下configure配置信息,仅供参考。

./configure --with-x11 --with-wx --enable-debugger --enable-disasm --enable-all-optimizations --enable-readline --enable-long-phy-address --enable-debugger-gui  --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-readline --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  

Bochs运行环境配置

下面的工作和VMware是一样的——创建一个虚拟机环境。这个环境是以配置文件的形式存在的,在bochs文件夹内会提供一个默认的系统环境配置文件.bochsrc。里面有配置选项的说明和实例供用户参考使用。读者们可以根据.bochsrc文件稍作修改,就可以配置出一个自己的虚拟机环境。

下面是配置文件的详细信息:

# configuration file generated by Bochs
plugin_ctrl: unmapped=1, biosdev=1, speaker=1, extfpuirq=1, parallel=1, serial=1, iodebug=1
config_interface: textconfig
display_library: x
#memory: host=2048, guest=2048
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
# no floppyb
ata0: enabled=1, ioaddr1=0x1f0, ioaddr2=0x3f0, irq=14
ata0-master: type=none
ata0-slave: type=none
ata1: enabled=1, ioaddr1=0x170, ioaddr2=0x370, irq=15
ata1-master: type=none
ata1-slave: type=none
ata2: enabled=0
ata3: enabled=0
pci: enabled=1, chipset=i440fx
vga: extension=vbe, update_freq=5

cpu: count=2:2:2, 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"

cpuid: x86_64=1,level=6, mmx=1, sep=1, simd=avx512, aes=1, movbe=1, xsave=1,apic=x2apic,sha=1,movbe=1,adx=1,xsaveopt=1,avx_f16c=1,avx_fma=1,bmi=bmi2,1g_pages=1,pcid=1,fsgsbase=1,smep=1,smap=1,mwait=1,vmx=1
cpuid: family=6, model=0x1a, stepping=5, vendor_string="GenuineIntel", brand_string="Intel(R) Core(TM) i7-4770 CPU (Haswell)"

print_timestamps: enabled=0
debugger_log: -
magic_break: enabled=0
port_e9_hack: enabled=0
private_colormap: enabled=0
clock: sync=none, time0=local, rtc_sync=0
# no cmosimage
# no loader
log: -
logprefix: %t%e%d
debug: action=ignore
info: action=report
error: action=report
panic: action=ask
keyboard: type=mf, serial_delay=250, paste_delay=100000, user_shortcut=none
mouse: type=ps2, enabled=0, toggle=ctrl+mbutton
speaker: enabled=1, mode=system
parport1: enabled=1, file=none
parport2: enabled=0
com1: enabled=1, mode=null
com2: enabled=0
com3: enabled=0
com4: enabled=0

megs: 2048

相关项说明:

  • 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),这里请注意,如果CentOS内没有足够的内存供Bochs使用的话,Bochs会运行失败。
    失败的提示信息如下所示:

    terminate called after throwing an instance of 'std::bad_alloc'
    what():  std::bad_alloc
    Aborted (core dumped)
    

Bochs相关的调试命令

指令 解释说明 举例
b address 在某物理地址上设置断点 b 0x7c00
c 继续执行,直到遇到断点 c
s 单步执行 s
info cpu
r
sreg
reg
查看寄存器信息 info cpu
r
sreg
creg
xp /nuf addr 查看内存物理地址内容 xp /10bx 0x100000
x /nuf addr 查看线性地址内容 x /40wd 0x90000
u start end 反汇编一段内存 u 0x100000 0x100010

附加解释: n 为显示的单元个数; u 为显示单元的大小(b:Byte、h:Word、w:DWord、g:QWrod(四字节)); f 为显示的格式(x:十六进制、d:十进制、t:二进制、c:字符);

以上这些命令都会在以后的系统开发中使用到,命令虽然少也很简单。如果没有它,在一开始就让我们的代码运行在物理平台上的话,一旦出现问题,会让分析错误变得举步艰难。就连查看寄存器状态和内存区数据都会变得茫然失措、无从下手,甚至绝望。考虑到这些原因,先让我们的程序在bochs里运行一段时间,等达到一定规模以后再把它移植到物理平台上运行。朋友们,让我们等待它破茧而出的时刻吧~~~!!

PS:由于是在创作初期,文章可能会有错误、内容遗漏、阐述的不到位或者读者想看而我没有写到的内容,希望大家多指点(留言或者发信息给我都可以)。
考虑到以上原因,希望大家能够关注或者推荐本书,让更多的人学习到,并避免错过之前内容的更新与补充