三、完善内核功能:添加字符、字符串、整数打印功能

函数调用约定

调用约定是为了解决汇编语言的问题才提出来的,高级语言如c语言为了方便就承担了这一切。
C语言遵循的调用约定是cdecl。
也就是说:
1,调用者将所有的参数从右向左入栈
2,调用者清理参数所占的栈空间
3,被调函数中要注意备份寄存器,最后用ret返回。

汇编语言和C语言的混合编程

分为两大类:
1,分别将c语言和汇编语言编译成可重定位的ELF目标文件,然后在链接成可执行的ELF文件
2,在C语言中内嵌汇编代码,直接编译成可执行代码。

C库函数与系统调用

系统调用与BIOS下中断的联系和区别:

1,系统调用是Linux内核提供的一套子程序,用户态下程序不能对硬件进行访问,系统调用就用来实现在用户态下不能实现的功能,比如在屏幕上显示信息,读写硬盘文件等等。BIOS的一些中断功能也是提供了硬件的操作。比如之前使用的内存容量检测的中断,int  0x15。
2,系统调用的中断号只有一个,0x80,其他中断号都有各自的用途。不像BIOS那样几乎每一个功能就是一个中断号。
3,系统调用利用的是中断描述符。BIOS利用的是中断向量表,是由开机BIOS自己建立的。
4,系统调用的子功能要用eax寄存器来指定。

使用系统调用:

Linux中的/usr/include/asm/unistd.h文件中,定义了所有的系统调用。man命令查看时候 : man 2 系统调用门名  (2表示查看system call 的信息)。比如简单的write系统调用:此功能就是把buf指向的缓冲区中count个字节写入fd指向的文件描述符,执行成功后返回写入的字节数。
#include<unistd.h>
int main()
{
write(1,"hello,world\n",11);
}
write函数内部封装的一定是系统调用指令。

系统调用的使用方式:(汇编代码版本)
当输入的参数小于等于5个时,Linux用寄存器传参数,当参数大于5个时,参数将放在连续的内存单元传递。并将该区域的首地址放到 ebx寄存器中。功能号放在eax中。
5个寄存器存放参数的顺序为:ebx存储第一个参数、ecx、edx、esi、edi 依次是后面的4个。然后调用
int 0x80        ;开始使用系统调用,之后程序运行后
mov eax,1    ;第一号功能是exit
int 0x80       ;发起中断,通知Linux完成请求的功能。

调用 系统调用有两种方式:
1,将系统调用封装在C库函数中,在高级语言中通过库函数进行系统调用,操作简单。上面介绍的C语言中就是这一方法,实现是将所用的系统调用封装在C库函数中。即:翻译成汇编代码后:    write(1,"hello,world\n",11):
write 是被调函数,它被主调函数调用,所以编译器完成的汇编代码有:
push 11    push str     push 1
call + 函数名(write)
add esp,12

函数(write)是被调函数,由操作系统工程师自己实现的:
//此时write在内部实现中又作为主调函数(C语言),来调用函数(汇编语言),根据系统真正的系统调用需要的参数来压栈
push xxx
call true_write //然后调用 系统调用的汇编代码(代码为)
add esp xxx

//真正的系统调用汇编为:
true_write:
push xxx    //压栈保护
mov eax,[esp+xx]  //从栈中取功能号
mov xxx,[esp+xx]
int 0x80
mov eax,1 //退出
int 0x80
pop xxx



2,不依赖任何库函数,直接通过汇编指令int 0x80 与操作系统通信.。这也是我们要实现的。

汇编语言与C语言可以互相调用

本质上是都编译成机器代码后,互相调用的。理解时候先分清楚哪个是主调函数,哪个是被调函数,然后按照cdecl翻译即可。 翻译成汇编语言层面理解就行。
1,汇编代码和C语言中都要注意声明。C语言中的声明是全局变量声明 (extern a),函数声明(extern void  show())。汇编代码中也要注意声明,只声明标号就行如 extern show。
2,汇编代码中的全局变量或者函数,导出符号供外部引用是用的关键字global。

