文章 35
评论 44
浏览 91955
[6]完善内核(内联汇编、c混编)

[6]完善内核(内联汇编、c混编)

完善内核

1:函数调用约定

参数传递方式---------存放在栈中

  1. 在函数未执行前发生进程切换,参数还是需要转移阵地,寄存器又太少,干脆直接放在内存中
  2. 为了避免多进程的参数覆盖问题,将参数放在进程自己的栈中。

参数压栈顺序---------从右向左,栈空间的清理员----------调用者(仅限在当前cdecl调用约定下)

由于编写函数的程序员或者调用函数的人都知道需要调用的参数,所以这些工作其实有谁做都可以,只需要双方做好约定即可。调用约定例如有cdeclsyscalloptlinkthiscall

cdecl ( C declaration,即 C 声明〉,起源于 C语言的一种调用约定,在c语言中,函数参数是 从右到左的顺序入梭的。 GNU/Linux GCC,把这一约定作为 准, x86 架构上的许多 C 编译器也都使用这个约定。在 cdecl 中,参数是在栈中传递的。 EAX、 ECX 和 EDX 寄存 器是由调用者保存的,其余的寄存器由被调用者保存.函数的返回值存储在 EAX 寄存器。 由 调用者清理栈空间

2:汇编、c混编

为何要混编:

  1. 汇编可以直接操作寄存器,对底层设计更方便
  2. 由于C代码经过编译成汇编过程中会经过编译器的优化,所以有时为了高效率采用内联汇编。

混编的分类

  1. 单独的汇编和C分别编译成待重定位文件(目标文件),再链接成可执行文件
  2. C中嵌入汇编,直接编译

系统调用:Linux内核提供给用户程序用来间接操作硬件的一套子程序,也称为操作系统功能调用

调用方式:通过中断描述符中唯一的系统调用入口0x80号中断,具体的子功能存在EAX中。区别于BIOS中断很多的中断号,是因为IDT中有很多已被预留的中断号。

混合编程总结:

  1. 在汇编代码中导出符号供外部引用是用的关键字 global引用外部文件的符号是用的关键宇 extern
  2. 在 C 代码中只要将符号定义为全局便可以被外部引用,引用外部符号时用 extern 声明即可。

2.1:内联汇编

扩展内联汇编要解决的是在同一个程序中C和汇编如何避免使用寄存器冲突

汇编执行前不知道那些寄存器C程序正在占用,所以用户在完成这步需要增加栈的压力,也会降低运行速度,因此这步由编译器来执行,汇编中提供要使用到的C程序中的变量和寄存器,编译器来提前进行保护

格式asm (volatile) ("assembly code" : output : input : clobber/modify)

  1. volatile等同于_ volatile _,和c中关键字volatile不一样:编译器不要优化代码,后面的指令保留原样 output:“操作数修饰符约束名” (C 变量名)
  2. input :“[操作数修饰符]约束名”( c 变量名)
  3. clobber/modify:输入汇编代码执行后可能破坏的寄存器或者内存,来通知编译器保护

上面对output和input的要求称为“约束”,它用来把C代码中的操作数(变量、立即数)映射为汇编中所使用的操作数(寄存器,内存地址)

2.1.1:约束

  1. 寄存器约束,把C中操作数存入寄存器中

    asm (”addl %%ebx, %%eax ”:”=a”( out_sum ):“ a”( in_a ),”b ”( in_b));
    “a” (in_a)表示把in_a存入eax中,b表示存入ebx中。
    ”=a”( out_sum )表示为out_sum = eax的值

  2. 内存约束:把c变量的内存地址当作内联汇编代码的操作数, 不需要寄存器做中转,直接进行内存读写,也就是汇编代码的操作数是变量的指针

  3. 立即数约束:传值的时候不通过内存和寄存器,直接作为立即数传给汇编代码,只能作为右值,放在 input 中。

  4. 通用约束

c语言中的volatile作用(同扩展内联汇编中的memory)

内存约束的内存地址,编译器可以知道,但如果有内存在汇编执行过程中被修改,就需要用在clobber/modify中加入“memory”来告诉gcc了。

memeory声明的另个作用是清除寄存器缓存:
内存的访问速度比cpu中的寄存器来说是比较慢的,所以gcc为了提速,把可能常用到的变量存入寄存器中,但这就带来一个问题,编译器编译程序时不知道变量的内存是否会发生变化,也就是说在程序运行过程中变量所在的内存可能会变化,改变的时间可以在CPU的线程调度过程,地点是其他线程的代码运行中。这就导致寄存器的值是“过时”的值

int main(void)
{
    int i;
    i = 1;
    i = 2;
    return i;
}
//没优化情况下
mov dword ptr[ebp-4],1
mov dword ptr[ebp-4],2
mov eax,dword ptr[ebp-4]	//访问内存的值
ret
//优化情况下
mov eax,2	//直接把变量的值放入寄存器中,[ebp-4]地址处的值如果变化,结果就会错误!
ret

因此volatile修饰变量,编译器就会放弃寄存器缓存的方法,采用标准寻址
memory声明告诉编译器变量所在的内存数据会改变,这样就可以从内存再读取一次新数据

占位符分为序号占位符和名称占位符

产生原因:

  1. 肯定是为了方便代码编写,也容易让编译器识别
  2. 寄存器约束中有一种r约束,即让编译器自主选择寄存器来映射C代码中的变量,但编写者不知道用哪个寄存器,所以引入占位符

扩展内联汇编中的占位符要有前缀%,所以描述寄存器要用两个%(%%ebx

//序号占位符 支持10个操作数
asm("movb %h1, %0;"\
    :"=m"(in_b)\
    :"a"(in_a));
//%0指output %1指input,有多输入则记为%2,%3,%4...
//操作数默认是32位,根据指令对操作数的要求再取8位、16位等
//%和序号之间添加h表示取寄存器的低16位,添加b表示取低8位
//名称占位符	数量不受限制  规则 [名称]"约束名"(C变量)
asm("movb %[xx],%[yy];"\
	:[yy]"=m"(in_b)\
	:[xx]"a"(in_a));

2.1.2:机器模式

机器模式用来在机器层面上指定数据的大小和格式,因为约束不能准确的表示数据对象,因此机器模式从更细的粒度上进行描述。

如序号占位符的例子中%h1,h表示寄存器高位部分的一个字节(ah)
常用的包括:
在这里插入图片描述

3:编写打印字符函数

背景信息:

  1. 与显存的寄存器(CRT Controller Data Registers),默认情况下I/OAS位置1,则CRT的Address Register的端口地址为0x3D4,Data Register 的端口地址 0x3D5,其中AR是用来索引端口,DR是存储端口对应的寄存器,而索引为0x0E和0x0F的寄存器分别存储光标的坐标的高低8位
  2. 光标是字符的坐标,是一维的线性坐标,以0为起始的顺序。在默认的 80*25 模式下,每行80个字符共25 行,屏幕上可以容纳 2000个字符(坐标值范围0~1999)
  3. 视频段选择子的索引号为0x03

实现步骤:

  1. 备份调用环境
  2. 读取光标坐标
  3. 获取栈中字符
  4. 判断字符类型,如果是换行\n0xa、回车\r0xd、退格0x8行为符号就另外处理,不然直接当成显示字符打印出来(打印前判断是否需要滚屏)
  5. 更新光标坐标

文本换行方式有两种:(其实结果没有区别)

  1. CRLF:windows下表示为“\r\n",先回到行首再换行
  2. LF:Linux下表示为"\n",直接换行

链接时注意要按照“调用在前,实现在后”的顺序输入待链接文件。

int main(void)
{
    int i;
    i = 1;
    i = 2;
    return i;
}
//没优化情况下
mov dword ptr[ebp-4],1
mov dword ptr[ebp-4],2
mov eax,dword ptr[ebp-4]	//访问内存的值
ret
//优化情况下
mov eax,2	//直接把变量的值放入寄存器中,[ebp-4]地址处的值如果变化,结果就会错误!
ret


标题:[6]完善内核(内联汇编、c混编)
作者:abandon
地址:HTTPS://www.songsci.com/articles/2021/06/20/1645784797731.html

Life Is Like A Boat

取消