《操作系统——真相还原》第六章笔记
Q&A
Q1:函数调用中的参数存放在哪里?
A1:有两种方案,一种是保存在寄存器中,一种是保存在内存中。但因为要考虑到多进程的参数覆盖问题,所以索性将函数的参数放在内存当中(32位机器,64位机器前六个参数用寄存器传递)。
又因为每个进程都有自己的栈,所以将函数的参数存放在各自进程的栈中。
Q2:计算机如何知道具体的参数?
A2:A1说了函数的参数存放在各自的栈中,但函数可能又不只一个参数,计算机如果知道具体的参数在栈中的位置呢?这就涉及到了函数调用约定。
函数调用约定规定了函数参数的压栈顺序,和主调函数被调函数谁来回收栈空间。
Q3:打印函数的实现逻辑?
1、语言:不管是printf,还是cout等输出函数,其本质都是写显存。因为没有高级语言支持直接写显存,所以只能用汇编来实现, 并将函数名称用global全局符号标注,使链接器能够发现。
global put_str
put str:
2、逻辑(以显示单个字符为例):
(1)备份寄存器现场 pushad ;会将所有32位寄存器压栈
(2)获取光标坐标值 (涉及到读取显卡的端口,这个不做重点,blog中就不再写了,十分折腾人)
(3)获取待打印的字符mov ecx [esp+36];我们是32位linux系统,参数传递通过栈来实现,pushad会将8个32位寄存器压栈,之上是返回地址,因此需要+36取得参数
(4)对字符进行判断,控制字符还是可见字符?
控制字符包括回车,后退等,除此之外都是可见字符。
对每个控制字符采用一种处理逻辑,可见字符采用统一的处理逻辑。
(5)判断是否需要滚屏?
如果待显示的字符超过了屏幕当先的显示范围,则需要滚屏
(6)更新光标,指向下一个字符
多个字符的显示逻辑和单个字符一样。
话说代码层面需要和显卡端口打很多的交道,调试特别烦人,实话实说,我并没有自己独立实现,而是照搬了《操作系统真相还原》一书中的代码,以后有时间再做尝试。
函数调用约定:
函数调用约定按照类别可分为调用者清理和被调用者清理两大类:
我们用的是cdcel调用约定,其最大的亮点是允许函数中参数的数量不固定,cdcel调用约定源自C语言,又称为C调用约定,具体为:
(1)调用者将参数从右向左入栈。
(2)调用者负责清理栈空间。
系统调用输入参数的传递方式:
但输入参数小于5个的时候,用寄存器传递参数,顺序为:
ebx,ecx,edx,esi,edi
C和汇编混合编程
C的汇编的混合编程方式:
(1)内联汇编(过后补充)
(2)单独的C和汇编链接成可执行程序
1、内联汇编:
2、c和汇编链接成可执行文件:
举个C调用汇编的例子,汇编使用系统调用打印hello world!:
test.c:
extern void asm_print(char* ,int ); //声明外部符号
int main()
{
char*s = "Hello world!\n";
int len = 0;
while(s[len++]);
asm_print(s,len);
return 0;
}
sys.S
section .text
global asm_print //声明为全局符号,使链接器能够链接
asm_print:
push ebp;
mov ebp, esp
mov eax, 4 ;write中断调用号
mov ebx, 1 ;文件描述符,标准输出指向屏幕
mov ecx, [ebp+8] ; No.1 parameter
mov edx, [ebp+12] ; No.2 parameter
int 0x80;
pop ebp;
ret
编译连接命令:
(1)将.c文件和.S文件汇编为.o文件:
.c文件: gcc -m32 -c -o test.o test.c
.S文件: nasm -f elf -o sys.o sys.S
!注意 因为汇编是在32位调用约定下,所以必须加上-m32使.c文件在32位环境下编译。
(2)将两个.o文件进行链接:
ld -m elf_i386 -e main -o test sys.o test.o
-e main是告诉链接器将main符号作为程序的入口地址
执行结果: