代码、内容参考来自于包括《操作系统真象还原》、《一个64位操作系统的设计与实现》以及《ORANGE’S:一个操作系统的实现》。
1.函数调用约定简介
调用约定:是调用函数时的一套约定,是被调用代码的接口。
用栈保存参数:每个进程都有自己的栈,是自己的专用内存空间。保存参数的地址不需要花费精力维护,因为已经有栈机制来维护地址的变化了,参数在栈中的位置可以通过栈顶的偏移量来得到
由此又带来两个问题:
- 参数若在栈中保存,由谁来负责回收参数所占的栈空间?
- 主调函数将参数以什么样的顺序传递?
高级语言中本身不存在这两个问题,这两个问题是高级语言在被编译为底层汇编语言时才有的,所以高级语言中不涉及调用约定。
比如一下C语言
int subtract(int a ,int b){return a-b;} //被调用者
int sub = subtract(3,2); //主调用者
在其被编译为汇编语言时,参数是要压入栈中的,以上c代码中的调用方和被调用方对应的汇编代码如下:
主调用者
push 2 ;压入参数b push 3 ;压入参数a call subtract ;调用参数subtract
被调用者
//push 寄存器是将寄存器的值压入堆栈,可以保留该值的副本,而不会影响后续指令对寄存器的操作 push ebp ;备份ebp,为以后用ebp作为基址来寻址参数。ebp之前为参数名和形参,ebp之后为函数体的参数。一般情况下,用[ss:bp+n]用作栈内寻址。 mov ebp,esp ;将当前栈顶赋值给ebp mov eax,[ebp+8] ;得到被减数,参数a sub eax,[ebp+12] ;得到减数,参数b pop ebp ;恢复ebp的值
上面知道调用顺序,如果不知道双方得协调个大家认同的参数入栈顺序,这就是最初调用约定的由来。
调用规约就是约定参数压栈顺序问题,还有栈空间的清理工作。

stdcall
- 调用者将所有参数从右往左入栈。
- 被调用者清理参数所占的栈空间。
int subtract(int a ,int b){return a-b;} //被调用者
int sub = subtract(3,2); //主调用者stdcall的调用者
push 2 ;压入参数b push 3 ;压入参数a call subtract ;调用参数subtract
stdcall的被调用者:
push ebp ;压入ebp备份 mov ebp,esp ;将esp备份给ebp,用ebp作为基址来访问栈中参数 mov eax,[ebp+0x8] ;第一个参数a sub eax,[ebp+0xc] ;a-b后存入a中 mov esp,ebp ;函数计算后将栈指针定位到返回地址处 pop ebp ;将ebp恢复 ret 8 ;返回后使esp+8,使esp置于栈顶,清理栈空间 ;因为返回地址在参数之下,所以ret指令执行时必须保证当前栈顶是返回地址。清理栈是在返回时顺便完成的。
ret指令将栈顶的数据弹出到寄存器eip后,栈指针esp自加4,由于还有个参数8,所以esp又被加了8,从而跳过了参数a和b,顺利地完成了被调用者清理栈的任务。
cdecl
cdecl起源于C语言,被称为C调用约定,是C语言默认的调用约定。
- 调用者将所有参数从右向左入栈。
- 调用者清理参数所占的栈空间。
cdecl调用约定最大的亮点就是它允许函数中参数数量不固定,printf能够支持变成参数,它的原理就是利用字符串参数format中的”%”来匹配栈中的参数。
subtract函数举例:
int subtract(int a ,int b){return a-b;} //被调用者
int sub = subtract(3,2); //主调用者主调用者:
push 2 ;压入参数b push 3 ;压入参数a call subtract ;调用函数subtract add esp,8 ;回收栈空间
被调用者:
push ebp mov ebp,esp mov eax,[ebp+0x8] sub eax,[ebp+0xc] mov esp,ebp pop ebp ret
和stdcall相比,在cdecl调用约定下生成的汇编代码,就是在被调用者中的回收栈空间操作挪到了主调用者中,在主调用者代码中的第4行,通过将esp加上8字节的方式回收了参数a和参数b,本例中的其他代码都和 stdcall一样。
2.汇编与C语言混合编程
C语言和汇编语言可以协作是因为有编译器。gcc是C语言编译器,nasm是汇编语言编译器,它们都可以把文件翻译为机器语言。
c语言程序
extern void asm_print(char*,int);
void c_print(char* str){
int len = 0;
while(str[len++]);
asm_print(str,len);
}
//在C语言中,只有符号定义为全局便可以被外部引用。汇编程序
section .data
str: db "asm_print says hello world!",0xa,0
;0xa为换行符,0为手工加上的字符串结束符\0的ASCII码
str_len equ $-str
section .text
extern c_print
global _start ;global将_start导出为全局符号,给编译器用。
_start:
;;;;;调用C代码中的函数c_print;;;;;
push str ;传入参数
call c_print ;调用C函数
add esp,4 ;回收栈空间
;;;;;推出程序;;;;;
mov eax,1 ;第1号子功能是exit系统调用
int 0x80 ;发起中断,通过Linux完成请求的功能
global asm_print ;相当于asm_print(str,size)
;在汇编语言中,符号定义为global才可以被外部引用,无论是函数还是变量。
asm_print:
push ebp ;备份ebp
mov ebp,esp
mov eax,4 ;第4号子功能是write系统调用
mov ebx,1 ;此项固定为文件描述符1,标准输出(stdout)指向屏幕
mov ecx,[ebp+8] ;第一个参数
mov edx,[ebp+12];第二个参数
int 0x80 ;发起中断,通过Linux完成请求的功能
pop ebp ;恢复ebp
ret
代码 C_with_S_c.c 中的函数 c_print 是被汇编代码 C_with_S_S.S 调用的,在c_print的实现中,它又调用汇编代码 中的asm_print。
如图所示:

