Tag Archives: 操作系统

简单的引导程序

看了赵博的《Linux内核源代码完全注释》,读了潇前辈的《操作系统引导探究》《8259A保护模式下中断编程》,受益菲浅。想起N久前看到某期开发高手上的写自己的OS的文章,不由冲动起来,我要写一个~~~~~~引导程序:)。很简单的一个引导程序,仅仅打印几个字符而已,耗费了我几个小时,学到了很多东西。
首先建立实验环境。WinXP+VMWare(linux),用VMWare自带的模拟软驱的功能做了一个软驱镜像,ok了
然后用C写个工具,把引导程序写到软盘0扇区中。并标志最后两个字节为“55 AA”
在这篇日志里我并不打算详细说明系统启动和内存分布情况,强烈推荐《操作系统引导探究》,顺便勘误,图十中的引导区应为0x07c00到0x07dff,而原图误为到0x07cff :) 。同时补充一点,在IBM PC机中,中断向量表中每个表项占4字节,前两字节是中断处理程序段地址,另两字节存储偏移地址。另,参考了唐朔飞老师《计算机组成原理》一书,在硬向量方式中,IBM PC机中的中断表和潇前辈提到的JMP式中断表是很普遍的两种方式。只不过PC机采用前者(清华大学《IBM-PC汇编语言程序设计》)

Ok了,进入正题。先把C代码贴出来。写入引导扇区的工具

/* the program is used to write a boot program to the boot section of
the floopy
*/

#include<sys/types.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<unistd.h>
#include<stdio.h>

main(int argc,char* argv[])
{
int bootp,flp; /* the bootp point to the boot program,flp point to the flooy */
char buf[512]; /* data buffer */
if(argc==1)
{
printf(“Use the command like this: writetoboot bootfilenamen”);
return;
}

if((bootp=open(argv[1],O_RDONLY))==-1)
printf(“Boot file not existn”);
read(bootp,buf,510);
close(bootp);

buf[511]=0×55;
buf[512]=0xaa;

if((flp=open(“/dev/fd0″,O_WRONLY))==-1)
printf(“Read the floopy driver error,please check your diskn”);
write(flp,buf,512);
close(flp);
}

这个程序应该不难理解,主要运用了linux系统调用open read write。不明白的可以man一下看看。

接着是引导程序。最早的原始程序如下:

.text

entry start
start:
mov ax,cs !ffmsg是显示的欢迎信息,这个字符串放在了.text段里面。
mov es,ax

mov cx,#20
mov bp,#ffmsg
mov bx,#0×0007
mov ax,#0×1301
int 0×10
loop1:
jmp loop1

ffmsg:
.byte 13,10
.ascii “YY,I love you…”
.byte 13,10

用这个作为引导程序,结果是能引导(0扇区结尾是55 AA的都能引导-_-b),但是显示不出文字。这个程序主要是利用了0×10号bios中断调用。显示字符串。其中字符串地址为es:bp 而这个程序之所以错就是es段寄存器值错误。我原来以为ffmsg既然定义在.text段里,那么其段基址肯定就是cs代码段寄存器的值了,偏移地址可以通过ffmsg本身的名字获得,把cs赋给es就ok了。这样想的错误在于弄混了概念。程序中的段与段寄存器不是那么直观地对应的。
cs段寄存器和IP程序基数器联用,确定下一条指令的位置。但cs不一定指向存储这个程序的第一个内存单元。因此把cs的值赋给es的值显然是错误的。ffmsg只能表示待显示字符串首字符的偏移地址,在es地址错误的情况下。整体就错误了。
在这个基础上修改程序。很简单,系统引导的时候,引导程序会被加载到物理地址0x07c00处,所以把刚才的mov ax,cs指令换为mov ax,#0x07c00就成功了。这时es段寄存器指向了这个程序的首地址,bp指向正确的偏移地址。所以显示成功。
那么进一步考虑。CS寄存器的地址是多少呢?写程序验证

.text

entry start
start:
! yes,it works,but I want to see what is the value of CS
mov ax,#0x07c0
mov es,ax

mov cx,#20
mov bp,#ffmsg
mov bx,#0×0007
mov ax,#0×1301
int 0×10

mov dx,cs !从这开始,把cs段的值赋给dx寄存器。然后显示
add dh,#65
add dl,#65
mov ax,#0xb800
mov es,ax
seg es
mov [0],dh
seg es
mov [1],#0x1f
seg es
mov [2],dl
seg es
mov [3],#0x1f
loop1:
jmp loop1