实现单个字符打印函数:put_char(被调函数)

以往我们打印功能都是利用BIOS功能或者系统调用C库函数write或者系统调用int 0x80 的子功能 mov eax,4。现在我们利用IO接口显卡中寄存器来操作。


我们只使用80*25的文本模式下的简单操作。
前四组寄存器属于分组寄存器,特征是被分为两类寄存器:Address Register 和 Data Register。这是使用的寄存器太多时候采取的策略,将很多寄存器看作成一个数组,一个寄存器提供索引,一个寄存器提供读写数据。Address Register作为数组的索引,Data Register 作为寄存器数组中该索引对应的寄存器,读写数据通过这个寄存器。我们只使用CRT Controller Register。里面有很多很多寄存器,我们将其看成数组用两个寄存器来访问:3x4h和3x5h。x表示地址不固定,由其他子段来决定。



默认情况下,Miscellaneous Output Register 寄存器的值为0x67,所以 I/OAS位,其值为1。所以是0x3D4和0x3D5.

1,打印字符的本质是把字符写入,文本模式下80*25,每行显示80个字符共25行=4000字节,每个字符占2字节,低字节是字符的Ascii码,高字节是前景色和背景色属性。所以在4000个字节的显存空间中,只要是起始地址是偶数的任意2字节我们都可以写入字符。
2,光标是字符的坐标,是一维的线性坐标,以0开始。80*25,所以坐标值范围是0-1999。第0行是0-79.。。光标乘以2才是字符在显存中的地址。光标的坐标位置存放在光标坐标寄存器中,并不会自动加1。 0Eh和0Fh分别存放坐标值的高8位和低8位。
3,PUSHAD 是push all double,意思是压入所有的双字长的寄存器,入栈顺序是:EAX-ECX-EDX-EBX-ESP-EBP-ESI-EDI。出栈时候用popad。对于in和out指令,dx储存寄存器的地址值,用al储存8位操作数,用ax储存16位操作数。不能将数字直接mov给段寄存器,mov ax,xxx   mov gs ,ax。
4,回车符的ASCII码为0xd,换行符为0xa。
5,内存由低地址向高地址赋值:ecx,mov byte[ebx+esi],eax     inc esi   loop
6,滚屏操作有两种方式:一:显存32K,大概32K/4000b=8屏,通过设置Start Address High Register 和 Low 这两个寄存器来设置滚屏,二是默认这两个寄存器的值为0,然后将 1-24 行的字符移到 0-23 行。

打印字符的步骤:
1、备份寄存器现场
2、获取光标值,这是下一个可打印字符的位置,即这次打印的位置
3、获取打印的字符
4、若字符是回车/换行符:坐标值减去除以80取余后的值得到行首坐标,加上80得到下一行行首新的坐标值。(可能滚屏)
   若字符是退格符:将坐标值减一后将那处的字符设为0x20(空格),得到新的坐标值。
   若字符是普通字符:将坐标值处设为预定字符后,坐标值加一得到新的坐标值。(可能滚屏)
5、判断新的坐标值是否大于2000,若大于2000做滚屏处理后得到新的坐标值
6、将新的坐标值写入写入光标寄存器,使其指向下一个可打印字符的位置
7、恢复寄存器现场,退出
****注意一定是:先把目前的字符(普通字符或者回车换行符)打印出来后,更新下一个光标值时(普通+1,换行+80)才判断是否滚屏的
调用时候我们用C语言进行调用:void put_char(char);  这是主调函数,编译器翻译成汇编语言时候会先将字符压入栈中,然后call put_char,最后add esp,4(32位下面压入8位立即数,会扩展到32位)

TI_GDT equ 0
RPL_0 equ 0
SELETOR_VIDEO equ (0x3<<3)+TI_GDT+RPL_0

section .data
put_int_buffer dq 0