在汇编代码中导出符号供外部引用是用的关键字global,引用外部文件的符号是用的关键字extern。在C代码中只要将符号定义为全局便可以被外部引用(一般情况下无需用额外关键字修饰),引用外部符号时用extern声明即可。
3.显卡的端口控制
之前,我们在往屏幕上输出文本时,是利用BIOS中断,和利用系统调用,这些都是依赖别人的方法。 如今我们要写一个打印函数了。
之前我们已经对硬盘有过端口操作,就是用in和out指令加不同的端口号,对显卡也是如此

前四组寄存器属于分组,它们有一个特征,就是被分成了两类寄存器,即 Address Register 和 Data Register。
端口实际上是IO接口电路上的寄存器,为了能访问到这些CPU外部的寄存器,计算机系统为这些寄存器同一编址,一个寄存器被赋予一个地址。这些地址可不是我们所说的内存地址,内存地址是用来访问内存用的,其范围取决于地址总线的宽度,而寄存器的地址范围是0~65535。
我们用专门的in和out指令来读写这些寄存器。
寄存器分组的原因:IO接口电路上的寄存器数量取决于具体外设。因为显卡上的寄存器太多了,如果每个寄存器都占用一个端口,资源会被浪费。所以计算机系统给的端口是固定的。
寄存器分组的使用:工程系将每个寄存器分组视作一个寄存器数组,提供了一个寄存器来指定数组下标,一个寄存器用于索引所指向的数组元素进行输入输出。这样两个寄存器就可以定位寄存器数组中的任何寄存器了。
这两个寄存器分别为Address Register和Data Register,Address Register中指定寄存器的索引值,Data Register中对索引的寄存器进行读写操作。
上面CRT Controller Registers寄存器组中的Address Register和Data Register的端口地址有些特殊,它的端口地址并不固定,具体值取决于Miscellaneous Output Register寄存器中的Input/Output Address Select字段。
如图:

完整介绍图:

这里 I/OAS (Input/Output Address Select)字段不仅影响CRT Controller Registers 寄存器组的AddressRegister和Data Register的端口地址,而且还影响Feature Control register寄存器的写端口地址和Input Status#1 Register寄存器的端口地址(此寄存器只有读端口),也就是影响了上面分组图中所有端口地址中包括x的寄存器。
I/OAS (Input/Output Address Select) 此位用来选择CRT controller寄存器组的地址,这里是指Address Register和Data Register的地址。
- 当此位为 0时:CRT controller寄存器组的端口地址被设置为0x3Bx,Address Register和Data Register的端口地址实际值为3B4h-3BSh。并且为了兼容monochrome适配器(显卡), Input Status #1 Register寄存器的端口地址被设置为 0x3BA。
- 当此位为 1 时:CRT controller寄存器组的端口地址被设置为0x3Dx,Address Register和Data Register的 端口地址实际值为3D4h-3DSh。并且为了兼容color/graphics适配器(显卡),Input Status #1Register寄存 器的端口地址被设置为0x3DA。
Feature Control register寄存器的写端口也是3xAh的形式,该端口地址取值以同样的方式受I/OAS位的影响。
- 如果I/OAS位为0,写端口地址为3BAh。
- 如果I/OAS位为1,写端口地址为3DAh。
默认情况下,Miscellaneous Output Register寄存器的值为0x67,其他字段不管,咱们只关注这最重要 的I/OAS位,其值为1。 也就是说:
- CRT controller寄存器组的Address Register的端口地址为0x3D4,Data Register的端口地址0x3D5
- Input Status #1 Register寄存器的端口地址被设置为0x3DA.
- Feature Control register 寄存器的写端口是 0x3DA.
4.参考
郑钢著操作系统真象还原
田宇著一个64位操作系统的设计与实现
丁渊著ORANGE’S:一个操作系统的实现


