x86汇编


前言

根据b站up主踌躇月光学习

视频链接

虽然是x86汇编语言,但是仍然需要一些基本的汇编基础,建议(必须)先过一遍王爽老师的汇编语言,再进行这部分的学习,这是部分是写os的前置需求

配置开发环境

wsl图形界面

由于调试需要使用bochs,这是一个图形界面的工具,使用命令行是会报错的,因此需要使用Gui,如果是虚拟机,或者是双系统可以直接跳过。

首先安装wsl,以及ubuntu20,直接根据官方文档安装即可。官译版正经安装WSL,非常适合小白(亲测有效) - 知乎 (zhihu.com)

两种方式,一种是安装ubuntu-desktop桌面,另一种是安装xfce4桌面。这里采用第二种。

首先Windows安装xfce4软件

下载地址

启动!软件

四种方式选一个就行,我这里选One large window

直接下一页

添加-ac

此时点击完成,就会弹出一个黑窗口,先不用管,打开ubuntu

安装xfce

sudo apt update
sudo apt install xfce4-terminal
sudo apt install xfce4

修改环境变量,在最后一行添加

cd ~
vi bashrc
export DISPLAY=IP:0

这里的IP,填写你自己的,在/etc/resolv.conf下查看

例如我的是这样

添加好后,不要忘记刷新环境变量

source .bashrc

在命令行启动

startxfce4

在启动命令等一会,那个黑窗口就有了图形界面


vscode配置

wsl在vsocde中不用配置,安装扩展

打开vscode,在远程连接的地方就可以看到ubuntu了

查看bin文件插件

汇编语言插件

makefile插件


配置bochs

使用Ubuntu22系统配置bochs,这里如果直接sudo apt install bochs-x配置的,是不能正确运行的,错误如下:

image-20231122214538322

解决方法:Stack Overflow

别的版本的Ubuntu应该是可以直接使用apt install安装的,22需要自己手动编译。

下载2.6.11版本的,如果使用2.7版本,大概率在make阶段会遇到SDL.h not found。但是使用2.6.11版本在编译的时候也会错误。所以我转用20版本

在Ubuntu20下完整配置

#安装
sudo apt update
sudo apt install bochs
sudo apt install bochs-x

进入工作空间

bochs
#step 1: 选4 save option to...
#step 2: bochs
#step 3: 选7 Quit now

此时在目录下面生成一个bocsrc的文件

创建一个磁盘

sudo bximage
#依次输入 1 hd flat 512 16 master.img

正确结果

修改bochsrc文件

修改三处

添加测试代码:

安装 nasm 汇编器
sudo apt install nasm

vi hello.asm

mov ax, 0xb800
mov ds, ax; 将代码段设置为 0xb800

mov byte [0], 'H'; 修改屏幕第一个字符为 T

; 下面阻塞停下来
halt:
    jmp halt

times 510 - ($ - $$) db 0 ; 用 0 填充满 510 个字节
db 0x55, 0xaa; 主引导扇区最后两个字节必须是 0x55aa

然后汇编成二进制代码:

nasm -f bin hello.asm -o hello.bin

然后将主引导扇区写入硬盘:

dd if=hello.bin of=master.img bs=512 count=1 conv=notrunc

启动 bochs 测试环境:

bochs -q

成功运行结果

错误

解决方法:
删除目录下的master.img.lock

hello world

代码解释

;将显示模式设置为文本
mov ax,3
int 0x10 

mov ax, 0xb800
mov ds, ax; 将代码段设置为 0xb800

mov byte [0], 'H';
mov byte [2], 'e';
mov byte [4], 'l';
mov byte [6], 'l';
mov byte [8], 'o';
mov byte [10], ',';
mov byte [12], 'w';
mov byte [14], 'o';
mov byte [16], 'r';
mov byte [18], 'l';
mov byte [20], 'd';
mov byte [22], '!';

; 下面阻塞停下来
halt:
    jmp halt ;进入死循环

times 510 - ($ - $$) db 0 ; 用 0 填充满 510 个字节
db 0x55, 0xaa; 主引导扇区最后两个字节必须是 0x55aa

运行结果:


mov ax,3
int 0x10

int 0x10 是一个中断调用,用于在 x86 架构的计算机上调用 BIOS 中断例程。这个特定的中断,0x10,是视频服务的中断向量。

在这个上下文中,mov ax,3int 0x10 一起使用,用于设置显示模式。ax 寄存器的值 3 表示文本模式 80x25 单色/颜色。所以,这两行代码的作用是将显示模式设置为文本模式


mov ax, 0xb800
mov ds, ax; 将代码段设置为 0xb800

首先将代码段设置为0xb800,不过8086的实际物理地址是段寄存器*16 + 地址

也就是 0xb800 * 0x10 + 0 = 物理地址 0xb8000。

使用b 0x7c00,跳转到这份地方

然后点c

代码运行处,s单步运行

这里就可以查看寄存器的情况,点击view,再点击第一个,输入0x7c00,就可以查看当前的内存

这里就可以看到hello world的16进制值

在这片区域中往后翻会找到一块内存中是0x55aa,因为主引导扇区的最后两个字节必须是0x55aa


mov byte [0], 'H';
mov byte [2], 'e';
mov byte [4], 'l';
mov byte [6], 'l';
mov byte [8], 'o';
mov byte [10], ',';
mov byte [12], 'w';
mov byte [14], 'o';
mov byte [16], 'r';
mov byte [18], 'l';
mov byte [20], 'd';
mov byte [22], '!';

在文本模式下,屏幕被视为一个二维字符数组,每个字符占用2个字节的空间。第一个字节用于表示字符本身,第二个字节用于表示字符的属性,如颜色和亮度等。

因此,当在屏幕上写入字符时,需要跳过属性字节,只修改字符字节。

例如,mov byte [0], 'H' 将字符 ‘H’ 写入屏幕的第一个位置,mov byte [2], 'e' 将字符 ‘e’ 写入屏幕的第二个位置,依此类推。每个字符的地址都是前一个地址加2,因为每个字符占用2个字节的空间。


times 510 - ($ - $$) db 0 ; 用 0 填充满 510 个字节
db 0x55, 0xaa; 主引导扇区最后两个字节必须是 0x55aa

是什么主引导扇区

主引导扇区(Master Boot Record,MBR)是硬盘的第一个扇区。它包含了一个小的启动程序(引导加载器)和分区表。当计算机启动时,BIOS会加载并执行主引导扇区的内容,从而开始启动操作系统。主引导扇区的大小是512个字节。其中,最后两个字节必须是0x55AA,这是一个标准的引导扇区签名,用于标识这个扇区是一个有效的主引导扇区。

db 0x55, 0xaa就是在设置这个签名,而times 510 - ($ - $$) db 0则是在填充剩余的空间,直到达到510个字节,以确保主引导扇区的总大小为512个字节。

$表示当前地址,$$表示当前段的开始地址。times是一个汇编指令,它的作用是重复执行后面的指令指定的次数。