[bits 32]
section .text
global put_char   ;此函数是被调函数,所以参数已经被主调函数压入栈中
put_char:

pushad
mov ax,SELETOR_VIDEO     ;放在函数中目的:用户调用完成后,当内核态便会用户态后CPU会将gs置0的,所以我们这里要在内核态情况下置1。
mov gs,ax

;获得光标高8位,储存在al中
mov dx,0x3d4	;寄存器组的索引寄存器
mov al,0Eh	
out dx,al
mov dx,0x3d5h	;寄存器组的数据寄存器
in al,dx
mov ah,al

;获得光标低8位,储存在al中
mov dx,0x3d4
mov al,0Fh
out dx,al
mov dx,0x3d5h
in al,dx

;获得字符,这个函数被被调函数,直接去栈上面取字符即可
mov bx,ax    	;基址寻址,习惯用bx
mov cl,[esp+36]
cmp cl,0xd   	 ;CR 
jz .is_carriage_return
cmp cl,0xa	;LF
jz .is_line_feed

cmp cl,0x8  ;backspace
jz .is_backspace
jmp .put_other

.is_carriage_return
.is_line_feed
;bx中存储的是光标值,bx*2得到偶地址为对应显示字符的字符值
;行首光标值加上80得到下一次光标值
xor dx,dx
mov ax,bx
mov si,80
div si		;dx:ax/80=ax:dx,得到了行数
sub bx,dx	;减去余数得到了光标行数坐标值

add bx,80		;bx中得到新的光标值,要和2000比较,小于时正常设置
cmp bx,2000
jl .set_cursor		;大小于时候滚屏
jmp .roll_screen

.roll_screen
cld			;大数据转移三剑客:cld (esi,edi) rep movs[bwd]
mov ecx,960		;2000-80=1920,1920*2=3840,每次搬运4字节
mov esi,0xb80a0		;第1行行首
mov edi,0xb8000		;第0行行首
rep movsd		;每次搬运完成后,esi加4,edi加4,ecx-1,和0比较直到相等为止。

;最后一行置0
mov ebx,3840
mov ecx,80
mov esi,0
mov eax,0x0720

.cls:
mov word[ebx+esi],eax	;内存由高地址往高地址赋值:ecx,ebx,esi,ecx,loop
add esi,2
loop .cls
mov ebx,1920

set_cursor:
;先设置高8为 bx中已经是更新的坐标值
mov dx,0x03d4
mov al,0x0e
out dx,al
mov dx,0x03d5
mov al,bh
out dx,al

;再设置低8位
mov dx,0x03d4
mov al,0x0e
out dx,al
mov dx,0x03d5
mov al,bl
out dx,al

.put_char_done
popad
ret

实现字符串打印:put_str(被调函数)

c编译器会把字符串的结尾自动加上 '|0',ASCII码为0,,也可以和0比较来确定字符串是否结束,有了这个标记我们可以确定字符串的长度。
c语言中字符串常量符号表示的是一个地址,代表的是字符串的首地址,put_char中主调函数将字符压栈,被调函数put_char去栈地址中找即可,我们可以通过调用put_char这个被调函数来打印字符串。
我们用C语言来调用:void put_str(char *);  ·作为主调函数:首先先往栈中压入字符串的首地址,然后call put_str ,最后add esp,4。所以需要注意的是我们这个压入的是一个起始地址,而不是字符串本身,但是putchar是在栈中找到字符本身的,我们需要在put_str 中调用put_char,此时put_str是主调函数,它需要往栈中压入字符本身。
步骤:
1,作为被调函数,首先要备份寄存器。
2,打印字符函数一次打印一个字符:将要打印的字符从地址处取出来,然后比较和0的大小,若和0不相等:压栈,然后call。
3,若和0相等,终止打印。因为是被调函数,要做好被调函数的工作:ret

global put_str
put_str:
pushad
mov ebx,dword[esp+36]
mov esi,0
xor eax,eax

