操作系统真象还原:完善内核

news2025/1/17 0:46:05

第6章-完善内核

这是一个网站有所有小节的代码实现,同时也包含了Bochs等文件

6.1函数调用约定简介

函数参数存储在栈空间,这有两个好处:

  1. 首先,每个进程都有自己的栈,这就是每个内存自己的专用内存空间。
  2. 其次,保存参数的内存地址不用再花精力维护,己经有战机制来维护地址变化了,参数在战中的位置可以通过楼顶的偏移量来得到。

cdecl调用约定:

  1. 调用者将所有参数从右向左入栈
  2. 调用者清理参数所占的栈空间

stdcalll调用约定:

  1. 调用者将所有参数从右向左入栈
  2. 被调用者清理参数所占的栈空间

6.2汇编语言和c语言混合编程

6.2.1浅析C库函数与系统调用
  1. 单独的汇编代码文件与单独的C语言文件分别编译成目标文件后, 一起链接成可执行程序。
  2. 在C语言中嵌入汇编代码,直接编译生成可执行程序。

系统调用是 Linux 内核提供的一套子程序,它和 Windows 的动态链接库 dll 文件的功能一样,用来实现一系列在用户态不能或不易实现的功能,系统调用是供用户程序来使用的,操作系统权利至高无上,不需要使用自己对外发布的功能接口,即系统调用 。 系统调用很像 BIOS 中断调用只不过系统调用的入口只有一个,即第0x80号中断。系统调用的子功能要用 eax 寄存器来指定。

调用“系统调用”有两种方式:

  1. 将系统调用指令封装为c库函数,通过库函数进行系统调用,操作简单
  2. 不依赖任何库函数,直接通过汇编指令 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 的规则 :

  1. 指令必须用双引号引起来,无论双引号中是一条指令或多条指令。
  2. 一对双引号不能跨行,如果跨行需要在结尾用反斜杠'\'转义。
  3. 指令之间用分号‘;’、换行符‘\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);
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1790784.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

安装和使用conda

Conda 是一个开源的软件包管理系统和环境管理系统&#xff0c;用于安装多个版本的软件包及其依赖关系&#xff0c;并在它们之间轻松切换。可以创建多个环境&#xff0c;并在环境中使用不同的python版本&#xff0c;并安装环境专属的python依赖包&#xff0c;可以用来避免python…

PyQt5+SQLlite3基于邮箱验证的登陆注册找回系统

本期教程投稿一篇实用性的基于邮箱登陆注册找回于一体的系统&#xff0c;在日常的开发和软件应用中非常常见&#xff0c;并且也使用了逻辑与界面分离的写法&#xff0c;那这个文章将详细的为大家介绍整个流程&#xff0c;但是细节的话还需要大家自己去完善&#xff0c;也欢迎大…

景源畅信数字:抖音直播人气品类有哪些?

随着短视频平台的兴起&#xff0c;抖音成为了人们日常生活中不可或缺的娱乐方式之一。而抖音直播作为平台的重要组成部分&#xff0c;吸引了大量的观众和主播参与。那么&#xff0c;在抖音直播中&#xff0c;哪些品类能够吸引更多的人气&#xff0c;成为观众们关注的焦点呢?接…

C++笔试强训day39

目录 1.神奇的字母&#xff08;二&#xff09; 2.字符编码 3.最少的完全平方数 1.神奇的字母&#xff08;二&#xff09; 链接https://ac.nowcoder.com/acm/problem/205832 看输出描述即可知输出次数最多的那个字母即可。 哈希表直接秒了&#xff1a; #include <iostre…

网络安全快速入门(十五)(中)用户的文件属性及用户相关文件详解

15.4 序言 我们之前已经了解了关于用户管理的一些基础命令&#xff0c;本章节我们就来了解一下关于文件权限的一些小知识以及基于某些文件来手动创建一个用户&#xff0c;话不多说&#xff0c;我们开始吧&#xff01; 15.5 文件权限 在linux中&#xff0c;文件都是通过查看属主…

基于深度学习YOLOv8\YOLOv5的骨科骨折诊断检测系统设计

本文将介绍基于深度学习YOLOv8\YOLOv5PySide6SQLite的骨折检测识别骨科诊断系统&#xff0c;该系统基于YOLOv8算法&#xff0c;并与YOLOv5版本进行比较&#xff0c;该系统不仅实现了对骨折骨损伤的识别&#xff0c;还提供了包括用户认证管理、模型快速切换及界面个性化定制在内…

我成功创建了一个Electron应用程序

1.创建electron项目命令&#xff1a; npm create quick-start/electron electron-memo 2选择&#xff1a;√ Select a framework: vue √ Add TypeScript? ... No √ Add Electron updater plugin? ... Yes √ Enable Electron download mirror proxy? ... Yes 3.命令&am…

【Qt知识】disconnect

在Qt框架中&#xff0c;disconnect函数用于断开信号与槽之间的连接。当不再需要某个信号触发特定槽函数时&#xff0c;或者为了防止内存泄漏和重复执行问题&#xff0c;你可以使用disconnect来取消这种关联。disconnect函数的基本用法可以根据不同的需求采用多种形式&#xff0…

JAVA:浅谈Stream流

