第6章-完善内核
这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件
6.1函数调用约定简介
函数参数存储在栈空间,这有两个好处:
- 首先,每个进程都有自己的栈,这就是每个内存自己的专用内存空间。
- 其次,保存参数的内存地址不用再花精力维护,己经有战机制来维护地址变化了,参数在战中的位置可以通过楼顶的偏移量来得到。
cdecl调用约定:
- 调用者将所有参数从右向左入栈
- 调用者清理参数所占的栈空间
stdcalll调用约定:
- 调用者将所有参数从右向左入栈
- 被调用者清理参数所占的栈空间
6.2汇编语言和c语言混合编程
6.2.1浅析C库函数与系统调用
- 单独的汇编代码文件与单独的C语言文件分别编译成目标文件后, 一起链接成可执行程序。
- 在C语言中嵌入汇编代码,直接编译生成可执行程序。
系统调用是 Linux 内核提供的一套子程序,它和 Windows 的动态链接库 dll 文件的功能一样,用来实现一系列在用户态不能或不易实现的功能,系统调用是供用户程序来使用的,操作系统权利至高无上,不需要使用自己对外发布的功能接口,即系统调用 。 系统调用很像 BIOS 中断调用只不过系统调用的入口只有一个,即第0x80号中断。系统调用的子功能要用 eax 寄存器来指定。
调用“系统调用”有两种方式:
- 将系统调用指令封装为c库函数,通过库函数进行系统调用,操作简单
- 不依赖任何库函数,直接通过汇编指令 int 与操作系统通信。
上诉两种方式的实现:
section .data
str_c_lib: db "c library says: hello world!",0xa
str_c_lib_len equ $-str_c_lib
str_syscall: db "syscall says: hello world!",0xa
str_syscall_len equ $-str_syscall
section .text
global _start
_start:
;方式1
push str_c_lib_len ;按照c调用约定压入参数
push str_c_lib
push 1
call simu_write ;
add esp, 12
;方式2
mov eax, 4 ;第4号子功能是write系统调用
mov ebx, 1
mov ecx, str_syscall
mov edx, str_syscall_len
int 0x80 ;发起中断,通知 Linux 完成请求的功能
;退出程序
mov eax, 1 ;第 1 号子功能是 exit
int 0x80
;下面自定义的 simu_write 用来模拟 c 库中系统调用函数 write ,这里模拟原理
simu_write:
push ebp
mov ebp, esp
mov eax, 4
mov ebx, [ebp+8]
mov ecx, [ebp+12]
mov edx, [ebp+16]
int 0x80
pop ebp
ret
;nasm -f elf -o syscall_write.o syscall_write.s 其中-f 参数用来指定编译输出的文件格式,这里需要指定为 elf,目的是将来要和 gcc 编译的 elf格式的目标文件链接,所以格式必须相同。
;ld -m elf_i386 -o output_file syscall_write.o 用ld程序将 syseall_write.o 链接成 e旺格式的二进制可执行文件
6.2.2汇编语言和C语言共同协作
例子:
#C_with_S_c.c
extern void asm_print(char*,int);
void c_print(char* str){
int len=0;
while(str[len++]);
asm_print(str,len);
}
#C_with_S_S.S
sect i on .data
str: db "asm_print says hello world !”, 0xa, 0
str_len equ $-str
section .text
extern c_print
global _start
start:
push str
call c_print
add esp,4
mov eax,1
int 0x80
global asm_print
asm_print:
push ebp
mov ebp,esp
mov eax,4
mov ebx, 1
mov ecx, [ebp+8J
mov edx, [ebp+12]
int 0x8O
pop ebp
ret
- 在汇编代码中导出符号供外部引用是用的关键字global,引用外部文件的符号是用的关键宇extern
- 在C代码中只要将符号定义为全局便可以被外部引用,引用外部符号时用 extern 声明即可。
6.3实现自己的打印函数
6.3.1显卡的端口控制
6.3.2实现单个字符打印
光标是字符的坐标,只不过该坐标不是二维的,而是一维的线性坐标,是屏幕上所有字符以 0 为起始的顺序。在默认的 80*25 模式下,每行 80 个字符共 25 行,屏幕上可以容纳 2000 个字符,故该坐标值的范围是 0~1999。第 0 行的所有字符坐标是 0~24,第 1 行的所有字符坐标是 25~49,以此类推,最后一行的所有字符是 1975~ 1999。由于一个字符占用 2 宇节,所以光标乘以 2 后才是字符在显存中的地址。
光标的坐标位置是存放在光标坐标寄存器中的,当我们在屏幕上写入一个字符时,光标的坐标并不会自动+ 1 ,因为光标跟随字符并不是必要的。所以,光标位置并不会自动更新,因为光标坐标寄存器是可写的,如果需要的话,程序员可以自己来维护光标的坐标 。
Controller Data Registers中索引为0Eh 的 Cursor Location High Register 寄存器和索引为 0Fh 的 Cursor Location Low Register 寄存器,这两个寄存器都是 8 位长度,分别用来存储光标坐标的低 8 位和高 8 位地址 。访问 CRT controller 寄存器组的寄存器,需要先往端口地址为 0x3D4 的 Address Register 寄存器中写入寄存器的索引,再从端口地址为 0x3D5 的 Data Register 寄存器读、写数据 。
注意:对于 in 指令,如果源操作是 8 位寄存器,目的操作数必须是al如果源操作数是 16 位寄存器,目的操作数必须是ax。
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
[bits 32]
section .text
;put_char功能描述:把栈中的一个字符写入光标所在处
global put_char
put_char:
pushad ;备份 32 位寄存器环境
;需要保证 gs 中为正确的视频段选择子
;为保险起见,l 每次打印时都为 gs 赋值
mov ax, SELECTOR_VIDEO ;不能直接把立即数送入段寄存器
mov gs, ax
;---------------------获取光标当前位置-----------------------------
;先获取高8位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高八位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3d5来获得或设置光标位置
in al, dx ;得到了光标位置的高8位
;为什么不直接是 in ah, dx,因为如果源操作是8位寄存器,目的操作数必须是al,如果源操作数是16位寄存器,目的操作数必须是ax。
mov ah, al
;在获取低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
;将光标存入 bx
mov bx, ax
;下面这行是在栈中获取待打印的字符
mov ecx, [esp+36] ;pushad 压入 4 x 8 = 32 字节,这是那8个寄存器
;加上主调函数 4 字节的返回地址,故 esp+36 字节
cmp cl, 0xd ;CR回车符是0xd,LF换行符是0x0a
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8 ;BS(backspace)退格键的asc码是8
jz .is_backspace
jmp .put_other
.is_backspace:
;----------------------------backspace 的一点说明-----------------------
;当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
;但有可能在键入 backspace 后并不再键入新的字符,这时光标已经向前移动到待删除的字符位置,但字符还在原处
;这就显得好怪异,所以此处添加了空格或空字符。
dec bx
shl bx, 1 ;光标左移1位等于乘2
;表示光标对应显存中的偏移字节,到显存上了
mov byte [gs:bx], 0x20 ;将待删除的字节补为 0 或空格皆可 ,因为字符占两个字节这相当于是显存地址的低字节为ACS
inc bx
mov byte [gs:bx], 0x07 ;这是高字节为属性
shr bx, 1 ;除2取整,回到光标位置
jmp .set_cursor ;设置光标流程
.put_other:
shl bx, 1
mov [gs:bx], cl
inc bx
mov byte [gs:bx], 0x07
shr bx, 1
inc bx ;下一个光标位置
cmp bx, 2000
jl .set_cursor ;如果光标值小于2000,表示未写到显存的最后位置,则去设置新的光标值,如果超出了2000则换行处理
.is_line_feed: ;换行符
.is_carriage_return: ;回车符
xor dx, dx ;dx 是被除数的高 16 位,清 0
mov ax, bx ;ax 是被除数的低 16 位
mov si, 80 ;由于是效仿 Linux ,Linux 中\n 表示下一行的行首,所以本系统中把\n和\r都处理为Linux中\n的意思
div si ;也就是下一行的行首,标值减去除80的余数便是取整
;商将会存储在累加器 AX 中。
;余数将会存储在 DX 寄存器中。
sub bx, dx ;以上4行处理\r的代码
.is_carriage_return_end: ;回车符 CR 处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end: ;若是 LF (\n),将光标移+80便可
jl .set_cursor
;屏幕行范围是 0-24 ,滚屏的原理是将屏幕的第 1-24 行搬运到第 0-23 行,再将第 24 行用空格填充
.roll_screen: ;若超出屏幕大小,开始滚屏
cld
mov ecx, 960 ;2000-80=1920字符要搬运,共190*2=3840字节 一次搬运4字节 就是960次
mov esi, 0xb80a0;第一行行首
mov edi, 0xb8000;di0行行首
rep movsd
;将最后一行填充为空白
mov ebx, 3840 ;最后行首字符的第个字节偏移=1920 * 2
mov ecx, 80 ;一行是 80 字符( 160 字节),每次清空 1 字符( 2 字节),一行需要移动 80 次
.cls:
mov word [gs:ebx], 0x0720 ; Ox0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx, 1920 ;将光标值重量为 1920 ,最后一行的首字符
.set_cursor:
;将光标设置为bx值
; ; ; ; ; ; ; 1 先设置高 8 位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高 8 位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3出来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低 8 位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0f ;用于提供光标位置的低 8 位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3出来获得或设置光标位置
mov al, bl
out dx, al
.put_char_done:
popad ;恢复压入栈的寄存器
ret
6.3.3实现字符串打印
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
[bits 32]
section .text
;------------------------------------------------------------------
;put_str 通过put_char来打印以0字符结尾的字符串
;------------------------------------------------------------------
global put_str
put_str:
;由于本函数只使用到了ebx和ecx寄存器
;编译器将该宇符串作为参数时,传递的是宇符串所在的内存起始地址,也就是说压入到校中的是存储该字符串的内存首地址。
push ebx
push ecx
xor ecx, ecx
mov ebx, [esp+12]
.goon:
mov cl, [ebx] ;从战中获取到的参数是字符串内存地址,我们需要对该地址进行内存寻址才能找到字符的 ASCII 码
cmp cl, 0
jz .str_over
push ecx ;将参数传给put_char 因为ecx寄存器存储了参数所以要压入栈,压入了32位操作数
call put_char
add esp, 4 ;回收参数所占用的空间 这个就是上面压入栈的空间
inc ebx ;使得ebx指向下一个字符
jmp .goon
.str_over:
pop ecx
pop ebx
ret
;put_char功能描述:把栈中的一个字符写入光标所在处
global put_char
put_char:
pushad ;备份 32 位寄存器环境
;需要保证 gs 中为正确的视频段选择子
;为保险起见,l 每次打印时都为 gs 赋值
mov ax, SELECTOR_VIDEO ;不能直接把立即数送入段寄存器
mov gs, ax
;---------------------获取光标当前位置-----------------------------
;先获取高8位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高八位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3d5来获得或设置光标位置
in al, dx ;得到了光标位置的高8位
;为什么不直接是 in ah, dx,因为如果源操作是8位寄存器,目的操作数必须是al,如果源操作数是16位寄存器,目的操作数必须是ax。
mov ah, al
;在获取低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
;将光标存入 bx
mov bx, ax
;下面这行是在栈中获取待打印的字符
mov ecx, [esp+36] ;pushad 压入 4 x 8 = 32 字节,这是那8个寄存器
;加上主调函数 4 字节的返回地址,故 esp+36 字节
cmp cl, 0xd ;CR回车符是0xd,LF换行符是0x0a
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8 ;BS(backspace)退格键的asc码是8
jz .is_backspace
jmp .put_other
.is_backspace:
;----------------------------backspace 的一点说明-----------------------
;当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
;但有可能在键入 backspace 后并不再键入新的字符,这时光标已经向前移动到待删除的字符位置,但字符还在原处
;这就显得好怪异,所以此处添加了空格或空字符。
dec bx
shl bx, 1 ;光标左移1位等于乘2
;表示光标对应显存中的偏移字节,到显存上了
mov byte [gs:bx], 0x20 ;将待删除的字节补为 0 或空格皆可 ,因为字符占两个字节这相当于是显存地址的低字节为ACS
inc bx
mov byte [gs:bx], 0x07 ;这是高字节为属性
shr bx, 1 ;除2取整,回到光标位置
jmp .set_cursor ;设置光标流程
.put_other:
shl bx, 1
mov [gs:bx], cl
inc bx
mov byte [gs:bx], 0x07
shr bx, 1
inc bx ;下一个光标位置
cmp bx, 2000
jl .set_cursor ;如果光标值小于2000,表示未写到显存的最后位置,则去设置新的光标值,如果超出了2000则换行处理
.is_line_feed: ;换行符
.is_carriage_return: ;回车符
xor dx, dx ;dx 是被除数的高 16 位,清 0
mov ax, bx ;ax 是被除数的低 16 位
mov si, 80 ;由于是效仿 Linux ,Linux 中\n 表示下一行的行首,所以本系统中把\n和\r都处理为Linux中\n的意思
div si ;也就是下一行的行首,标值减去除80的余数便是取整
;商将会存储在累加器 AX 中。
;余数将会存储在 DX 寄存器中。
sub bx, dx ;以上4行处理\r的代码
.is_carriage_return_end: ;回车符 CR 处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end: ;若是 LF (\n),将光标移+80便可
jl .set_cursor
;屏幕行范围是 0-24 ,滚屏的原理是将屏幕的第 1-24 行搬运到第 0-23 行,再将第 24 行用空格填充
.roll_screen: ;若超出屏幕大小,开始滚屏
cld
mov ecx, 960 ;2000-80=1920字符要搬运,共190*2=3840字节 一次搬运4字节 就是960次
mov esi, 0xb80a0;第一行行首
mov edi, 0xb8000;di0行行首
rep movsd
;将最后一行填充为空白
mov ebx, 3840 ;最后行首字符的第个字节偏移=1920 * 2
mov ecx, 80 ;一行是 80 字符( 160 字节),每次清空 1 字符( 2 字节),一行需要移动 80 次
.cls:
mov word [gs:ebx], 0x0720 ; Ox0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx, 1920 ;将光标值重量为 1920 ,最后一行的首字符
.set_cursor:
;将光标设置为bx值
; ; ; ; ; ; ; 1 先设置高 8 位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高 8 位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3出来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低 8 位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0f ;用于提供光标位置的低 8 位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3出来获得或设置光标位置
mov al, bl
out dx, al
.put_char_done:
popad ;恢复压入栈的寄存器
ret
编译器将该宇符串作为参数时,传递的是宇符串所在的内存起始地址,也就是说压入到校中的是存储该字符串的内存首地址。 从战中获取到的参数是字符串内存地址,我们需要对该地址进行内存寻址才能找到字符的 ASCII 码 .
6.3.4实现整数打印
函数转换实现的原理是按十六进制来处理 32 位数字,每4位二进制表示1为十六进制,将各十六进制数字转换成对应的字符, 一共8个十六进制数字要处理。
咱们只支持 32 位数字的输出,按每 4 位二进制数为 1 位十六进制数计算,共 8 个十六进制数字要处理,每个数宇虽然只是 4 位,但它们转换成对应的字符后,这些数字就得变成对应的 ASCII 码, ASCII 码是 宇节大小,所以每个字符需要1字节的空间,这就是需要8字节缓冲区的原因.
TI_GDT equ 0
RPL0 equ 0
SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
[bits 32]
section .data
put_int_buffer dq 0 ;定义8字节缓冲区用于数字到字符串的转换
section .text
;------------------------------------------------------------------
;put_str 通过put_char来打印以0字符结尾的字符串
;------------------------------------------------------------------
global put_str
put_str:
;由于本函数只使用到了ebx和ecx寄存器
;编译器将该宇符串作为参数时,传递的是宇符串所在的内存起始地址,也就是说压入到校中的是存储该字符串的内存首地址。
push ebx
push ecx
xor ecx, ecx
mov ebx, [esp+12]
.goon:
mov cl, [ebx] ;从战中获取到的参数是字符串内存地址,我们需要对该地址进行内存寻址才能找到字符的 ASCII 码
cmp cl, 0
jz .str_over
push ecx ;将参数传给put_char 因为ecx寄存器存储了参数所以要压入栈,压入了32位操作数
call put_char
add esp, 4 ;回收参数所占用的空间 这个就是上面压入栈的空间
inc ebx ;使得ebx指向下一个字符
jmp .goon
.str_over:
pop ecx
pop ebx
ret
;put_char功能描述:把栈中的一个字符写入光标所在处
global put_char
put_char:
pushad ;备份 32 位寄存器环境
;需要保证 gs 中为正确的视频段选择子
;为保险起见,l 每次打印时都为 gs 赋值
mov ax, SELECTOR_VIDEO ;不能直接把立即数送入段寄存器
mov gs, ax
;---------------------获取光标当前位置-----------------------------
;先获取高8位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高八位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3d5来获得或设置光标位置
in al, dx ;得到了光标位置的高8位
;为什么不直接是 in ah, dx,因为如果源操作是8位寄存器,目的操作数必须是al,如果源操作数是16位寄存器,目的操作数必须是ax。
mov ah, al
;在获取低8位
mov dx, 0x03d4
mov al, 0x0f
out dx, al
mov dx, 0x03d5
in al, dx
;将光标存入 bx
mov bx, ax
;下面这行是在栈中获取待打印的字符
mov ecx, [esp+36] ;pushad 压入 4 x 8 = 32 字节,这是那8个寄存器
;加上主调函数 4 字节的返回地址,故 esp+36 字节
cmp cl, 0xd ;CR回车符是0xd,LF换行符是0x0a
jz .is_carriage_return
cmp cl, 0xa
jz .is_line_feed
cmp cl, 0x8 ;BS(backspace)退格键的asc码是8
jz .is_backspace
jmp .put_other
.is_backspace:
;----------------------------backspace 的一点说明-----------------------
;当为backspace时,本质上只要将光标移向前一个显存位置即可.后面再输入的字符自然会覆盖此处的字符
;但有可能在键入 backspace 后并不再键入新的字符,这时光标已经向前移动到待删除的字符位置,但字符还在原处
;这就显得好怪异,所以此处添加了空格或空字符。
dec bx
shl bx, 1 ;光标左移1位等于乘2
;表示光标对应显存中的偏移字节,到显存上了
mov byte [gs:bx], 0x20 ;将待删除的字节补为 0 或空格皆可 ,因为字符占两个字节这相当于是显存地址的低字节为ACS
inc bx
mov byte [gs:bx], 0x07 ;这是高字节为属性
shr bx, 1 ;除2取整,回到光标位置
jmp .set_cursor ;设置光标流程
.put_other:
shl bx, 1
mov [gs:bx], cl
inc bx
mov byte [gs:bx], 0x07
shr bx, 1
inc bx ;下一个光标位置
cmp bx, 2000
jl .set_cursor ;如果光标值小于2000,表示未写到显存的最后位置,则去设置新的光标值,如果超出了2000则换行处理
.is_line_feed: ;换行符
.is_carriage_return: ;回车符
xor dx, dx ;dx 是被除数的高 16 位,清 0
mov ax, bx ;ax 是被除数的低 16 位
mov si, 80 ;由于是效仿 Linux ,Linux 中\n 表示下一行的行首,所以本系统中把\n和\r都处理为Linux中\n的意思
div si ;也就是下一行的行首,标值减去除80的余数便是取整
;商将会存储在累加器 AX 中。
;余数将会存储在 DX 寄存器中。
sub bx, dx ;以上4行处理\r的代码
.is_carriage_return_end: ;回车符 CR 处理结束
add bx, 80
cmp bx, 2000
.is_line_feed_end: ;若是 LF (\n),将光标移+80便可
jl .set_cursor
;屏幕行范围是 0-24 ,滚屏的原理是将屏幕的第 1-24 行搬运到第 0-23 行,再将第 24 行用空格填充
.roll_screen: ;若超出屏幕大小,开始滚屏
cld
mov ecx, 960 ;2000-80=1920字符要搬运,共190*2=3840字节 一次搬运4字节 就是960次
mov esi, 0xb80a0;第一行行首
mov edi, 0xb8000;di0行行首
rep movsd
;将最后一行填充为空白
mov ebx, 3840 ;最后行首字符的第个字节偏移=1920 * 2
mov ecx, 80 ;一行是 80 字符( 160 字节),每次清空 1 字符( 2 字节),一行需要移动 80 次
.cls:
mov word [gs:ebx], 0x0720 ; Ox0720是黑底白字的空格键
add ebx, 2
loop .cls
mov bx, 1920 ;将光标值重量为 1920 ,最后一行的首字符
.set_cursor:
;将光标设置为bx值
; ; ; ; ; ; ; 1 先设置高 8 位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0e ;用于提供光标位置的高 8 位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3出来获得或设置光标位置
mov al, bh
out dx, al
;;;;;;; 2 再设置低 8 位
mov dx, 0x03d4 ;索引寄存器
mov al, 0x0f ;用于提供光标位置的低 8 位
out dx, al
mov dx, 0x03d5 ;通过读写数据端口 Ox3出来获得或设置光标位置
mov al, bl
out dx, al
.put_char_done:
popad ;恢复压入栈的寄存器
ret
;-------------------------将小端字节序的数字变成对应的ASCII后,倒置------------------------
;输入:栈中参数为待打印的数字
;输出:在屏幕上打印十六进制数字,并不会打印前缀0x
;如:打印十进制15时,只会打印f,不会打印0xf
;------------------------------------------------------------------------------------------
global put_int
put_int:
pushad
mov ebp, esp
mov eax, [ebp+4*9]
mov edx, eax
mov edi, 7 ;指定在put_int_buffer中初始的偏移
mov ecx, 8 ;32位数字中,十六进制的位数是八个
mov ebx, put_int_buffer
;将 32 位数字按照十六进制的形式从低位到高位逐个处理。一共处理8个十六进制数字
.16based_4bits: ;每4位二进制是十六进制数字的1位
;遍历每一个十六进制数字
and edx, 0x0000000F ;解析十六进制数字的每一位,and与操作后,edx只有低4位
cmp edx, 9 ;数字0-9和a-f需要分别处理成对应的字符
jg .is_A2F
add edx, '0' ;ASCII码是8位大小 ,add求和操作后,edx低8位有效
jmp .store
.is_A2F:
sub edx, 10 ;A-F减去10所得到的差,再加上字符A的ASCII码,便是A-F对应的ASCII码
add edx, 'A'
;将每一位数字转换成对应的字符后,按照类似“大端”的顺序存储到缓冲区 put_int_buffer
;高位字符放在低地址,低位字符要放在高地址,这样和大端字节序类似,只不过咱们这里是字符序
.store:
;此时dl中应该是数字对应的字符的 ASCII 码
mov [ebx+edi],dl
dec edi
shr eax, 4
mov edx, eax
loop .16based_4bits
;现在 put_int_buffer 中已全是字符,打印之前;把高位连续的字符去掉,比如把字符000123变成123
.ready_to_print:
inc edi ;此时edi退减为-1(Oxffffffff ),加1使其为0。
.skip_prefix_0:
cmp edi, 8 ;若已经比较第九个字符l
;表示待打印的字符串为全0。
je .full0
;找出连续的0字符, edi作为非0的最高位字符的偏移
.go_on_skip:
mov cl, [put_int_buffer+edi]
inc edi
cmp cl, '0'
je .skip_prefix_0 ;继续判断下一位字符是否为字符0。
dec edi ;edi在上面的inc操作中指向了下一个字符,若当前字符不为’0’,要使edi减l恢复指向当前字符
jmp .put_each_num
.full0:
mov cl, '0' ;输入的数字全为0时,则只打印0
.put_each_num:
push ecx
call put_char
add esp, 4
inc edi
mov cl, [put_int_buffer+edi]
cmp edi, 8
jl .put_each_num
popad
ret
6.4内联汇编
6.4.1什么是内联汇编
内联汇编称为 inline assembly, GCC 支持在 C代码中直接嵌入汇编代码,所以称为 GCC inline assembly.内联汇编按格式分为两大类, 一类是最简单的基本内联汇编,另一类是复杂一些的扩展内联汇编 。内联汇编中所用的汇编语言,其语法是 AT&T,并不是咱们熟悉的 Intel 汇编语法, GCC 只支持它。
6.4.2汇编语言AT&T语法简介
AT&T 是汇编语言的一种语法风格、格式。
6.4.3基本内联汇编
基本内联汇编是最简单的内联形式,其格式为: asm [volatitle] ("assembly code")
各关键字之间可以用空格或制表符分隔,也可以紧凑挨在一起不分隔.
关键字 asm
用于声明内联汇编表达式,这是内联汇编固定的部分,不可少。 asm
和__asm__
是一样的,是由 gcc
定义的宏: #define __asm__ asm
。
因为 gcc
有个优化选项-o,可以指定优化级别。当用-o
来编译时, gcc
按照自己的意图优化代码,说不定就会把自己所写的代码修改了。关键字volatitle
是可选项,它告诉 gcc
:“不要修改我写的汇编代码,请原样保留”。 volatitle
和__volatitle__
是一样的,是由 gcc
定义的宏: #define __volatile__ volatile
。
“ assembly eode”
是咱们所写的汇编代码,它必须位于圆括号中,而且必须用双引号引起来。
下面说下 assembly eode
的规则 :
- 指令必须用双引号引起来,无论双引号中是一条指令或多条指令。
- 一对双引号不能跨行,如果跨行需要在结尾用反斜杠
'\'
转义。 - 指令之间用分号
‘;’
、换行符‘\n’
或换行符加制表符'\n''\t'
分隔
提醒一下,即使是指令分布在多个双引号中,gcc
最终也要把它们合并到一起来处理,合并之后,指 令间必须要有分隔符。所以,当指令在多个双引号中时,除最后一个双引号外,其余双引号中的代码最后 一定要有分隔符,这和其他编程语言中表示代码结束的分隔符是一样的,
char* str = "hello inlineasm\n";
int count = 0;
int main() {
asm("pusha;\
movl $4,%eax; \
movl $1, %ebx; \
movl str,%ecx; \
movl $16,%edx; \
int $0x80; \
mov %eax,count; \
popa; \
");
}
//gcc -m32 -o inlineASM.bin inlineASM.c
6.4.4扩展内联汇编
in/out 指令,就得使用 al 作为数据寄存器
扩展后的内联汇编: asm [volatitle] ("assembly code":output : input : clobber/modify)
和前面的基本内联汇编相比,扩展内联汇编在圆括号中变成了 4部分,多了 output 、 input 和 clobber/modify 三项。其中的每一部分都可以省略,甚至包括 assembly code 。省略的部分要保留冒号分隔符来占位,如果省略的是后面的一个或多个连续的部分,分隔符也不用保留,比如省略了 clobber/modify,不需要保留 input 后面的冒号。
input 和 output 正是 C 为汇编提供输入参数和存储其输出的部分,这是汇编与 c 交互的关键。
output 中每个操作数的格式为:“操作数修饰符约束名”( C变量名),其中的引号和圆括号不能少,操作数修饰符通常为等号'='。多个操作数之间用逗号’,’分隔。
input 用来指定 C中数据如何输入给汇编使用。要想让汇编使用C中的变量作为参数,就要在此指定 。 input 中每个操作数的格式为:“[操作数修饰符]约束名”( C变量名)
上面所说的“要求飞在扩展内联汇编中称为“约束” 它所起的作用就是把C代码中的操作数 映射为汇编中所使用的操作数,实际就是描述 C 中的操作数如何变成汇编操作数。这些约束的作用域是 input 和 output 部分,咱们看看这些约束是怎么体现的,约束分为四大类。
- 寄存器约束
/*
* @Author: Adward-DYX 1654783946@qq.com
* @Date: 2024-03-27 09:41:04
* @LastEditors: Adward-DYX 1654783946@qq.com
* @LastEditTime: 2024-03-27 09:51:46
* @FilePath: /OS/chapter6/6.3.4/base_asm.c
* @Description: 这是默认设置,请设置`customMade`, 打开koroFileHeader查看配置 进行设置: https://github.com/OBKoro1/koro1FileHeader/wiki/%E9%85%8D%E7%BD%AE
*/
#include<stdio.h>
int in_a=1,in_b=2,out_sum;
void main(){
asm(" pusha; \
movl in_a, %eax; \
movl in_b, %ebx; \
addl %ebx, %eax; \
movl %eax, out_sum; \
popa");
printf("sum is %d\n",out_sum);
}
#include<stdio.h>
void main(){
int in_a=1,in_b=2,out_sum;
asm("addl %%ebx, %%eax":"=a"(out_sum):"a"(in_a),"b"(in_b));
printf("sum is %d\n",out_sum);
}
- 内存约束
内存约束是要求 gcc 直接将位于 input 和 output 中的 C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是C变量的指针。
m:表示操作数可以使用任意一种内存形式。
o:操作数为内存变量,但访问它是通过偏移量的形式访问,即包含 offset address 的格式。
#include<stdio.h>
void main(){
int in_a=1,in_b=2;
printf("in_b is %d\n",in_b);
asm("movb %b0, %1;"::"a"(in_a),"m"(in_b));
printf("in_b is %d\n",in_b);
}
- 立即数约束
立即数即常数,此约束要求 gcc 在传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码 。由于立即数不是变量,只能作为右值,所以只能放在 input 中。
i:表示操作数为整数立即数
F:表示操作数为浮点数立即数
I :表示操作数为 0~ 31 之间的立即数
J:表示操作数为 0~ 63 之间的立即数
N:表示操作数为 0~255 之间的立即数
O:表示操作数为 0~32 之间的立即数
X:表示操作数为任何类型立即数
- 通用约束
0~ 9:此约束只用在 input 部分,但表示可与 output 和 input 中第 n 个操作数用相同的寄存器或内存 。
约束的作用是让 C代码的操作数变成汇编代码能使用的操作数,所有的约束形式其实都是给汇编用的。故,约束是 C语言中的操作数(变量或立即数〉与汇编代码中的操作数之间的映射,它告诉 gcc,同一个操作数在两种环境下如何变换身份,如何对接沟通。
序号占位符 是对在 output 和 input 中的操作数,按照它们从左到右出现的次序从 0 开始编号,一直到9,也就是说最多支持 10 个序号占位符。操作数用在 assembly code 中,引用它的格式是%0~9 。
asm("addl %%ebx, %%eax":"=a"(out_sum):"a"(in_a),"b"(in_b));
等价于asm("addl %2,%1:"=a"(out_sum):"a"(in_a),"b"(in_b));
由于扩展内联汇编中的占位符要有前缀%,为了区别占位符和寄存器,只好在寄存器前用两个%做前缀啦,这就是本节前面解释在扩展内联汇编中寄存器前面要有两个%做前缀的原因。
#include <stdio.h>
void main()
{
int in_a = 0x12345678, in_b = 0;
asm("movw %1,%0;":"=m"(in_b):"a"(in_a));
printf("word in_b is 0x%x\n", in_b);
in_b = 0;
asm("movb %1,%0;":"=m"(in_b):"a"(in_a));
printf("low byte in_b is 0x%x\n", in_b);
in_b = 0;
asm("movb %h1,%0;":"=m"(in_b):"a"(in_a));
printf("high byte in_b is 0x%x\n", in_b);
}
名称占位符与序号占位符不同,序号占位符靠本身出现在 output 和 input 中的位置就能被编译器辨识出来。而名称占位序需要在 output 和 input 中把操作数显式地起个名字,它用这样的格式来标识操作数: [名称]”约束名” (C 变量) 这样,该约束对应的汇编操作数便有了名字,在 assembly eode 中引用操作数时,采用%[名称]的形式就可以了。
#include <stdio.h>
void main()
{
int in_a = 18,in_b = 3,out = 0;
asm("divb %[divisor];movb %%al,%[result];"\
:[result]"=m"(out) \
:"a"(in_a),[divisor]"m"(in_b) \
);
printf("in_a = %d,in_b = %d,result = %d\n",in_a,in_b,out);
}
//divb将eax寄存器中的值作为被除数,将商存储在 al 中,余数存储在 ah 中
强调与总结: 无论是哪种占位符,它都是指代 C 变量经过约束后、由 gcc 分配的对应于汇编代码中的操作数,和 C 变量本身无关。 这个操作数就是通过约 束名所指定的寄存器、内存、立即数等,最终编译器要将占位符转换成这三种操作数类型之一。
output 中有以下 3 种 :
=:表示操作数是只写,相当于为 output 括号中的 C 变量赋值,如=a(c_var),此修饰符相当于 c_var=eax 。
+:表示操作数是可读写的,告诉 gcc 所约束的寄存器或内存先被读入,再被写入。
&:表示此 output 中的操作数要独占所约束(分配)的寄存器,只供 output 使用,任何 input 中所分配的寄存器不能与此相同。注意,当表达式中有多个修饰符时,&要与约束名挨着,不能分隔。
在 input 中:
%:该操作数可以和下一个输入操作数互换。
//+示例
#include <stdio.h>
void main()
{
int in_a = 1,in_b = 2;
asm("add %%ebx,%%eax":"+a"(in_a):"b"(in_b) );
printf("now in_a = %d\n",in_a);
}
//表示将 ebx 寄存器的值加到 eax 寄存器的值上,并将结果存储回 eax 寄存器中
//结果为3
//&示例
#include <stdio.h>
void main()
{
int ret_cnt = 0, test = 0;
char* fmt ="hello,world\n"; //共 12 个字符
asm(" push %1; \
call printf; \
add $4, %%esp; \
movl $6, %2"
:"=a"(ret_cnt) \
:"m"(fmt),"r"(test)
);
printf("the number of bytes written is %d\n", ret_cnt);
}
//输出为
//hello,world\n
//段错误
//call printf;返回后屏幕会打印 hello, world 换行。在此, eax寄存器中值是 printf 的返回值,应该为 12 。
//add $4, %%esp; 回收参数所占的栈空间
//出错原因是%2被gcc分配为寄存器 eax 了
//这时候修饰符’&’就派上用场了,只要为 test 约束的寄存器不要和 ret_ent 相同就行,就是不能都用上了eax,用了&她会给你分配这一系列的其他寄存器避免使用已经或者使用的寄存器
#include <stdio.h>
void main()
{
int ret_cnt = 0, test = 0;
char* fmt ="hello,world\n"; //共 12 个字符
asm(" push %1; \
call printf; \
add $4, %%esp; \
movl $6, %2"
:"=&a"(ret_cnt) \
:"m"(fmt),"r"(test)
);
printf("the number of bytes written is %d\n", ret_cnt);
}
修饰符’%’表示 input 中的输入可以和下一个 input 操作数互换,通常用在计算结果与操作数顺序无关的指令中
//%示例
#include <stdio.h>
void main()
{
int in_a = 1,sum = 0;
asm("addl %1,%0":"=a"(sum):"%I"(2),"0"(in_a));
printf("sum is = %d\n",sum);
}