.loop_char
mov al,[ebx+esi]
inc esi
cmp al,0
jz .str_over
push eax
call .put_char
add esp,4
jmp .loop_char

.str_over
popad
ret

实现整数打印:put_int(被调函数)

实现的功能是:当我们输入8位16进制的数字,打印出来对应的字符,如输入0x12345678,转换为机器码后是小端顺序78 56 34 12,要打印出来'1','2','3','4','5','6','7','8'八个字符,我们输入的共4个字节,一个字节两位,所以我们输入一个字节的要转换成两个字节,所以我们输入的要转换成为8个字节,由大端顺序储存。

我们调用时候的格式是C语言是主调函数put_int(0x12345678),将12345678压入4字节的栈,小端储存,则在内存中为:78 56 34 12。因为要把4字节分别分为8字节的,所以我们需要提前备好8个字节的缓冲区,然后开始在4字节的栈中取出一字节8位:0x78,然后转化成两个字符 '7' '8'。。。依次转换后存到缓冲区中高字节,在内存中的顺序变为:'1' ‘2’ ‘3’ ‘4’ ‘5’ ‘6’ ‘7’ ‘8’。然后存完后开始打印,打印时候要判断前几位是否为0,为0的要删除:0000123-----123

global put_int
.put_int
pushad
mov ebx,put_int_buffer	;数字缓冲区起始地址
add ebx,8		;ebx指向缓冲区最顶端,从上往下减
mov ebp,esp+36		;将数字的起始地址给ebp
mov esi,0		;数字栈区往上加
xor ecx,ecx
mov ecx,4		;主调函数压栈4个字节,循环4次
xor eax,eax
xor edx,edx

.loop_get_byte
dec ebx
mov al,[ebp+esi]		;;将第一个字节给移动到al中
mov dl,al		;分开al,dl存高4位,al存低4位
and al,0000_1111b 	;高4位置0,低4位不变  8
shr dl,4		;高4位置0,低4位不变  7
add al,'0'
add dl,'0'
mov [ebx],al
mov [ebx-1],dl
inc esi
loop .loop_get_byte

;现在ebx指向缓冲区起始地址
;开始处理高位连续0字符, 000123--123
mov esi,0
xor eax,eax
.loop_zero
mov al,[ebx+esi]
inc esi
cmp al,'0'
jz .loop_zero		;和‘0’字符比较,相等继续循环
jmp .print_int		;不等则说明可以打印,调转

.print_int
xor eax,eax
xor ecx,ecx
xor edx,edx
dec esi
mov edx,8
sub edx,esi
mov ecx,edx		;打印次数
.loop_print
mov eax,[ebx+esi]	;此时该地址的字符可以打印了
inc esi
push eax
call .put_char
add esp,4
loop .loop_print

内联汇编

GCC支持在C代码中直接嵌入汇编代码,所以叫做GCC inline assembled。内联汇编中所用的汇编语言,语法是AT&T。而我们常用的是nasm支持的Intel语法。

AT&T

操作数方面:

Intel的操作数想要成为内存地址,要用【】括起来。AT&T认为,操作数就是地址,所以遇到操作数时优先认为是地址,若要当成操作数,要加上$

内存寻址方面:

AT&T:segreg(段基址):bese_address(offset_address,index,size)==segreg:bese_address+offset_address+index*size

Intel:segret:[base+index*size+offset]

base_address是基地址,可以是整数、变量名

offset_address是偏移地址,index是索引值,这两个必须是8个通用寄存器之一。

size只能是1,2,4,8

直接寻址:

只有base_address   eg: movl $222,0xc000_0000

寄存器间接寻址:
只有offset_address,格式为(offset_address),只能是通用寄存器。eg:movl (%eax),%ebx
寄存器相对寻址:
只有offset_address和base_address,格式为base_address(offset_address),eg:movb -4(%ebx),%al
变址寻址:

利用Index中的来。既然是变址,只有index和size即可,其他可有可无,没有的保留逗号来占位。movl %eax,(,%esi,2)