ffmsg:
.byte 13,10
.ascii “YY,I love you…”
.byte 13,10

把cs段的值赋给dx寄存器。分别把低16位(dl)与高16位(dh)加上65(字符A的ascii码),然后送往显存(0xb8000处开始),打印出来,0x1f显示属性表示蓝底白字。这时候引导我们可以很清楚的看到,打印了两个A字符,说明cs段值为0。现在可以充分证明最早程序的错误所在。再进一步可以打印出IP的值,IP寄存器不可以直接访问,不过我们可以利用call指令使IP入栈的特性来取得IP。如下所示

.text

entry start
start:
! yes,it works,but I want to see what is the value of CS and IP
mov ax,#0x07c0
mov es,ax

mov cx,#20
mov bp,#ffmsg
mov bx,#0×0007
mov ax,#0×1301
int 0×10

mov dx,cs
add dh,#65
add dl,#65
mov ax,#0×9000 !把IP的值赋给BX寄存器
mov ss,ax !Set the stack sec pointer
call next1
next1:
pop bx !bx is the value of IP reg

mov ax,#0xb800
mov es,ax
seg es
mov [0],bh
seg es
mov [1],#0x1f
seg es
mov [2],bl
seg es
mov [3],#0x1f
loop1:
jmp loop1

ffmsg:
.byte 13,10
.ascii “YY,I love you…”
.byte 13,10

这次分别打印出bx寄存器的高位和低位,可以发现打印出了’|’与’#’两个字符,查一查对应的ascii码表,可以看出执行到next1时ip寄存器值为0x7c23。说明我们程序确实是从0x7c00处开始执行的。

as86 man手册

as86的资料实在是少之又少。翻译了man文档….不过翻译的很烂,就当练英文水平吧:) Linus为什么要用它来写boot程序呢,nnd..翻译真是一项辛苦的工作啊…特别是对翻译的东西还不了解的时候….

as86(1)

名称

as86 – as86-8086..80386处理器的汇编程序

概要格式

as86 [-0123agjuw] [-lm[list]] [-n name] [-o obj] [-b[bin]] [-s sym]
[-t textseg] src

as86_encap prog.s prog.v [prefix_] [as86 options]

描述

as86是8086..80386处理器下的汇编程序,它所采用的语法与Intel/MS采取的语法类似,而不同于广泛运用于UNIX下的汇编语法(译注,gas中的语法,AT&T汇编)

命令行中的src参数可为’-',代表对标准输入进行汇编。

as86_encap是一个脚本,使用了as86汇编程序,并且把生成的二进制文件转为一个C文件prog.v,用于被连接或者包含到程序里,例如引导块安装程序。prefix_参数定义一个加到源文件中所有定义的变量的前缀,缺省前缀是源文件名。…

选项

-0 以16位代码段运行,当使用了高于8086指令集的指令时警告。

-1 以16位代码段运行,当使用了高于80186指令集的指令时警告。

-2 以16位代码段运行,当使用了高于80286指令集的指令时警告。

-3 以32位代码段运行,不对任何指令发出警告信息(就算使用了486或586的指令)

-a 使汇编程序部分兼容于Minix asld.交换了[]与()的用法,并且改变了一些16位跳转与调用的语法(“jmp @(bx)” 就成了一个合法的指令)

-g 仅仅把global符号写入目标或者符号文件中

-j 把所有短跳转指令(译注:8位跳转称为短跳转)换成相似的16位或者32位跳转。并且把16位条件转移指令换为一个条件短转移命令与一个无条件长跳转组合

-O 汇编程序会做几遍额外的工作,以尝试支持向前引用。最多30遍。不推荐使用

-l 产生清单文件(list file),文件名写在选项后

-m 把宏展开后写在清单文件里

-n 把模块名写在选项之后(目标模块,而非源文件)

-o 生成目标文件,文件名写在选项之后

-b 生成纯二进制文件,文件名写在后面。这是一个没有头部的纯二进制文件(译注:类似Dos下的com和sys)如果没有-s选项程序将会在内存地址0处开始执行

-s 生成一个ASCII码符号文件,文件名写在选项后。很简单就能将其转换,用于与-b选项生成的二进制文件相关联和封装。如果二进制文件不从地址0处开始执行。那么符号文件表中前两项分别代表起始地址与结束地址

-u 假定未定义符号在未指定的段中被导入了

-w- 允许汇编程序输出警告信息

-t n 把所有text段的数据放到段n+3中.

AS86 资料