times 510 - ($ - $$) db 0的含义就是:计算当前地址$与当前段的开始地址$$之间的差值,然后从510中减去这个差值,得到的结果就是需要填充的字节数。db 0表示填充的字节值为0。

所以当我们查看0x7c00的时候中间的代码都是0,由于主引导扇区的大小是512,所以减去末尾的55aa,就剩下510个字节需要填充

0x7c00

这个地址在上面反复出现,那么这个代表什么意思。

0x7c00就是MAR加载区域,加载到内存中的默认地址。当计算机启动并执行 POST(Power-On Self Test)后,BIOS 会从启动设备(如硬盘)的第一个扇区(即主引导扇区)读取 512 个字节的数据,并将这些数据加载到物理内存地址 0x7C00 处,然后跳转到这个地址开始执行。

也就是这个区域就是你的代码所在的地址

还有别的一些默认地址

起始地址 结束地址 大小 用途
0x000 0x3ff 1kb 中断向量表
0x400 0x4ff 256b BIOS数据区
0x500 0x7bff 29.75kb 可用区域
0x7c00 0x7dff 512b MBR 加载区域
0x7e00 0x9fbff 607.6kb 可用区域
0x9fc00 0x9ffff 1kb 扩展BIOS数据区
0xa0000 0xaffff 64kb 彩色显示适配器
0xb0000 0xb7fff 32kb 黑白显示适配器
0xb8000 0xbffff 32kb 文本显示适配器
0xc0000 0xc7fff 32kb 显示适配器 BIOS
0xc8000 0xeffff 160kb 映射内存
0xf0000 0xfffef 64kb-16b 系统BIOS
0xffff0 0xfffff 16b 系统BIOS入口地址

基础

首先将上面所有的命令都编写到一个makefile文件中。

# 编译boot.asm
boot.bin: boot.asm
	nasm boot.asm -o boot.bin

# 写入到虚拟硬盘
master.img: boot.bin
	dd if=boot.bin of=master.img bs=512 count=1 conv=notrunc

#删除生成的文件,和img.lock文件,不清楚这个文件为什么会出现,但是一出现这个,bochs就会报错,所以要删掉
.PHONY: clean # 伪目标,不生成文件,只是执行命令,不管有没有clean文件,都执行命令
clean:
	rm -f *.bin *.img.lock

# 运行bochs
.PHONY: bochs
bochs:
	bochs -q

然后再编写一个shell,添加内容

make clean
make boot.bin
make master.img
make bochs

这样不需要繁琐的运行了,直接运行这个shell脚本就可以了

工作空间

8086的寄存器

通用寄存器

寄存器 描述
AX 累加结果数据
BX 数据段数据指针
CX 字符串和循环计数器
DX I/O 指针
DI 目的数据指针
SI 源数据指针
SP 栈指针
BP 栈数据指针

前四位可以分成高八位和第八位

寄存器 高八位 低八位
AX AH AL
BX BH BL
CX CH CL
DX DH DL

段寄存器

段寄存器 描述
CS 代码段寄存器
DS 数据段寄存器
SS 栈段寄存器
ES 额外的寄存器

IP和FLAG/PSW

标志寄存器
标志 英文 描述
0 CF Carry 进位标志
1
2 PF Parity 奇偶标志
3
4 AF Auxiliary 辅助进位标志
5
6 ZF Zero 零标志
7 SF Sign 符号标志
8 TF Trap 陷阱标志
9 IF Interrupt 中断允许标志
10 DF Direction 方向标志
11 OF Overflow 溢出标志

示例:

加之前

标志位是82

加之后

标志位是13

  • 82: 1000_0010

  • 13: 0001_0011

这里的第零位从0变为了1

数据类型

  • db 字节
  • dw 字
  • dd 双字

一个字是两个字节

新建一个type.asm,编译

touch type.asm
make tpye.bin

看一下两个数据类型,添加两行代码

编译成功后查看二进制文件

成功的显示出了我们定义的数据

我们本来存的是aa55,但是在内存中显示是55 aa,在8086的设计中使用的都是小端存储。这种设计延续至今,英特尔的CPU都是小端存储

使用数据类型,hello,world:

org 0x7c00 ;代码的起始位置

mov ax,3
int 0x10;清空屏幕

mov ax,0xb800
mov es,ax ;将es赋值0xb800

mov ax,0
mov ds,ax ;ds赋值0

mov si,message ;把message的地址赋给si寄存器
mov di,0;把0赋给di寄存器,di也是一个索引寄存器,它可以用来指定一个字符串的目的地址,这里就是把di寄存器的值设置为0,也就是显存的起始地址。
mov cx,(message_end-message);把(message_end-message)的值赋给cx寄存器,cx是一个计数寄存器

loop1:
    mov al,[ds:si] ;[ds:si]的值赋给al寄存器,al是一个8位的寄存器,它可以用来存储一个字符,[ds:si]是一个内存地址,它表示从ds寄存器的值开始,加上si寄存器的值的偏移量,得到的内存地址,这里就是把[ds:si]的值赋给al寄存器,也就是把消息的第一个字符赋给al寄存器。
    mov [es:di],al;这一行是用来把al寄存器的值赋给[es:di],[es:di]也是一个内存地址,它表示从es寄存器的值开始,加上di寄存器的值的偏移量,得到的内存地址,这里就是把al寄存器的值赋给[es:di],也就是把消息的第一个字符写入显存的第一个位置。

    inc si;si取出下一个字符
    add di,2;字符显示偶数,所以继续+2

    loop loop1

halt:
    jmp halt

message:
    db "hello,world!!",0;0,表示字符串的结束。
message_end:

times 510 -($-$$) db 0
db 0x55,0xaa

运行结果

这里可以跳转到0x7c00单步调试查看运行的结果

寻址方式

物理地址

物理地址 = 段地址*16 + 偏移地址,为什么不能直接使用物理地址?

8086是16位的cpu,也就是2的十六次方,可以访问64kb

但是8086最多可以访问1M的内存,也就是2的20次方

如何使用16位访问20位的地址,比一个形象的例子

此时从家到图书馆的物理地址是2326,也可以表示为2000+326

相对的如果你只有一个3个格子表示数字,但是表示2326需要四个格子,那么是不是可以使用200*10+326来表示物理地址。

在cpu中偏移地址就相当于326,段地址就是2000,那么表示物理地址就是那个公式,为什么*16?

这就可以看作是段地址 << 4,那么这样就是20位的地址再加上偏移地址就是实际的地址

3.汇编指令:【寻址方式】立即数寻址、寄存器寻址、存储器寻址

算数运算指令

加减法

org 0x7c00

mov ax,3
int 0x10

add word [number],5 ;加法运算
mov ax,5
mov bx,6
sub bx,ax; bx - ax 减法运算
halt:
    jmp halt

number:
    dw 0x3456

times 510 -($-$$) db 0
db 0x55,0xaa

乘法