扩展内联汇编

asm [ volatile ] ( " assembly code ":output : intput : clobber / modify)

assembly code:用户写入的汇编指令。

output : inpt:这是汇编和C语言交互的关键。正式C为汇编提供输入参数和存储其输出的部分。

output:用来指定汇编代码的数据如何输出给C代码使用,格式:  “操作数修饰符约束名”(C变量名)

input:用来指定C中数据如何输入给汇编使用。格式: “ [操作数修饰符]  约束名 “(C变量名)

clobber/modify:汇编代码执行后可能破坏一些寄存器或者内存资源,通过此项,可以让编译器提前将这些资源保存起来。

assembly code中引用的所有操作数其实是经过GCC转换后的副本,”原件“都是在output和input括号中的C变量。

扩展内联汇编中的”约束“,起的作用就是把C代码中的操作数(变量、立即数)映射为汇编中使用的操作数,实际描述的就是C中的操作数怎么样变成汇编的操作数。

约束分为四大类:

1.寄存器约束

就是要求GCC使用哪个寄存器,将input和output中变量约束在某个寄存器中。常见的寄存器约束有:

a:代表eax/ax/al   b:代表ebx/bx/bl    c:代表ecx/cx/cl     d:代表edx/dx/dl    D:代表edi/di     S:代表esi/si    q:代表eax/ebx/ecx/edx中任意一个     r:代表eax/ebx/ecx/edx/esi/edi任意

eg:

void main()

{

int in_a=1,in_b=2,out_sum;

asm( " addl  %%ebx , %%eax : " = a " (out_sum) : " a " (in_a) , " b "( in_b ) " ) ;

}

可以看到寄存器约束的作用:顺序永远都是先输入,然后代码,然后输出。

2,内存约束

要求GCC直接将位于input和output中的C变量的内存地址作为内联汇编代码的操作数,不需要寄存器做中转,直接进行内存读写。

m:表示操作数可以使用任意一种内存形式。

o:操作数为内存变量,但访问它是通过偏移量的形式访问

void main()

{

int in_a=1,in_b=2,out_sum;

asm( " addl  %b0 , %1 ;” : :  " a " (in_a) , " m "( in_b ) " ) ;

}

%0和%1是指占位符,%b0的b代表byte,低8个字节。  

3,立即数约束

GCC传值时候不通过内存或寄存器,直接作为立即数传给汇编代码,只能放在input中。

i:表示操作数为整数立即数

F:表示操作数为浮点立即数

4,通用约束

0-9:此约束只用在input部分。


占位符:序号占位符和名称占位符两种。

序号占位符:是对在output和intput中的操作数,按照它们从左到右出现的次序从0开始编号,一直到9。操作数用在assembly code中,引用它的格式是%0-9。在%和序号之前插入字符 ‘ h ’ 表示操作数为ah,插入字符 ‘ b  ‘来表示操作数al

占位符只表示C变量经过约束后,由GCC分配的对应于汇编代码中的操作数,和C变量本身无关。就是汇编中的寄存器、内存和立即数。

output:

=:表示操作数是只写,相当于为output括号中的C变量赋值。

+:表示操作数是可读写的,告诉GCC所约束的寄存器或内存先被读入,再被写入。即既可以作为输入,也可以作为输出,省去了在input中声明约束。

&:表示output中的操作数要独占所约束的寄存器,只供output使用。

input:

%:该操作数可以和下一个输入操作数互换。

扩展内联汇编之机器模式简介

机器模式是用来在机器层面上指定数据大小及格式。

h-输出寄存器高位部分中的那一字节对应的寄存器名称。eg:ah、bh、ch、dh

b-输出寄存器中低部分1字节对应的名称,eg:al、bl、cl、dl

w-输出寄存器中大小2个字节对应的部分,eg:ax、bx、cx、dx

k-输出寄存器的四字节部分,eg:eax ebx ecx edx

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值