OS lab1
Outline:
- 汇编问答
- 调用约束
- 汇编避坑
NASM tutorial
NASM教程
汇编问答
用于OS第一次作业
请简述 80x86 系列的发展历史
- 1978年6月,intel推出第一款16位微处理器8086,采用20位地址线
- 1982年发布80286,主频提高至12MHz
- 1985年发布80386,处理器变为32位,地址线扩展至32位
- 1989年发布80486,1993年发布80586并命名为奔腾
说明小端和大端的区别,并说明 80x86 系列采用了哪种方式?
- 小端存储:数据的高字节保存在内存的高地址中,而数据的低字节保存在内存的低地址中
- 大端存储反之
- 80x86采用小端存储
8086 有哪五类寄存器,请分别举例说明其作用?
- 通用寄存器:
- 数据寄存器: AX,BX,CX,DX
- AX (Accumulator):累加寄存器,也称之为累加器;
- BX (Base):基地址寄存器;
- CX (Count):计数器寄存器;
- DX (Data):数据寄存器;
- 指针寄存器:
- SP (Stack Pointer):堆栈指针寄存器;
- BP (Base Pointer):基指针寄存器;
- 变址寄存器:
- SI (Source Index):源变址寄存器;
- DI (Destination Index):目的变址寄存器;
- 数据寄存器: AX,BX,CX,DX
- 控制寄存器:
- IP (Instruction Pointer):指令指针寄存器;
- FLAG:标志寄存器;
- 段寄存器:
- CS (Code Segment):代码段寄存器;
- DS (Data Segment):数据段寄存器;
- SS (Stack Segment):堆栈段寄存器;
- ES (Extra Segment):附加段寄存器;
- 通用寄存器:
什么是寻址?立即寻址和直接寻址的区别是什么?
- 找到操作数的地址(从而能够取出操作数)
- 区别:
- 立即寻址
MOV AX 1234H
- 直接给出了操作数,事实上没有“寻址”
- 直接寻址
MOV AX [1234H]
- 直接给出了地址1234H,用[]符号取数
- 立即寻址
请举例说明寄存器间接寻址、寄存器相对寻址、基址加变址寻址、相对基址加变址寻址四种方式的 区别
寄存器间接寻址
MOV AX [BX]
操作数有效地址在寄存器之中(SI、DI、BX、BP)寄存器相对寻址 •
MOV AX [SI+3]
基址加变址 •
MOV AX [BX+DI]
• 把一个基址寄存器(BX、BP)的内容,加上变址寄存器(SI、DI)的内容。相对基址加变址 •
MOV AX [3+BX+DI]
请分别简述 MOV 指令和 LEA 指令的用法和作用?
- LEA:load effective address, 将一个内存地址直接赋给目的操作数
lea eax,[ebx+8]
就是将ebx+8这个值直接赋给eax,而不是把ebx+8处的内存地址里的数据赋给eax。
- MOV: 与LEA相反
- LEA:load effective address, 将一个内存地址直接赋给目的操作数
请说出主程序与子程序之间至少三种参数传递方式
利用寄存器传递参数,就是把参数放在约定的寄存器中
利用约定的存储单元传递参数
利用堆栈传递参数
如何处理输入和输出,代码中哪里体现出来?
- 宏,系统调用
有哪些段寄存器
- 见上文
通过什么寄存器保存前一次的运算结果,在代码中哪里体现出来。
- 注释里写了
解释
boot.asm
文件中,org 0700h
的作用- 告诉汇编器,当前这段代码会放在07c00h处。所以,如果之后遇到需要绝对寻址的指令,那么绝对地址就是07c00h加上相对地址。
- 在第一行加上
org 07c00h
只是让编译器从相对地址07c00h处开始编译第一条指令,相对地址被编译加载后就正好和绝对地址吻合
boot.bin 应该放在软盘的哪一个扇区?为什么?
- first
- 开机,从ROM运行BIOS程序,BIOS程序检查软盘0面0磁道1扇区,如果扇区以
0xaa55
结束,则认定为引导扇区,将其512字节的数据加载到内存的07c00
处,然后设置PC,跳到内存07c00处开始执行代码。
loader 的作用有哪些?
- 为了突破512字节的限制,我们引入另外一个重要的文件loader.asm,引导扇区只负责把loader加载入内存并把控制权交给他,这样将会灵活得多。
- 最终,由loader将内核kernel加载入内存,才开始了真正操作系统内核的运行。
- 跳入保护模式
- 最开始的x86处理器16位,寄存器用ax, bx等表示,称为实模式。后来扩 充成32位,eax,ebx等,为了向前兼容,提出了保护模式
- 必须从实模式跳转到保护模式,才能访问1M以上的内存。
- 启动内存分⻚
- 从kernel.bin中读取内核,并放入内存,然后跳转到内核所在的 开始地址,运行内核
- 跟boot类似,使用汇编直接在软盘下搜索kernel.bin
- 但是,不能把整个kernel.bin放在内存,而是要以ELF文件的格式读取并 提取代码。
- 跳入保护模式
解释 NASM 语言中
[ ]
的作用- 解地址
解释语句 times 510-(\(-\)$) db 0 ,为什么是 510? $ 和 $$ 分别表示什么?
- 填充剩下的空间,使得生成的二进制代码恰好为512字节( dw是两个字节 )
- $表示当前行的偏移地址, $$表示当前段的起始偏移地址,
解释配置文件
bochsrc
文件中如下参数的含义1
2
3
4megs:32
display_library: sdl
floppya: 1_44=a.img, status=inserted
boot: floppy- megs:虚拟机内存大小 (MB)
- display_library:bochs使用的GUI库,在Ubuntu下面是sdl
- floppya:虚拟机外设,软盘为a.img文件
- boot:虚拟机启动方式,从软盘启动
调用约束
64 位 Linux 写一个集成了 C 语言库的程序时,你必须遵循以下的调用约束条件:
- 传递参数时,按照从左到右的顺序,将尽可能多的参数依次保存在寄存器中。存放位置的寄存器顺序是确定的:
- 对于整数和指针,
rdi
,rsi
,rdx
,rcx
,r8
,r9
。 - 对于浮点数(float 和 double 类型),
xmm0
,xmm1
,xmm2
,xmm3
,xmm4
,xmm5
,xmm6
,xmm7
。
- 对于整数和指针,
- 剩下的参数将按照从右到左的顺序压入栈中,并在调用之后 由调用函数推出栈 。
- 等所有的参数传入后,会生成调用指令。所以当被调用函数得到控制权后,返回地址会被保存在
[rsp]
中,第一个局部变量会被保存在[rsp+8]
中,以此类推。 - 栈指针
rsp
在调用前必须进行 16 字节对齐处理 。当然,调用的过程中只会把一个 8 bytes 的返回地址推入栈中,所以当函数得到控制权时,rsp
并没有对齐。你需要向栈中压入数据或者从rsp
减去 8 来使之对齐。 - 调用函数需要保存如下的寄存器:
rbp
,rbx
,r12
,r13
,r14
,r15
。其他的寄存器可以自由使用。 - 被调用函数也需要保存 XMCSR 的控制位和 x87 指令集的控制字,但是 x87 指令在 64 位系统上十分少见所以你不必担心这点。
- 整数返回值存放在
rax
或者rdx:rax
中,浮点数返回值存放在xmm0
或者xmm1:xmm0
中。
Tips
栈是由高地址向低地址增长的.
传递参数时,按照从左到右的顺序,将尽可能多的参数依次保存在寄存器中。存放位置的寄存器顺序是确定的:
- 对于整数和指针,
rdi
,rsi
,rdx
,rcx
,r8
,r9
。 - 对于浮点数(float 和 double 类型),
xmm0
,xmm1
,xmm2
,xmm3
,xmm4
,xmm5
,xmm6
,xmm7
。
- 对于整数和指针,
剩下的参数将按照从右到左的顺序压入栈中,并在调用之后 由调用函数推出栈 。
等所有的参数传入后,会生成调用指令。所以当被调用函数得到控制权后,返回地址会被保存在
[rsp]
中,第一个局部变量会被保存在[rsp-8]
中,以此类推。局部变量的入栈顺序:
在没有溢出保护机制下的编译时,我们可以发现,所有的局部变量入栈的顺序(准确来说是系统为局部变量申请内存中栈空间的顺序)是正向的,即哪个变量先申明哪个变量就先得到空间, 也就是说,编译器给变量空间的申请是直接按照变量申请顺序执行的。
在有溢出保护机制下的编译时,情况有了顺序上的变化,对于每一种类型的变量来说,栈空间申请的顺序都与源代码中相反,即哪个变量在源代码中先出现则后申请空间;而对不同的变量来说,申请的顺序也不同,有例子可以看出,int型总是在char的buf型之后申请,不管源代码中的顺序如何(这应该来源于编译器在进行溢出保护时设下的规定)。
汇编避坑
- 慎用宏定义,它内部无法跳转和定义标签,因此无法封装复杂逻辑 ; 同时,使用时要记住宏定义内部对寄存器的更改
- 可以
push rax
, 并使用al
, 事实上你无法push al
,PUSH 指令只对至少16位的寄存器使用