乘法由于一个寄存器可能存不下,因此如果一个寄存器存不下的就会存入dx之中

org 0x7c00
mov ax,3
int 0x10
mov ax,6
mov bx,5
mul bx;dx:ax = ax * bx,这里是mul后面接的乘数,而不是ax
halt:
    jmp halt

times 510 -($-$$) db 0
db 0x55,0xaa

dx是0,ax是30

除法

就是ax除以操作数 = 商存在ax,余数存在dx

org 0x7c00
mov ax,3
int 0x10
mov ax,6
mov bx,5
mul bx;dx:ax = ax * bx,这里是mul后面接的乘数,而不是ax
mov bx,4
div bx; ax: 7 bx: 2 | 30/4=7...2
halt:
    jmp halt

times 510 -($-$$) db 0
db 0x55,0xaa

与预期结果一致

逻辑运算指令

  • and 与 —-> 还有一个test 这两个的关系和sub cmp一样,只做运算不更新数据更新标志位
  • or 或
  • not 非
  • xor 异或
  • SHL/SHR 移位
  • ROL/ROR 循环移位
  • RCL/RCR 带进位的循环移位

转移指令

8086 cs:ip 下一条指令的地址,物理地址 = cs << 4 + ip

org 0x7c00
mov ax,3
int 0x10

mov ax,0
mov cx,100

start:
    add ax,cx
    sub cx,1   
    jz end ; 如果cx == 0 就跳转到end
    jmp start
end:
halt:
    jmp halt

times 510 -($-$$) db 0
db 0x55,0xaa

100累加计算结果

这是使用jmp实现的累加,但是如果是使用loop

start:
	add ax,cx
	loop start

这样就不需要自己动手cx-1,也不需要判断cx为0

堆栈和函数

栈顶指针:ss:sp

org 0x7c00
mov ax,3
int 0x10
mov ax,0
mov ss,ax
mov sp,0x7c00
push byte 4
push dword 7
push word 5
pop ax
pop bx
pop cx
pop dx
halt:
    jmp halt
times 510 -($-$$) db 0
db 0x55,0xaa

进栈的顺序是4,0,7,5 ——> 这个0是dword的,双字。

因此得到的ax = 5 | bx = 7 | cx = 0 | dx = 4

这个过程中栈指针的变化:

7c00 –> 7bfe – >7bfa –> 7bf8 –> 7bfa –>7bfc –>7bfe –> 7bc00

可以看到在入栈的过程在,栈指针是减少的,在8086中栈的从高位到地位的,当push dword的时候,栈指针是-4的,word和byte是-2的。

函数

函数调用的时候就会创建一个栈帧,这是C语言里面学过的,在汇编中可以明显的看到这一个过程

org 0x7c00
mov ax,3
int 0x10

mov cx,25
loop1:
    call print
    loop loop1
halt:
    jmp halt
video:
    dw 0x0
print:
	push ax
	push es
	push bx
    
    mov ax,0xb800
    mov es,ax
    mov bx,[video]
    mov byte [es:bx],'|'
    add word [video],2
    
    pop bx
    pop es
    pop ax
    ret
times 510 -($-$$) db 0
db 0x55,0xaa

运行结果

在调用print函数的时候,会先将loop loop1这条指令的地址入栈,然后再跳转到print处,因此call print这条命令的作用其实等价于

push ip ;汇编中是没有这个ip的,不过意思是这个意思
jmp print

同时在print中使用了三个寄存器,ax,bx,es,为了不影响其他函数的正常运行就需要先入栈寄存器的状态,然后在函数结束之前,再还原寄存器,这也就是函数的栈帧

输入输出

org 0x7c00
mov ax,3
int 0x10
mov ax,0xb800
mov es,ax
mov si,message
mov di,0
mov cx,message_end-message
print:
    mov bl,[si]
    mov [es:di],bl
    inc si
    add di,2
    loop print
halt:
    ;hlt ;关闭CPU,等待外中断的到来
    jmp halt
message:
    db "hello world",0
message_end:
times 510 -($-$$) db 0
db 0x55,0xaa

这段hello world的代码还有一些问题,就是窗口闪动的光标一直在第一个位置,并没有移动到后面

为了读取这个光标的位置,就需要输入输出指令来控制硬件(外围设备)

  • 显示器
  • 键盘
  • 鼠标

两种方式:端口 映射内存

英特尔的端口是在0 - 65535

CRT(地址端口):0x3D4 CRT(数据端口):0x3D5

这里主要就涉及了in 和 out两个指令,不过学过8255A的话应该也比较好理解

org 0x7c00
CRT_ADDR_REG equ 0x3D4
CRT_DATA_REG equ 0x3D5
CRT_CURSOR_H equ 0x0E ;光标的高八位
CRT_CURSOR_L equ 0x0F ;光标的低八位

mov ax,3
int 0x10
mov ax,0
mov ds,ax
mov ss,ax
mov sp,0x7c00
mov ax,0xb800
mov es,ax
mov si, message
print:
    call get_cursor
    mov di,ax
    shl di,1

    mov bl,[si]
    cmp bl,0
    jz print_end

    mov [es:di],bl

    inc si
    inc ax
    call set_cursor
    jmp print
print_end:
halt:
    hlt
    jmp halt
get_cursor:
    push dx

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_H
    out dx,al

    mov dx,CRT_DATA_REG
    in al,dx
    shl ax,8

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_L
    out dx,al

    mov dx,CRT_DATA_REG
    in al,dx
    
    pop dx
    ret
set_cursor:
    push dx
    push bx
    mov bx,ax

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_L
    out dx,al

    mov dx,CRT_DATA_REG
    mov al,bl
    out dx,al

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_H
    out dx,al

    mov dx,CRT_DATA_REG
    mov al,bh
    out dx,al

    pop bx
    pop dx

    ret
message:
    db 'Hello, World!',0
times 510 -($-$$) db 0
db 0x55,0xaa

光标正确的移动了

字符样式

偶数位是控制的文本,奇数位就是文本的字符样式

  • 高四位表示背景色 |K|R|G|B|
  • 第四位表示前景色 |I |R|G|B|

K = 0 背景不闪烁 =1 闪烁

I = 0 浅色 =1 深色

org 0x7c00
CRT_ADDR_REG equ 0x3D4
CRT_DATA_REG equ 0x3D5
CRT_CURSOR_H equ 0x0E ;光标的高八位
CRT_CURSOR_L equ 0x0F ;光标的低八位

mov ax,3
int 0x10
mov ax,0
mov ds,ax
mov ss,ax
mov sp,0x7c00
mov ax,0xb800
mov es,ax
mov si, message
print:
    call get_cursor
    mov di,ax
    shl di,1

    mov bl,[si]
    cmp bl,0
    jz print_end

    mov [es:di],bl
	mov byte [es:di+1], 0b0000_0011 ;设置样式
	
    inc si
    inc ax
    call set_cursor
    jmp print