特殊字符

* 本行起始地址

;或! 注释起始符,另外,在一行起始处的“unexpected”字符被认为是注释(但是仍然会被显示在终端上)

$ 16进制数的前缀, C风格的前缀, 比如0×1234, 也可以使用.

% 2进制数的前缀.

# 立即数的前缀.

[ ] 间接寻址运算符.

与MASM不同,汇编程序没有标识符的类型信息,每个标识符仅仅代表是一个段地址和偏移地址。[]与立即数操作与传统汇编程序一致

例:

mov ax,bx
jmp bx
寄存器寻址, jmp指令把bx寄存器中的值拷到程序计数器中

mov ax,[bx]
jmp [bx]
简单的寄存器间接寻址, jmp指令把bx寄存器值指向的内存单元的值拷到程序计数器中

mov ax,#1234
立即数, 把1234赋值给ax寄存器

mov ax,1234
mov ax,_hello
mov ax,[_hello]
直接寻址,内存地址1234处的存储字赋给ax寄存器。注意第三个指令并不十分严格,只是为了与asld保持兼容所以保留(译注:若想将_hello标识符表示的值作为立即数使用,需要加上#前缀 #_hello)

mov ax,_table[bx]
mov ax,_table[bx+si]
mov eax,_table[ebx*4]
mov ax,[bx+_table]
mov ax,[bx+si+_table]
mov eax,[ebx*4+_table]
变址寻址。两种形式都可以,但是我认为第一种要更正确些,但是我往往用第二种形式:)

条件判断

IF, ELSE, ELSEIF, ENDIF

数字比较

IFC, ELSEIFC

字符串比较 (str1,str2)

FAIL .FAIL

生成用户错误

段相关

.TEXT .ROM .DATA .BSS
设置当前段。可以在前面加上关键字.SECT

LOC 数字表示段 0=TEXT, 3=DATA,ROM,BSS, 14=MAX. 连接器设定的段顺序现在是0,4,5,6,7,8,9,A,B,C,D,E,1,2,3.段 0 以及所有3以上的段都假设为text段。注意64K限制对3-14的段不适用。

标识符类型定义

EXPORT PUBLIC .DEFINE
导出符号

ENTRY 强制连接器在a.out文件里包含这个特殊符号

.GLOBL .GLOBAL
将一个标识符定义为外部的,并且强制就算不使用,也必须导入

EXTRN EXTERN IMPORT .EXTERN
导入外部标识符列表

NB: bin格式的文件不支持外部变量(译注:关于这些格式,推荐参考一下NASM的手册。纯C论坛上有中文的NASM手册)

.ENTER 标识出旧式bin格式(obs)的程序入口

数据定义

DB .DATA1 .BYTE FCB
1字节的对象列表

DW .DATA2 .SHORT FDB .WORD
2字节的对象列表

DD .DATA4 .LONG
4字节的对象列表

.ASCII FCC
写到输出的Ascii码字符串.

.ASCIZ Ascii 写到输出的Ascii码字符串,末尾添加nul

空间定义

.BLKB RMB .SPACE
以字节为单位计算空间

.BLKW .ZEROW
以字为单位计算空间 (一字2字节)

COMM .COMM LCOMM .LCOMM
通用数据域定义

其他实用伪指令

.ALIGN .EVEN
对齐

EQU 定义标识符(译注:可参考NASM或者MASM的EQU)

SET 定义可重定义的标识符

ORG .ORG
定义汇编位置(译注:即设置地址计数器的值,建议参考MASM的资料)

BLOCK 定义汇编位置并且把原来的汇编位置入栈

ENDB 回到刚才栈里记录的汇编位置

GET INCLUDE
插入新文件 (no quotes on name)

USE16 [cpu]
定义默认操作数大小为16位,参数表示程序代码将会运行在什么样的CPU的(86,186, 286,386,486,586)指令集上.使用了指定指令集之上的指令会产生警告信息

USE32 [cpu]
定义默认操作数大小为32位,参数表示程序代码将会运行在什么样的CPU的(86,186, 286,386,486,586)指令集上.使用了指定指令集之上的指令会产生警告信息

END 标识出本文件停止汇编的地方

.WARN 警告信息开关

.LIST 清单 on/off (1,-1)

.MACLIST
宏清单 on/off (1,-1)

宏的使用形式如下

MACRO sax
mov ax,#?1
MEND
sax(1)

未实现/未使用的

IDENT Define object identity string.

SETDP Set DP value on 6809

MAP Set binary symbol table map number.

