手写操作系统(十三)-调用规约

代码、内容参考来自于包括《操作系统真象还原》、《一个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:一个操作系统的实现

暂无评论

发送评论 编辑评论

|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