print_end:
halt:
    hlt
    jmp halt
get_cursor:
    push dx

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_H
    out dx,al

    mov dx,CRT_DATA_REG
    in al,dx
    shl ax,8

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_L
    out dx,al

    mov dx,CRT_DATA_REG
    in al,dx
    
    pop dx
    ret
set_cursor:
    push dx
    push bx
    mov bx,ax

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_L
    out dx,al

    mov dx,CRT_DATA_REG
    mov al,bl
    out dx,al

    mov dx,CRT_ADDR_REG
    mov al,CRT_CURSOR_H
    out dx,al

    mov dx,CRT_DATA_REG
    mov al,bh
    out dx,al

    pop bx
    pop dx

    ret
message:
    db 'Hello, World!',0
times 510 -($-$$) db 0
db 0x55,0xaa

中断

内中断和异常

远调用:如果函数的调用在寄存器之外就需要远调用

普通调用就只需要保存下一条指令的IP就可以了,但是远调用不仅仅需要需要保存,还需要保存寄存器。

call func –> push IP jmp func

call far func –> push cs push ip jmp func

org 0x7c00
mov ax,3
int 0x10

mov cx,25
loop1:
    call 0:print ;callf 
    loop loop1
halt:
    jmp halt
video:
    dw 0x0
print:
    push ax
    push es
    push bx
    mov ax,0xb800
    mov es,ax
    mov bx,[video]
    mov byte [es:bx],'|'
    add word [video],2
    pop bx
    pop es
    pop ax
    retf ;远调用是需要使用retf
times 510 -($-$$) db 0
db 0x55,0xaa

这种是直接跳转还有一种间接跳转的方式

org 0x7c00
mov ax,3
int 0x10

mov ax,0
mov dx,ax
mov cx,25
loop1:
    ;call 0:print ;callf 
    call far [func]
    loop loop1
halt:
    jmp halt
video:
    dw 0x0
print:
    push ax
    push es
    push bx
    mov ax,0xb800
    mov es,ax
    mov bx,[video]
    mov byte [es:bx],'|'
    add word [video],2
    pop bx
    pop es
    pop ax
    retf ;远调用是需要使用retf
func:
	dw print,0
times 510 -($-$$) db 0
db 0x55,0xaa

内中断表的起始位置是0x000,结束位置是0x3ff,也就是0x400

一个中断向量占4个字节,一个是段地址,一个偏移地址,0x400 / 4 = 0x100 也就是一共有256个中断向量

中断号就是0 - 255号

0 号是除法异常,内存就是从0到3

0x80是linux的系统调用,不过8086中是没有Linux的,从386开始才有,不过这里就用与代表软中断

0x80的向量地址就是0x80 * 4 + 3

在使用内中断之前要是注册中断函数

org 0x7c00
mov ax,3
int 0x10

;mov word [0x80 * 4], print
;mov word [0x80 * 4 + 2], 0

mov word [0x0 * 4], div_error ;根据中断号找到入口程序
mov word [0x0 * 4 + 2], 0
mov dx,0
mov ax,0
mov bx,0
div bx
halt:
    jmp halt
video:
    dw 0x0
print:
    push ax
    push es
    push bx
    mov ax,0xb800
    mov es,ax
    mov bx,[video]
    mov byte [es:bx],'|'
    add word [video],2
    pop bx
    pop es
    pop ax
    ;ret
    ;retf
    iret 
div_error:
    push ax
    push es
    push bx
    mov ax,0xb800
    mov es,ax
    mov ax,0
    mov ds,ax 
    mov si,message
    mov di,0;
    mov cx,(message_end-message);
    loop1:
        mov al,[ds:si];
        mov [es:di],al;
        inc si;
        add di,2;
        loop loop1
    pop bx
    pop es
    pop ax
    ;ret
    ;retf
    iret 
message:
    db "div is error",0
message_end:
times 510 -($-$$) db 0
db 0x55,0xaa

这段代码在运行的时候div的除数的0,这就触发中断函数,从而调用div_error,在屏幕中打印div is error

并且中断调用不仅仅会保存下一个指令的IP,CS,还有保存标志寄存器

这里段代码使用的函数返回是iret,到这里已经使用过3中ret

ret:就只返回IP 普通函数调用

retf:返回IP还有CS 远调用

iret:返回IP,CS以及标志寄存位 中断调用

外中断和时钟

8259 - 可编程中断控制器 / PIC Programmable Interrupt Controller

  • 主芯片 - 8 1
  • 从芯片 - 8
  • 级联方式

中断向量表:

向量 功能
0 除法溢出
1 单步 (用于调试)
2 非屏蔽中断 NMI
3 断点 (用于调试)
4 溢出中断
5 打印屏幕
6-7 保留
8 时钟
9 键盘
A 保留
B 串行通信COM2
C 串行通信COM1
D 保留
E 软盘控制器
F 并行打印机
端口 说明 标记
0x20 主 PIC 命令端口 PIC_M_CMD
0x21 主 PIC 数据端口 PIC_M_DATA
0xA0 从 PIC 命令端口 PIC_S_CMD
0xA1 从 PIC 数据端口 PIC_S_DATA
  • ICW1 ~ ICW4 用于初始化 8259 initialization Command Words
  • OCW1 ~ OCW3 用于操作 8259 Operation Commands Words

这里以处理时钟的中断为例

  • 向 OCW1 写入屏蔽字,打开时钟中断
  • sti 设置 CPU 允许外中断
  • 向 OCW2 写入 0x20, 表示中断处理完毕
  • IF 是 CPU 的外中断开关
  • MASK 中断屏蔽开关 是 8259 总开关
org 0x7c00
PIC_M_CMD equ 0x20
PIC_M_DATA equ 0x21
mov ax,3
int 0x10
mov ax,0
mov ds,ax
mov ss,ax
mov sp,0x7c00
;注册中断函数
mov word [8*4],clock
mov word [8*4+2],0
mov al,0b1111_1110
out PIC_M_DATA,al
sti; IF = 1  set interrupt CPU允许中断
;cli         clear interrupt
;clc         clear carry
loopa:
    mov bx,3
    mov al,'A'
    call blink
    jmp loopa
clock:
    push bx
    push ax
    mov bx,4
    mov al,'C'
    call blink
    mov al,0x20
    out PIC_M_CMD,al
    pop ax
    pop bx
    iret
blink:
    push es 
    push bx
    mov dx,0xb800
    mov es,dx

    shl bx,1
    mov dl,[es:bx]
    cmp dl,' '
    jnz .set_space
    .set_char:
        mov [es:bx],al
        jmp .done
    .set_space:
        mov byte [es:bx], ' '
    .done:
        shr bx,1
    pop bx
    pop es
    ret
times 510 -($-$$) db 0
db 0x55,0xaa

这段代码a和c会在3,4的位置闪烁,在调用clock函数的时候会的会将三个值压入栈中

push ip
push cs
push flags