寄存器

BP BX DI SI
EAX EBP EBX ECX EDI EDX ESI ESP
AX CX DX SP
AH AL BH BL CH CL DH DL
CS DS ES FS GS SS
CR0 CR2 CR3 DR0 DR1 DR2 DR3 DR6 DR7
TR3 TR4 TR5 TR6 TR7 ST

操作数类型说明

BYTE DWORD FWORD FAR PTR PWORD QWORD TBYTE WORD NEAR

near和far关键字并没有提供段间寻址编程的能力,所有”far”操作都是
都是通过显式地使用以下指令得到的:指令: jmpi, jmpf, callf, retf,
等等. Near关键字可以被用来强制使用80386的16位条件跳转指令.
‘Dword’和’word’ 能控制远跳转和远调用的操作数的大小

普通指令

这些指令和其他8086汇编程序所提供的指令大体上差不多,(译注:后面的
看不明白了.我的英语功底啊~555) the main exceptions being a few ‘
Bcc’ (BCC, BNE, BGE, etc) instructions which are shorthands f
or a short branch plus a long jump and ‘BR’ which is the longest
unconditional jump (16 or 32 bit).

长分支

BCC BCS BEQ BGE BGT BHI BHIS BLE BLO BLOS BLT BMI BNE BPC BPL
BPS BVC BVS BR

段间操作

CALLI CALLF JMPI JMPF

段修饰符指令

ESEG FSEG GSEG SSEG

字节操作指令

ADCB ADDB ANDB CMPB DECB DIVB IDIVB IMULB INB INCB MOVB MULB
NEGB NOTB ORB OUTB RCLB RCRB ROLB RORB SALB SARB SHLB SHRB SBBB
SUBB TESTB XCHGB XORB

标准指令

AAA AAD AAM AAS ADC ADD AND ARPL BOUND BSF BSR BSWAP BT BTC BTR
BTS CALL CBW CDQ CLC CLD CLI CLTS CMC CMP CMPS CMPSB CMPSD CMPSW
CMPW CMPXCHG CSEG CWD CWDE DAA DAS DEC DIV DSEG ENTER HLT IDIV
IMUL IN INC INS INSB INSD INSW INT INTO INVD INVLPG INW IRET
IRETD J JA JAE JB JBE JC JCXE JCXZ JE JECXE JECXZ JG JGE JL JLE
JMP JNA JNAE JNB JNBE JNC JNE JNG JNGE JNL JNLE JNO JNP JNS JNZ
JO JP JPE JPO JS JZ LAHF LAR LDS LEA LEAVE LES LFS LGDT LGS LIDT
LLDT LMSW LOCK LODB LODS LODSB LODSD LODSW LODW LOOP LOOPE
LOOPNE LOOPNZ LOOPZ LSL LSS LTR MOV MOVS MOVSB MOVSD MOVSW MOVSX
MOVW MOVZX MUL NEG NOP NOT OR OUT OUTS OUTSB OUTSD OUTSW OUTW
POP POPA POPAD POPF POPFD PUSH PUSHA PUSHAD PUSHF PUSHFD RCL RCR
REP REPE REPNE REPNZ REPZ RET RETF RETI ROL ROR SAHF SAL SAR SBB
SCAB SCAS SCASB SCASD SCASW SCAW SEG SETA SETAE SETB SETBE SETC
SETE SETG SETGE SETL SETLE SETNA SETNAE SETNB SETNBE SETNC SETNE
SETNG SETNGE SETNL SETNLE SETNO SETNP SETNS SETNZ SETO SETP
SETPE SETPO SETS SETZ SGDT SHL SHLD SHR SHRD SIDT SLDT SMSW STC
STD STI STOB STOS STOSB STOSD STOSW STOW STR SUB TEST VERR VERW
WAIT WBINVD XADD XCHG XLAT XLATB XOR

浮点

F2XM1 FABS FADD FADDP FBLD FBSTP FCHS FCLEX FCOM FCOMP FCOMPP
FCOS FDECSTP FDISI FDIV FDIVP FDIVR FDIVRP FENI FFREE FIADD
FICOM FICOMP FIDIV FIDIVR FILD FIMUL FINCSTP FINIT FIST FISTP
FISUB FISUBR FLD FLD1 FLDL2E FLDL2T FLDCW FLDENV FLDLG2 FLDLN2
FLDPI FLDZ FMUL FMULP FNCLEX FNDISI FNENI FNINIT FNOP FNSAVE
FNSTCW FNSTENV FNSTSW FPATAN FPREM FPREM1 FPTAN FRNDINT FRSTOR
FSAVE FSCALE FSETPM FSIN FSINCOS FSQRT FST FSTCW FSTENV FSTP
FSTSW FSUB FSUBP FSUBR FSUBRP FTST FUCOM FUCOMP FUCOMPP FWAIT
FXAM FXCH FXTRACT FYL2X FYL2XP1

