环境搭建:

标准的开发环境的搭建并不是容易的事情,遇到和处理器平台相关的问题的时候更是无解,比如我现在要给予x86的cpu做arm的开发,或者基于arm的cpu开发x86的开发,这是件很头疼的事情,比较好的是我们可以使用docker来生成标准的开发环境,这样就可以屏蔽本机硬件的耦合。

  • Dockerfile 构建环境:
1
2
3
4
5
6
7
8
9
10
11
FROM randomdude/gcc-cross-x86_64-elf

RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y nasm
RUN apt-get install -y xorriso
RUN apt-get install -y grub-pc-bin
RUN apt-get install -y grub-common

VOLUME /root/env
WORKDIR /root/env

上述就是构建x86汇编语言标准的镜像,这里我们是基于ubuntu的,构建命令行如下:

1
docker build buildenv -t myos-buildenv

在vscode的环境下启动如下命令:

1
docker run --rm -it -v "$(pwd)":/root/env myos-buildenv

至此,我们就完成了本地环境和容器环境的共享,其中的-v就是将本地的目录和容器的环境共享,最后完成环境搭建,我们可以通过helloworld代码进行测试:

基本语法

操作系统切换线程或者进程,是通过中断来实现的,中断可以分为两种:软中断、硬中断,所谓软终端是我们在程序的过程中可能发生了异常、程序退出,所谓硬中断是cpu在一个时钟周期切换的时候发现终端标记位上有程序要切换,从而通过系统调用触发程序的切换。 操作系统内核提供了系统调用,我们可以通过寄存器来进行参数的传递,且传递参数的数量是有上限的:

这里我们通过系统调用传递参数的时候,id对应了syscall的唯一的标识,或者可以将其认为是函数的唯一标识,其他的寄存器则是传递的参数。

以上是系统调用的功能表,详见:https://chromium.googlesource.com/chromiumos/docs/+/master/constants/syscalls.md#x86_64-64_bit ,其中的参数类型为*的代表了指针类型,其指向内存中的地址。

汇编语言通常通过section来定义段,我们常见的有

  • .data(数据段):用来存放已经初始化的变量的区域

  • .text(代码段):用来存放程序执行代码的区域

  • .bss(用来存放未初始化的全局变量)

  • global:当模块中有需要被连接的模块的时候可以使用global定义

  • qeu 用来定义常数

目前暂时用到这几种,后面再补充

数据类型

针对不同的段,我们需要使用不同类型的数据,对于data段,我们常见的数据类型为

  • db:defined bytes,代表了一个8位 1字节 的数据
  • dw:defined words,在汇编中通常16位对应了一个word,即是一个字
  • dd:defined double words,同上代表了32位
  • dq:代表了64位

以上数据类型对应了已经初始化的变量

对于.bss段,我们通常的数据类型为:resb resw resd resq和data段类型是对应的,不过数据并未初始化。以上关键字需要带一个操作数,比如:resb 1代表了保留一个字节的未初始化的空间

寄存器

x86架构中存在如下寄存器,这里我们可以看到寄存器是可以复用的,也就是8位的寄存器可以是64位寄存器的低8位,同理其他情况也是:

前面我们看到了寄存器,并且看到了寄存器是可以进行拆分的,这里我们继续看一些特殊的寄存器,或者叫做标记位会更合适:

指针也是一种特殊类型的寄存器,其用来指向一块内存空间,在其指向的内存空间中保存着数据: 上面是比较常用的几种类型的指针,其中rip就是我们常说的程序计数器pc,rsp中的s代表了stack、rbp中的b代表了bottom,也即是stack的栈底

输入输出

如本小结开始所说,函数的调用是通过syscall来触发的,输出:

1
2
3
4
5
mov rax,1
mov rdi,1;d可以认为是destionation,也就是写出到哪里,1的话是标准输出,也就是输出到屏幕上
mov rsi,text ;这一步代表了参数传递,将想要输出到屏幕的参数传递给rsi寄存器
mov rdx,length ;这一步用来指定text的长度
syscall

以上就是输出数据到屏幕上的操作,接下来看一下从命令行读入参数的代码:

1
2
3
4
5
mov rax, 0
mov rdi, 0
mov rsi, text
mov rdx, length
syscall

上面的代码可以看到,输入和输出代码非常类似,不过这里我们读取数据的时候数据的内容和长度都不确定,因此我们需要时bss段来申请一块存储空间,申请的时候会指定长度,这里的length就是我们申请的时候指定的长度

控制流

常见的控制程序的方式在高等语言中有if、while、for等,这些对应到汇编中也是存在相应的影子的,if对应到汇编可以使用cmp & jne、jge等来实现,循环也可以通过jmp或者jmp的衍生来指令来进行控制

宏是一种特殊的代码,其在编译的时候会展开为指定代码,其定义如上图所示,%macro%endmacro用于标记宏的开始和结束的区间,后面的名称是我们在其他的代码片段可以引用的标识,或者将其理解为函数名称也可以,最后的argc则指代了该宏的参数的个数,我们获取参数的时候通过%num的方式来指定获取第几个参数,并且参数传递给宏的时候,使用,进行分割,我们下面通过一个简单的示例来说明宏的定义及使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
section .data
text: db "hello"

section .text
global _start

%macro print 1
mov rax,1
mov rdi,1
mov rsi,%1
mov rdx,14
syscall
%endmacro
;宏的定义必须要放到引用前面,通过include过来的也可以认为是放到了前面
%macro sumall 2
;这里类似于函数的定义
%%test:
mov rax, %1
add rax, %2
mov [sumof], rax; 将rbx中保存的临时数据写入到bss预申请的内存空间中
%endmacro

%macro exit 0
mov rax, 60
mov rdi, 0
syscall
%endmacro



_start:
sumall 3,1

mov rax, 1
mov rdi, 1
mov rsi, [sumof]
mov rdx, 16
syscall

exit

section .bss
sumof: resb 16

上面我们看到了宏的定义及使用,宏的定义必须在使用之前,否则会出现符号不识别的问题,另外在宏中定义label的时候尽量使用%%开头,原因是当多次调用宏的时候,由于宏最终会被中间的代码给替代,这样就出现了多个同样的标记,因此就会出现模棱两可的问题,如下:

上面的代码还存在一个问题,就是数字的打印是有问题的,因此尝试使用汇编来完成数字的打印

读写文件

接收参数

采用堆栈来接收参数

小结

参考:https://github.com/davidcallanan/os-series https://www.youtube.com/watch?v=XuUD0WQ9kaE&list=PLetF-YjXm-sCH6FrTz4AQhfH6INDQvQSn&index=1