在阅读本文章之前请了解什么叫 Lambda表达式 以及 如何使用 一、Stream流 Stream流的使用步骤&#xff1a; 获得一条Stream流&#xff0c;并且将数据放上去 单列集合获取Stream流 // 1. 单列集合获取Stream ArrayList<String> list new ArrayList<String>()…

YOLO10:手把手安装教程与使用说明

目录 前言一、YOLO10检测模型二、YOLO安装过程1.新建conda的环境 yolo10安装依赖包测试 总结 前言 v9还没整明白&#xff0c;v10又来了。而且还是打败天下无敌手的存在&#xff0c;连最近很火的RT-DETR都被打败了。那么&#xff0c;笑傲目标检测之林的v10又能持续多久呢&#…

【CTF Web】BUUCTF BUU LFI COURSE 1 Writeup(代码审计+PHP+文件包含漏洞)

BUU LFI COURSE 1 1 点击启动靶机。 解法 <?php /*** Created by PhpStorm.* User: jinzhao* Date: 2019/7/9* Time: 7:07 AM*/highlight_file(__FILE__);if(isset($_GET[file])) {$str $_GET[file];include $_GET[file]; }如果GET请求中接收到file参数&#xff0c;就会…

【vue实战项目】通用管理系统:作业列表

目录 目录 1.前言 2.后端API 3.前端API 4.组件 5.分页 6.封装组件 1.前言 本文是博主前端Vue实战系列中的一篇文章&#xff0c;本系列将会带大家一起从0开始一步步完整的做完一个小项目&#xff0c;让你找到Vue实战的技巧和感觉。 专栏地址&#xff1a; https://blog…

python ---requests

python包管理工具 pip 若发现报错&#xff0c;则可以通过 -i 命令指定软件源 requests库安装 通过 pip &#xff0c;如上 或通过 pycharm 搜索 requests &#xff0c;并安装即可 下载成功的证明 requests库使用 模拟 http 重要参数如下 如何模拟发包 支持模拟各种 http meth…

【机器学习-09】 | Scikit-Learn工具包进阶指南:Scikit-Learn工具包之高斯混合sklearn.mixture模块研究

&#x1f3a9; 欢迎来到技术探索的奇幻世界&#x1f468;‍&#x1f4bb; &#x1f4dc; 个人主页&#xff1a;一伦明悦-CSDN博客 ✍&#x1f3fb; 作者简介&#xff1a; C软件开发、Python机器学习爱好者 &#x1f5e3;️ 互动与支持&#xff1a;&#x1f4ac;评论 &…

智能监测,无忧续航!Battery Indicator for Mac,让电池状态尽在掌握

Battery Indicator for Mac 是一款设计精良的电池状态监测软件&#xff0c;它极大地增强了Mac用户对电池使用情况的感知和管理能力。 首先&#xff0c;Battery Indicator for Mac 能够实时显示电池电量百分比。这意味着&#xff0c;无论你是在处理文件、浏览网页还是观看视频&…

JL-8B/1111电流继电器 带板前接线附件 约瑟JOSEF

JL-8系列继电器型号&#xff1a; 电流继电器JL-8GB/11 电流继电器JL-8GA/21 过电流继电器JL-8GB/1 电流继电器JL-8B/521DK 电流继电器JL-8B/222DK 电流继电器JL-8B/1121 电流继电器JL-8B/12 电流继电器JL-8B/3211 电流继电器JL-8B/E2 电流继电器JL-8B/E3 过电流继电器JL-…

双向链表的讲解与实现

双向链表的讲解与实现 一、双向链表的结构二、顺序表和双向链表的优缺点分析三、双向链表的实现(使用VS2022)1.初始化、销毁、打印、判空2.尾插尾删、头插头删3.查找、指定插入、指定删除 四、代码优化五、完整 List.c 源代码 一、双向链表的结构 带头”跟前面我们说的“头节点…

DNS解析与Bond

一、DNS 1、DNS概念 DNS是域名系统的简称&#xff1a;域名和ip地址之间的映射关系互联网中IP地址是通信的唯一标识&#xff0c;逻辑地址访问网站&#xff0c;有域名&#xff0c;ip地址不好记&#xff0c;域名朗朗上口&#xff0c;好记。 域名解析的目的&#xff1a;实现访问…

Ivy优化算法-2024年7月SCI一区顶刊新算法!公式原理详解与性能测评 Matlab代码免费获取

声明&#xff1a;文章是从本人公众号中复制而来&#xff0c;因此&#xff0c;想最新最快了解各类智能优化算法及其改进的朋友&#xff0c;可关注我的公众号&#xff1a;强盛机器学习&#xff0c;不定期会有很多免费代码分享~ 目录 原理简介 一、初始化 二、协调有序的种群增…

李廉洋:6.5黄金原油亚盘震荡,美盘行情分析及最新策略。

黄金消息面分析&#xff1a;黄金价格周二下跌超过1%&#xff0c;原因是美元在本周晚些时候美国就业数据公布前趋于稳定&#xff0c;该数据可能为美联储的利率策略定下基调。另外&#xff0c;中东达成停火协议的可能性增加&#xff0c;也打压黄金的避险需求。不过&#xff0c;隔…