赞
踩
如何用自己手写的C和汇编代码启动软盘映像
介绍
我认为这篇文章是关于用C和汇编编写引导加载器的介绍,我不想在编写引导加载器方面与用C和Assembly编写的代码进行性能比较。在本文中,我将只尝试向您介绍如何通过编写自己的代码来引导软盘映像,并将其注入设备的引导扇区(引导加载程序)。在这个过程中,我将把文章分成不同的部分。很难在一篇文章中解释计算机、可引导设备以及如何编写代码,所以我已经尽力解释学习计算机的最常见方面以及引导的含义。我试图概括每个阶段的意义和重要性,以便它也易于理解和记忆。如果你需要更详细的解释,你可以浏览互联网上提供的许多文章。
文章的范围是什么?
我将把本文的范围限制在如何编写程序代码以及如何将其复制到软盘映像的引导扇区,然后如何使用x86仿真器(如Linux上的bochs)测试软盘是否使用您的程序代码引导。
我在文章中没有解释的内容
我没有解释为什么引导加载程序不能用其他语言(如汇编语言)编写,以及与另一种语言相比,用一种语言编写引导加载程序的缺点。由于这是一篇关于如何编写引导代码的入门学习文章,所以我不想在速度、编写更小的代码等更高级的主题上打扰您。
文章的组织方式
就我而言,我想在文章中介绍一些基本知识,然后用代码来遵循这些知识。这里是内容的分解,按照我向您简要介绍如何编写引导加载程序的顺序。
可引导设备简介。
开发环境简介。
微处理器简介。
在汇编程序中编写代码。
在编译器中编写代码。
显示矩形的小项目。
注:
如果您有任何语言的编程经验,这篇文章确实对您有很大帮助。虽然这篇文章看起来很简单,但在启动时用汇编语言和C语言编写程序可能是一项艰巨的任务。如果你是计算机编程新手,那么我建议你阅读一些关于介绍编程和计算机基础知识的教程,然后再回到本文。
在本文中,我将以问答的方式向您介绍与计算机相关的各种术语。坦率地说,我会写这篇文章,就像是在向自己介绍这篇文章一样。为了确保我了解它在我日常生活中的重要性和目的,我们进行了许多问答式的对话。你说的电脑是什么意思?,或者我为什么需要他们,因为我比他们聪明得多?
那么,让我们开始……:)
可引导设备简介
当典型的计算机通电时会发生什么?
通常情况下,当电脑开机时,电源按钮发出电源信号,向电脑和其他组件(如CPU、监视器、键盘、鼠标)发送适当的电压。CPU初始化基本输入输出系统只读存储器芯片以加载可执行程序。一旦BIOS芯片被初始化,它将一个特殊的程序传递给CPU执行,称为BIOS,以下是它的功能。
BIOS是一种嵌入在BIOS芯片中的特殊程序。
执行BIOS程序,然后执行以下任务。
运行通电自检。
检查时钟和各种可用总线。
检查CMOS RAM中的系统时钟和硬件信息
验证系统设置、预配置的硬件设置等。,
从RAM、磁盘驱动器、光盘驱动器、硬件驱动器等设备开始测试连接的硬件。
根据BIOS可引导设备信息中预先配置的信息,它根据设置中可用的信息搜索引导驱动器,并开始初始化以继续。
注意:在引导过程中,所有x86兼容的CPU都以称为Real mode的操作模式启动。
什么是可引导设备?
如果设备包含引导扇区或引导块,则该设备为可引导设备,bios通过首先将引导扇区加载到内存(RAM)中以供执行来读取该设备,然后继续执行。
什么是扇区?
扇区是可引导磁盘的特定大小的分区。通常一个扇区的大小为512字节。在接下来的章节中,我将向您详细解释如何测量计算机内存以及与之相关的各种术语。
什么是引导扇区?
引导扇区或引导块是可引导设备上的一个区域,其中包含计算机系统的内置固件在初始化期间要加载到RAM中的机器代码。软盘上有512字节。在接下来的章节中,您将了解更多有关字节的信息。
可引导设备如何工作?
每当初始化可引导设备时,bios都会搜索并将第一个扇区(称为引导扇区或引导块)加载到RAM中,并开始执行它。无论引导扇区内的代码是什么,都是您可以编辑的第一个程序,以便在剩余时间内定义计算机的功能。我这里的意思是,你可以编写自己的代码并将其复制到引导扇区,以使计算机按照你的要求工作。您打算写入设备引导扇区的程序代码也称为引导加载程序。
什么是引导加载器?
在计算机中,引导加载程序是一种特殊的程序,每次计算机在开机或复位期间初始化可引导设备时都会执行该程序。它是一个可执行的机器代码,它对CPU或微处理器类型的硬件架构非常特定。
有多少种微处理器可用?
我将主要列出以下内容。
16位
32位
64位
通常情况下,位数越多,程序访问的内存空间就越多,在临时存储等方面获得的性能也就越高。目前有两个主要的微处理器制造商,它们是Intel和AMD。在本文的其余部分中,我将只提及基于Intel的系列(x86)微处理器。
基于Intel的微处理器和基于AMD的微处理器之间有什么区别?
每个公司都有自己独特的设计微处理器的方法,包括用于交互的硬件和指令集。
开发环境简介。
什么是实模式?
在前面的“当计算机启动时会发生什么”一节中,我提到了从设备启动时所有的x86 CPU都以实模式启动。在为任何设备编写引导代码时,记下这一点非常重要。实模式仅支持16位指令。因此,您编写的加载到设备的引导记录或引导扇区的代码应该编译为仅16位兼容代码。在实际模式下,指令最多可以同时使用16位,例如:一个16位CPU将有一个特定的指令,它可以在一个CPU周期内将两个16位数字相加,如果一个进程需要将两个32位数字相加在一起,那么使用16位相加将需要更多的周期。
什么是指令集?
一种异构的实体集合,这些实体非常特定于微处理器的体系结构(就设计而言),用户可以使用这些实体与微处理器交互。我指的是一组实体,包括本地数据类型、指令、寄存器、寻址模式、内存架构、中断和异常处理以及外部I/O。通常,一组指令对于一系列微处理器来说是通用的。8086微处理器是8086、80286、80386、80486、Pentium、Pentium I、II、III…系列之一…。也称为X86系列。在本文中,我将参考x86系列微处理器的指令集。
如何编写自己的代码来引导设备的扇区?
为了成功完成这项任务,我们需要了解以下内容。
操作系统(GNU Linux)
汇编程序(GNU汇编程序)
指令集(x86系列)
在GNU汇编程序上为x86微处理器编写x86指令。
编译器(C编程语言-可选)
链接器(GNU链接器ld)
用于测试目的的x86仿真器,如bochs。
什么是操作系统?
我将以非常简单的方式解释这一点。由100和1000名专业人士编写的各种程序的大集合包括帮助全球个人和人民的应用程序和实用程序。一般来说,从技术角度来看,操作系统的一部分主要是为了提供各种应用程序,以帮助人们进行日常生活活动。比如连接互联网、聊天、浏览网络、创建文件、保存文件、数据、处理数据等等。我仍然不明白。我这里的意思是,你可能想和你的朋友聊天,你可能想要在网上看新闻,你可能希望将一些个人信息写入文件,你可能会想看一些电影,你可能需要计算一些数学方程式,你可能要玩游戏,你可能还要编写程序等等……所有这些任务都可以通过操作系统实现。操作系统的任务是提供足够的工具来帮助您并为您服务。有些活动你也需要同时处理,操作系统的工作就是管理硬件并为你提供最佳体验。
此外,请注意,所有现代操作系统都在保护模式下运行。
什么是不同类型的操作系统?
Windows
Linux
macOS
还有更多…
什么是保护模式?
与实模式不同,保护模式支持32位指令。现在不要太担心,因为我们不太担心操作系统的工作方式等。
什么是汇编器?
汇编器将用户给出的指令转换成机器代码。
即使编译器也会这样做……不是吗?
在更高层次上是的……但实际上是嵌入在编译器中的汇编器执行此操作。
那么为什么编译器不能直接生成机器代码呢?
编译器的主要工作主要是将用户编写的指令转换为一组中间指令,称为汇编语言指令。然后,汇编器将使用这些指令并将其转换为相应的机器代码。
为什么我需要一个操作系统来编写引导扇区的代码?
现在,我不想进行非常详细的解释,但让我就本文的范围进行解释。好前面我提到,为了编写微处理器可以理解的指令,我们需要编译器,而这个编译器是作为操作系统中的实用程序开发的。我告诉过你,操作系统旨在帮助人们提供各种实用程序,编译器也是其中一种实用程序。
我可以使用哪种操作系统?
我已经在Ubuntu操作系统上编写了程序,可以从软盘设备启动,所以本文推荐Ubuntu。
我应该使用哪个编译器?
我已经使用GNU GCC编译器编写了程序,我将如何使用相同的编译器编译代码。如何将手写代码测试到设备的引导扇区?我将向您介绍一个x86仿真器,它可以帮助我们达到更高的水平,而无需在每次编辑设备的引导扇区时重新启动计算机。
微处理器简介
为了学习微处理器编程,首先我们需要学习如何使用寄存器。
什么是寄存器?
寄存器就像微处理器的实用程序,用于临时存储数据并根据我们的要求对其进行操作。假设假设用户想用2加3,用户要求计算机将数字3存储在一个寄存器中,将数字2存储在更多寄存器中,然后将这两个寄存器的内容相加,结果由CPU放入另一个寄存器,这是我们希望看到的输出。寄存器有四种类型,如下所示。
通用寄存器
段寄存器
栈寄存器
索引寄存器
让我向您介绍每种类型。
通用寄存器:这些寄存器用于存储程序在其生命周期中所需的临时数据。这些寄存器中的每一个都是16位宽或2字节长。
AX—累加器寄存器
BX—基址寄存器
CX—计数寄存器
DX—数据寄存器
段寄存器:要表示微处理器的内存地址,需要注意两个术语:
段:它通常是内存块的开始。
偏移量:它是内存块的索引。
示例:假设有一个字节的值为“X”,该字节存在于起始地址为0x7c00的内存块上,并且该字节位于起始位置的第10个位置。在这种情况下,我们将段表示为0x7c00,偏移量表示为10。
绝对地址为0x7c00+10。
我想列出四个类别。
CS—代码段
SS—栈段
DS—数据段
ES—扩展段
但这些寄存器总是有限制的。您不能直接将地址分配给这些寄存器。我们可以做的是,将地址复制到通用寄存器,然后将地址从该寄存器复制到段寄存器。示例:为了解决定位字节“X”的问题,我们采用以下方法
- movw $0x07c0, %ax
- movw %ax , %ds
- movw (0x0A) , %ax
在我们的案例中,发生的是
在AX中设置0x07c0*16
设置DS=AX=0x7c00
将0x7c00+0x0a设置为ax
我将描述编写程序时需要了解的各种寻址模式。
栈寄存器:
BP—基本指针
SP—栈指针
索引寄存器:
SI—源索引寄存器。
DI—目标索引寄存器。
AX:CPU使用它进行算术运算。
BX:它可以保存过程或变量的地址(SI、DI和BP也可以)。并执行算术和数据移动。
CX:它充当重复或循环指令的计数器。
DX:它在乘法运算中保持乘积的高16位(也处理除法运算)。
CS:它保存程序中所有可执行指令的基本位置。
SS:它保存堆栈的基本位置。
DS:它保存变量的默认基位置。
ES:它为内存变量保存了额外的基本位置。
BP:它包含SS寄存器的假定偏移量。通常由子程序用来定位调用程序在堆栈上传递的变量。
SP:包含堆栈顶部的偏移。
SI:用于字符串移动指令。SI寄存器指向源字符串。
DI:充当字符串移动指令的目标。
什么是1bit(一位)?
在计算中,位是可以存储数据的最小单位。位以二进制形式存储数据。1(开)或0(关)。
有关寄存器的更多信息:
寄存器按以下从左到右的顺序或位进一步划分:
AX:AX的前8位被标识为AL,后8位被识别为AH
BX:BX的前8位被标识为BL,后8位被识别为BH
CX:CX的前8位被标识为CL,最后8位被识别为CH
DX:DX的前8位被标识为DL,后8位被识别为DH
如何访问BIOS功能?
BIOS提供了一组功能,让我们引起CPU的注意。可以通过中断访问BIOS功能。
什么是中断?
为了中断程序的正常流程并处理需要快速响应的事件,我们使用中断。计算机的硬件提供一种叫做中断的机制来处理事件。例如,当鼠标移动时,鼠标硬件中断当前程序以处理鼠标移动(移动鼠标光标等)。中断处理程序是处理中断的例程。每种类型的中断都分配一个整数。在物理内存的开头,存在一个中断向量表,其中包含中断处理程序的分段地址。中断次数本质上是该表的索引。我们也可以将中断称为BIOS提供的服务。
我们将在程序中使用哪种中断服务?
Bios中断0x10。
在用汇编器编写代码
GNU汇编程序中有哪些不同的数据类型?
一组位,用于表示一个单元,以构成各种数据类型的帧。
什么是数据类型?
数据类型用于标识数据的特征。各种数据类型如下。
字节
单词
整数
ascii码
ASCII码
byte:八位长。字节被认为是计算机上通过编程可以存储数据的最小单位。
word:它是一个16位长的数据单位。
什么是int?
int是表示32位长数据的数据类型。四个字节或两个字构成一个整数。
什么是ascii?
一种数据类型,表示一组没有空终止符的字节。
什么是asciz?
一种数据类型,表示结尾以空字符结尾的一组字节。
如何通过汇编器生成实模式代码?
当CPU以实模式(16位)启动时,从设备启动时,我们所能做的就是利用BIOS提供的内置功能来继续。我这里的意思是,我们可以利用BIOS的功能编写自己的引导加载程序代码,然后转储到设备的引导扇区,然后引导它。让我们看看如何在汇编程序中编写一小段代码,通过GNU汇编器生成16位CPU代码。
示例:test.S
- .code16 #generate 16-bit code
- .text #executable code location
- .globl _start;
- _start: #code entry point
- . = _start + 510 #mov to 510th byte from 0 pos
- .byte 0x55 #append boot signature
- .byte 0xaa #append boot signature
让我解释一下上面代码中的每个语句。
.code16:它是给汇编器的指令或命令,用于生成16位代码而不是32位代码。为什么这个提示是必要的?请记住,您将使用操作系统来利用汇编程序和编译器来编写引导加载程序代码。然而,我也提到了操作系统在32位保护模式下工作。因此,当您在保护模式操作系统上使用汇编器时,它默认配置为生成32位代码而不是16位代码,因为我们需要16位代码。为了避免汇编器和编译器生成32位代码,我们使用此指令。
.text:.text部分包含组成程序的实际机器指令。
.global_start:.global<symbol>使符号对链接器可见。如果在分部程序中定义符号,则其值可用于与其链接的其他分部程序。否则,符号将从链接到同一程序的另一个文件中的同名符号中获取其属性。
_start:进入主代码,_start是链接器的默认入口点。
.=_start+510:从开始到第510字节遍历
.byte 0x55:它是标识为引导签名一部分的第一个字节。(第511字节)
.byte 0xaa:它是标识为引导签名一部分的最后一个字节。(第512字节)
如何编译汇编程序?
将代码保存为test.S文件。在命令提示符下键入以下内容:
- as test.S -o test.o
- ld –Ttext 0x7c00 --oformat=binary test.o –o test.bin
以上命令对我们意味着什么?
as test.S–o test.o:此命令将给定的汇编代码转换为各自的目标代码,这是汇编器在转换为机器代码之前生成的中间代码。
--ofomat=binary开关告诉链接器您希望输出文件是一个纯二进制映像(无启动代码、无重定位…)。
–Ttext 0x7c00告诉链接器您希望将“文本”(代码段)地址加载到0x7c00,从而计算绝对寻址的正确地址。
什么是引导签名?
还记得早些时候我介绍了BIOS程序加载的引导记录或引导扇区。BIOS如何识别设备是否包含引导扇区?为了回答这个问题,我可以告诉您,引导扇区长度为512字节,在第510字节中预期符号0x55,在第511字节中预期另一个符号0xaa。因此,我验证引导扇区的最后两个字节是否为0x55和0xaa,如果是,则它将该扇区标识为引导扇区,并继续执行引导扇区代码,否则会抛出设备不可引导的错误。使用十六进制编辑器,您可以以更可读的方式查看二进制文件的内容,下面是使用hexedit工具查看文件时的快照,供您参考。
如何将可执行代码复制到可启动设备,然后对其进行测试?
要创建1.4mb大小的软盘映像,请在命令提示符下键入以下命令。
dd if=/dev/zero of=floppy.img bs=512 count=2880
要将代码复制到软盘映像文件的引导扇区,请在命令提示符下键入以下命令。
dd if=test.bin of=floppy.img
要测试程序,请在命令提示符下键入以下命令
bochs
如果未安装bochs,则可以键入以下命令
sudo apt-get install bochs-x
bochsrc.txt文件示例:
- megs: 32
- #romimage: file=/usr/local/bochs/1.4.1/BIOS-bochs-latest, address=0xf0000
- #vgaromimage: /usr/local/bochs/1.4.1/VGABIOS-elpin-2.40
- floppya: 1_44=floppy.img, status=inserted
- boot: a
- log: bochsout.txt
- mouse: enabled=0
您应该看到一个典型的bochs模拟窗口,如下所示。
观察结果:
现在,如果您在十六进制编辑器中查看test.bin文件,您将看到引导签名位于第510个字节的末尾,下面是供您参考的屏幕截图。
没有发生任何事情,因为我们没有在代码中编写任何内容显示在屏幕上。所以你只看到一条消息“从软盘启动”。让我们再看一些在汇编程序上编写汇编代码的示例。
示例:test2.S
- .code16 #generate 16-bit code
- .text #executable code location
- .globl _start;
- _start: #code entry point
-
- movb $'X' , %al #character to print
- movb $0x0e, %ah #bios service code to print
- int $0x10 #interrupt the cpu now
-
- . = _start + 510 #mov to 510th byte from 0 pos
- .byte 0x55 #append boot signature
- .byte 0xaa #append boot signature
键入上述内容后,保存到test2.S,然后按照前面的指示通过更改源文件名进行编译。当您编译并成功地将此代码复制到引导扇区并运行bochs时,应该会看到下面的屏幕。在命令提示符下键入bochs以查看结果,您应该在屏幕上看到字母“X”,如下图所示。
恭喜!!!
观察结果:
如果在十六进制编辑器中查看,您将看到字符“X”位于起始地址的第二个位置。
现在让我们做一些不同的事情,比如在屏幕上打印文本。
示例:test3.S
.code16 #generate 16-bit code .text #executable code location .globl _start; _start: #code entry point #print letter 'H' onto the screen movb $'H' , %al movb $0x0e, %ah int $0x10 #print letter 'e' onto the screen movb $'e' , %al movb $0x0e, %ah int $0x10 #print letter 'l' onto the screen movb $'l' , %al movb $0x0e, %ah int $0x10 #print letter 'l' onto the screen movb $'l' , %al movb $0x0e, %ah int $0x10 #print letter 'o' onto the screen movb $'o' , %al movb $0x0e, %ah int $0x10 #print letter ',' onto the screen movb $',' , %al movb $0x0e, %ah int $0x10 #print space onto the screen movb $' ' , %al movb $0x0e, %ah int $0x10 #print letter 'W' onto the screen movb $'W' , %al movb $0x0e, %ah int $0x10 #print letter 'o' onto the screen movb $'o' , %al movb $0x0e, %ah int $0x10 #print letter 'r' onto the screen movb $'r' , %al movb $0x0e, %ah int $0x10 #print letter 'l' onto the screen movb $'l' , %al movb $0x0e, %ah int $0x10 #print letter 'd' onto the screen movb $'d' , %al movb $0x0e, %ah int $0x10 . = _start + 510 #mov to 510th byte from 0 pos .byte 0x55 #append boot signature .byte 0xaa #append boot signature
将其保存为test3.S。当您编译并成功将此代码复制到引导扇区并运行bochs时,您应该看到下面的屏幕。
观察结果:
OK,现在我们做的事情比以前的程序更加不同。
让我们编写一个汇编程序,将字母“Hello,World”打印到屏幕上。
我们还将尝试定义函数和宏,通过这些函数和宏我们将尝试打印字符串。
示例:test4.S
#generate 16-bit code .code16 #hint the assembler that here is the executable code located .text .globl _start; #boot code entry _start: jmp _boot #jump to boot code welcome: .asciz "Hello, World\n\r" #here we define the string .macro mWriteString str #macro which calls a function to print a string leaw \str, %si call .writeStringIn .endm #function to print the string .writeStringIn: lodsb orb %al, %al jz .writeStringOut movb $0x0e, %ah int $0x10 jmp .writeStringIn .writeStringOut: ret _boot: mWriteString welcome #move to 510th byte from the start and append boot signature . = _start + 510 .byte 0x55 .byte 0xaa
将其保存为test4.S。当您编译并成功将此代码复制到引导扇区并运行bochs时,您应该看到下面的屏幕。
很好!如果你真的理解我所做的,并且能够编写类似的程序,那么再次祝贺你!
观察结果:
什么是函数?
函数是一个有名称的代码块,它具有可重用的属性。
什么是宏?
宏是一段代码,它已被命名。每当使用该名称时,它都会被宏的内容替换。
宏和函数在语法方面有什么区别?
要调用函数,我们使用以下语法。
push<argument>
call<function name>
要调用宏,我们使用以下语法
macroname<argument>
但是与函数相比,宏的调用和使用语法非常简单。因此,我倾向于编写宏并使用它,而不是在主代码中调用函数。关于如何在GNU汇编器上编写汇编代码,您可以在网上查阅更多资料。
在C编译器中编写代码
什么是C?
在计算领域,C是一种通用编程语言,最初由丹尼斯·里奇于1969年至1973年在at&T贝尔实验室开发。
为什么使用C?一种依赖于机器的语言,但用C语言编写的程序通常很小,执行速度很快。该语言包括通常仅在汇编语言或机器语言中可用的低级功能。C是一种结构化编程语言。
为什么我需要用C编写代码?
如果你想编写更小的程序,并希望它们速度更快,那就去做吧。
用C语言编写代码需要什么?
我们将使用名为gcc的GNU C编译器来编写C代码。
如何用C语言在GCC编译器中编写程序?
让我们写一个程序来看看它的样子。
示例:test.c
- __asm__(".code16\n");
- __asm__("jmpl $0x0000, $main\n");
-
- void main() {
- }
文件:test.ld
- ENTRY(main);
- SECTIONS
- {
- . = 0x7C00;
- .text : AT(0x7C00)
- {
- *(.text);
- }
- .sig : AT(0x7DFE)
- {
- SHORT(0xaa55);
- }
- }
如何编译C程序?在命令提示符下键入以下内容:
- gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o
- ld -static -Ttest.ld -nostdlib --nmagic -o test.elf test.o
- objcopy -O binary test.elf test.bin
以上命令对我们意味着什么?
该命令将给定的C代码转换为各自的目标代码,该目标代码是编译器在转换为机器代码之前生成的中间代码。
gcc -c -g -Os -march=i686 -ffreestanding -Wall -Werror test.c -o test.o:
每个flag意味着什么?
-c: 它用于在没有链接的情况下编译给定的源代码。
-g: 生成GDB调试器使用的调试信息。
-Os:代码大小的优化
-march:生成特定CPU架构的代码(在我们的例子中是i686)
-ffreestanding:独立环境是指标准库可能不存在的环境,程序启动不一定在“main”。
-Wall:启用所有编译器的警告消息。为了生成更好的代码,应始终使用此选项。
-Weror:启用将警告视为错误
test.c:输入源文件名
-o: 生成目标代码
test.o:输出对象代码文件名。
使用编译器的所有上述标志组合,我们尝试生成目标代码,帮助我们识别错误和警告,并为CPU类型生成更高效的代码。如果您没有指定march=i686,它会为您所拥有的机器类型生成代码,否则它会为端口而生成代码,最好指定您要使用的CPU类型。
ld -static -Ttest.ld -nostdlib --nmagic test.elf -o test.o
这是从命令提示符调用链接器的命令,我在下面解释了我们如何使用链接器。
每个flag意味着什么?
-static:不链接共享库。
-Ttest.ld:此功能允许链接器遵循链接器脚本中的命令。
-nostdlib:此功能允许链接器通过链接任何标准C库启动函数来生成代码。
--nmagic:此功能允许链接器生成没有_start_SECTION和_stop_SECTION代码的代码。
test.elf:输入文件名(存储可执行文件的平台相关文件格式Windows:PE,Linux:elf)
-o: 生成目标代码
test.o:输出对象代码文件名。
什么是链接器?
这是编译的最后阶段。ld(链接器)将一个或多个对象文件或库作为输入,并将它们组合在一起以生成单个(通常是可执行的)文件。在这样做的过程中,它解析对外部符号的引用,为过程/函数和变量分配最终地址,并修改代码和数据以反映新地址(称为重新定位的过程)。
还要记住,我们的代码中没有标准库和所有花哨的函数。
objcopy -O binary test.elf test.bin
此命令用于生成独立于平台的代码。注意,Linux以与windows不同的方式存储可执行文件。每个人都有自己的文件存储方式,但我们只是在开发一个小代码来启动,目前不依赖于任何操作系统。因此,我们既不依赖于这两者,也不需要操作系统在启动时运行代码。
为什么在C程序中使用汇编语句?
在实模式下,可以使用汇编语言指令通过软件中断轻松访问BIOS功能。这导致在我们的C代码中使用内联程序集。
如何将可执行代码复制到可启动设备,然后对其进行测试?
要创建1.4mb大小的软盘映像,请在命令提示符下键入以下命令。
dd if=/dev/zero of=floppy.img bs=512 count=2880
要将代码复制到软盘映像文件的引导扇区,请在命令提示符下键入以下命令。
dd if=test.bin of=floppy.img
要测试程序,请在命令提示符下键入以下命令
bochs
您应该看到一个典型的bochs模拟窗口,如下所示。
观察:没有发生任何事情,因为我们没有在代码中编写任何内容显示在屏幕上。所以你只看到一条消息“Booting from Floppy...”。恭喜!!!
我们使用__asm_关键字将汇编语言语句嵌入到C程序中。此关键字提示编译器识别它是用户给出的汇编指令。
我们还使用__volatile_来提示汇编器不要修改代码,让它保持原样。
这种将汇编代码嵌入C代码的方式称为内联汇编。
让我们再看一些关于在编译器上编写代码的示例。
让我们编写一个汇编程序,将字母“X”打印到屏幕上。
示例:test2.c
- __asm__(".code16\n");
- __asm__("jmpl $0x0000, $main\n");
-
- void main() {
- __asm__ __volatile__ ("movb $'X' , %al\n");
- __asm__ __volatile__ ("movb $0x0e, %ah\n");
- __asm__ __volatile__ ("int $0x10\n");
- }
键入上述内容后,保存到test2.c,然后按照前面的指示通过更改源文件名进行编译。当您编译并成功地将此代码复制到引导扇区并运行bochs时,应该会看到下面的屏幕。在命令提示符下键入bochs以查看结果,您应该在屏幕上看到字母“X”,如下图所示。
现在,让我们编写一个c程序,将字母“Hello,World”打印到屏幕上。
示例:test3.c
/*generate 16-bit code*/ __asm__(".code16\n"); /*jump boot code entry*/ __asm__("jmpl $0x0000, $main\n"); void main() { /*print letter 'H' onto the screen*/ __asm__ __volatile__("movb $'H' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'e' onto the screen*/ __asm__ __volatile__("movb $'e' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'l' onto the screen*/ __asm__ __volatile__("movb $'l' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'l' onto the screen*/ __asm__ __volatile__("movb $'l' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'o' onto the screen*/ __asm__ __volatile__("movb $'o' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter ',' onto the screen*/ __asm__ __volatile__("movb $',' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter ' ' onto the screen*/ __asm__ __volatile__("movb $' ' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'W' onto the screen*/ __asm__ __volatile__("movb $'W' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'o' onto the screen*/ __asm__ __volatile__("movb $'o' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'r' onto the screen*/ __asm__ __volatile__("movb $'r' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'l' onto the screen*/ __asm__ __volatile__("movb $'l' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); /*print letter 'd' onto the screen*/ __asm__ __volatile__("movb $'d' , %al\n"); __asm__ __volatile__("movb $0x0e, %ah\n"); __asm__ __volatile__("int $0x10\n"); }
现在将上述代码保存为test3.c,然后按照通过更改输入源文件名给出的编译指令进行操作,并按照给出的指令将编译后的代码复制到软盘的引导扇区。现在观察结果。如果一切正常,您应该看到下面的屏幕输出。
让我们编写一个C程序,将字母“Hello,World”打印到屏幕上。
我们还将尝试定义函数,通过该函数我们将尝试打印字符串。
示例:test4.c
/*generate 16-bit code*/ __asm__(".code16\n"); /*jump boot code entry*/ __asm__("jmpl $0x0000, $main\n"); /* user defined function to print series of characters terminated by null character */void printString(constchar* pStr) { while(*pStr) { __asm__ __volatile__ ( "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007) ); ++pStr; } } void main() { /* calling the printString function passing string as an argument */ printString("Hello, World"); }
现在将上述代码保存为test4.c,然后按照通过更改输入源文件名给出的编译指令进行操作,并按照给出的指令将编译后的代码复制到软盘的引导扇区。现在观察结果。如果一切正常,您应该看到下面的屏幕输出。
我想提醒你一点。我们所要做的只是通过学习将之前编写的汇编程序转换为C程序。到目前为止,您应该能够熟练地用汇编语言和C语言编写程序,并且也很清楚如何编译和测试它们。
现在,我们将继续编写循环,并使它们在函数中工作,还将看到更多的bios服务。
显示矩形的小项目
现在让我们来看看更大的东西…比如显示图形。
示例:test5.c
/* generate 16 bit code */ __asm__(".code16\n"); /* jump to main function or program code */ __asm__("jmpl $0x0000, $main\n"); #define MAX_COLS 320 /* maximum columns of the screen */ #define MAX_ROWS 200 /* maximum rows of the screen */ /* function to print string onto the screen */ /* input ah = 0x0e */ /* input al = <character to print> */ /* interrupt: 0x10 */ /* we use interrupt 0x10 with function code 0x0e to print */ /* a byte in al onto the screen */ /* this function takes string as an argument and then */ /* prints character by character until it founds null */ /* character */ void printString(const char* pStr) { while(*pStr) { __asm__ __volatile__ ( "int $0x10" : : "a"(0x0e00 | *pStr), "b"(0x0007) ); ++pStr; } } /* function to get a keystroke from the keyboard */ /* input ah = 0x00 */ /* input al = 0x00 */ /* interrupt: 0x10 */ /* we use this function to hit a key to continue by the */ /* user */ void getch() { __asm__ __volatile__ ( "xorw %ax, %ax\n" "int $0x16\n" ); } /* function to print a colored pixel onto the screen */ /* at a given column and at a given row */ /* input ah = 0x0c */ /* input al = desired color */ /* input cx = desired column */ /* input dx = desired row */ /* interrupt: 0x10 */ void drawPixel(unsigned char color, int col, int row) { __asm__ __volatile__ ( "int $0x10" : : "a"(0x0c00 | color), "c"(col), "d"(row) ); } /* function to clear the screen and set the video mode to */ /* 320x200 pixel format */ /* function to clear the screen as below */ /* input ah = 0x00 */ /* input al = 0x03 */ /* interrupt = 0x10 */ /* function to set the video mode as below */ /* input ah = 0x00 */ /* input al = 0x13 */ /* interrupt = 0x10 */ void initEnvironment() { /* clear screen */ __asm__ __volatile__ ( "int $0x10" : : "a"(0x03) ); __asm__ __volatile__ ( "int $0x10" : : "a"(0x0013) ); } /* function to print rectangles in descending order of */ /* their sizes */ /* I follow the below sequence */ /* (left, top) to (left, bottom) */ /* (left, bottom) to (right, bottom) */ /* (right, bottom) to (right, top) */ /* (right, top) to (left, top) */ void initGraphics() { int i = 0, j = 0; int m = 0; int cnt1 = 0, cnt2 =0; unsigned char color = 10; for(;;) { if(m < (MAX_ROWS - m)) { ++cnt1; } if(m < (MAX_COLS - m - 3)) { ++cnt2; } if(cnt1 != cnt2) { cnt1 = 0; cnt2 = 0; m = 0; if(++color > 255) color= 0; } /* (left, top) to (left, bottom) */ j = 0; for(i = m; i < MAX_ROWS - m; ++i) { drawPixel(color, j+m, i); } /* (left, bottom) to (right, bottom) */ for(j = m; j < MAX_COLS - m; ++j) { drawPixel(color, j, i); } /* (right, bottom) to (right, top) */ for(i = MAX_ROWS - m - 1 ; i >= m; --i) { drawPixel(color, MAX_COLS - m - 1, i); } /* (right, top) to (left, top) */ for(j = MAX_COLS - m - 1; j >= m; --j) { drawPixel(color, j, m); } m += 6; if(++color > 255) color = 0; } } /* function is boot code and it calls the below functions */ /* print a message to the screen to make the user hit the */ /* key to proceed further and then once the user hits then */ /* it displays rectangles in the descending order */ void main() { printString("Now in bootloader...hit a key to continue\n\r"); getch(); initEnvironment(); initGraphics(); }
现在将上述代码保存为test5.c,然后按照通过更改输入源文件名给出的编译指令进行操作,并按照给出的指令将编译后的代码复制到软盘的引导扇区。
现在观察结果。如果一切正常,您应该看到下面的屏幕输出。
现在按下一个键,看看接下来会发生什么。
观察结果:
如果您仔细查看可执行文件的内容,您会发现我们几乎耗尽了空间。由于引导扇区只有512字节,我们只能在程序中嵌入一些函数,比如初始化环境,然后打印彩色矩形,但不能超过512字节,因为它需要512字节的空间。以下是快照,供您参考。
这就是本文的全部内容。玩得开心,写更多的程序来探索真实模式,你会发现使用bios Interrupts在真实模式下编程真的很有趣。在下一篇文章中,我将尝试解释用于访问数据、读取软盘及其体系结构的寻址模式,以及为什么引导加载程序主要用汇编语言而不是C语言编写,以及在代码生成方面用C语言编写引导加载程序有哪些限制:)
许可证
本文以及任何相关的源代码和文件均根据The Code Project Open License (CPOL)获得许可
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。