为什么要保存flags,在调用之前flags是0x206,调用之后是0x006
0x206: 0b0010_0000_0110
0x006: 0b0000_0000_0110
第9位由1置为了0
第9位
由于是已经调用了中断,因此需要将这个标志位置为0,防止CPU误以为可以中断,导致中断嵌套


代码整理

新建几个文件夹,将对应的文件放入其中
修改makefile文件

# 编译boot.asm
build/%.bin: src/%.asm
	nasm $< -o $@

# 写入到虚拟硬盘
build/master.img: build/boot.bin
	dd if=build/boot.bin of=build/master.img bs=512 count=1 conv=notrunc

#删除生成的文件,和img.lock文件,不清楚这个文件为什么会出现,但是一出现这个,bochs就会报错,所以要删掉
.PHONY: clean # 伪目标,不生成文件,只是执行命令,不管有没有clean文件,都执行命令
clean:
	rm -rf build/bx_enh_dbg.ini
	rm -f build/*.bin build/*.img.lock

# 运行bochs
.PHONY: bochs
bochs: build/master.img
	cd build && bochs -q

修改run.sh文件

#!/bin/bash
set -e

make build/boot.bin || { echo "make boot.bin错误!"; exit 1; }
make build/master.img || { echo "make master.img错误!"; exit 1; }
make bochs || { echo "make bochs错误"; exit 1; }

PC启动

flowchart TD
    CPU加电自检 -->进入BIOS --> 主引导扇区0x7c00 --> 读取loader --> 进入Loader --> 检测内存 --> 准备保护模式 --> 进入保护模式 --> 内存映射 --> 加载内核

硬盘读写

pc的状态

flowchart LR;
    加电-->BOIS执行-->主引导扇区(读到0x7c00)-->跳转到0x7c00执行

主引导扇区只有512个字节,而且并不是所有的都可以用,这里512个字节还包括了硬盘的分区
如果程序大于512就无法使用,就必须从内存中读取在继续执行

bochs硬盘存储结构

Primary 通道 Secondary 通道 in 操作 out 操作
Command Block Registers
0x1F0 0x170 Data Data
0x1F1 0x171 Error Features
0x1F2 0x172 Sector count Sector count
0x1F3 0x173 LBA low LBA low
0x1F4 0x174 LBA mid LBA mid
0x1F5 0x175 LBA high LBA high
0x1F6 0x176 Device Device
0x1F7 0x177 Status Command

  • 0x1F0 / 16bit 读写数据

  • 0x1F1

  • 0x1F2 / 扇区数量

  • 0x1F3 - 0x1F5 / 起始扇区的前 24 位 0 ~ 23 位

  • 0x1F6

    • 0 - 3 / LBA 24 ~ 27 位
    • 4:0 主盘,1 从盘
    • 6:0 CHS 模式 , 1 LBA
    • 5 7 : 固定为 1
  • 0x1F7: out

    • 0xEC:识别硬盘
    • 0x20:读硬盘
    • 0x30:写硬盘

    in

    • 0 ERR
    • 3 DRQ 数据准备完毕
    • 7 BUSY 硬盘繁忙
org 0x7c00 ; 0x7c00是BIOS将要加载的地址
mov ax,3   ; 设置显存模式
int 0x10   ; 调用BIOS中断

mov ax,0   ; 设置段寄存器  
mov ds,ax  ; 设置ds寄存器
mov ss,ax  ; 设置ss寄存器
mov sp,0x7c00 ; 设置栈指针
mov dx,0x1f2  ; 设置硬盘扇区数
mov al,1   ; 设置读取扇区数
out dx,al  ; 写入端口,读取一个扇区
mov al,0   ; 设置读取扇区的低8位
inc dx; 0x1f3 ;0x1f3是读取扇区的低8位
out dx,al  ; 写入端口
inc dx; 0x1f4 ;0x1f4是读取扇区的中8位
out dx,al   ; 写入端口
inc dx; 0x1f5 ;0x1f5是读取扇区的高8位
out dx,al   ; 写入端口
inc dx;     ; 将dx指向0x1f6
mov al,0b1110_0000 ; LBA模式
out dx,al   ; 写入端口
inc dx;0x1f7 
mov al,0x20 ;读硬盘
out dx,al   ; 写入端口
.check_read_state: ;然后开始读取状态
    nop
    nop 
    nop ;加延迟 ATA的要求

    in al,dx ;读取端口 dx --> 1f7
    and al,0b1000_1000 ; 读取状态,判断是否读取完成
    cmp al,0b0000_1000 ; 硬盘是否读取完成,如果没有完成,则继续读取
    jnz .check_read_state ;
mov ax,0x100 ;要读取的内存地址
mov es,ax    ;设置段寄存器
mov di,0     
mov dx,0x1f0 ;1f0是读写的端口

read_loop: ;循环读取
    nop
    nop
    nop

    in ax,dx ;读取端口 dx --> 1f0
    mov [es:di],ax ;将读取的数据写入内存
    add di,2    ;di指针加2
    cmp di,512  ;判断是否读取完成
    jnz read_loop ;如果没有读取完成,则继续读取

;---------------------------------------
mov dx,0x1f2
mov al,1    
out dx,al
mov al,2    ;需要写入的扇区
inc dx; 0x1f3
out dx,al
mov al,0 
inc dx; 0x1f4
out dx,al
inc dx; 0x1f5
out dx,al
inc dx;
mov al,0b1110_0000 ; LBA模式
out dx,al
inc dx;0x1f7
mov al,0x30 ;写硬盘
out dx,al

mov ax,0x100
mov es,ax
mov di,0
mov dx,0x1f0

write_loop:
    nop
    nop
    nop
    mov ax,[es:di];读取内存数据
    out dx,ax     ;写入端口
    add di,2      ;di指针加2
    cmp di,512    ;判断是否写入完成
    jnz write_loop 
mov dx,0x1f7 
.check_write_state:
    nop
    nop
    nop ;加延迟 ATA的要求

    in al,dx
    and al,0b1000_0000 ;
    cmp al,0b1000_0000 ;检查硬盘是否繁忙
    jz .check_write_state
jmp $
times 510 -($-$$) db 0
db 0x55,0xaa

将0x1000的磁盘写到0x400

内核加载器

PC 在进入主引导扇区后,会读取loader,加载操作系统。在主引导扇区只有446个可用字节,64个字节是硬盘分区表,不能直接在主引导扇区加载操作系统,boot(MBR)的主要作用就是加载loader

编写读取硬盘的函数

org 0x7c00
mov ax,3
int 0x10

mov ax,0;
mov ds,ax  
mov ss,ax
mov sp,0x7c00

mov edi,0x1000
mov ecx,0
mov bl,1
call read_disk
jmp $
read_disk:
    pushad ;将a,b,c,d,si,di,sp,bp压栈
    ;读取硬盘
    ; edi --> 存储内存位置
    ; edi是di的扩展,就是32位的di
    ; ecx --> 读取的扇区位置
    ; bl  --> 读取扇区的数量

    mov dx,0x1f2
    mov al,bl
    out dx,al ; 扇区数量

    mov al,cl ;扇区位置低8位
    inc dx; 0x1f3
    out dx,al  ;扇区位置低8位
    
    shr ecx,8  ;ecx右移8位。扇区中间8位位置就到了低8位
    inc dx; 0x1f4
    out dx,al  ;中间8位
    
    shr ecx,8  ;ecx右移8位。高8位就到了低8位
    inc dx; 0x1f5
    out dx,al
    
    shr ecx,8
    and cl,0b0000_1111 ;只取低4位
    inc dx; 0x1f6
    mov al,0b1110_0000 ;
    or al,cl 
    out dx,al
    
    inc dx;0x1f7
    mov al,0x20 ;读硬盘
    out dx,al
    .check_read_state:
        nop
        nop
        nop ;加延迟 ATA的要求

        in al,dx
        and al,0b1000_1000
        cmp al,0b0000_1000
        jnz .check_read_state
    
    xor eax,eax
    mov al,bl
    mov dx,256
    mul dx; ax = bl * 256

    mov dx,0x1f0
    mov cx,ax
    .read_loop:
        nop
        nop
        nop
        in ax,dx
        mov [edi],ax
        add di,2
        loop .read_loop
    popad
    ret

jmp $
times 510 -($-$$) db 0
db 0x55,0xaa

成功读取到0x1000

写一个loader

[org 0x1000]

mov ax,0XB800
mov es,ax
mov byte [es:0],"L"
jmp $

修改boot

org 0x7c00
mov ax,3
int 0x10

mov ax,0;
mov ds,ax  
mov ss,ax
mov sp,0x7c00

mov edi,0x1000
mov ecx,2
mov bl,4
call read_disk
jmp 0:0x1000 ;跳转到0x1000处执行
read_disk:
    pushad ;将a,b,c,d,si,di,sp,bp压栈
    ;读取硬盘
    ; edi --> 存储内存位置
    ; edi是di的扩展,就是32位的di
    ; ecx --> 读取的扇区位置
    ; bl  --> 读取扇区的数量

    mov dx,0x1f2
    mov al,bl
    out dx,al ; 扇区数量

    mov al,cl ;扇区位置低8位
    inc dx; 0x1f3
    out dx,al  ;扇区位置低8位
    
    shr ecx,8  ;ecx右移8位。扇区中间8位位置就到了低8位
    inc dx; 0x1f4
    mov al,cl
    out dx,al  ;中间8位
    
    shr ecx,8  ;ecx右移8位。高8位就到了低8位
    inc dx; 0x1f5
    mov al,cl
    out dx,al
    
    shr ecx,8
    and cl,0b0000_1111 ;只取低4位
    inc dx; 0x1f6
    mov al,0b1110_0000 ;
    or al,cl 
    out dx,al
    
    inc dx;0x1f7
    mov al,0x20 ;读硬盘
    out dx,al
    .check_read_state:
        nop
        nop
        nop ;加延迟 ATA的要求

        in al,dx
        and al,0b1000_1000
        cmp al,0b0000_1000
        jnz .check_read_state
    
    xor eax,eax
    mov al,bl
    mov dx,256
    mul dx; ax = bl * 256

    mov dx,0x1f0
    mov cx,ax
    .read_loop:
        nop
        nop
        nop
        in ax,dx
        mov [edi],ax
        add di,2
        loop .read_loop
    popad
    ret

jmp $
times 510 -($-$$) db 0
db 0x55,0xaa

此时需要写入硬盘的文件发生变化,修改Makeflie文件

# 编译boot.asm
build/%.bin: src/%.asm
	nasm $< -o $@

# 写入到虚拟硬盘
build/master.img: build/boot.bin build/loader.bin
	dd if=build/boot.bin of=build/master.img bs=512 count=1 conv=notrunc
	dd if=build/loader.bin of=build/master.img bs=512 count=4 seek=2 conv=notrunc # seek=2表示从第二个扇区开始写入
#删除生成的文件,和img.lock文件,不清楚这个文件为什么会出现,但是一出现这个,bochs就会报错,所以要删掉
.PHONY: clean # 伪目标,不生成文件,只是执行命令,不管有没有clean文件,都执行命令
clean:
	rm -rf build/bx_enh_dbg.ini
	rm -f build/*.bin build/*.img.lock

# 运行bochs
.PHONY: bochs
bochs: build/master.img
	cd build && bochs -q

运行程序,0x1000写入了loader程序

loader.bin文件
0x1000内存

此时bochs成功显示字符”L”

32位架构

  • 实模式 – 8086
  • 保护模式 – 386
    保护模式对内存进行了保护,80286的时候就有了16位了保护模式,1985年的时候80386开始就有了32位的保护模式,1989年inter发布80486引入了内存分页的机制

32位架构的寄存器
8086的寄存器是a/b/c/dx sp/bp/si/di
32位的寄存器是ea/eb/ec/ed …

  • IP –> EIP
  • flags –> eflags
  • cs/ds/ss/es
  • 多出来 fs/gs等
    还有一些寄存器

寻址方式

8086的寻址方式: 偏移地址+bx/bp + si/di
80386的寻址方式:ERX + ERX * (1/2/4/8) + 偏移地址

写一个32位的程序

[bits 32]
section .text
global main
main:
    mov eax, 4 ; sys_write
    mov ebx, 1 ; stdout
    mov ecx, msg ; msg
    mov edx, msglen ; msglen
    int 80h ; syscall
    mov eax, 1 ; sys_exit
    mov ebx, 0 ; exit code
    int 80h

section .data
    msg db "Hello, World!", 10,13,0
    msglen equ $-msg

这是一个heLlo world的汇编
运行:


nasm -f elf32 hello.asm
gcc -m32 hello.o -o hello
这运行第二条命令可能会报错,报错如下:

/usr/bin/ld: cannot find Scrt1.o: No such file or directory
/usr/bin/ld: cannot find crti.o: No such file or directory
/usr/bin/ld: skipping incompatible /usr/lib/gcc/x86_64-linux-gnu/9/libgcc.a when searching for -lgcc
/usr/bin/ld: cannot find -lgcc
/usr/bin/ld: skipping incompatible /usr/lib/x86_64-linux-gnu/libgcc_s.so.1 when searching for libgcc_s.so.1
/usr/bin/ld: cannot find libgcc_s.so.1
/usr/bin/ld: skipping incompatible /usr/lib/gcc/x86_64-linux-gnu/9/libgcc.a when searching for -lgcc
/usr/bin/ld: cannot find -lgcc
collect2: error: ld returned 1 exit status

报错提示没有找到Scrt1.0,但是我这里是已经安装了的,因此是由于我的电脑没有安装32位的运行环境执行sudo apt-get install gcc-multilib安装32位运行环境即可


32位与64位兼容的问题
在csapp中看到一段话:使用int类型保存指针在32位是被允许的但是在64位机器就会报错,一开始没理解什么意思,了解到在32位机中指针的空间大小是4字节而64位是8位

#include <stdio.h>

int main() {
    int *p;
    int t;
    printf("int *P size is %ld\n",sizeof(p));
    printf("int  t size is %ld\n",sizeof(t));
    return 0;
}

两种机器指针大小

内存检测

如果需要读取多个硬盘,就要检测当前数据有没有准备好,之前的代码一次检测准备好只能读一个扇区,也就是读第二个扇区就要重新等待,导致数据不对。

BIOS 0x15 0xe820

Address Range Descriptor Structure ARDS

字节偏移量 属性名称 描述
0 BaseAddrLow 基地址的低 32 位
4 BaseAddrHigh 基地址的高 32 位
8 LengthLow 内存长度的低 32 位,以字节为单位
12 LengthHigh 内存长度的高 32 位,以字节为单位
16 Type 本段内存的类型

Type 字段

Type 值 名称 描述
1 AddressRangeMemory 这段内存可以被操作系统使用
2 AddressRangeReserved 内存使用中或者被系统保留,操作系统不可以用此内存
其他 未定义 未定义,将来会用到.目前保留. 但是需要操作系统一样将其视为ARR(AddressRangeReserved)

调用前输入

寄存器或状态位 参数用途
EAX 子功能号: EAX 寄存器用来指定子功能号,此处输入为 0xE820
EBX 内存信息需要按类型分多次返回,由于每次执行一次中断都只返回一种类型内存的ARDS 结构,所以要记录下一个待返回的内存ARDS,在下一次中断调用时通过此值告诉 BIOS 该返回哪个 ARDS,这就是后续值的作用。第一次调用时一定要置为0,EBX 具体值我们不用关注,字取决于具体 BIOS 的实现,每次中断返回后,BIOS 会更新此值
ES: DI ARDS 缓冲区:BIOS 将获取到的内存信息写入此寄存器指向的内存,每次都以 ARDS 格式返回
ECX ARDS 结构的字节大小:用来指示 BIOS 写入的字节数。调用者和 BIOS 都同时支持的大小是 20 字节,将来也许会扩展此结构
EDX 固定为签名标记 0x534d4150,此十六进制数字是字符串 SMAP 的ASCII 码: BIOS 将调用者正在请求的内存信息写入 ES: DI 寄存器所指向的ARDS 缓冲区后,再用此签名校验其中的信息

返回值

寄存器或状态位 参数用途
CF 位 若CF 位为 0 表示调用未出错,CF 为1,表示调用出错
EAX 字符串 SMAP 的 ASCII 码 0x534d4150
ES:DI ARDS 缓冲区地址,同输入值是一样的,返回时此结构中己经被BIOS 填充了内存信息
ECX BIOS 写入到 ES:DI 所指向的 ARDS 结构中的字节数,BIOS 最小写入 20 字节
EBX 后续值:下一个 ARDS 的位置。每次中断返回后,BIOS 会更新此值, BIOS 通过此值可以找到下一个待返回的 ARDS 结构,咱们不需要改变 EBX 的值,下一次中断调用时还会用到它。在 CF 位为 0 的情况下,若返回后的 EBX 值为 0,表示这是最后一个 ARDS 结构

修改代码

org 0x7c00
mov ax,3
int 0x10

mov ax,0;
mov ds,ax  
mov es,ax
mov ss,ax
mov sp,0x7c00

mov edi,0x1000
mov ecx,2
mov bl,4
call read_disk
jmp 0:0x1000 ;跳转到0x1000处执行
read_disk:
    pushad ;将a,b,c,d,si,di,sp,bp压栈
    ;读取硬盘
    ; edi --> 存储内存位置
    ; edi是di的扩展,就是32位的di
    ; ecx --> 读取的扇区位置
    ; bl  --> 读取扇区的数量

    mov dx,0x1f2
    mov al,bl
    out dx,al ; 扇区数量

    mov al,cl ;扇区位置低8位
    inc dx; 0x1f3
    out dx,al  ;扇区位置低8位
    
    shr ecx,8  ;ecx右移8位。扇区中间8位位置就到了低8位
    inc dx; 0x1f4
    mov al,cl
    out dx,al  ;中间8位
    
    shr ecx,8  ;ecx右移8位。高8位就到了低8位
    inc dx; 0x1f5
    mov al,cl
    out dx,al
    
    shr ecx,8
    and cl,0b0000_1111 ;只取低4位
    inc dx; 0x1f6
    mov al,0b1110_0000 ;
    or al,cl 
    out dx,al
    
    inc dx;0x1f7
    mov al,0x20 ;读硬盘
    out dx,al

    xor ecx,ecx ;清零
    mov cl,bl

.read:
    push cx
    call .waits
    call .reads
    pop cx
    loop .read
    popad
    ret

.waits:
    mov dx,0x1f7
    .check_read_state:
        nop
        nop
        nop ;加延迟 ATA的要求

        in al,dx
        and al,0b1000_1000
        cmp al,0b0000_1000
        jnz .check_read_state
    ret

.reads:
    mov dx,0x1f0
    mov cx,256 ;一个扇区256个字
    .read_loop:
        nop
        nop
        nop
        in ax,dx
        mov [edi],ax
        add di,2
        loop .read_loop
    ret


jmp $
times 510 -($-$$) db 0
db 0x55,0xaa

loader.asm

[org 0x1000]

check_memory:
    mov ax,0
    mov es,ax
    xor ebx,ebx
    mov edx,0x534D4150
    mov edi,ards_buffer
.next:
    mov eax,0xe820
    mov ecx,20
    int 0x15
    
    jc .error
    add di,cx
    inc word [ards_count]
    cmp ebx,0
    jnz .next
    mov cx,[ards_count]
    mov si,0
.show:  ;显示内存信息
    mov eax,[ards_buffer+si]
    mov ebx,[ards_buffer+si+8]
    mov edx,[ards_buffer+si+16]
    add si,20
    loop .show
.error:
jmp $

ards_count:
    dw 0
ards_buffer:   

show内存

保护模式和全局描述符

… 待

内存映射

… 待

C语言篇

EFL文件

Executable and Linking Format / 可执行和链接的格式

  • 可执行程序 python / bash / gcc –> PE (.exe)
  • 可重定位文件 / 静态库 / gcc -c .o –> .o.a / .lib
  • 共享的目标文件 / 动态链接库 –> .so(linux)/.dll(windows)

文件格式:

  • 代码 .text
  • 数据
    • .data 已经初始话的数据
    • .bss 没有初始化的数据 -buffer 缓存区域

使用readelf -e ./hello.o命令来查看elf文件

ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          64 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         7
  Section header string table index: 3

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            00000000 000000 000000 00      0   0  0
  [ 1] .text             PROGBITS        00000000 000160 000022 00  AX  0   0 16
  [ 2] .data             PROGBITS        00000000 000190 000010 00  WA  0   0  4
  [ 3] .shstrtab         STRTAB          00000000 0001a0 000031 00      0   0  1
  [ 4] .symtab           SYMTAB          00000000 0001e0 000070 10      5   6  4
  [ 5] .strtab           STRTAB          00000000 000250 000020 00      0   0  1
  [ 6] .rel.text         REL             00000000 000270 000008 08      4   1  4
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  p (processor specific)

There are no program headers in this file.

EFL Headers

  • Magic 标识文件格式 7f 45 4c 46对应ASCLL中的.EFl
  • class – ELF32 表明是32位的ELF文件
  • Data 指明文件的编码格式,Inter机一般都是小段存储的
  • Version 版本
  • OS/ABI 指定文件的操作系统和应用程序的二进制接口
  • Machine 知名兼容机 – 80386

Section Headers

  • 0 段是一个空段,它没有任何数据,也没有任何属性。它的存在是为了使段头表的索引从1开始,方便后续的引用。
  • 1 .text 段是一个程序段,它包含了文件中的代码部分。它的类型是 PROGBITS,表示它包含了程序定义的数据。它的属性是 AX,表示它可以分配内存(A)并且可以执行(X)。它的偏移量是 0x160,表示它在文件中的位置是从第 0x160 字节开始。它的大小是 0x22,表示它占用了 34 个字节。它的对齐方式是 16,表示它在内存中的地址必须是 16 的倍数。
  • 2 .data 段是一个数据段,它包含了文件中的初始化数据部分。它的类型是 PROGBITS,表示它包含了程序定义的数据。它的属性是 WA,表示它可以分配内存(A)并且可以写入(W)。它的偏移量是 0x190,表示它在文件中的位置是从第 0x190 字节开始。它的大小是 0x10,表示它占用了 16 个字节。它的对齐方式是 4,表示它在内存中的地址必须是 4 的倍数。
  • 3 .shstrtab 段是一个字符串表段,它包含了文件中的段名字符串。它的类型是 STRTAB,表示它是一个字符串表。它没有任何属性,表示它不需要在内存中分配或执行。它的偏移量是 0x1a0,表示它在文件中的位置是从第 0x1a0 字节开始。它的大小是 0x31,表示它占用了 49 个字节。它的对齐方式是 1,表示它在内存中的地址可以是任意值。它是段头表中的一个特殊段,它的索引是 3,表示它是段头表中的第 4 个段。它的作用是为其他段提供名称,通过段头结构中的 name 字段,可以从这个段中查找到对应的段名字符串。
  • 4 .symtab 段是一个符号表段,它包含了文件中的符号信息。它的类型是 SYMTAB,表示它是一个符号表。它没有任何属性,表示它不需要在内存中分配或执行。它的偏移量是 0x1e0,表示它在文件中的位置是从第 0x1e0 字节开始。它的大小是 0x70,表示它占用了 112 个字节。它的对齐方式是 4,表示它在内存中的地址必须是 4 的倍数。它的每个元素的大小是 0x10,表示每个符号占用了 16 个字节。它的链接字段是 5,表示它与第 6 个段(.strtab)相关联。它的信息字段是 6,表示它包含了 6 个符号。它的作用是为文件中的代码和数据提供符号名,通过符号结构中的 name 字段,可以从相关联的字符串表段中查找到对应的符号名字符串。
  • 5 .strtab 段是一个字符串表段,它包含了文件中的符号名字符串。它的类型是 STRTAB,表示它是一个字符串表。它没有任何属性,表示它不需要在内存中分配或执行。它的偏移量是 0x250,表示它在文件中的位置是从第 0x250 字节开始。它的大小是 0x20,表示它占用了 32 个字节。它的对齐方式是 1,表示它在内存中的地址可以是任意值。它是符号表段的一个特殊段,它与第 5 个段(.symtab)相关联。它的作用是为符号表段提供符号名,通过符号结构中的 name 字段,可以从这个段中查找到对应的符号名字符串。
  • 6 .rel.text 段是一个重定位段,它包含了文件中的重定位信息。它的类型是 REL,表示它是一个重定位表。它没有任何属性,表示它不需要在内存中分配或执行。它的偏移量是 0x270,表示它在文件中的位置是从第 0x270 字节开始。它的大小是 0x8,表示它占用了 8 个字节。它的对齐方式是 4,表示它在内存中的地址必须是 4 的倍数。它的每个元素的大小是 0x8,表示每个重定位占用了 8 个字节。它的链接字段是 4,表示它与第 5 个段(.symtab)相关联。它的信息字段是 1,表示它包含了 1 个重定位。它的作用是为文件中的代码段提供重定位信息,通过重定位结构中的 offset 和 info 字段,可以指定需要重定位的地址和符号。

AT&T语法格式

区别

GCC生成的汇编语言有很多gas的伪指令

AT&T 默认数内存地址

立即数
立即数前面需要加上$
AT&T: $123 #
Inter: 123

寄存器
寄存器器前面加%
AT&T: movl $msg,%eax
Inter: mov dword eax,msg
movl == mov dword,这里不能省略
数据传送指令
数据传送方向是和Inter相反的
AT&T: movl %eax,%ebx
Inter: movx bx,ax

movx 是一个变量

mov 描述 Intel
movl 32位 mov dword
movw 16位 mov word
movb 8位 mov byte

数据类型

命令 数据类型 nasm
.ascii 字符串 db
.asciz \0 结尾的字符串 db 0
.byte 字节 db
.double 双精度浮点 dq
.float 单精度浮点 dd
.int 32位整数 dd
.long 32位整数和(.int 相同) dd
.octa 16字节整数
.quad 8字节整数 dq
.short 16位整数 dw
.single 单精度浮点 dd
这个只针对汇编器的,伪指令(CPU不会执行,汇编器认识的东西)

伪指令
节定义 .sectionsegment
代码段 .text 数据段 .data
bss段 .bss – (BSS block started symbol,保留字段)

命令 描述
.comm 通用缓存区域
.lcomm 本地缓存区域 (只本文件可用的区域)

寻址方式

[offset_address + index * size + base_address] ; 32 位的寻址方式

base_address(offset_address, index, size)

offset_address, index 必须是寄存器,size 1, 2, 4, 8

movl %edx, 4(%edi, %eax, 8) # att 语法
mov dword [edi + eax * 8 + 4] ; Intel 语法

实现hello

.section .text
.globl main
main:

    movl $4,%eax
    movl $1,%ebx
    movl $msg,%ecx
    movl $(msg_end-msg),%edx
    int  $0x80

    movl $1,%eax
    movl $0,%ebx
    int  $0x80

.section .data
msg:
    .asciz "hello world!\n"
msg_end: #注释是#号并且前面有空格

运行编译代码:

as -32 att.s -o att
gcc -m32 att.s -static

两个细节一个注释,还有一个就是代码结束最后要添加一行空格,不然会报错
报错信息


文章作者: 园崎魅音
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 园崎魅音 !
  目录