Linux下的用户级多线程库 Fthread

1.Fthread简介

Linux下的用户级线程。不调用内核的任何线程功能。仅包含一句汇编,程序长度百行左右。非常简化的线程库。

2.线程简单说明

线程的特征
(1)并发执行。需要独立的程序计数器
(2)独立性。独立性弱于进程,可以互相影响。但是也有一定的独立性。因此必须为每个进程维护专属于线程的资源。
(3)协作性。线程不完全独立,都能使用进程的全局量、打开文件表等等资源。
(4)由协作性所引发的,同步问题。

教材所提及的线程包括的项:程序计数器、堆栈、寄存器集、子线程、状态。
Fthread对这些项做了简化,去掉了子线程和状态两项。
在线程间来回切换实际上就是对每个线程专属资源的切换,简单描述:开始线程1正在执行,在某一时刻线程切换信号产生,调度程序被唤醒,保存当前线程的专属资源,选择下一个执行的线程,并恢复其程序计数器、堆栈指针、相关寄存器值。线程即投入运行。

3.线程的专属资源

程序计数器:在32位机中即为EIP寄存器,包含下一条指令的地址。
堆栈与寄存器:与函数调用、局部变量等有关。
一个典型的Linux下进程内存分布见下图:(摘自《深入理解计算机系统》)

说明:
(1)不同于DOS等16位操作系统,Linux使用32位的虚拟地址空间。在操作系统课也学过,每个进程都独立地拥有4GB的虚拟内存空间。互相不影响。在这些系统中,指针变量是32位的,记录32位的地址值。不需要像在16位系统中一样通过段偏移寻址和DOS特有的near far限定符来访问内存。

(2)Linux下的进程,其内存模型是分段的,各个段有自己的属性,比如只读可写等等。大体分为代码文本段、数据段和堆栈段,以及一些系统保留区域和禁止访问的区域,比如0地址。所有的程序代码指令都存于代码段中,数据段存储全局变量与静态变量。又可细分出BSS未初始化的数据与已初始化的数据、常量区等等。Malloc所分配的空间位于数据段中,称为heap堆,是由低地址空间往高地址空间增长的。

非法访问了或者企图改写禁止的区域,将会产生一个段错误信号,SIGSEGV。这是指针编程中常出的错误。在Windows下通常表现为一个非法操作。举个简单的例子
main()
{
int *p=0;
*p=32;
}
试图访问0地址(NULL指针)引发段错误。
运用gdb调试器有助于发现这种错误。在Windows下也有gcc系列工具,Devcpp开发环境就用的是gcc编译器。在windows下同样能运用gdb发现和改正错误。

(3)函数调用和访问局部变量都需要用到堆栈(Stack)。局部变量空间分配在栈里,函数调用记录也存储在栈里。一些函数调用参数等也存在栈里。
堆栈是一种LIFO后进先出的数据结构。栈由地址高空间往地址低空间向下增长。每当有数据进栈的时候,栈顶指针往下移,数据进栈。出栈时,栈顶指针往上移,数据出栈。

(4)与堆栈相关的寄存器:
ESP寄存器:栈顶指针。
EBP寄存器:基址寄存器。主要用于访问局部变量和分隔函数调用记录

void holdtest()
{
int a = 3;
}

为例。调用该函数过后,堆栈的分布如下:

高地址
————————
返回地址
————————
原来的EBP值 <===========EBP的值
————————
a
————————<===========ESP的值
低地址

a=3这句赋值语句,在内部实际上是这样一句汇编指令来实现的
MOV [ebp-4],3
意思是把3存到ebp寄存器的值减去4所在的内存里。这里的ebp-4其实就是a的地址值。减4是因为a的类型是int,在32位系统中占4个字节。
由此可以看到访问局部变量是通过访问EBP寄存器完成的。ESP作为栈顶指针,也是非常重要的。

通过以上分析可以看出,线程切换时恢复了程序计数器EIP与EBP ESP寄存器,则函数调用记录、局部变量均恢复了。除了这三个寄存器,还需要保存和恢复EBX基址变址存器,ESI EDI源变址/目的变址寄存器。简单说明:EBX寄存器主要在计算存储器地址时作为基址寄存器,ESI EDI主要与DS联用用于确定数据段的内容。这些寄存器可以通过汇编指令来保存,也可以利用C标准库中的setjmp/longjmp, sigsetjmp/longjmp来保存与恢复。

4.Fthread说明

(1)程序组织
一共就两个文件,一个头文件声明fthread.h,相关函数实现和一些辅助函数 fthread.c。

(2)线程队列
线程队列作为进程的全局量。线程结点定义如下:

typedef struct fthread
{
int tid; /* the ID of thread */
sigjmp_buf jmp_status; /* store the info of thread */
void *sp; /* point to the own stack */
void (*callback)(void *); /* the callback procedure */
void *param;
struct fthread *next; /* point to the next fthread object */
} fthread;

Tid字段是线程的唯一标识。

Jmp_status字段是sigjmp_buf型的,其原型定义在标准库文件setjmp.h中,作用就是保存程序计数器、堆栈指针、相关寄存器。sigsetjmp将这些信息存入这个字段,由siglongjmp 恢复这些信息。这样就省去了自己实现上下文切换的工作,减少了汇编的工作。

Sp指针指向线程独立的堆栈段。为什么需要这个字段呢?设想假如不要这个字段,线程切换时通过sigsetjmp/siglongjmp,确实能无差错地恢复上下文,但是这种上下文并不是数据,而仅仅是寄存器。也就是说,假如不为每个线程分配各自独立的栈空间,那么每个线程很可能破坏掉别的线程的栈空间,就算寄存器、堆栈指针都恢复了,同样的语句访问同样的内存,但是这些内存的值已经不同了。严重的情况下,比如破坏了函数的返回地址,根本不能运行。
简单举个例子,假设主线程main创建了线程1,调度程序调度到线程1执行。即调用线程1的回调函数。那么此时堆栈内存分布如下:

高地址
——————–
Main的相关信息
———————-
线程1函数的相关信息
———————
低地址

如果某一时刻,调度程序调度回main执行,改变的仅仅是寄存器的值,而内存并未改变。此时若main再建立一个线程2,调度到线程2执行的结果将是使线程2的相关信息覆盖掉线程1的相关信息。那么再调度回线程1的时候,线程1显然不能正确执行。

综上,必须为每个线程分配一个独立的堆栈空间,这个空间由malloc来分配。在新线程刚开始执行的时候,只需要简单地把ESP指针指向新分配的空间即可。这条指令需要使用汇编来实现。这是整个程序唯一需要使用汇编的地方。由于只需要一条指令,所以采用GCC内联汇编,关于内联汇编的语法,可以查阅GCC手册,这里不再做介绍了。
这里还有个烦人的陷阱,堆空间是由下往上增长的,栈是从上往下增长的。Malloc返回的指针指向的是堆空间的最低地址,所以必须把ESP的值改为malloc返回的指针加上其长度。
Sp还有一个重要作用是当线程结束时,free这段内存。

Callback和param字段:callback是一个函数指针,指向的函数无返回值(void),能接受一个void *参数,也就是param字段所保存的。
Next字段指向队列的下一个节点。

(3)调度相关
调度模块是一个函数。处理SIGALRM信号(或者其他时钟相关的信号)。SIGALRM信号是由alarm系统调用产生的。调度函数fthread_change捕捉这个信号,保存当前线程上下文并从队列中取出下一个线程,恢复其上下文,调度到这个线程执行。

5.杂项与bug

(1)栈空间不能分配的过小。关于一个简单的函数如printf调用的开销有多大,可以自己试一下,用gdb查看寄存器信息即可。
(2)信号处理函数中有“可再入”与“不可再入”的区别,比如malloc与printf都是不可再入的函数。使用在信号处理函数中可能有想象不到的错误。具体可以参阅《UNIX高级编程》
(3)Setjmp/longjmp并没有完全保存和恢复所有寄存器。比如eax就没有保存和恢复。在C中函数返回值是由EAX来存储的,因此假如在函数调用后EAX的值没来得及赋给一个变量的时候发生中断,调度到别的线程更改了EAX过后,这个返回值就被破坏。
(4)其余不再赘述,有兴趣的同学可与我发信交流:)

源代码就不贴了,Sun老师应该会公布:)

参考文献:
《操作系统概念》
《现代操作系统》
《UNIX高级编程》
《深入理解计算机系统》
《C专家编程》 《汇编语言编程艺术》