开发一个OS,尽管绝大部分代码只需要用C/C++等高级语言就可以了,但至少和硬件相关部分的代码需要使用汇编语言,另外,由于启动部分的代码有大小限制,使用精练的汇 编可以缩小目标代码的尺寸。另外,对于某些需要被经常调用的代码,使用汇编可以提高性
能。所以我们必须了解汇编语言,即使你有可能并不喜欢它。
如果你是计算机专业的话,在大学里你应该学习过Intel格式的8086/80386汇编,这 里就不再讨论。如果我们选择的OS开发工具是GCC以及GAS的话,就必须了解AT&T 汇编语言语法,因为GCC/GAS只支持这种汇编语法。
本书不会去讨论8086/80386的汇编编程,这类的书籍很多,你可以参考它们。这里只 会讨论AT&T的汇编语法,以及GCC的内嵌汇编语法。
1. Syntax
Register Reference
゚ 引用寄存器要在寄存器号前加百分号%,如“movl %eax, %ebx”。
゚ 80386有如下寄存器:
゚ 8个32-bit寄存器 %eax,%ebx,%ecx,%edx,%edi,%esi,%ebp,%esp;
゚ 8个16-bit寄存器,它们事实上是上面8个32-bit寄存器的低16位:%ax,%bx,
%cx,%dx,%di,%si,%bp,%sp;
゚ 8个8-bit寄存器:%ah,%al,%bh,%bl,%ch,%cl,%dh,%dl。它们事实上 是寄存器%ax,%bx,%cx,%dx 的高8位和低8位;
゚ 6个段寄存器:%cs(code),%ds(data),%ss(stack),%es,%fs,%gs;
゚ 3个控制寄存器:%cr0,%cr2,%cr3;
゚ 6个debug寄存器:%db0,%db1,%db2,%db3,%db6,%db7;
゚ 2个测试寄存器:%tr6,%tr7;
゚ 8 个浮点寄存器栈:%st(0),%st(1),%st(2),%st(3),%st(4),%st(5),%st(6),
%st(7)。
Operator Sequence
操作数排列是从源(左)到目的(右),如“movl%eax(源),%ebx(目的)”
ImmediatelyOperator
使用立即数,要在数前面加符号$, 如“movl$0x04,%ebx”
或者:
para=0x04
movl$para,%ebx
指令执行的结果是将立即数04h装入寄存器ebx。
Symbol Constant
符号常数直接引用如 value:.long0x12a3f2de movlvalue,%ebx
指令执行的结果是将常数0x12a3f2de装入寄存器ebx。
引用符号地址在符号前加符号$, 如“movl$value,%ebx”则是将符号value的地址装入 寄存器ebx。
Length of Operator
操作数的长度用加在指令后的符号表示b(byte, 8-bit), w(word, 16-bits), l(long,
32-bits),如“movb %al, %bl”,“movw%ax,%bx”,“movl%eax,%ebx”。
如果没有指定操作数长度的话,编译器将按照目标操作数的长度来设置。比如指令“mov
%ax,%bx”,由于目标操作数bx的长度为word,那么编译器将把此指令等同于“movw %ax,
%bx”。同样道理,指令“mov$4,%ebx”等同于指令“movl$4,%ebx”,“push%al”等同于
“pushb%al”。对于没有指定操作数长度,但编译器又无法猜测的指令,编译器将会报错,比如指令“push$4”。
Sign and Zero Extension
绝大多数面向80386的AT&T汇编指令与Intel格式的汇编指令都是相同的,符号扩展 指令和零扩展指令则是仅有的不同格式指令。
符号扩展指令和零扩展指令需要指定源操作数长度和目的操作数长度,即使在某些指令中这些操作数是隐含的。
在AT&T语法中,符号扩展和零扩展指令的格式为,基本部分"movs"和"movz"(对应 Intel语法的movsx和movzx),后面跟上源操作数长度和目的操作数长度。movsbl意味着 movs(from)byte(to)long;movbw意味着movs(from)byte(to)word;movswl 意味着movs (from)word (to)long。对于movz指令也一样。比如指令“movsbl %al,
%edx”意味着将al寄存器的内容进行符号扩展后放置到edx寄存器中。
其它的Intel格式的符号扩展指令还有:
゚ cbw--sign-extendbytein%altowordin%ax;
゚ cwde--sign-extendwordin%axtolongin%eax;
゚ cwd--sign-extendwordin%axtolongin%dx:%ax;
゚ cdq--sign-extenddwordin%eaxtoquadin%edx:%eax;
对应的AT&T语法的指令为cbtw,cwtl,cwtd,cltd。
Call and Jump
段内调用和跳转指令为"call","ret"和"jmp",段间调用和跳转指令为"lcall","lret"和
"ljmp"。
段间调用和跳转指令的格式为“lcall/ljmp$SECTION,$OFFSET”,而段间返回指令则 为“lret$STACK-ADJUST”。
Prefix
操作码前缀被用在下列的情况:
゚ 字符串重复操作指令(rep,repne); ゚ 指定被操作的段(cs,ds,ss,es,fs,gs); ゚ 进行总线加锁(lock);
゚ 指定地址和操作的大小(data16,addr16);
在AT&T汇编语法中,操作码前缀通常被单独放在一行,后面不跟任何操作数。例如, 对于重复scas指令,其写法为:
repne
scas
上述操作码前缀的意义和用法如下:
゚ 指定被操作的段前缀为cs,ds,ss,es,fs,和gs。在AT&T 语法中,只需要按照 section:memory-operand 的格式就指定了相应的段前缀。比如: lcall%cs:realmode_swtch
゚ 操作数/地址大小前缀是“data16”和"addr16",它们被用来在32-bit操作数/地址 代码中指定16-bit的操作数/地址。
゚ 总线加锁前缀“lock”,它是为了在多处理器环境中,保证在当前指令执行期间禁止 一切中断。这个前缀仅仅对ADD,ADC,AND,BTC,BTR,BTS,CMPXCHG,DEC, INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,XCHG指令有效,如果将Lock前 缀用在其它指令之前,将会引起异常。
゚ 字符串重复操作前缀"rep","repe","repne"用来让字符串操作重复“%ecx”次。
Memory Reference
Intel语法的间接内存引用的格式为:
section:[base+index*scale+displacement]
而在AT&T语法中对应的形式为:
section:displacement(base,index,scale)
其中,base和index是任意的32-bitbase和index寄存器。scale可以取值1,2,4,8。 如果不指定scale值,则默认值为1。section可以指定任意的段寄存器作为段前缀,默认的段寄存器在不同的情况下不一样。如果你在指令中指定了默认的段前缀,则编译器在目标代 码中不会产生此段前缀代码。
下面是一些例子:
-4(%ebp):base=%ebp,displacement=-4,section没有指定,由于base=%ebp,所 以默认的section=%ss,index,scale没有指定,则index为0。
foo(,%eax,4):index=%eax,scale=4,displacement=foo。其它域没有指定。这里默认 的section=%ds。
foo(,1):这个表达式引用的是指针foo指向的地址所存放的值。注意这个表达式中没有
base和index,并且只有一个逗号,这是一种异常语法,但却合法。
%gs:foo:这个表达式引用的是放置于%gs段里变量foo的值。
如果call和jump操作在操作数前指定前缀“*”,则表示是一个绝对地址调用/跳转,也 就是说jmp/call指令指定的是一个绝对地址。如果没有指定"*",则操作数是一个相对地址。
任何指令如果其操作数是一个内存操作,则指令必须指定它的操作尺寸
(byte,word,long),也就是说必须带有指令后缀(b,w,l)。
2. GCC Inline ASM
GCC 支持在C/C++代码中嵌入汇编代码,这些汇编代码被称作GCC Inline ASM— — GCC 内联汇编。这是一个非常有用的功能,有利于我们将一些C/C++语法无法 表达的指令直接潜入C/C++代码中,另外也允许我们直接写C/C++代码中使用汇编编写简 洁高效的代码。
2.1 EssentialInlineASM
GCC中基本的内联汇编非常易懂,我们先来看两个简单的例子:
__asm__("movl%esp,%eax"); //看起来很熟悉吧!
或者是
__asm__("
movl$1,%eax //SYS_exit xor%ebx,%ebx
int $0x80
");
或
__asm__(
"movl$1,%eax\r\t"
"xor%ebx,%ebx\r\t"
"int$0x80"
);
基本内联汇编的格式是
__asm____volatile__("InstructionList");
1.__asm__
__asm__是GCC关键字asm的宏定义:
#define__asm__asm
__asm__或 asm 用来声明一个内联汇编表达式,所以任何一个内联汇编表达式都是以 它开头的,是必不可少的。
2.InstructionList
InstructionList是汇编指令序列。它可以是空的,比如:__asm__ __volatile__(""); 或
__asm__("");都是完全合法的内联汇编表达式,只不过这两条语句没有什么意义。但并非所 有InstructionList为空的内联汇编表达式都是没有意义的,比如:__asm__ ("":::"memory"); 就非常有意义,它向GCC 声明:“我对内存作了改动”,GCC 在编译的时候,会将此因素 考虑进去。
我们看一看下面这个例子:
$catexample1.c
int main(int__argc,char*__argv[])
{
int*__p=(int*)__argc;
(*__p)=9999;
//__asm__("":::"memory");
if((*__p)==9999)
return5;
return(*__p);
}
在这段代码中,那条内联汇编是被注释掉的。在这条内联汇编之前,内存指针__p所指 向的内存被赋值为9999,随即在内联汇编之后,一条if语句判断__p所指向的内存与9999 是否相等。很明显,它们是相等的。GCC 在优化编译的时候能够很聪明的发现这一点。我 们使用下面的命令行对其进行编译:
$gcc-O-Sexample1.c
选项-O表示优化编译,我们还可以指定优化等级,比如-O2表示优化等级为2;选项-S
表示将 C/C++源文件编译为汇编文件,文件名和 C/C++文件一样,只不过扩展名由.c 变
为.s。
我们来查看一下被放在example1.s中的编译结果,我们这里仅仅列出了使用gcc 2.96 在redhat7.3上编译后的相关函数部分汇编代码。为了保持清晰性,无关的其它代码未被列 出。
$catexample1.s main:
pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%eax #int*__p=(int*)__argc movl $9999,(%eax) # (*__p) = 9999
movl $5,%eax #return5
popl %ebp ret
参照一下C源码和编译出的汇编代码,我们会发现汇编代码中,没有if语句相关的代 码,而是在赋值语句(*__p)=9999后直接return5;这是因为GCC认为在(*__p)被赋值之后, 在if语句之前没有任何改变(*__p)内容的操作,所以那条if语句的判断条件(*__p)==9999 肯定是为true 的,所以GCC就不再生成相关代码,而是直接根据为true的条件生成return
5的汇编代码(GCC使用eax作为保存返回值的寄存器)。
我们现在将example1.c中内联汇编的注释去掉,重新编译,然后看一下相关的编译结 果。
$gcc-O-Sexample1.c
$catexample1.s main:
pushl %ebp
movl %esp,%ebp
movl 8(%ebp),%eax #int*__p=(int*)__argc movl $9999,(%eax) # (*__p) = 9999
#APP
#__asm__("":::"memory")
#NO_APP
cmpl $9999,(%eax) #(*__p)==9999
jne .L3 #false
movl $5,%eax #true,return5
jmp .L2
.L3:
movl (%eax),%eax
.L2:
popl %ebp ret
由于内联汇编语句__asm__("":::"memory")向 GCC 声明,在此内联汇编语句出现的位 置内存内容可能了改变,所以GCC 在编译时就不能像刚才那样处理。这次,GCC 老老实 实的将if语句生成了汇编代码。
可能有人会质疑:为什么要使用__asm__("":::"memory")向GCC声明内存发生了变化? 明明“InstructionList”是空的,没有任何对内存的操作,这样做只会增加GCC生成汇编代 码的数量。
确实,那条内联汇编语句没有对内存作任何操作,事实上它确实什么都没有做。但影响 内存内容的不仅仅是你当前正在运行的程序。比如,如果你现在正在操作的内存是一块内存 映射,映射的内容是外围I/O设备寄存器。那么操作这块内存的就不仅仅是当前的程序,I/O 设备也会去操作这块内存。既然两者都会去操作同一块内存,那么任何一方在任何时候都不 能对这块内存的内容想当然。所以当你使用高级语言C/C++写这类程序的时候,你必须让 编译器也能够明白这一点,毕竟高级语言最终要被编译为汇编代码。
你可能已经注意到了,这次输出的汇编结果中,有两个符号:#APP和#NO_APP,GCC
将内联汇编语句中"Instruction List"所列出的指令放在#APP 和#NO_APP 之间,由于
__asm__("":::"memory")中“InstructionList”为空,所以#APP和#NO_APP中间也没有任何 内容。但我们以后的例子会更加清楚的表现这一点。
关于为什么内联汇编__asm__("":::"memory")是一条声明内存改变的语句,我们后面会 详细讨论。
刚才我们花了大量的内容来讨论"InstructionList"为空是的情况,但在实际的编程中,
"InstructionList"绝大多数情况下都不是空的。它可以有1条或任意多条汇编指令。
当在"InstructionList"中有多条指令的时候,你可以在一对引号中列出全部指令,也可 以将一条或几条指令放在一对引号中,所有指令放在多对引号中。如果是前者,你可以将每 一条指令放在一行,如果要将多条指令放在一行,则必须用分号(;)或换行符(\n,大多 数情况下\n后还要跟一个\t,其中\n是为了换行,\t是为了空出一个tab宽度的空格)将 它们分开。下面的例子都是合法的写法。
__asm__("movl %eax, %ebx sti
popl%edi
subl%ecx,%ebx");
__asm__("movl%eax,%ebx;sti popl%edi;subl%ecx,%ebx");
__asm__("movl%eax,%ebx;sti\n\tpopl%edi subl%ecx,%ebx");
如果你将指令放在多对引号中,则除了最后一对引号之外,前面的所有引号里的最后一 条指令之后都要有一个分号(;)或(\n)或(\n\t)。比如:
__asm__("movl %eax, %ebx sti\n"
"popl%edi;"
"subl%ecx,%ebx");
__asm__("movl%eax,%ebx;sti\n\t"
"popl%edi;subl%ecx,%ebx");
__asm__("movl%eax,%ebx;sti\n\tpopl%edi\n"
"subl%ecx,%ebx");
__asm__("movl%eax,%ebx;sti\n\tpopl%edi;"
"subl%ecx,%ebx");
上述原则可以归结为:
゚ 任意两个指令间要么被分号(;)分开,要么被放在两行;
゚ 放在两行的方法既可以从通过\n的方法来实现,也可以真正的放在两行;
゚ 可以使用1对或多对引号,每1对引号里可以放任一多条指令,所有的指令都要被放到引号中。
在基本内联汇编中,“InstructionList”的书写的格式和你直接在汇编文件中写非内联汇 编没有什么不同,你可以在其中定义Label,定义对齐(.alignn),定义段(.section name )。 例如:
__asm__(".align2\n\t"
"movl%eax,%ebx\n\t"
"test%ebx,%ecx\n\t"
"jneerror\n\t"
"sti\n\t"
"error:popl%edi\n\t"
"subl%ecx,%ebx");
上面例子的格式是 Linux 内联代码常用的格式,非常整齐。也建议大家都使用这种格 式来写内联汇编代码。
3.__volatile__
__volatile__是GCC关键字volatile的宏定义:
#define__volatile__volatile
__volatile__或 volatile 是可选的,你可以用它也可以不用它。如果你用了它,则是向 GCC声明“不要动我所写的InstructionList,我需要原封不动的保留每一条指令”,否则当 你使用了优化选项(-O)进行编译时,GCC 将会根据自己的判断决定是否将这个内联汇编表 达式中的指令优化掉。
那么GCC 判断的原则是什么?我不知道(如果有哪位朋友清楚的话,请告诉我)。我 试验了一下,发现一条内联汇编语句如果是基本内联汇编的话(即只有“InstructionList”, 没有 Input/Output/Clobber 的内联汇编,我们后面将会讨论这一点),无论你是否使用
__volatile__来修饰,GCC 2.96 在优化编译时,都会原封不动的保留内联汇编中的
“InstructionList”。但或许我的试验的例子并不充分,所以这一点并不能够得到保证。 为了保险起见,如果你不想让GCC的优化影响你的内联汇编代码,你最好在前面都加上__volatile__,而不要依赖于编译器的原则,因为即使你非常了解当前编译器的优化原则,
你也无法保证这种原则将来不会发生变化。而__volatile__的含义却是恒定的。
2.2 Inline ASM with C/C++ Expression
GCC 允许你通过 C/C++表达式指定内联汇编中"Instrcuction List"中指令的输入和输 出,你甚至可以不关心到底使用哪个寄存器被使用,完全靠GCC来安排和指定。这一点可以让程序员避免去考虑有限的寄存器的使用,也可以提高目标代码的效率。先来看几个例子:
__asm__("":::"memory"); //前面提到的
__asm__("mov%%eax,%%ebx"
:"=b"(rv)
:"a"(foo)
:"eax","ebx");
__asm____volatile__("lidt%0"
:"=m"(idt_descr));
__asm__("subl%2,%0\n\t"
"sbbl%3,%1"
:"=a"(endlow),"=d"(endhigh)
: "g" (startlow), "g" (starthigh),
"0"(endlow),"1"(endhigh));
怎么样,有点印象了吧,是不是也有点晕?没关系,下面讨论完之后你就不会再晕了。
(当然,也有可能更晕☺)。讨论开始——
带有C/C++表达式的内联汇编格式为:
__asm__ __volatile__("InstructionList"
:Output
:Input
:Clobber/Modify);
从中我们可以看出它和基本内联汇编的不同之处在于:它多了3个部分(Input,Output,
Clobber/Modify)。在括号中的4个部分通过冒号(:)分开。
这4个部分都不是必须的,任何一个部分都可以为空,其规则为:
゚ 如果Clobber/Modify 为空,则其前面的冒号(:)必须省略。比如__asm__("mov
%%eax, %%ebx" : "=b"(foo) : "a"(inp) : )就是非法的写法;而__asm__("mov
%%eax,%%ebx":"=b"(foo):"a"(inp))则是正确的。
゚ 如果InstructionList为空,则Input,Output,Clobber/Modify可以不为空,也 可以为空。比如__asm__("":::"memory");和__asm__(""::);都是合法的写法。
゚ 如果Output,Input,Clobber/Modify都为空,Output,Input之前的冒号(:)既 可以省略,也可以不省略。如果都省略,则此汇编退化为一个基本内联汇编,否则, 仍然是一个带有C/C++表达式的内联汇编,此时"InstructionList"中的寄存器写法 要遵守相关规定,比如寄存器前必须使用两个百分号(%%),而不是像基本汇编格 式一样在寄存器前只使用一个百分号(%)。比如__asm__( " mov %%eax,
%%ebx"::);__asm__("mov%%eax,%%ebx":)和__asm__("mov%eax,%ebx")
都是正确的写法,而__asm__("mov%eax,%ebx"::);__asm__("mov%eax,
%ebx":)和__asm__("mov%%eax,%%ebx")都是错误的写法。
゚ 如果Input,Clobber/Modify为空,但Output不为空,Input前的冒号(:)既可以 省略,也可以不省略。比如__asm__( " mov %%eax, %%ebx" : "=b"(foo) : );
__asm__("mov%%eax,%%ebx":"=b"(foo))都是正确的。
゚ 如果后面的部分不为空,而前面的部分为空,则前面的冒号(:)都必须保留,否则无 法说明不为空的部分究竟是第几部分。比如,Clobber/Modify,Output 为空, 而Input不为空,则Clobber/Modify前的冒号必须省略(前面的规则),而Output 前的冒号必须为保留。如果Clobber/Modify不为空,而Input和Output都为空, 则Input和Output 前的冒号都必须保留。比如__asm__("mov%%eax,%%ebx"::
"a"(foo))和__asm__("mov%%eax,%%ebx":::"ebx")。
从上面的规则可以看到另外一个事实,区分一个内联汇编是基本格式的还是带有 C/C++表达式格式的,其规则在于在"InstructionList"后是否有冒号(:)的存在,如果没有则 是基本格式的,否则,则是带有C/C++表达式格式的。
两种格式对寄存器语法的要求不同:基本格式要求寄存器前只能使用一个百分号(%), 这一点和非内联汇编相同;而带有C/C++表达式格式则要求寄存器前必须使用两个百分号
(%%),其原因我们会在后面讨论。
1.Output
Output用来指定当前内联汇编语句的输出。我们看一看这个例子:
__asm__("movl%%cr0,%0":"=a"(cr0));
这个内联汇编语句的输出部分为"=r"(cr0),它是一个“操作表达式”,指定了一个输出操 作。我们可以很清楚得看到这个输出操作由两部分组成:括号里的部分(cr0)和引号引住的部 分"=a"。这两部分都是每一个输出操作必不可少的。括号里的部分是一个C/C++表达式, 用来保存内联汇编的一个输出值,其操作就等于C/C++的相等赋值cr0=output_value, 因此,括号中的输出表达式只能是C/C++的左值表达式,也就是说它只能是一个可以合法 的放在C/C++赋值操作中等号(=)左边的表达式。那么右值output_value从何而来呢?
答案是引号中的内容,被称作“操作约束”(OperationConstraint),在这个例子中操 作约束为"=a",它包含两个约束:等号(=)和字母a,其中等号(=)说明括号中左值表达式cr0 是一个Write-Only的,只能够被作为当前内联汇编的输入,而不能作为输入。而字母a是 寄存器EAX/AX/AL的简写,说明cr0的值要从eax寄存器中获取,也就是说cr0=eax, 最终这一点被转化成汇编指令就是movl%eax,address_of_cr0。现在你应该清楚了吧,操 作约束中会给出:到底从哪个寄存器传递值给cr0。
另外,需要特别说明的是,很多文档都声明,所有输出操作的操作约束必须包含一个等号(=),但GCC的文档中却很清楚的声明,并非如此。因为等号(=)约束说明当前的表达式 是一个Write-Only的,但另外还有一个符号— — 加号(+)用来说明当前表达式是一个
Read-Write的,如果一个操作约束中没有给出这两个符号中的任何一个,则说明当前表达式是Read-Only的。因为对于输出操作来说,肯定是必须是可写的,而等号(=)和加号(+) 都表示可写,只不过加号(+)同时也表示是可读的。所以对于一个输出操作来说,其操作约 束只需要有等号(=)或加号(+)中的任意一个就可以了。
二者的区别是:等号(=)表示当前操作表达式指定了一个纯粹的输出操作,而加号(+)则 表示当前操作表达式不仅仅只是一个输出操作还是一个输入操作。但无论是等号(=)约束还 是加号(+)约束所约束的操作表达式都只能放在Output域中,而不能被用在Input域中。
另外,有些文档声明:尽管GCC文档中提供了加号(+)约束,但在实际的编译中通不过;我不知道老版本会怎么样,我在GCC2.96中对加号(+)约束的使用非常正常。
我们通过一个例子看一下,在一个输出操作中使用等号(=)约束和加号(+)约束的不同。
$catexample2.c
intmain(int__argc,char*__argv[])
{
intcr0=5;
__asm____volatile__("movl%%cr0,%0"
:"=a"(cr0));
return0;
}
$gcc-Sexample2.c
$catexample2.s main:
pushl %ebp
movl %esp, %ebp subl $4,%esp
movl $5, -4(%ebp) #cr0=5
#APP
movl%cr0,%eax
#NO_APP
movl %eax,%eax
movl %eax,-4(%ebp) #cr0=%eax movl $0,%eax
leave ret
这个例子是使用等号(=)约束的情况,变量 cr0 被放在内存-4(%ebp)的位置,所以指令
mov %eax,-4(%ebp)即表示将%eax的内容输出到变量cr0中。
下面是使用加号(+)约束的情况:
$catexample3.c
intmain(int__argc,char*__argv[])
{
intcr0=5;
__asm____volatile__("movl%%cr0,%0"
:"+a"(cr0));
return0;
}
$gcc-Sexample3.c
$catexample3.s main:
pushl %ebp
movl %esp,%ebp subl $4,%esp
movl $5, -4(%ebp) #cr0= 5
movl -4(%ebp),%eax #input(%eax=cr0)
#APP
movl %cr0,%eax
#NO_APP
movl %eax,-4(%ebp) #output(cr0=%eax)
movl $0,%eax leave
ret
从编译的结果可以看出,当使用加号(+)约束的时候,cr0不仅作为输出,还作为输入, 所使用寄存器都是寄存器约束(字母a,表示使用eax寄存器)指定的。关于寄存器约束我们 后面讨论。
在Output域中可以有多个输出操作表达式,多个操作表达式中间必须用逗号(,)分开。 例如:
$catexample3.c
__asm__(
"movl %%eax, %0\n\t"
"pushl %%ebx\n\t"
"popl %1\n\t"
"movl %1,%2"
:"+a"(cr0),"=b"(cr1),"=c"(cr2));
2.Input
Input域的内容用来指定当前内联汇编语句的输入。我们看一看这个例子:
__asm__("movl %0, %%db7"::"a"(cpu->db7));
例中 Input 域的内容为一个表达式"a"[cpu->db7),被称作“输入表达式”,用来表示一 个对当前内联汇编的输入。
像输出表达式一样,一个输入表达式也分为两部分:带括号的部分(cpu->db7)和带引号 的部分"a"。这两部分对于一个内联汇编输入表达式来说也是必不可少的。
括号中的表达式cpu->db7是一个C/C++语言的表达式,它不必是一个左值表达式, 也就是说它不仅可以是放在C/C++赋值操作左边的表达式,还可以是放在C/C++赋值操作 右边的表达式。所以它可以是一个变量,一个数字,还可以是一个复杂的表达式(比如 a+b/c*d)。比如上例可以改为:
__asm__("movl %0, %%db7": : "a" (foo));
__asm__("movl %0, %%db7": : "a" (0x1000));
__asm__("movl %0, %%db7"::"a"(va*vb/vc));
引号号中的部分是约束部分,和输出表达式约束不同的是,它不允许指定加号(+)约束 和等号(=)约束,也就是说它只能是默认的Read-Only的。约束中必须指定一个寄存器约束, 例中的字母a表示当前输入变量cpu->db7要通过寄存器eax输入到当前内联汇编中。
我们看一个例子:
$catexample4.c
intmain(int__argc,char*__argv[])
{
intcr0 =5;
__asm____volatile__("movl%0,%%cr0"::"a"(cr0));
return0;
}
$gcc-Sexample4.c
$catexample4.s main:
pushl %ebp
movl %esp,%ebp subl $4,%esp
movl $5, -4(%ebp) #cr0=5
movl -4(%ebp),%eax #%eax=cr0
#APP
movl %eax,%cr0
#NO_APP
movl $0,%eax leave
ret
我们从编译出的汇编代码可以看到,在"InstructionList"之前,GCC按照我们的输入约 束"a",将变量cr0的内容装入了eax寄存器。
3.OperationConstraint
每一个Input和Output表达式都必须指定自己的操作约束Operation Constraint,我 们这里来讨论在80386平台上所可能使用的操作约束。
16 Developing Your OwnUnix-LikeOS on IBM PC
3.1 Register Constraint
当你当前的输入或输入需要借助一个寄存器时,你需要为其指定一个寄存器约束。你可 以直接指定一个寄存器的名字,比如:
__asm____volatile__("movl%0,%%cr0"::"eax"(cr0));
也可以指定一个缩写,比如:
__asm____volatile__("movl%0,%%cr0"::"a"(cr0));
如果你指定一个缩写,比如字母a,则GCC将会根据当前操作表达式中C/C++表达式 的宽度决定使用%eax,还是%ax或%al。比如:
unsignedshort__shrt;
__asm__("mov%0,%%bx"::"a"(__shrt));
由于变量__shrt是16-bitshort类型,则编译出来的汇编代码中,会让变量__shrt使用
%ex寄存器。编译结果为:
Movw -2(%ebp),%ax #%ax=__shrt
#APP
movl %ax,%bx
#NO_APP
无论是Input,还是Output操作表达式约束,都可以使用寄存器约束。
下表中列出了常用的寄存器约束的缩写。
约束 意义
r 表示使用一个通用寄存器,由 GCC 在%eax/%ax/%al,%ebx/%bx/%bl,
%ecx/%cx/%cl,%edx/%dx/%dl中选取一个GCC认为合适的。
g 表示使用任意一个寄存器,由GCC在所有的可以使用的寄存器中选取一个
GCC认为合适的。
q 表示使用一个通用寄存器,和约束r的意义相同。
a 表示使用%eax/%ax/%al
b 表示使用%ebx/%bx/%bl
c 表示使用%ecx/%cx/%cl
d 表示使用%edx/%dx/%dl
D 表示使用%edi/%di
S 表示使用%esi/%si
f 表示使用浮点寄存器
t 表示使用第一个浮点寄存器
u 表示使用第二个浮点寄存器
3.2 Memory Constraint
如果一个Input/Output 操作表达式的C/C++表达式表现为一个内存地址,不想借助 于任何寄存器,则可以使用内存约束。比如:
__asm__("lidt%0":"=m"(__idt_addr));
__asm__("lidt%0"::"m"(__idt_addr));
我们看一下它们分别被放在一个C源文件中,然后被GCC编译后的结果:
$catexample5.c
/*本例中,变量__sh被作为一个内存输入*/
intmain(int__argc,char*__argv[])
{
char*__sh=(char*)&__argc;
__asm____volatile__(
"lidt%0"
:/*nooutput*/
:"m"(__sh)
);
return0;
}
$gcc-Sexample5.c
$catexample5.s main:
pushl %ebp
movl %esp,%ebp subl $4,%esp
leal 8(%ebp),%eax
movl %eax,-4(%ebp) #sh=(char*)&__argc
#APP
lidt -4(%ebp)
#NO_APP
movl $0,%eax leave
ret
$catexample6.c
/*本例中,变量__sh被作为一个内存输出*/
intmain(int__argc,char*__argv[])
{
char*__sh=(char*)&__argc;
__asm____volatile__(
"lidt%0"
:"=m"(__sh)
);
return0;
}
$gcc-Sexample6.c
$catexample6.s main:
pushl %ebp
movl %esp,%ebp subl $4,%esp
leal 8(%ebp),%eax
movl %eax,-4(%ebp) # sh = (char*) &__argc
#APP
lidt -4(%ebp)
#NO_APP
movl $0,%eax leave
ret
首先,你会注意到,在这两个例子中,变量sh没有借助任何寄存器,而是直接参与了 指令lidt的操作。
其次,通过仔细观察,你会发现一个惊人的事实,两个例子编译出来的汇编代码是一样 的!虽然,一个例子中变量sh作为输入,而另一个例子中变量sh作为输出。这是怎么回事?
原来,使用内存方式进行输入输出时,由于不借助寄存器,所以GCC不会按照你的声 明对其作任何的输入输出处理。GCC只会直接拿来用,究竟对这个C/C++表达式而言是输 入还是输出,完全依赖与你写在"InstructionList"中的指令对其操作的指令。
由于上例中,对其操作的指令为lidt,lidt指令的操作数是一个输入型的操作数,所以 事实上对变量sh的操作是一个输入操作,即使你把它放在Output 域也不会改变这一点。 所以,对此例而言,完全符合语意的写法应该是将sh放在Input域,尽管放在Output域 也会有正确的执行结果。
所以,对于内存约束类型的操作表达式而言,放在Input域还是放在Output域,对编 译结果是没有任何影响的,因为本来我们将一个操作表达式放在Input域或放在Output域 是希望GCC能为我们自动通过寄存器将表达式的值输入或输出。既然对于内存约束类型的 操作表达式来说,GCC 不会自动为它做任何事情,那么放在哪儿也就无所谓了。但从程序 员的角度而言,为了增强代码的可读性,最好能够把它放在符合实际情况的地方。
约束 意义
M 表示使用系统所支持的任何一种内存方式,不需要借助寄存器。
3.3 ImmediatelyNumberConstraint
如果一个Input/Output 操作表达式的C/C++表达式是一个数字常数,不想借助于任 何寄存器,则可以使用立即数约束。
由于立即数在C/C++中只能作为右值,所以对于使用立即数约束的表达式而言,只能 放在Input域。比如:
__asm____volatile__("movl%0,%%eax"::"i"(100));
立即数约束很简单,也很容易理解,我们在这里就不再赘述。
约束 意义
i 表示输入表达式是一个立即数(整数),不需要借助任何寄存器。
F 表示输入表达式是一个立即数(浮点数),不需要借助任何寄存器。
3.4 Generic Constraint
约束 输入/输出 意义
g I,O 表示可以使用通用寄存器,内存,立即数等任何一种处理方式。
0-9 I 表示和第n个操作表达式使用相同的寄存器/内存。
通用约束g是一个非常灵活的约束,当程序员认为一个C/C++表达式在实际的操作中, 究竟使用寄存器方式,还是使用内存方式或立即数方式并无所谓时,或者程序员想实现一个灵活的模板,让GCC可以根据不同的C/C++表达式生成不同的访问方式时,就可以使用 通用约束g。比如:
#defineJUST_MOV(foo) \
__asm__("movl%0,%%eax"::"g"(foo))
JUST_MOV(100)和JUST_MOV(var)则会让编译器产生不同的代码。
intmain(int__argc,char*__argv[])
{
JUST_MOV(100);
return0;
}
编译后生成的代码为:
main:
pushl %ebp
movl %esp,%ebp
#APP
movl$100,%eax
#NO_APP
movl $0,%eax popl %ebp
ret
很明显这是立即数方式。而下一个例子:
intmain(int__argc,char*__argv[])
{
JUST_MOV(__argc);
return0;
}
经编译后生成的代码为:
main:
pushl %ebp
movl %esp,%ebp
#APP
movl 8(%ebp),%eax
#NO_APP
movl $0,%eax popl %ebp
ret
这个例子是使用内存方式。
一个带有C/C++表达式的内联汇编,其操作表达式被按照被列出的顺序编号,第一个 是0,第2个是1,依次类推,GCC最多允许有10个操作表达式。比如:
__asm__("popl%0\n\t"
"movl%1,%%esi\n\t"
"movl%2,%%edi\n\t"
:"=a"(__out)
:"r"(__in1),"r"(__in2));
此例中,__out所在的Output操作表达式被编号为0,"r"(__in1)被编号为1,"r"(__in2)
被编号为2。
再如:
__asm__("movl%%eax,%%ebx"::"a"(__in1),"b"(__in2));
此例中,"a"(__in1)被编号为0,"b"(__in2)被编号为1。
如果某个Input操作表达式使用数字0到9中的一个数字(假设为1)作为它的操作约 束,则等于向GCC声明:“我要使用和编号为1的Output操作表达式相同的寄存器(如果 Output操作表达式1使用的是寄存器),或相同的内存地址(如果Output操作表达式1使 用的是内存)”。上面的描述包含两个限定:数字0到数字9作为操作约束只能用在Input 操作表达式中,被指定的操作表达式(比如某个Input操作表达式使用数字1作为约束,那 么被指定的就是编号为1的操作表达式)只能是Output操作表达式。
由于GCC规定最多只能有10个Input/Output操作表达式,所以事实上数字9作为操 作约束永远也用不到,因为Output操作表达式排在Input操作表达式的前面,那么如果有一个Input操作表达式指定了数字9作为操作约束的话,那么说明Output操作表达式的数 量已经至少为10个了,那么再加上这个Input 操作表达式,则至少为11个了,以及超出 GCC的限制。
5、Modifier Characters(修饰符)
等号(=)和加号(+)用于对Output操作表达式的修饰,一个Output操作表达式要么被等 号(=)修饰,要么被加号(+)修饰,二者必居其一。使用等号(=)说明此Output操作表达式是 Write-Only的,使用加号(+)说明此Output操作表达式是Read-Write的。它们必须被放在 约束字符串的第一个字母。比如"a="(foo)是非法的,而"+g"(foo)则是合法的。
当使用加号(+)的时候,此Output 表达式等价于使用等号(=)约束加上一个Input 表达 式。比如
__asm__("movl%0,%%eax;addl%%eax,%0": "+b"(foo))
等价于
__asm__("movl%0,%%eax;addl%%eax,%0":"+b"(foo))
但如果使用后一种写法,"InstructionList"中的别名也要相应的改动。关于别名,我们
后面会讨论。
像等号(=)和加号(+)修饰符一样,符号(&)也只能用于对Output操作表达式的修饰。当 使用它进行修饰时,等于向GCC声明:"GCC不得为任何Input操作表达式分配与此Output 操作表达式相同的寄存器"。其原因是&修饰符意味着被其修饰的Output操作表达式要在所 有的Input操作表达式被输入前输出。我们看下面这个例子:
intmain(int__argc,char*__argv[])
{
int__in1=8,__in2=4,__out=3;
__asm__("popl%0\n\t"
"movl%1,%%esi\n\t"
"movl%2,%%edi\n\t"
:"=a"(__out)
:"r"(__in1),"r"(__in2));
return0;
}
此例中,%0 对应的就是Output 操作表达式,它被指定的寄存器是%eax,整个 InstructionList的第一条指令popl%0,编译后就成为popl%eax,这时%eax的内容已经 被修改,随后在InstructionList后,GCC会通过movl%eax,address_of_out 这条指令将
%eax的内容放置到Output变量__out中。对于本例中的两个Input操作表达式而言,它们 的寄存器约束为"r",即要求GCC为其指定合适的寄存器,然后在Instruction List之前将
__in1和__in2的内容放入被选出的寄存器中,如果它们中的一个选择了已经被__out 指定的 寄存器%eax,假如是__in1,那么GCC 在Instruction List 之前会插入指令movl address_of_in1,%eax,那么随后popl%eax指令就修改了%eax的值,此时%eax中存放的 已经不是Input变量__in1的值了,那么随后的movl%1,%%esi指令,将不会按照我们的 本意— — 即将__in1的值放入%esi中— — 而是将__out的值放入%esi中了。
下面就是本例的编译结果,很明显,GCC为__in2选择了和__out相同的寄存器%eax, 这与我们的初衷不符。
main:
pushl %ebp
movl %esp,%ebp subl $12,%esp movl $8, -4(%ebp) movl $4, -8(%ebp) movl $3, -12(%ebp)
movl -4(%ebp),%edx #__in1使用寄存器%edx movl -8(%ebp),%eax #__in2使用寄存器%eax
#APP
popl %eax
movl %edx,%esi movl %eax,%edi
#NO_APP
movl %eax,%eax
movl %eax,-12(%ebp) #__out使用寄存器%eax movl $0,%eax
leave
为了避免这种情况,我们必须向GCC声明这一点,要求GCC为所有的Input操作表 达式指定别的寄存器,方法就是在Output操作表达式"=a"(__out)的操作约束中加入&约束, 由于GCC规定等号(=)约束必须放在第一个,所以我们写作"=&a"(__out)。
下面是我们将&约束加入之后编译的结果:
main:
pushl %ebp
movl %esp,%ebp subl $12,%esp movl $8,-4(%ebp) movl $4,-8(%ebp) movl $3,-12(%ebp)
movl -4(%ebp),%edx #__in1使用寄存器%edx movl -8(%ebp),%eax
movl %eax,%ecx #__in2使用寄存器%ecx
#APP
popl %eax
movl %edx,%esi movl %ecx,%edi
#NO_APP
movl %eax,%eax
movl %eax,-12(%ebp) #__out使用寄存器%eax movl $0,%eax
leave ret
OK!这下好了,完全与我们的意图吻合。
如果一个Output操作表达式的寄存器约束被指定为某个寄存器,只有当至少存在一个 Input 操作表达式的寄存器约束为可选约束时,(可选约束的意思是可以从多个寄存器中选 取一个,或使用非寄存器方式),比如"r"或"g"时,此Output操作表达式使用&修饰才有意 义。如果你为所有的Input操作表达式指定了固定的寄存器,或使用内存/立即数约束,则 此Output操作表达式使用&修饰没有任何意义。比如:
__asm__("popl%0\n\t"
"movl%1,%%esi\n\t"
"movl%2,%%edi\n\t"
:"=&a"(__out)
:"m"(__in1),"c"(__in2));
此例中的Output操作表达式完全没有必要使用&来修饰,因为__in1和__in2都被指定 了固定的寄存器,或使用了内存方式,GCC无从选择。
但如果你已经为某个Output操作表达式指定了&修饰,并指定了某个固定的寄存器, 你就不能再为任何Input操作表达式指定这个寄存器,否则会出现编译错误。比如:
__asm__("popl%0\n\t"
"movl%1,%%esi\n\t"
"movl%2,%%edi\n\t"
:"=&a"(__out)
:"a"(__in1),"c"(__in2));
本例中,由于__out已经指定了寄存器%eax,同时使用了符号&修饰,则再为__in1指 定寄存器%eax就是非法的。
反过来,你也可以为Output 指定可选约束,比如"r","g"等,让GCC为其选择到底使 用哪个寄存器,还是使用内存方式,GCC在选择的时候,会首先排除掉已经被Input操作 表达式使用的所有寄存器,然后在剩下的寄存器中选择,或干脆使用内存方式。比如:
__asm__("popl%0\n\t"
"movl%1,%%esi\n\t"
"movl%2,%%edi\n\t"
:"=&r"(__out)
: "a" (__in1), "c"(__in2));
本例中,由于__out指定了约束"r",即让GCC为其决定使用哪一格寄存器,而寄存器
%eax和%ecx已经被__in1和__in2使用,那么GCC在为__out选择的时候,只会在%ebx
和%edx中选择。
前3个修饰符只能用在Output 操作表达式中,而百分号[%]修饰符恰恰相反,只能用 在Input操作表达式中,用于向GCC声明:“当前Input操作表达式中的C/C++表达式可 以和下一个Input操作表达式中的C/C++表达式互换”。这个修饰符号一般用于符合交换律 运算,比如加(+),乘(*),与(&),或(|)等等。我们看一个例子:
intmain(int__argc,char*__argv[])
{
int__in1=8,__in2=4,__out=3;
__asm__("addl%1,%0\n\t"
:"=r"(__out)
:"%r"(__in1),"0"(__in2));
return0;
}
在此例中,由于指令是一个加法运算,相当于等式__out=__in1+__in2,而它与等式
__out=__in2+__in1没有什么不同。所以使用百分号修饰,让GCC知道__in1和__in2可 以互换,也就是说GCC可以自动将本例的内联汇编改变为:
__asm__("addl%1,%0\n\t"
:"=r"(__out)
:"%r"(__in2),"0"(__in1));
下表总结了各种修饰符的意义:
修饰符 输入/输出 意义
= O 表示此Output操作表达式是Write-Only的。
+ O 表示此Output操作表达式是Read-Write的。
& O 表示此Output操作表达式独占为其指定的寄存器。
% I 表示此Input 操作表达式中的C/C++表达式可以和下一 个Input操作表达式中的C/C++表达式互换。
4. 占位符
什么叫占位符?我们看一看下面这个例子:
__asm__("addl%1,%0\n\t"
:"=a"(__out)
:"m"(__in1),"a"(__in2));
这个例子中的%0和%1就是占位符。每一个占位符对应一个Input/Output操作表达式。 我们在之前已经提到,GCC规定一个内联汇编语句最多可以有10个Input/Output操作表 达式,然后按照它们被列出的顺序依次赋予编号0到9。对于占位符中的数字而言,和这些 编号是对应的。
由于占位符前面使用一个百分号(%),为了区别占位符和寄存器,GCC 规定在带有
C/C++表达式的内联汇编中,"InstructionList"中直接写出的寄存器前必须使用两个百分号
(%%)。
GCC对其进行编译的时候,会将每一个占位符替换为对应的Input/Output 操作表达 式所指定的寄存器/内存地址/立即数。比如在上例中,占位符%0对应Output操作表达式
"=a"(__out),而"=a"(__out)指定的寄存器为%eax,所以把占位符%0 替换为%eax,占位符
%1对应Input 操作表达式"m"(__in1),而"m"(__in1)被指定为内存操作,所以把占位符%1
替换为变量__in1的内存地址。
也许有人认为,在上面这个例子中,完全可以不使用%0,而是直接写%%eax,就像这 样:
__asm__("addl%1,%%eax\n\t"
: "=a"(__out)
:"m"(__in1),"a"(__in2));
和上面使用占位符%0 没有什么不同,那么使用占位符%0 就没有什么意义。确实,两 者生成的代码完全相同,但这并不意味着这种情况下占位符没有意义。因为如果不使用占位符,那么当有一天你想把变量__out的寄存器约束由a改为b时,那么你也必须将addl指 令中的%%eax改为%%ebx,也就是说你需要同时修改两个地方,而如果你使用占位符,你 只需要修改一次就够了。另外,如果你不使用占位符,将不利于代码的清晰性。在上例中,如果你使用占位符,那么你一眼就可以得知,addl 指令的第二个操作数内容最终会输出到 变量__out中;否则,如果你不用占位符,而是直接将addl指令的第2个操作数写为%%eax, 那么你需要考虑一下才知道它最终需要输出到变量__out中。这是占位符最粗浅的意义。毕 竟在这种情况下,你完全可以不用。
但对于这些情况来说,不用占位符就完全不行了:
首先,我们看一看上例中的第1个Input操作表达式"m"(__in1),它被GCC替换之后, 表现为addladdress_of_in1,%%eax,__in1的地址是什么?编译时才知道。所以我们完全 无法直接在指令中去写出__in1的地址,这时使用占位符,交给GCC在编译时进行替代, 就可以解决这个问题。所以这种情况下,我们必须使用占位符。
其次,如果上例中的Output 操作表达式"=a"(__out)改为"=r"(__out),那么__out 在究 竟使用那么寄存器只有到编译时才能通过GCC来决定,既然在我们写代码的时候,我们不 知道究竟哪个寄存器被选择,我们也就不能直接在指令中写出寄存器的名称,而只能通过占位符替代来解决。
5.Clobber/Modify
有时候,你想通知GCC当前内联汇编语句可能会对某些寄存器或内存进行修改,希望 GCC在编译时能够将这一点考虑进去。那么你就可以在Clobber/Modify 域声明这些寄存 器或内存。
这种情况一般发生在一个寄存器出现在"Instruction List",但却不是由 Input/Output 操作表达式所指定的,也不是在一些 Input/Output 操作表达式使用"r","g"约束时由 GCC 为其选择的,同时此寄存器被"InstructionList"中的指令修改,而这个寄存器只是供当前内 联汇编临时使用的情况。比如:
__asm__("movl%0,%%ebx"::"a"(__foo):"bx");
寄存器%ebx 出现在"Instruction List 中",并且被movl 指令修改,但却未被任何 Input/Output操作表达式指定,所以你需要在Clobber/Modify域指定"bx",以让GCC知 道这一点。
因为你在Input/Output操作表达式所指定的寄存器,或当你为一些Input/Output操 作表达式使用"r","g"约束,让GCC为你选择一个寄存器时,GCC对这些寄存器是非常清楚 的— — 它知道这些寄存器是被修改的,你根本不需要在Clobber/Modify域再声明它们。但 除此之外,GCC 对剩下的寄存器中哪些会被当前的内联汇编修改一无所知。所以如果你真 的在当前内联汇编指令中修改了它们,那么就最好在Clobber/Modify中声明它们,让GCC 针对这些寄存器做相应的处理。否则有可能会造成寄存器的不一致,从而造成程序执行错误。
在Clobber/Modify域中指定这些寄存器的方法很简单,你只需要将寄存器的名字使用双引号("")引起来。如果有多个寄存器需要声明,你需要在任意两个声明之间用逗号隔开。 比如:
__asm__("movl%0,%%ebx;popl%%ecx"
:/*nooutput*/
:"a"(__foo):"bx","cx");
这些串包括:
声明的串 代表的寄存器
"al","ax","eax" %eax
"bl","bx","ebx" %ebx
"cl","cx","ecx" %ecx
"dl","dx","edx" %edx
"si","esi" %esi
"di","edi" %edi
由上表可以看出,你只需要使用"ax","bx","cx","dx","si","di"就可以了,因为其它的都和 它们中的一个是等价的。
如果你在一个内联汇编语句的Clobber/Modify域向GCC声明某个寄存器内容发生了 改变,GCC 在编译时,如果发现这个被声明的寄存器的内容在此内联汇编语句之后还要继 续使用,那么GCC会首先将此寄存器的内容保存起来,然后在此内联汇编语句的相关生成 代码之后,再将其内容恢复。我们来看两个例子,然后对比一下它们之间的区别。
这个例子中声明了寄存器%ebx内容发生了改变:
$catexample7.c
intmain(int__argc,char*__argv[])
{
intin=8;
__asm__("addl%0,%%ebx"
:/*nooutput*/
:"a"(in):"bx");
return0;
}
$gcc-O-Sexample7.c
$catexample7.s main:
pushl %ebp
movl %esp,%ebp
pushl %ebx #%ebx内容被保存
movl $8,%eax
#APP
addl %eax,%ebx
#NO_APP
movl $0,%eax
movl (%esp),%ebx #%ebx内容被恢复
leave ret
下面这个例子的C源码与上一个例子除了没有声明%ebx寄存器发生了改变之外,其它 都相同。
$catexample8.c
intmain(int__argc,char*__argv[])
{
intin=8;
__asm__("addl%0,%%ebx"
:/*nooutput*/
:"a"(in));
return0;
}
$ gcc-O-Sexample8.c
$catexample8.s main:
pushl %ebp
movl %esp,%ebp movl $8,%eax
#APP
addl%eax,%ebx
#NO_APP
movl $0,%eax popl %ebp
ret
仔细对比一下example7.s和example8.s,你就会明白在Clobber/Modify域声明一个 寄存器的意义。
另外需要注意的是,如果你在Clobber/Modify域声明了一个寄存器,那么这个寄存器 将不能再被用做当前内联汇编语句的Input/Output 操作表达式的寄存器约束,如果 Input/Output 操作表达式的寄存器约束被指定为"r"或"g",GCC也不会选择已经被声明在 Clobber/Modify中的寄存器。比如:
__asm__("movl%0,%%ebx"
::"a"(__foo):"ax","bx");
此例中,由于Output操作表达式"a"(__foo)的寄存器约束已经指定了%eax寄存器,那 么再在Clobber/Modify域中指定"ax"就是非法的。编译时,GCC会给出编译错误。
除了寄存器的内容会被改变,内存的内容也可以被修改。如果一个内联汇编语句
"InstructionList"中的指令对内存进行了修改,或者在此内联汇编出现的地方内存内容可能发生改变,而被改变的内存地址你没有在其Output 操作表达式使用"m"约束,这种情况下 你需要使用在Clobber/Modify域使用字符串"memory"向GCC声明:“在这里,内存发生 了,或可能发生了改变”。例如:
void*memset(void*__s,char__c,size_t__count)
{
__asm__("cld\n\t"
"rep\n\t"
"stosb"
:/*nooutput*/
:"a"(__c),"D"(__s),"c"(__count)
:"cx","di","memory");
return__s;
}
此例实现了标准函数库memset,其内联汇编中的stosb对内存进行了改动,而其被修 改的内存地址s被指定装入%edi,没有任何Output操作表达式使用了"m"约束,以指定内 存地址s处的内容发生了改变。所以在其Clobber/Modify域使用"memory"向GCC声明: 内存内容发生了变动。
如果一个内联汇编语句的Clobber/Modify域存在"memory",那么GCC会保证在此内 联汇编之前,如果某个内存的内容被装入了寄存器,那么在这个内联汇编之后,如果需要使 用这个内存处的内容,就会直接到这个内存处重新读取,而不是使用被存放在寄存器中的拷 贝。因为这个时候寄存器中的拷贝已经很可能和内存处的内容不一致了。
这只是使用"memory"时,GCC 会保证做到的一点,但这并不是全部。因为使用
"memory"是向GCC 声明内存发生了变化,而内存发生变化带来的影响并不止这一点。比 如我们在前面讲到的例子:
intmain(int__argc,char*__argv[])
{
int* __p = (int*)__argc;
(*__p)=9999;
__asm__("":::"memory");
if((*__p)==9999)
return5;
return(*__p);
}
本例中,如果没有那条内联汇编语句,那个if语句的判断条件就完全是一句废话。GCC 在优化时会意识到这一点,而直接只生成return 5的汇编代码,而不会再生成if语句的相 关代码,而不会生成return(*__p)的相关代码。但你加上了这条内联汇编语句,它除了声明 内存变化之外,什么都没有做。但GCC此时就不能简单的认为它不需要判断都知道(*__p) 一定与9999相等,它只有老老实实生成这条if语句的汇编代码,一起相关的两个return语 句相关代码。
当一个内联汇编指令中包含影响eflags寄存器中的条件标志(也就是那些Jxx等跳转指 令要参考的标志位,比如,进位标志,0标志等),那么需要在Clobber/Modify域中使用"cc" 来声明这一点。这些指令包括adc,div,popfl,btr,bts等等,另外,当包含call指令时, 由于你不知道你所call的函数是否会修改条件标志,为了稳妥起见,最好也使用"cc"。
我很少在相关资料中看到有关"cc"的确切用法,只有一份文档提到了它,但还不是i386 平台的,只是说"cc"是处理器平台相关的,并非所有的平台都支持它,但即使在不支持它的 平台上,使用它也不会造成编译错误。我做了一些实验,但发现使用"cc"和不使用"cc"所生 成的代码没有任何不同。但Linux2.4的相关代码中用到了它。如果谁知道在i386平台上"cc" 的细节,请和我联系。
另外,还可以在Clobber/Modify域指定数字0到9,以声明第n个Input/Output操 作表达式所使用的寄存器发生了变化,但正如我们在前面所提到的,如果你为某个 Input/Output 操作表达式指定了寄存器,或使用"g","r"等约束让 GCC 为其选择寄存器, GCC 已经知道哪个寄存器内容发生了变化,所以这么做没有什么意义;我也作了相关的试 验,没有发现使用它会对GCC 生成的汇编代码有任何影响,至少在i386 平台上是这样。 Linux 2.4的所有i386平台相关内联汇编代码中都没有使用这一点,但S390平台相关代码 中有用到,但由于我对S390汇编没有任何概念,所以,也不知道这么做的意义何在。
2007年9月17日星期一
2007年8月30日星期四
2007年8月26日星期日
QEMU简单模拟Plan9
2007年8月18日星期六
2007年8月15日星期三
2007年8月13日星期一
About Plan9 Shell-rc
I plan to translate some information of Plan9,I use the key of "Plan9翻译“ from Google search,that I found "寒蝉退士" had translated "rc-the Plan9 Shell".
1 介绍
rc 在精神上类似于 UNIX 的 Bourne shell 但在细节上有很多不同。本文使用很多小例子和一些大点的例子来描述 rc 的首要特征。假定读者熟悉 Bourne shell。
2 简单命令
对于最简单的使用,rc 有着 Bourne-shell 用户很熟悉的语法。下列命令都会如愿而行:
datecat /lib/news/buildwho >user.nameswho >>user.nameswc fileecho [a-f]*.cwho wcwho; datevc *.c &mk && v.out /*/bin/fb/*rm -r junk echo rm failed!
3 引用
包含一个空格或 rc 的其他语法字符之一的参数必须包围在单引号(')之中:
rm 'odd file name'
在已被引号括起来的参数中的单引号必须是双重的:
echo 'How''s your father?'
4 模式
包含字符 * ? [ 中任何一个的未被引号括起来的参数都是同文件名字匹配的模式。字符 * 匹配字符的任意序列,字符 ? 匹配任何单一字符,而 [class] 匹配在 class 中的任意字符,除非 class 的第一个字符是 ~,在这种情况下取这个类的补集。class 也可以包含用 - 分隔的成对的字符,它表示词法上在两者之间的所有字符。字符 / 在模式中必须显式的出现,路径名分量 . 和 .. 也是如此。模式被替代为一个参数的列表,每个匹配的路径名字都是一个参数,作为例外,没有匹配的名字的模式不被替代为空列表;而是保持为自身。
5 变量
UNIX 的 Bourne shell 提供字符串值的变量。rc 提供的变量值是参数的列表 - 就是说,字符串的数组。这是在 rc 和传统的 UNIX 命令解释器之间的原则性区别。变量可以通过键入来给出值,例如:
path=(. /bin)user=tdfont=/lib/font/bit/pelm/ascii.9.font
圆括号指示赋予 path 的值是两个字符串的一个列表。变量 user 和 font 被赋予包含一个单一字符串的列表。
通过在变量的名字前面前导一个 $ 可以把变量的值替换到一个命令中,比如:
echo $path
如果 path 已经做如上设置,它将等价于
echo . /bin
变量可以有数字或数字的列表作为下标,比如:
echo $path(2)echo $path(2 1 2)
它们等价于
echo /binecho /bin . /bin
在左圆括号和变量名字之间必须没有空格分隔;否则,这些下标将被当作一个独立的圆括号中的列表。
在变量中的字符串的数目可以用 $# 操作符确定。例如,
echo $#path
对于上面的例子将输出 2。
下面的两个赋值有着微妙的不同:
empty=()null=''
第一个设置 empty 为不包含字符串的一个列表。第二个设置 null 为包含一个单一的字符串的一个列表,但是这个字符串不包含字符。
尽管它们或多或少是同样的东西(在 Bourne 的 shell 中,它们是不可区分的),它们在几乎所有情况下都表现的很不同。在其他东西当中
echo $#empty
打印 0,而
echo $#null
打印 1。
所有未设置的变量都有值 ()。
偶尔的,把变量的值作为一个单一的字符串是方便的。通过 $" 操作符把字符串的所有元素联接到一个单一字符串中,在元素之间带有空格。这样,如果我们设置
list=(How now brown cow)string=$"list
则
echo $list
和
echo $string
二者导致相同的输出,就是:
How now brown cow
而
echo $#list $#string
将输出
4 1
因为 $list 有四个成员,而 $string 有一个单一的成员,带有三个空格分隔它的单字。
6 参数
当 rc 从一个文件读取它的输入的时候,这个文件有权访问在 rc 的命令行上提供的参数。变量 $* 最初被赋值为参数的列表。名字 $1、$2 等是 $*(1)、$*(2) 等的同义字。此外,$0 是一个文件的名字,rc 的输入从其中读入。
7 联接
rc 有一个字符串联接操作符,插入符 ^,用来建造由多个片断组成的参数。
echo hully^gully
完全等价于
echo hullygully
假定变量 i 包含命令的名字。则
vc $i^.cvl -o $1 $i^.v
可以编译这个命令的源代码,把结果留在适当的文件中。
联接可以应用于列表之上。下面的
echo (a b c)^(1 2 3)src=(main subr io)cc $src^.c
等价于
echo a1 b2 c3cc main.c subr.c io.c
在细节上,规则是: 如果 ^ 的两个操作数(operand)是有相同的非零个字符串的列表,则成对的连接它们。否则,如果操作数之一是一个单一的字符串,它依次与另一个操作数的每个参数联接。操作数的任何其他组合都是错误的。
8 无约束的插入符
用户需求决定了 rc 在特定位置插入插入符,来使语法看起来更像 Bourne shell。例如,下面的:
cc -$flags $stems.c
等价于
cc -^$flags $stems^.c
一般的,rc 会在不由空白分隔的两个参数之间插入 ^。特别是,只要 $ ' ` 跟随着一个被引号括起来的或未被引号括起来的字,或者一个未被引号括起来的字跟随着一个被引号括起来的字并且无空白或 Tab 介于其间,就在二者之间插入一个隐含的 ^。如果立即跟随着一个 $ 的一个未被引号括起来的字包含不是一个字母下划线或 * 的字符,在第一个这样的字符之前插入 ^。
9 命令替换
通常用来从一个命令的输出建造一个参数列表。rc 允许一个命令,包围在花括号中并前导着一个反引号 `{...},出现在需要参数的任何地方。执行这个命令并捕获它的标准输出。使用存储在变量 ifs 中的字符来把输入分解到参数中。例如,
cat
`{ls -trsed 10q}
将按时间次序连接当前目录中十个最旧的文件,假定缺省 ifs 设置为空格、Tab 和换行符。
10 管道线分支
一般的管道表示法对于几乎所有情况都是足够用了。有时后拥有非线性的管道线是有用的。比树状结构更一般化的管道线拓扑结构可能要求任意大的管道缓冲区,甚至更坏、导致死锁。rc 拥有某种非线性的树状管道线的语法。例如,
cmp <{old} <{new} 将回归测试一个命令的新版本。跟随着在花括号中的命令的 < 或 >,导致运行这个命令并把它的标准输出或输入连结到一个管道上。父命令(本例中的 cmp)开始于连结到某个文件描述符或其他东西上的管道线的另一端,并带有在打开的时候会连接到管道的一个参数(例如,/dev/fd/6)(译注:一个命名管道的名字)。一些命令不准备处理关闭就不可搜寻的输入文件。例如 diff 需要读取它的输入两次。
11 退出状态
在命令退出的时候,它向执行它的进程返回一个状态。在 Plan 9 中,状态是描述错误状况的一个字符串。在正常终止的使用它是空的。
rc 把命令退出状态捕获到变量 $status 中。对于一个简单命令 $status 的值就如同上面描述的一样。对于一个管道线 $status 被设置为管道线构件的状态的联接、并带有 字符作为分隔符。
rc 有多种控制流,其中很多控制流以前面执行的命令返回的状态作为条件。所有只包含 0 和 的 $status 拥有逻辑值 true。任何其他状态都是 false。
12 命令组合
包围在 {} 中的命令序列可以用在需要命令的任何地方。例如:
{sleep 3600;echo 'Time''s up!'}&
将在后台等待一小时,接着打印一个消息。不带花括号,
sleep 3600;echo 'Time''s up!'&
将锁住终端一个小时,接着在后台打印这个消息。
13 控制流 - for
为键入的列表中的每个成员执行一次命令,例如:
for(i in printf scanf putchar) look $i /usr/td/lib/dw.dat
它在给定文件中查找单字 printf、scanf 和 putchar 中的每一个。一般形式是
for(name in list) command
或
for(name) command
在第一种情况下为 list 的每个成员执行一次 command,并把这个成员赋予变量 name。如果缺少子句“in list”,假定为“in $*”。
14 条件执行 - if
rc 还提供一个通用 if 语句。例如:
for(i in *.c) if(cpp $i >/tmp/$i) vc /tmp/$i
在 cpp 处理而没有错误的每个 C 源程序上运行 C 编译器。‘if not’语句提供双尾(two-tailed)条件。例如:
for(i){ if(test -f /tmp/$i) echo $i already in /tmp if not cp $i /tmp}
它是在 $* 中的每个文件之上的循环,把不存在于 /tmp 中的文件复制到这里,并对那些已经存在于这里的文件打印一个消息。
15 控制流 - while
rc 的 while 语句看起来如下:
while(newer subr.v subr.c) sleep 5
它等待直到 subr.v 比 subr.c 新,原因大概是 C 编译处理完了它。
如果控制命令是空的,循环将不会终止。这样,
while() echo y
将模拟 yes 命令。
16 控制流 - switch
rc 提供一个 switch 语句来在任意字符串上做模式匹配。它的一般形式是
switch(word){case pattern ... commands case pattern ... commands ...}
Rc 依次尝试对在每个 case 语句中的模式去匹配这个 word。除了 / 和 . 和 .. 不需要被显式的匹配之外,模式与用于文件名匹配的一样。
如果有任何模式匹配,执行紧随这个 case 之后直到下一个 case 之前(或 switch 的结束处)的命令,并且这个 switch 的执行完成。例如,
switch($#*){case 1 cat >>$1case 2 cat >>$2 <$1case * echo 'Usage: append [from] to'} 是一个 append 命令。调用时带有一个文件参数,它把它的标准输入添加到指名的文件中。带有两个参数时,把第一个文件添加到第二个文件中。任何其他参数数目引发一个错误消息。 内置 ~ 命令也匹配模式,并且经常比 switch 更加简洁。它的参数是一个字符串和一个模式的列表。只有在所有模式都匹配这个字符串的时候,设置 $status 为真。下列例子为 man(1) 命令处理选项参数: opt=()while(~ $1 -* [1-9] 10){ switch($1){ case [1-9] 10 sec=$1 secn=$1 case -f c=f s=f case -[qwnt] cmd=$1 case -T* T=$1 case -* opt=($opt $1) } shift} 17 函数
可以通过如下键入来定义函数
fn name { commands }
此后,在遇到叫做 name 的命令的时候,命令的参数列表的余下部分将赋给 $* ,接着 rc 将执行 commands。在完成的时候恢复 $* 的值。例如:
fn g { grep $1 *.[hcyl]}
定义 g pattern (用来查找在当前目录中的所有程序源文件中查找 pattern 的出现)。
通过写
fn name
而没有函数体来删除函数定义。
18 命令执行
rc 做许多事情来执行一个简单命令。如果这个命令名字是使用 fn 定义的一个函数的名字,则执行这个函数。否则,如果它是一个内置命令的名字,rc 直接执行这个内置命令。否则,查找在变量 $path 提及的目录,直到找到一个可执行文件。在 Plan 9 中不鼓励 $path 变量的广泛使用。转而,使用缺省(. /bin) 和把你需要的绑定入 /bin。
19 内置命令
许多命令由 rc 内部执行,原因是不这样就难于实现。
. [-i] file ...
执行来自 file 的命令。在此期间 $* 设置为紧随 file 之后的参数列表的余下部分。使用 $path 来查找 file。选项 -i 指示交互式输入 在读取每个命令之前打印一个提示(在 $prompt 找到)。
builtin command ...
除了忽略叫做 command 的任何函数之外,正常的执行 command。例如,
fn cd{ builtin cd $* && pwd}
定义 cd 内置命令(见后)的一个替代者,它宣布新目录的全名。
cd [dir]
改变当前目录到 dir。缺省参数是 $home。$cdpath 是在其中查找 dir 的位置的一个列表。
eval [arg ...]
参数被联接(由空格分隔)到一个字符串中,读为给 rc 的输入并执行它。例如,
x='$y'y=Doodyeval echo Howdy, $x
将回显
Howdy, Doody
因为在替换了 $x 之后,eval 的参数是
echo Howdy, $y
exec [command ...]
Rc 用给定命令替代自身。有点像一个 goto - rc 不等待命令退出,并且不回来读更多的命令。
exit [status]
Rc 立即退出并返回给定状态。如果未给出,则使用 $status 的当前的值。
flag f [+-]
这个命令操纵并测试命令行标志(后面描述).
flag f +
设置标志 f。
flag f -
清除标志 f。
flag f
测试标志 f,恰当的设置 $status。所以
if(flag x) flag v +
如果已经设置了 -x 标志则设置 -v 标志。
rfork [nNeEsfF]
它使用 Plan 9 rfork 系统入口来把 rc 放置到带有下列属性的新进程组之中:
标志
名字
功能
n
RFNAMEG
制做父名字空间的一个复本
N
RFCNAMEG
开始于一个新的空名字空间
e
RFENVG
制做父环境的一个复本
E
RFCENVG
开始于一个新的空环境
s
RFNOTEG
制做一个新的通知组
f
RFFDG
制做父文件描述符空间的一个复本
F
RFCFDG
开始于一个新的空文件描述符空间
程序员手册的 fork(2) 章节详细描述这些属性。
shift [n]
删除 $* 的前 n(缺省为 1)个元素。
wait [pid]
等待有给定 pid 的进程退出。如果未给出 pid, 等待所有未完结的进程。
whatis name ...
以适合输入到 rc 的形式打印每个名字的值。输出的是到变量的赋值,函数的定义,到对一个内置命令的 builtin 的调用,或一个二进制程序的路径名字。例如,
whatis path g cd who
可能打印
path=(. /bin)fn g {gre -e $1 *.[hycl]}builtin cd/bin/who
~ subject pattern ...
依次针对每个 pattern 匹配 subject。在匹配时,$status 设置为真。否则,设置为‘不匹配’。模式同于文件名匹配的。在执行 ~ 命令之前模式不受文件名替代的支配,所以它们不需要被包围在引号之中,当然,除非想要的是对 * [ or ? 的一个文字上的匹配。例如
~ $1 ?
匹配任何单一字符,而
~ $1 '?'
只匹配一个文字问号。
20 高级 I/O 重定向
rc 允许在 < 或 > 之后的方括号 [ ] 中指定文件描述符,来做不是 0 和 1(标准输入和输出)的文件描述符的重定向。例如,
vc junk.c >[2]junk.diag
把编译器的诊断从标准错误保存到 junk.diag 中。
通过如下键入,这些文件描述符可以被替代为一个已经打开了的文件的 dup(2) 意义上的复制:
vc junk.c >[2=1]
它用文件描述符 1 的一个复件替换文件描述符 2。在与其它重定向联合时非常有用,比如
vc junk.c >junk.out >[2=1]
从左至右求值重定向,所以重定向文件描述符 1 到 junk.out,接着把文件描述符 2 指向相同的文件。与之相对,
vc junk.c >[2=1] >junk.out
重定向文件描述符 2 到文件描述符 1(大概是终端), 并接着定向文件描述符 1 到一个文件。在第一种情况下,标准和诊断输出将被混合到 junk.out 中。在第二种情况下,诊断输出将出现在终端上,而标准输出将发送到文件。
可以使用带有右手边为空的复制记号(notation)关闭文件描述符。例如,
vc junk.c >[2=]
将丢弃来自编译的诊断。
通过如下键入,可以派遣(send)任意的文件描述符经过管道,
vc junk.c [2] grep -v '^$'
它从 C 编译器的错误输出中删除空行。注意 grep 的输出仍出现在文件描述符 1 上。
有时你可能希望把一个管道的输入端连接到不是 0 的某个文件描述符。记号
cmd1 [5=19] cmd2
建立一个管道线,并且 cmd1'的文件描述符 5 通过管道连接到 cmd2'的文件描述符 19。
21 立即文档
rc 过程可以包含叫做“立即文档”数据,它被提供为给命令的输入,比如在下面这版本的 tel 命令中
for(i) grep $i <22>捕获通知
在接收到来自终端的中断的时候,rc 脚本通常终止。以通常的方式定义带有小写的一个 UNIX 信号名字的函数,但在 rc 收到对应的通知的时候调用。程序员手册的 notify(2) 章节详细的讨论了通知。有意思的通知有:
sighup
通知是‘hangup’。Plan 9 在终端从 rc 断开的时候发送这个通知。
sigint
通知是‘interrupt’,通常在终端上键入中断字符(ASCII DEL)的时候发出。
sigterm
通知是‘kill’,通常由 kill(1) 发送。
sigexit
在 rc 将要退出的时候发送的人为通知。
作为一个例子,
fn sigint{ rm /tmp/junk exit}
为键盘中断设置一个陷入来在退出之前删除临时文件。
如果通知例程被设置为 {} 则忽略通知。在删除它的处理器(handler)定义的时候信号恢复它们的缺省行为。
23 环境
环境是对于执行的二进制程序可以获得的名字-值对的一个列表。在 Plan 9 上,环境存储在叫做 #e 的文件系统中,它通常挂装在 /env 上。每个变量的值都保存在一个单独的文件中,都带有终止于 0 字节的分量(component)。(文件系统完全维持在内存中,所以不涉及磁盘或网络。) /env 的内容在一个每进程(per-process)组基础上共享 - 在建立一个新进程组的时候,把 /env 有效的连结到一个新文件系统上,它被初始化为旧文件系统的复件。这种组织的一个结果是命令可以改变环境条目并看到改变反射到 rc 中。
函数也出现在环境中,通过对它们的名字前导 fn# 来命名,如 /env/fn#roff。
24 局部变量
在一个单一命令期间设置一个变量经常是有用的。一个赋值紧随一个命令有这种效果。例如
a=globala=local echo $aecho $a
将打印
localglobal
这对复合命令也有效,如
f=/fairly/long/file/name { { wc $f; spell $f; diff $f.old $f } pr -h 'Facts about '$f lp -dfn}
25 例子 - cd, pwd
下面的这对函数提供标准 cd 和 pwd 命令的增强版本。(为此感谢 Rob Pike。)
ps1='% ' # default prompttab=' ' # a tab characterfn cd{ builtin cd $1 && switch($#*){ case 0 dir=$home prompt=($ps1 $tab) case * switch($1) case /* dir=$1 prompt=(`{basename `{pwd}}^$ps1 $tab) case */* ..* dir=() prompt=(`{basename `{pwd}}^$ps1 $tab) case * dir=() prompt=($1^$ps1 $tab) } }}fn pwd{ if(~ $#dir 0) dir=`{/bin/pwd} echo $dir}
函数 pwd 是标准 pwd 的一个增强版本,它在变量 $dir 中缓冲它的值,因为真正的 pwd 可能执行起来非常慢。(最新版本的 Plan 9 已经有了非常快的 pwd 实现,这减低了 pwd 函数的优势。)
函数 cd 调用 cd 内置命令,并检查它是否成功。如果成功,它设置 $dir 和 $prompt。提示将包含当前目录的最后的成员(除非在 home 目录中,在这里它将是空),并且 $dir 将被重置为正确的值或者是 (),这样 pwd 函数将正确的工作。
26 例子 - man
man 命令打印程序员手册的页面。例如,下面的调用
man 2 sinhman rcman -t cat
在第一种情况,打印章节 2 中的 sinh 页面。在第二种情况,打印 rc 的手册页。因为未指定章节,在所有章节中查找这个页面,在章节 1 中找到了它。在第三种情况,排版 cat 的页面(使用了 -t 选项)。
cd /sys/man { echo $0: No manual! >[1=2] exit 1}NT=n # default nroffs='*' # section, default try allfor(i) switch($i){case -t NT=tcase -n NT=ncase -* echo Usage: $0 '[-nt] [section] page ...' >[1=2] exit 1case [1-9] 10 s=$icase * eval 'pages='$s/$i for(page in $pages){ if(test -f $page) $NT^roff -man $page if not echo $0: $i not found >[1=2] }}
注意使用了 eval 来制作候选手册页的一个列表。不使用 eval,存储在 $s 中的 * 将不触发文件名匹配 - 它被包围在引号中,并且即使不这样,它将在赋值给 $s 的时候被展开。 eval 导致它的参数被 rc 的分析器和解释器重新处理,有效的延迟 * 的求值直到赋值给 $pages.
27 例子 - holmdel
下列 rc 脚本玩一个假装的简单游戏 holmdel,在其中游戏者交替的指名 Bell Labs 的位置,第一个提及 Holmdel 的获胜。
t=/tmp/holmdel$pidfn read{$1=`{awk '{print;exit}'}}ifs='' # just a newlinefn sigexit sigint sigquit sighup{rm -f $texit}cat <<'!' >$tAllentown AtlantaCedar CrestChesterColumbusElmhurstFullertonHolmdelIndian HillMerrimack ValleyMorristownNeptunePiscatawayReadingShort HillsSouth PlainfieldSummitWhippanyWest Long Branch!while(){ lab=`{fortune $t} echo $lab if(~ $lab Holmdel){ echo You lose. exit } while(read lab; ! grep -i -s $lab $t) echo No such location. if(~ $lab [hH]olmdel){ echo You win. exit }}
这个脚本值得详细描述(至少它不蠢。)
变量 $t 是对临时文件名字的一个简写。在这个脚本的多个实例同时运行的情况下,在临时文件名字包括 $pid 能确保它们的名字不会冲突,rc 把 $pid 初始化为它的进程-id。
函数 read 的参数是一个变量的名字,把从的标准输入读取来的行汇集到其中。$ifs 被设置未只有一个换行符。这样 read 的输入不会在空格处被分离,但是终止的换行被删除了。
设置了一个处理器来捕获 sigint、sigquit 和 sighup,还有一个人工的 sigexit 信号。它们只是删除临时文件并退出。
从包含 Bell Labs 位置的一个列表的立即文档来初始化临时文件,接着主循环开始。
首先,程序使用 fortune 程序从位置列表中挑出随机的一行来猜测一个位置(在 $lab 中),如果它猜到 Holmdel,打印一个消息并退出。
接着它使用 read 函数来从标准输入得到一些行,并对它们做有效性检查直到得到一个合法的名字。注意 while 的条件部分可以是一个复合命令。只检查在序列中最后一个命令的退出状态。
最后,如果结果是 Holmdel,打印一个消息并退出。否则它回到循环的顶部。
28 设计原理
Rc 在很大程度上吸取了 Steve Bourne 的 /bin/sh。任何 Bourne shell 的后继者都纳入此类比较。我尝试着去修补它最周知的缺点,并通过省略无关紧要的特征近可能的去简化事情。只在遇到不可抗拒的诱惑的时候我才介入新颖的想法。明显的,我广泛的修补了 Bourne 的语法。
在 rc 的设计中的最重要的原理是它不是一个宏处理器。词法和语法分析代码从不扫描输入多于一次(当然 eval 命令是个例外,它的存在就是为了打破这个规则)。
通过把包含空格的参数传递给 Bourne shell 脚本,它们经常被粗暴的运行。经常在不恰当的时候使用 $IFS 把它们分割到多个参数中。在 rc 中,变量的值,包括命令行参数,在替换到命令中的时候是不重新扫描的。参数大概已经在父进程中扫描过了,并且不应当被重新扫描。
为什么 Bourne 在变量替换之后重新扫描命令? 它需要能够在值是字符串的变量中存储参数列表。如果我们除去了重新扫描,我们必须改变变量的类型,这样它们可以显式的承载字符串的列表。
这介入了一些概念上的复杂性。我们需要对字的列表的表示法。有两种不同的联接,对字符串的 - $a^$b, 和对列表的 - ($a $b)。 在 () 和 '' 之间的区别对初学者是容易混淆的,尽管差别是确切明显的 - 一个空参数和没有参数是不一样的。
Bourne 在做命令替换的时候也需要重新扫描输入。这是因为包围在反引号之中的文本不是字符串而是一个命令。严格的说,在包围着命令是嵌套的命令替换时候必须分析它,但是这使处理它很困难,比如:
size=`wc -l \`ls -tsed 1q\``
内部的反引号必须被转义来避免终止外部的命令。比上面的例子更糟的是,需要的转义数目是嵌套深度的指数。rc 通过把后引用改为参数是命令的一个一元操作符来修正这个问题,例如:
size=`{wc -l `{ls -tsed 1q}}
不需要转义,并且在一遍之中分析所有事情。
出于类似的原因,rc 定义信号处理器如同它们是函数,而不是如同 Bourne 那样为每个信号关联上一个字符串,伴有在键入中断字符时得到语法错误作为响应的可能性。因为 rc 在键入的时候分析输入,它在你制作它们的时候就报告错误。
解决了所有这些麻烦,我们获得了实质的语义上的简化。不需要区分 $* 和 $@。不需要四种类型的引用,没有支配它们的非常复杂的规则。在 rc 中你在想让一个语法字符出现在参数中、或者参数是空串的时候,而不在其他时候使用引号。IFS 不再使用,除了在绝对必须的一种情况下: 在命令替换期间把命令输出转换到参数列表中。
这也避免了一个重要的 UNIX 安全漏洞。在 UNIX 中,system() 和 popen() 函数调用 /bin/sh 来执行一个命令。不可能使用任何一个这种例程而且保证指定的命令会执行,即使 system() 或 popen() 的调用者为这个命令指定了全路径名字。如果这发生在 set-userid 程序中将是破坏性的。问题在于使用 IFS 把命令分解到一组字中,所以攻击者在运行特权程序之前,只需要在他的环境中设置 IFS=/ 并在当前目录下留下一个叫做 usr 或 bin 的木马程序。rc 通过永不为任何原因而重新扫描输入来解决修正问题。
在 rc 和 Bourne shell 之间的多数其他区别都是不严重的。我去除了 Bourne 变量替换的怪异形式,如
echo ${a=b} ${c-d} ${e?error}
因为它们很少使用,多余和易于用不深奥的术语表达。我删除了内置的 export、readonly、break、continue、read、return、set、times 和 unset,因为他们好象是多余的或只有少量的使用。
在 Bourne 出自 Algol 68 语法的地方,rc 转而基于 C 或 Awk。这是很难辩护的。我相信,比如
if(test -f junk) rm junk
优于
if test -f junk; then rm junk; fi
因为它更少受关键字的困扰。它避免了 Bourne 在奇怪的地方需要的分号,并且语法字符更好的突出了命令的起作用的部分。
Bourne 无可争议的比 rc 好的一个大范围的语法是带有 else 子句的 if 语句。rc 的 if 没有终止的 fi 式的括号(bracket)。结果是,分析器不预先查看它的输入就不能断定是否预期有一个 else 子句。问题出在读取之后,例如
if(test -f junk) echo junk found
在交互模式下,rc 不能确定是立即执行它并打印 $prompt(1),还是打印 $prompt(2) 并等待键入 else。在 Bourne shell 中,这不是个问题,因为 if 语句必须以 fi 结束,不管它是否包含 else。
Rc 公认的薄弱的解决方案是在一个单独的语句中声明 else 子句,带有它必须立即跟随在 if 之后的语义附加条件,并把它叫做 if not 而不是 else,作为快要接近怪物的警示。它唯一值得注意的结果是在下面的构造中需要花括号。
for(i){ if(test -f $i) echo $i found if not echo $i not found}
rc 解决“摇摆的 else”二义性的方法与多数人的预期相反。
值得注意的是在 UNIX 系统程序员手册的四个最新的版本中,手册页中描述的 Bourne shell 文法不认可命令 whowc。这的确是一个疏漏,但它暗示了更黑暗的事情: 没有人真正知道 Bourne shell 的语法是什么。即使是查看源代码也少有帮助。分析器是以递归下降方式实现的,但是对应于文法类属(category)的例程都有一个标志参数,依据上下文巧妙的改变它们的操作。rc 的分析器是使用 yacc 实现的,所以我们可以精确的说出它的语法。
29 致谢
Rob Pike、Howard Trickey 和其他 Plan 9 用户是好想法和批评的持久的、无尽的来源。本文档中的一些例子提取自 [Bourne],这些是 rc 最好的特征。
30 引用
S. R. Bourne, UNIX Time-Sharing System: The UNIX Shell, Bell System Technical Journal, Volume 57 number 6, July-August 1978 Copyright © 2000 Lucent Technologies Inc. All rights reserved.
1 介绍
rc 在精神上类似于 UNIX 的 Bourne shell 但在细节上有很多不同。本文使用很多小例子和一些大点的例子来描述 rc 的首要特征。假定读者熟悉 Bourne shell。
2 简单命令
对于最简单的使用,rc 有着 Bourne-shell 用户很熟悉的语法。下列命令都会如愿而行:
datecat /lib/news/buildwho >user.nameswho >>user.nameswc fileecho [a-f]*.cwho wcwho; datevc *.c &mk && v.out /*/bin/fb/*rm -r junk echo rm failed!
3 引用
包含一个空格或 rc 的其他语法字符之一的参数必须包围在单引号(')之中:
rm 'odd file name'
在已被引号括起来的参数中的单引号必须是双重的:
echo 'How''s your father?'
4 模式
包含字符 * ? [ 中任何一个的未被引号括起来的参数都是同文件名字匹配的模式。字符 * 匹配字符的任意序列,字符 ? 匹配任何单一字符,而 [class] 匹配在 class 中的任意字符,除非 class 的第一个字符是 ~,在这种情况下取这个类的补集。class 也可以包含用 - 分隔的成对的字符,它表示词法上在两者之间的所有字符。字符 / 在模式中必须显式的出现,路径名分量 . 和 .. 也是如此。模式被替代为一个参数的列表,每个匹配的路径名字都是一个参数,作为例外,没有匹配的名字的模式不被替代为空列表;而是保持为自身。
5 变量
UNIX 的 Bourne shell 提供字符串值的变量。rc 提供的变量值是参数的列表 - 就是说,字符串的数组。这是在 rc 和传统的 UNIX 命令解释器之间的原则性区别。变量可以通过键入来给出值,例如:
path=(. /bin)user=tdfont=/lib/font/bit/pelm/ascii.9.font
圆括号指示赋予 path 的值是两个字符串的一个列表。变量 user 和 font 被赋予包含一个单一字符串的列表。
通过在变量的名字前面前导一个 $ 可以把变量的值替换到一个命令中,比如:
echo $path
如果 path 已经做如上设置,它将等价于
echo . /bin
变量可以有数字或数字的列表作为下标,比如:
echo $path(2)echo $path(2 1 2)
它们等价于
echo /binecho /bin . /bin
在左圆括号和变量名字之间必须没有空格分隔;否则,这些下标将被当作一个独立的圆括号中的列表。
在变量中的字符串的数目可以用 $# 操作符确定。例如,
echo $#path
对于上面的例子将输出 2。
下面的两个赋值有着微妙的不同:
empty=()null=''
第一个设置 empty 为不包含字符串的一个列表。第二个设置 null 为包含一个单一的字符串的一个列表,但是这个字符串不包含字符。
尽管它们或多或少是同样的东西(在 Bourne 的 shell 中,它们是不可区分的),它们在几乎所有情况下都表现的很不同。在其他东西当中
echo $#empty
打印 0,而
echo $#null
打印 1。
所有未设置的变量都有值 ()。
偶尔的,把变量的值作为一个单一的字符串是方便的。通过 $" 操作符把字符串的所有元素联接到一个单一字符串中,在元素之间带有空格。这样,如果我们设置
list=(How now brown cow)string=$"list
则
echo $list
和
echo $string
二者导致相同的输出,就是:
How now brown cow
而
echo $#list $#string
将输出
4 1
因为 $list 有四个成员,而 $string 有一个单一的成员,带有三个空格分隔它的单字。
6 参数
当 rc 从一个文件读取它的输入的时候,这个文件有权访问在 rc 的命令行上提供的参数。变量 $* 最初被赋值为参数的列表。名字 $1、$2 等是 $*(1)、$*(2) 等的同义字。此外,$0 是一个文件的名字,rc 的输入从其中读入。
7 联接
rc 有一个字符串联接操作符,插入符 ^,用来建造由多个片断组成的参数。
echo hully^gully
完全等价于
echo hullygully
假定变量 i 包含命令的名字。则
vc $i^.cvl -o $1 $i^.v
可以编译这个命令的源代码,把结果留在适当的文件中。
联接可以应用于列表之上。下面的
echo (a b c)^(1 2 3)src=(main subr io)cc $src^.c
等价于
echo a1 b2 c3cc main.c subr.c io.c
在细节上,规则是: 如果 ^ 的两个操作数(operand)是有相同的非零个字符串的列表,则成对的连接它们。否则,如果操作数之一是一个单一的字符串,它依次与另一个操作数的每个参数联接。操作数的任何其他组合都是错误的。
8 无约束的插入符
用户需求决定了 rc 在特定位置插入插入符,来使语法看起来更像 Bourne shell。例如,下面的:
cc -$flags $stems.c
等价于
cc -^$flags $stems^.c
一般的,rc 会在不由空白分隔的两个参数之间插入 ^。特别是,只要 $ ' ` 跟随着一个被引号括起来的或未被引号括起来的字,或者一个未被引号括起来的字跟随着一个被引号括起来的字并且无空白或 Tab 介于其间,就在二者之间插入一个隐含的 ^。如果立即跟随着一个 $ 的一个未被引号括起来的字包含不是一个字母下划线或 * 的字符,在第一个这样的字符之前插入 ^。
9 命令替换
通常用来从一个命令的输出建造一个参数列表。rc 允许一个命令,包围在花括号中并前导着一个反引号 `{...},出现在需要参数的任何地方。执行这个命令并捕获它的标准输出。使用存储在变量 ifs 中的字符来把输入分解到参数中。例如,
cat
`{ls -trsed 10q}
将按时间次序连接当前目录中十个最旧的文件,假定缺省 ifs 设置为空格、Tab 和换行符。
10 管道线分支
一般的管道表示法对于几乎所有情况都是足够用了。有时后拥有非线性的管道线是有用的。比树状结构更一般化的管道线拓扑结构可能要求任意大的管道缓冲区,甚至更坏、导致死锁。rc 拥有某种非线性的树状管道线的语法。例如,
cmp <{old} <{new} 将回归测试一个命令的新版本。跟随着在花括号中的命令的 < 或 >,导致运行这个命令并把它的标准输出或输入连结到一个管道上。父命令(本例中的 cmp)开始于连结到某个文件描述符或其他东西上的管道线的另一端,并带有在打开的时候会连接到管道的一个参数(例如,/dev/fd/6)(译注:一个命名管道的名字)。一些命令不准备处理关闭就不可搜寻的输入文件。例如 diff 需要读取它的输入两次。
11 退出状态
在命令退出的时候,它向执行它的进程返回一个状态。在 Plan 9 中,状态是描述错误状况的一个字符串。在正常终止的使用它是空的。
rc 把命令退出状态捕获到变量 $status 中。对于一个简单命令 $status 的值就如同上面描述的一样。对于一个管道线 $status 被设置为管道线构件的状态的联接、并带有 字符作为分隔符。
rc 有多种控制流,其中很多控制流以前面执行的命令返回的状态作为条件。所有只包含 0 和 的 $status 拥有逻辑值 true。任何其他状态都是 false。
12 命令组合
包围在 {} 中的命令序列可以用在需要命令的任何地方。例如:
{sleep 3600;echo 'Time''s up!'}&
将在后台等待一小时,接着打印一个消息。不带花括号,
sleep 3600;echo 'Time''s up!'&
将锁住终端一个小时,接着在后台打印这个消息。
13 控制流 - for
为键入的列表中的每个成员执行一次命令,例如:
for(i in printf scanf putchar) look $i /usr/td/lib/dw.dat
它在给定文件中查找单字 printf、scanf 和 putchar 中的每一个。一般形式是
for(name in list) command
或
for(name) command
在第一种情况下为 list 的每个成员执行一次 command,并把这个成员赋予变量 name。如果缺少子句“in list”,假定为“in $*”。
14 条件执行 - if
rc 还提供一个通用 if 语句。例如:
for(i in *.c) if(cpp $i >/tmp/$i) vc /tmp/$i
在 cpp 处理而没有错误的每个 C 源程序上运行 C 编译器。‘if not’语句提供双尾(two-tailed)条件。例如:
for(i){ if(test -f /tmp/$i) echo $i already in /tmp if not cp $i /tmp}
它是在 $* 中的每个文件之上的循环,把不存在于 /tmp 中的文件复制到这里,并对那些已经存在于这里的文件打印一个消息。
15 控制流 - while
rc 的 while 语句看起来如下:
while(newer subr.v subr.c) sleep 5
它等待直到 subr.v 比 subr.c 新,原因大概是 C 编译处理完了它。
如果控制命令是空的,循环将不会终止。这样,
while() echo y
将模拟 yes 命令。
16 控制流 - switch
rc 提供一个 switch 语句来在任意字符串上做模式匹配。它的一般形式是
switch(word){case pattern ... commands case pattern ... commands ...}
Rc 依次尝试对在每个 case 语句中的模式去匹配这个 word。除了 / 和 . 和 .. 不需要被显式的匹配之外,模式与用于文件名匹配的一样。
如果有任何模式匹配,执行紧随这个 case 之后直到下一个 case 之前(或 switch 的结束处)的命令,并且这个 switch 的执行完成。例如,
switch($#*){case 1 cat >>$1case 2 cat >>$2 <$1case * echo 'Usage: append [from] to'} 是一个 append 命令。调用时带有一个文件参数,它把它的标准输入添加到指名的文件中。带有两个参数时,把第一个文件添加到第二个文件中。任何其他参数数目引发一个错误消息。 内置 ~ 命令也匹配模式,并且经常比 switch 更加简洁。它的参数是一个字符串和一个模式的列表。只有在所有模式都匹配这个字符串的时候,设置 $status 为真。下列例子为 man(1) 命令处理选项参数: opt=()while(~ $1 -* [1-9] 10){ switch($1){ case [1-9] 10 sec=$1 secn=$1 case -f c=f s=f case -[qwnt] cmd=$1 case -T* T=$1 case -* opt=($opt $1) } shift} 17 函数
可以通过如下键入来定义函数
fn name { commands }
此后,在遇到叫做 name 的命令的时候,命令的参数列表的余下部分将赋给 $* ,接着 rc 将执行 commands。在完成的时候恢复 $* 的值。例如:
fn g { grep $1 *.[hcyl]}
定义 g pattern (用来查找在当前目录中的所有程序源文件中查找 pattern 的出现)。
通过写
fn name
而没有函数体来删除函数定义。
18 命令执行
rc 做许多事情来执行一个简单命令。如果这个命令名字是使用 fn 定义的一个函数的名字,则执行这个函数。否则,如果它是一个内置命令的名字,rc 直接执行这个内置命令。否则,查找在变量 $path 提及的目录,直到找到一个可执行文件。在 Plan 9 中不鼓励 $path 变量的广泛使用。转而,使用缺省(. /bin) 和把你需要的绑定入 /bin。
19 内置命令
许多命令由 rc 内部执行,原因是不这样就难于实现。
. [-i] file ...
执行来自 file 的命令。在此期间 $* 设置为紧随 file 之后的参数列表的余下部分。使用 $path 来查找 file。选项 -i 指示交互式输入 在读取每个命令之前打印一个提示(在 $prompt 找到)。
builtin command ...
除了忽略叫做 command 的任何函数之外,正常的执行 command。例如,
fn cd{ builtin cd $* && pwd}
定义 cd 内置命令(见后)的一个替代者,它宣布新目录的全名。
cd [dir]
改变当前目录到 dir。缺省参数是 $home。$cdpath 是在其中查找 dir 的位置的一个列表。
eval [arg ...]
参数被联接(由空格分隔)到一个字符串中,读为给 rc 的输入并执行它。例如,
x='$y'y=Doodyeval echo Howdy, $x
将回显
Howdy, Doody
因为在替换了 $x 之后,eval 的参数是
echo Howdy, $y
exec [command ...]
Rc 用给定命令替代自身。有点像一个 goto - rc 不等待命令退出,并且不回来读更多的命令。
exit [status]
Rc 立即退出并返回给定状态。如果未给出,则使用 $status 的当前的值。
flag f [+-]
这个命令操纵并测试命令行标志(后面描述).
flag f +
设置标志 f。
flag f -
清除标志 f。
flag f
测试标志 f,恰当的设置 $status。所以
if(flag x) flag v +
如果已经设置了 -x 标志则设置 -v 标志。
rfork [nNeEsfF]
它使用 Plan 9 rfork 系统入口来把 rc 放置到带有下列属性的新进程组之中:
标志
名字
功能
n
RFNAMEG
制做父名字空间的一个复本
N
RFCNAMEG
开始于一个新的空名字空间
e
RFENVG
制做父环境的一个复本
E
RFCENVG
开始于一个新的空环境
s
RFNOTEG
制做一个新的通知组
f
RFFDG
制做父文件描述符空间的一个复本
F
RFCFDG
开始于一个新的空文件描述符空间
程序员手册的 fork(2) 章节详细描述这些属性。
shift [n]
删除 $* 的前 n(缺省为 1)个元素。
wait [pid]
等待有给定 pid 的进程退出。如果未给出 pid, 等待所有未完结的进程。
whatis name ...
以适合输入到 rc 的形式打印每个名字的值。输出的是到变量的赋值,函数的定义,到对一个内置命令的 builtin 的调用,或一个二进制程序的路径名字。例如,
whatis path g cd who
可能打印
path=(. /bin)fn g {gre -e $1 *.[hycl]}builtin cd/bin/who
~ subject pattern ...
依次针对每个 pattern 匹配 subject。在匹配时,$status 设置为真。否则,设置为‘不匹配’。模式同于文件名匹配的。在执行 ~ 命令之前模式不受文件名替代的支配,所以它们不需要被包围在引号之中,当然,除非想要的是对 * [ or ? 的一个文字上的匹配。例如
~ $1 ?
匹配任何单一字符,而
~ $1 '?'
只匹配一个文字问号。
20 高级 I/O 重定向
rc 允许在 < 或 > 之后的方括号 [ ] 中指定文件描述符,来做不是 0 和 1(标准输入和输出)的文件描述符的重定向。例如,
vc junk.c >[2]junk.diag
把编译器的诊断从标准错误保存到 junk.diag 中。
通过如下键入,这些文件描述符可以被替代为一个已经打开了的文件的 dup(2) 意义上的复制:
vc junk.c >[2=1]
它用文件描述符 1 的一个复件替换文件描述符 2。在与其它重定向联合时非常有用,比如
vc junk.c >junk.out >[2=1]
从左至右求值重定向,所以重定向文件描述符 1 到 junk.out,接着把文件描述符 2 指向相同的文件。与之相对,
vc junk.c >[2=1] >junk.out
重定向文件描述符 2 到文件描述符 1(大概是终端), 并接着定向文件描述符 1 到一个文件。在第一种情况下,标准和诊断输出将被混合到 junk.out 中。在第二种情况下,诊断输出将出现在终端上,而标准输出将发送到文件。
可以使用带有右手边为空的复制记号(notation)关闭文件描述符。例如,
vc junk.c >[2=]
将丢弃来自编译的诊断。
通过如下键入,可以派遣(send)任意的文件描述符经过管道,
vc junk.c [2] grep -v '^$'
它从 C 编译器的错误输出中删除空行。注意 grep 的输出仍出现在文件描述符 1 上。
有时你可能希望把一个管道的输入端连接到不是 0 的某个文件描述符。记号
cmd1 [5=19] cmd2
建立一个管道线,并且 cmd1'的文件描述符 5 通过管道连接到 cmd2'的文件描述符 19。
21 立即文档
rc 过程可以包含叫做“立即文档”数据,它被提供为给命令的输入,比如在下面这版本的 tel 命令中
for(i) grep $i <22>捕获通知
在接收到来自终端的中断的时候,rc 脚本通常终止。以通常的方式定义带有小写的一个 UNIX 信号名字的函数,但在 rc 收到对应的通知的时候调用。程序员手册的 notify(2) 章节详细的讨论了通知。有意思的通知有:
sighup
通知是‘hangup’。Plan 9 在终端从 rc 断开的时候发送这个通知。
sigint
通知是‘interrupt’,通常在终端上键入中断字符(ASCII DEL)的时候发出。
sigterm
通知是‘kill’,通常由 kill(1) 发送。
sigexit
在 rc 将要退出的时候发送的人为通知。
作为一个例子,
fn sigint{ rm /tmp/junk exit}
为键盘中断设置一个陷入来在退出之前删除临时文件。
如果通知例程被设置为 {} 则忽略通知。在删除它的处理器(handler)定义的时候信号恢复它们的缺省行为。
23 环境
环境是对于执行的二进制程序可以获得的名字-值对的一个列表。在 Plan 9 上,环境存储在叫做 #e 的文件系统中,它通常挂装在 /env 上。每个变量的值都保存在一个单独的文件中,都带有终止于 0 字节的分量(component)。(文件系统完全维持在内存中,所以不涉及磁盘或网络。) /env 的内容在一个每进程(per-process)组基础上共享 - 在建立一个新进程组的时候,把 /env 有效的连结到一个新文件系统上,它被初始化为旧文件系统的复件。这种组织的一个结果是命令可以改变环境条目并看到改变反射到 rc 中。
函数也出现在环境中,通过对它们的名字前导 fn# 来命名,如 /env/fn#roff。
24 局部变量
在一个单一命令期间设置一个变量经常是有用的。一个赋值紧随一个命令有这种效果。例如
a=globala=local echo $aecho $a
将打印
localglobal
这对复合命令也有效,如
f=/fairly/long/file/name { { wc $f; spell $f; diff $f.old $f } pr -h 'Facts about '$f lp -dfn}
25 例子 - cd, pwd
下面的这对函数提供标准 cd 和 pwd 命令的增强版本。(为此感谢 Rob Pike。)
ps1='% ' # default prompttab=' ' # a tab characterfn cd{ builtin cd $1 && switch($#*){ case 0 dir=$home prompt=($ps1 $tab) case * switch($1) case /* dir=$1 prompt=(`{basename `{pwd}}^$ps1 $tab) case */* ..* dir=() prompt=(`{basename `{pwd}}^$ps1 $tab) case * dir=() prompt=($1^$ps1 $tab) } }}fn pwd{ if(~ $#dir 0) dir=`{/bin/pwd} echo $dir}
函数 pwd 是标准 pwd 的一个增强版本,它在变量 $dir 中缓冲它的值,因为真正的 pwd 可能执行起来非常慢。(最新版本的 Plan 9 已经有了非常快的 pwd 实现,这减低了 pwd 函数的优势。)
函数 cd 调用 cd 内置命令,并检查它是否成功。如果成功,它设置 $dir 和 $prompt。提示将包含当前目录的最后的成员(除非在 home 目录中,在这里它将是空),并且 $dir 将被重置为正确的值或者是 (),这样 pwd 函数将正确的工作。
26 例子 - man
man 命令打印程序员手册的页面。例如,下面的调用
man 2 sinhman rcman -t cat
在第一种情况,打印章节 2 中的 sinh 页面。在第二种情况,打印 rc 的手册页。因为未指定章节,在所有章节中查找这个页面,在章节 1 中找到了它。在第三种情况,排版 cat 的页面(使用了 -t 选项)。
cd /sys/man { echo $0: No manual! >[1=2] exit 1}NT=n # default nroffs='*' # section, default try allfor(i) switch($i){case -t NT=tcase -n NT=ncase -* echo Usage: $0 '[-nt] [section] page ...' >[1=2] exit 1case [1-9] 10 s=$icase * eval 'pages='$s/$i for(page in $pages){ if(test -f $page) $NT^roff -man $page if not echo $0: $i not found >[1=2] }}
注意使用了 eval 来制作候选手册页的一个列表。不使用 eval,存储在 $s 中的 * 将不触发文件名匹配 - 它被包围在引号中,并且即使不这样,它将在赋值给 $s 的时候被展开。 eval 导致它的参数被 rc 的分析器和解释器重新处理,有效的延迟 * 的求值直到赋值给 $pages.
27 例子 - holmdel
下列 rc 脚本玩一个假装的简单游戏 holmdel,在其中游戏者交替的指名 Bell Labs 的位置,第一个提及 Holmdel 的获胜。
t=/tmp/holmdel$pidfn read{$1=`{awk '{print;exit}'}}ifs='' # just a newlinefn sigexit sigint sigquit sighup{rm -f $texit}cat <<'!' >$tAllentown AtlantaCedar CrestChesterColumbusElmhurstFullertonHolmdelIndian HillMerrimack ValleyMorristownNeptunePiscatawayReadingShort HillsSouth PlainfieldSummitWhippanyWest Long Branch!while(){ lab=`{fortune $t} echo $lab if(~ $lab Holmdel){ echo You lose. exit } while(read lab; ! grep -i -s $lab $t) echo No such location. if(~ $lab [hH]olmdel){ echo You win. exit }}
这个脚本值得详细描述(至少它不蠢。)
变量 $t 是对临时文件名字的一个简写。在这个脚本的多个实例同时运行的情况下,在临时文件名字包括 $pid 能确保它们的名字不会冲突,rc 把 $pid 初始化为它的进程-id。
函数 read 的参数是一个变量的名字,把从的标准输入读取来的行汇集到其中。$ifs 被设置未只有一个换行符。这样 read 的输入不会在空格处被分离,但是终止的换行被删除了。
设置了一个处理器来捕获 sigint、sigquit 和 sighup,还有一个人工的 sigexit 信号。它们只是删除临时文件并退出。
从包含 Bell Labs 位置的一个列表的立即文档来初始化临时文件,接着主循环开始。
首先,程序使用 fortune 程序从位置列表中挑出随机的一行来猜测一个位置(在 $lab 中),如果它猜到 Holmdel,打印一个消息并退出。
接着它使用 read 函数来从标准输入得到一些行,并对它们做有效性检查直到得到一个合法的名字。注意 while 的条件部分可以是一个复合命令。只检查在序列中最后一个命令的退出状态。
最后,如果结果是 Holmdel,打印一个消息并退出。否则它回到循环的顶部。
28 设计原理
Rc 在很大程度上吸取了 Steve Bourne 的 /bin/sh。任何 Bourne shell 的后继者都纳入此类比较。我尝试着去修补它最周知的缺点,并通过省略无关紧要的特征近可能的去简化事情。只在遇到不可抗拒的诱惑的时候我才介入新颖的想法。明显的,我广泛的修补了 Bourne 的语法。
在 rc 的设计中的最重要的原理是它不是一个宏处理器。词法和语法分析代码从不扫描输入多于一次(当然 eval 命令是个例外,它的存在就是为了打破这个规则)。
通过把包含空格的参数传递给 Bourne shell 脚本,它们经常被粗暴的运行。经常在不恰当的时候使用 $IFS 把它们分割到多个参数中。在 rc 中,变量的值,包括命令行参数,在替换到命令中的时候是不重新扫描的。参数大概已经在父进程中扫描过了,并且不应当被重新扫描。
为什么 Bourne 在变量替换之后重新扫描命令? 它需要能够在值是字符串的变量中存储参数列表。如果我们除去了重新扫描,我们必须改变变量的类型,这样它们可以显式的承载字符串的列表。
这介入了一些概念上的复杂性。我们需要对字的列表的表示法。有两种不同的联接,对字符串的 - $a^$b, 和对列表的 - ($a $b)。 在 () 和 '' 之间的区别对初学者是容易混淆的,尽管差别是确切明显的 - 一个空参数和没有参数是不一样的。
Bourne 在做命令替换的时候也需要重新扫描输入。这是因为包围在反引号之中的文本不是字符串而是一个命令。严格的说,在包围着命令是嵌套的命令替换时候必须分析它,但是这使处理它很困难,比如:
size=`wc -l \`ls -tsed 1q\``
内部的反引号必须被转义来避免终止外部的命令。比上面的例子更糟的是,需要的转义数目是嵌套深度的指数。rc 通过把后引用改为参数是命令的一个一元操作符来修正这个问题,例如:
size=`{wc -l `{ls -tsed 1q}}
不需要转义,并且在一遍之中分析所有事情。
出于类似的原因,rc 定义信号处理器如同它们是函数,而不是如同 Bourne 那样为每个信号关联上一个字符串,伴有在键入中断字符时得到语法错误作为响应的可能性。因为 rc 在键入的时候分析输入,它在你制作它们的时候就报告错误。
解决了所有这些麻烦,我们获得了实质的语义上的简化。不需要区分 $* 和 $@。不需要四种类型的引用,没有支配它们的非常复杂的规则。在 rc 中你在想让一个语法字符出现在参数中、或者参数是空串的时候,而不在其他时候使用引号。IFS 不再使用,除了在绝对必须的一种情况下: 在命令替换期间把命令输出转换到参数列表中。
这也避免了一个重要的 UNIX 安全漏洞。在 UNIX 中,system() 和 popen() 函数调用 /bin/sh 来执行一个命令。不可能使用任何一个这种例程而且保证指定的命令会执行,即使 system() 或 popen() 的调用者为这个命令指定了全路径名字。如果这发生在 set-userid 程序中将是破坏性的。问题在于使用 IFS 把命令分解到一组字中,所以攻击者在运行特权程序之前,只需要在他的环境中设置 IFS=/ 并在当前目录下留下一个叫做 usr 或 bin 的木马程序。rc 通过永不为任何原因而重新扫描输入来解决修正问题。
在 rc 和 Bourne shell 之间的多数其他区别都是不严重的。我去除了 Bourne 变量替换的怪异形式,如
echo ${a=b} ${c-d} ${e?error}
因为它们很少使用,多余和易于用不深奥的术语表达。我删除了内置的 export、readonly、break、continue、read、return、set、times 和 unset,因为他们好象是多余的或只有少量的使用。
在 Bourne 出自 Algol 68 语法的地方,rc 转而基于 C 或 Awk。这是很难辩护的。我相信,比如
if(test -f junk) rm junk
优于
if test -f junk; then rm junk; fi
因为它更少受关键字的困扰。它避免了 Bourne 在奇怪的地方需要的分号,并且语法字符更好的突出了命令的起作用的部分。
Bourne 无可争议的比 rc 好的一个大范围的语法是带有 else 子句的 if 语句。rc 的 if 没有终止的 fi 式的括号(bracket)。结果是,分析器不预先查看它的输入就不能断定是否预期有一个 else 子句。问题出在读取之后,例如
if(test -f junk) echo junk found
在交互模式下,rc 不能确定是立即执行它并打印 $prompt(1),还是打印 $prompt(2) 并等待键入 else。在 Bourne shell 中,这不是个问题,因为 if 语句必须以 fi 结束,不管它是否包含 else。
Rc 公认的薄弱的解决方案是在一个单独的语句中声明 else 子句,带有它必须立即跟随在 if 之后的语义附加条件,并把它叫做 if not 而不是 else,作为快要接近怪物的警示。它唯一值得注意的结果是在下面的构造中需要花括号。
for(i){ if(test -f $i) echo $i found if not echo $i not found}
rc 解决“摇摆的 else”二义性的方法与多数人的预期相反。
值得注意的是在 UNIX 系统程序员手册的四个最新的版本中,手册页中描述的 Bourne shell 文法不认可命令 whowc。这的确是一个疏漏,但它暗示了更黑暗的事情: 没有人真正知道 Bourne shell 的语法是什么。即使是查看源代码也少有帮助。分析器是以递归下降方式实现的,但是对应于文法类属(category)的例程都有一个标志参数,依据上下文巧妙的改变它们的操作。rc 的分析器是使用 yacc 实现的,所以我们可以精确的说出它的语法。
29 致谢
Rob Pike、Howard Trickey 和其他 Plan 9 用户是好想法和批评的持久的、无尽的来源。本文档中的一些例子提取自 [Bourne],这些是 rc 最好的特征。
30 引用
S. R. Bourne, UNIX Time-Sharing System: The UNIX Shell, Bell System Technical Journal, Volume 57 number 6, July-August 1978 Copyright © 2000 Lucent Technologies Inc. All rights reserved.
2007年8月10日星期五
2007年8月9日星期四
James Blunt - Goodbye My Lover
Did I disappoint you or let you down?
Should I be feeling guilty or let the judges frown?
'Cause I saw the end before we'd begun,
Yes I saw you were blinded and I knew I had won.
So I took what's mine by eternal right.
Took your soul out into the night.
It may be over but it won't stop there,
I am here for you if you'd only care.
You touched my heart you touched my soul.
You changed my life and all my goals.
And love is blind and that I knew when,
My heart was blinded by you.
I've kissed your lips and held your head.
Shared your dreams and shared your bed.
I know you well, I know your smell.
I've been addicted to you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I am a dreamer but when I wake,
You can't break my spirit
it's my dreams you take.
And as you move on, remember me,
Remember us and all we used to be
I've seen you cry, I've seen you smile.
I've watched you sleeping for a while.
I'd be the father of your child.
I'd spend a lifetime with you.
I know your fears and you know mine.
We've had our doubts but now we're fine,
And I love you, I swear that's true.
I cannot live without you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
And I still hold your hand in mine.
In mine when I'm asleep.
And I will bear my soul in time,
When I'm kneeling at your feet.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I'm so hollow, baby, I'm so hollow.
so, I'm so, I'm so hollow.
Should I be feeling guilty or let the judges frown?
'Cause I saw the end before we'd begun,
Yes I saw you were blinded and I knew I had won.
So I took what's mine by eternal right.
Took your soul out into the night.
It may be over but it won't stop there,
I am here for you if you'd only care.
You touched my heart you touched my soul.
You changed my life and all my goals.
And love is blind and that I knew when,
My heart was blinded by you.
I've kissed your lips and held your head.
Shared your dreams and shared your bed.
I know you well, I know your smell.
I've been addicted to you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I am a dreamer but when I wake,
You can't break my spirit
it's my dreams you take.
And as you move on, remember me,
Remember us and all we used to be
I've seen you cry, I've seen you smile.
I've watched you sleeping for a while.
I'd be the father of your child.
I'd spend a lifetime with you.
I know your fears and you know mine.
We've had our doubts but now we're fine,
And I love you, I swear that's true.
I cannot live without you.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
And I still hold your hand in mine.
In mine when I'm asleep.
And I will bear my soul in time,
When I'm kneeling at your feet.
Goodbye my lover.
Goodbye my friend.
You have been the one.
You have been the one for me.
I'm so hollow, baby, I'm so hollow.
so, I'm so, I'm so hollow.
2007年8月5日星期日
《嵌入式设计及Linux驱动开发指南——基于ARM9处理器》读书笔记
第一章 嵌入式系统基础
1、 嵌入式系统定义:
“嵌入式系统是用来控制或者监视机器、装置、工厂等大规模系统的设备。”
——电气工程师协会
“嵌入到对象体系中的专用计算机系统”
——北京航空航天大学 何立民教授
“嵌入性”、“专用性”与“计算机系统”是嵌入式系统的三个基本要素。
2、 嵌入式操作系统:
硬实时系统有一个刚性的、不可改变的时间限制,它不允许任何超出时限的错误超时错误会带来损害甚至导致系统失败、或者导致系统不能实现它的预期目标。
软实时系统的时限是柔性灵活的,它可以容忍偶然的超时错误。失败造成的后果并不严重,仅仅是轻微地降低了系统的吞吐量。
我们可以认为至少嵌入式系统都是软实时系统,所有的嵌入式系统都是实时系统,但并不是所有的实时系统都是嵌入式系统。
常用的嵌入式操作系统有:Linux, uC/OS, Windows CE, VxWorks, Palm OS, QNX等。
3、 选择Embedded OS的原则:
系统成本;
市场进入时间及技术支持;
可移植性;
可利用资源;
系统定制能力。
第二章 基于ARM9处理器的硬件开发平台
1、 ARM的历史:
ARM(Advanced RISC Machine)公司于1990年11月在英国剑桥成立。
1991年,ARM推出第一个嵌入式RISC核心——ARM6系列处理器,VLSI、夏普、GEC Plessey、德州仪器、Cirrus Logic等公司相继同ARM公司签署了授权协议。
1998年4月,ARM在伦敦证券交易所和纳斯达克交易所上市。
ARM中国安谋咨询上海有限公司于2002年7月在中国上海成立。
目前基于ARM核的处理器有以下几类:
ARM7家族;
ARM9家族;
ARM9E家族;
ARM10E家族;
ARM11家族;
SecurCore家族;
OptimoDE数据引擎内核;
MPCore多处理器家族;
Intel公司的StrongARM/XScale。
2、ARM7和ARM9处理器的主要区别:
指令流水线:
ARM7:三级,(取指令,译码,执行);
ARM9:五级,(取指,译码,执行,缓冲/数据,回写)。
3、三星S3C2410X处理器:
基于ARM920T核(由ARM9TDMI、存储管理单元MMU和高速缓存三部分组成),片上资源包括:
1个LCD控制器(支持STN和TFT带有触摸屏的液晶显示屏);
SDRAM控制器;
3个通道的UART;
4个通道的DMA;
4个具有PWM功能的计时器和1个内部时钟;
8通道的10位ADC;
触摸屏接口;
I2S总线接口;
2个USB主机接口,1个USB设备接口;
2个SPI接口;
SD接口和MMC卡接口;
看门狗计数器;
117位通用I/O口和24位外部中断源;
第三章 调试嵌入式系统程序
1、 嵌入式系统调试方法:
1) 实时在线仿真(In-Circuit Emulator, ICE)
优点:功能非常强大,软硬件均可做到完全实时在线调试。
缺点:价格昂贵。
2) 模拟调试
优点:简单方便,不需要目标板,成本低。
缺点:功能非常有限,无法实时调试。
3) 软件调试
优点:纯软件,价格较低,简单,软件调试能力较强。
缺点:需要事先烧制监控程序(Monitor)(往往需多次实验才能成功)且目标板工作正常,功能有限,特别是硬件调试能力较差。
4) JTAG调试
优点:方便、简单,无需制作Monitor,软硬件均可调适。
缺点:需要工作基本正常(至少CPU工作正常)的目标板,仅适用于有调试接口的芯片。
2、 ARM仿真器工作原理:
利用高速JTAG(Joint Test Action Group)串行扫描链,通过调试通信通道(Debug Communication Channel, DCC)连接ARM核心内嵌的名为“Embedded-ICE”的调试逻辑,调试逻辑实时监测ARM核心的寄存器、数据总线和地址总线。调试器设置Breakpoint及Watchpoint后,程序在ARM内核全速运行,调试程序实时监测地址与数据总线并与预设值比较,在吻合时产生异常中断通知内核并把控制权交给调试器。这样,在程序全速运行时,可以在断点处停止,可以设置条件断点、条件观测断点等,而又不占用CPU时间及内存资源。
3、 JTAG接口:
1985年制定的检测PCB和IC的一个标准,1990年被修改后成为IEEE的一个标准,及IEEE1149.1-1990。通过这个标准,可对具有JTAG接口芯片的电路进行边界扫描和故障检测。
第四章 创建嵌入式系统开发环境
1、交叉编译步骤:
(1) 创建编译环境。在这个过程中,将设置一些环境变量,创建安装目录,安装内核源代码和头文件等。
(2) 创建binutils。这个过程结束后,会创建类似arm-linux-ld等工具。
Binutils是一组开发工具,包括链接器、汇编器以及其他用于目标文件和档案的工具
首先要安装的软件包使binutils。这非常重要,因为glibc和gcc会针对可用的连接器和汇编器进行多种测试,以决定打开某些特性。
(3) 创建一个交叉编译版本的gcc。注意:在这个过程中只能编译C程序,而不能编译C++程序。
创建交叉编译版本的gcc,需要交叉编译版本的glibc及其头文件,而交叉编译版本的glibc是通过交叉编译版本的gcc创建的。面对这个先有鸡还是先有蛋的问题,解决办法是先只编译对C语言的支持,并禁止支持线程。
(4) 创建一个交叉编译版本的glibc。这里最容易出现问题。
glibc是一个提供系统调用和基本函数的C语言库,比如open,malloc和printf等,所有动态链接的程序都要用到它。创建glibc需要的时间更长。
(5) 创建一个交叉编译版本的gdb。在这个过程结束后,会创建ARM-Linux-gdb。
(6) 重新创建gcc。前面创建gcc的过程没有编译C++编译器,现在glibc已经准备好了,所以这个步骤将完善gcc的交叉编译。
(7) 重新创建glibc。如果成功执行了这个过程,那么你就拥有了一套属于自己的交叉编译工具链。
2、如果在交叉编译过程中出现错误,那么请检查:
版本选择是否正确,以及是否安装了相应的补丁;
库文件路径是否正确;
系统环境变量是否设置正确。
第五章 Bootloader
1、 Bootloader(引导加载程序)是系统加电后运行的第一段代码。一般它只在系统启动时运行非常短的一段时间,但对于嵌入式系统来说,这是一个非常重要的系统组成部分。
2、 嵌入式Linux系统从软件的角度看通常可以分成4个层次:
(1) 引导加载程序。包括固化在固件(Firmware)中的启动代码(可选)和Bootloader两大部分。
(2) 内核。特定于嵌入式板子的定制内核以及控制内核引导系统的参数。
(3) 文件系统。包括根文件系统和建立于Flash内存设备之上的文件系统。通常用Ramdisk作为根文件系统。它是提供管理系统的各种配置文件以及系统执行用户应用程序的良好的运行环境的载体。
(4) 用户应用程序。特定于用户的应用程序。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。
3、 Bootloader包含两种不同的操作模式:
1) 启动加载(Bootloading)模式;
2) 下载(Downloading)模式。
4、 Bootloader的启动流程:
第一阶段:主要包含依赖于CPU的体系结构硬件初始化的代码,通常都用汇编实现。这个阶段的任务有:
基本的硬件设备初始化(屏蔽所有的中断、关闭处理器内部指令/数据Cache等)
为第二阶段准备RAM空间
如果是从某个固态存储媒质中,则复制Bootloader的第二段代码到RAM
设置堆栈
跳转到第二阶段的程序入口点
第二阶段:通常用C语言完成,以便实现更复杂的功能,也使程序有更好的可读性和可移植性。这个阶段的任务有:
初始化本阶段要使用到的硬件设备
检测系统内存映射
将内核映像和根文件系统映像从FLASH读到RAM
为内核设置启动参数
调用内核
5、 常用U-Boot命令:
命令名
功能
help / ?
帮助命令。用于查询U-Boot支持的命令并列出简单说明,和“?”是同一个命令
bdinfo
察看目标系统参数和变量、目标板的硬件配置、各种变量参数
setenv
设置环境变量。比较常用的有:
setenv ipaddr *.*.*.*
setenv severip *.*.*.*
setenv gatewayip *.*.*.*
setenv ethaddr *.*.*.*.*.*
printenv
查看环境变量
saveenv
保存设置的环境变量到Flash
mw
写内存
md
察看内存
mm
修改内存
flinfo
察看Flash的信息
erase [起始地址 结束地址]
搽除Flash内容,必须以扇区为单位进行搽除
cp [源地址 目标地址 大小]
内存复制,可以在Flash和ram中交换数据
imi [起始地址]
察看内核映像文件
bootm [起始地址]
从某个地址启动内核
tftpboot [起始地址 镜像名]
通过ftp从主机系统下载内核映像文件
reset
复位
第六章 Linux系统在ARM平台的移植
1、 使某一个平台的代码运行在其它平台上的过程就叫做移植。
2、 Linux内核结构:
/arch包含了所有硬件结构特定的内核代码。
Linux系统能支持如此多平台的部分原因是因为内河把原程序代码清晰的划分为体系结构无关部分和体系结构相关部分。对于任何平台,都必须包含以下几个目录:
u boot:包括启动内核所使用的部分或全部平台特有代码。
u kernel:存放支持体系结构特有的(如信号处理和SMP)特征的实现。
u lib:存放高速体系结构特有的(如strlen和memcpy)同用函数的实现。
u mm:存放体系结构特有的内存管理程序的实现。
u math-emu:模拟FPU的代码。对于ARM处理器来说,此目录用mach-xxx代替。
显然,移植工作的重点就是移植arch目录下的文件。
/drivers包含了内核中所有的设备驱动程序。
/fs包含了所有的文件系统的代码。
/include包含了建立内核代码时所需的大部分库文件,这个模块利用其他模块重建内核。该目录也包含了不同平台需要的库文件。比如,asm-arm是arm平台需要的库文件。
/init包含了内核的初始化代码,内核从此处工作。
不是系统的引导代码,由main.c和version.c两个文件。这是研究核心如何工作的好起点。
/ipc包含了进程间通信代码。
/kernel包含了主内核代码。
/mm包含了所有内存管理代码。
/net包含了和网络相关的代码。
3、 在移植过程中,定时器、中断、CACHE管理、MMU等和硬件密切相关的地方都是要相关平台的底层代码支持的,要特别注意。
4、 编译内核:
1) 配置内核
make ARCH=arm CROSS_COMPILE=arm-Linux- menuconfig
2) 创建内核依赖关系
make dep
3) 创建内核镜像文件
make zImage
4) 创建内核模块
make modules
make modules_install
对每一个配置来说,内核生成以后包括4个文件:没有压缩的内核镜像(zImage或bzImage),压缩的内核镜像(vmlinux),内核符号映射文件(System.map)以及配置文件(config)。
第七章 Linux设备驱动程序开发
1、 设备驱动的任务包括:
1) 自动配置和初始化子程序。这部分程序仅在初始化的时候被调用一次。
2) 服务于I/O请求的子程序。这部分是系统调用的结果。在执行这部分程序的时候,系统仍认为和进行调用的进程属于同一个进程,只是由用户态变成了核心态,并具有进行此系统调用的用户程序的运行环境,所以可以在其中调用sleep()等与进程运行环境有关的函数。
2、 设备类型分类:
1) 字符设备(char device)。字符设备是Linux最简单的设备,可以向文件一样访问。
初始化字符设备时,它的设备驱动程序向Linux登记,并在字符设备向量表中增加一个device_struct数据结构条目,这个设备的主设备标识符用作这个向量表的索引。一个设备的主设备标识符是固定的。chrdevs向量表中的每一个条目,一个device_struct数据结构,包括两个元素:一个登记的设备驱动程序的名称的指针和一个指向一组文件操作的指针。参见include/linux/major.h。
2) 块设备(block device)。是文件系统的物质基础,它也支持像文件一样被访问。
Linux用blkdevs向量表维护已经登记的块设备文件。它像chrdevs向量表一样,使用设备的主设备号作为索引。它的条目也是device_struct数据结构。与字符设备不同的是,块设备分为SCSI类和IDE类。类向Linux内核登记并向河心提供文件操作。一种块设备类的设备驱动程序向这种类提供和类相关的接口。参见fs/devices.c。
每一个块设备驱动程序必须提供普通的文件操作接口和对于buffer cache的接口。每一个块设备驱动程序填充blk_dev向量表中的blk_dev_struct数据结构。这个向量表的索引还是设备的主设备号。这个blk_dev_struct数据结构包括一个请求例程的地址和一个指针,指向一个request数据结构的列表,每一个都表达buffer cache向设备读/写一块数据的一个请求。参见drivers/block/ll_rw_blk.c和include/linux/blkdev.h。
当buffer_cache从一个已登记的设备读/写一块数据,或者希望读写一块数据到其他位置时,他救灾blk_dev_struct中增加一个request数据结构。每个request数据结构都有一个指向一个或多个buffer_head数据结构的指针,每一个都是读/写一块数据的请求。如果buffer_head数据结构被锁定(buffer_cache),可能会有一个进程在等待这个缓冲区的阻塞进成完成。每一个request数据结构都是从all_request表中分配的。如果request增加到空的request列表,就调用驱动程序的request函数处理这个request队列,否则驱动程序只是简单的处理request队列中的每一个请求。
块设备驱动程序和字符设备驱动程序的主要区别是:在对字符设备发出读写请求时,实际的硬件I/O一般紧接着就发生了,块设备则不然,它利用一块系统内存作为缓冲区,当用户进程对设备请求能满足用户的要求时,就返回请求的数据,如果不能就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备的,以免耗费过多地CPU时间来等待。
3) 网络设备。
3、 设备驱动中关键数据结构:
1) file_operations:内核内部通过file结构识别设备,通过file_operations数据结构提供文件系统的入口点函数。file_operations定义在中的函数指针表。这个结构的每一个成员的名字都对应着一个系统调用。从某种意义上说,写驱动程序的任务之一就是完成file_operations中的函数指针。如果在2.4版本内核下开发的驱动很可能在2.6版本中无法使用,需要进行移植。
2) inode:文件系统处理的文件所需要的信息在inode(索引节点)中。
3) file:主要用于与文件系统对应的设备驱动程序使用。
第八章 网络设备驱动程序开发
1、 网络设备使用网络接口管理表dev_base。dev_base是一个指向device结构的指针,因为网络设备是通过device数据结构来表示的。
2、 网络驱动程序必须解决的两个问题:
1) 不是所有建立在Linux内核的网络设备驱动程序都会有控制的设备。
2) 系统中的以太网设备总是叫做/dev/eth0到/dev/eth7,而不管底层的设备驱动程序是什么。当驱动程序找到它的以太网设备时,它就填充它现在拥有的ethn的device数据结构。这时网络驱动程序也要初始化它控制的物理硬件,并找出它使用的IRQ、DMA等。一旦所有的8个标准的/dev/ethn都分配了,就不会再探测更多的以太网设备。
3、 两个重要的数据结构:
1) device:这个数据结构是在系统中每一个设备的代表,它提供了多个设备方法,供操作系统或协议层使用。这其中就包括设备初始化时调用的init函数、打开和关闭设备的open和stop函数、处理数据包发送的hard_start_xmit函数等。
2) sk_buff:Linux网络各层之间的数据传送都是通过sk_buff(套接字缓冲区)完成的。每个sk_buff包括一些控制方法和一块数据缓冲区,这个区域存放了网络传输的数据包。控制方法按功能分为两种类型,一种是控制整个buffer链的方法,另一种是控制数据缓冲区的方法。sk_buff组成双向链表的形式,对链表的操作主要是删除链表头的元素和添加到链表尾。sk_buff数据结构再include/linux/skbuff.h文件中定义。
4、 内核的驱动程序接口:
1) 打开函数net_open:执行“ifconfig eth0 up”命令时网络层调用它,把设备连到线路上并启用来接受/发送数据。open函数在网络设备驱动程序里是网络设备被激活的时候被调用的,即设备状态由down至up。所以实际上很多在初始化的工作可以放到这里来做。
2) 关闭函数net_close:执行“ifconfig eth0 down”时使网卡进入一个清醒的状态。如果硬件许可那么它会释放中断和DMA通道,并完全关闭以节约能源。
3) 探测函数probe/probe1:在启动时调用以检测网卡存在与否。如果可以通过读取内存等非强制手段进行检查最好,也可以从I/O端口读取。在探测函数的最后它会填充device结构各域。
4) 发送函数:它与dev->hard_start_xmit()连接,在内核想通过设备传送数据时调用它。如果发送成功,hard_start_xmit()方法里释放sk_buff,返回0,否则返回1。如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0后才会再次发送;也可以不置dev->tbusy为非0,这样系统会不断尝试重发。tbusy的置0任务一般由中断完成,然后用mark_bh()调用通知系统可以再次发送。
5) 接收函数:它把数据从硬件移出,放在sk_buff结构中,执行netif_rx(sk_buff)告诉内核数据所在位置。真正的处理是在中断返回之后,这样可以减少中断时间。在协议层,接收数据包的流程控制分两个层次:首先,netif_rx()函数限制了从物理层到协议层的数据帧的数量;然后,每一个套接字都有一个队列,限制从协议层到套接字层的数据帧的数量。
6) 中断处理函数:需要了解相关的中断状态位以进行相应的操作。
7) 其他函数。
第九章 USB驱动程序开发
1、 OHCI简介:
OHCI规范定义了两个主机控制器(HC)与主机控制器驱动(HDC)的通信通道(Communication Channel)。第一个为主机控制器操作寄存器,第二个为主机控制器通信域(Host Controller Communications Area, HCCA)。
OHCI规范支持USB四种数据通信方式,并根据数据传输特定,将中断数据传输和等时数据传输归为同一类周期性数据传输方式。在HCCA中定义了4个链表。其中除完成数据链表外,其他的周期性数据链表、控制传输数据链表和批量传输数据链表都是二维链表。每个ED(Endpoint Descriptor)描述USB设备的一个端点的所有的数据传输,所有的ED被连接在一起,而TD(Transfer Descriptor)描述的才是最终要在USB总线上传输的数据包。属于同一个USB设备的端点的TD被连接在一起,并挂在相应的ED上。
主机控制器硬件通过寄存器访问该链表来得到相关的USB传输数据包,并将其发送到USB总线上。主机控制器驱动程序则根据实际的数据传输需要,将要发送的数据包添加到相应的链表上。
OHCI定义了两类TD:通用TD(General TD)和等时TD(Isochronous TD)。通用TD被用来支持USB中断、批量和控制三类传输方式,而等时TD被用来支持USB等时数据传输。用一个单独类型的TD来实现USB等时数据传输的目的是为了方便实现DMA数据传输功能。
2、 Linux下USB系统文件节点:同其他外设一样,上层应用软件对连接在系统地USB设备访问是通过文件系统的形式进行的。每个连接到系统总线上的USB设备可以同时对应有一个或者多个驱动程序。即一个USB设备可以在Linux系统上形成一个或多个设备节点,以供应用程序使用。在Linux系统上,每个设备节点都有其相关的主设备号和次设备号。
3、 USB主机驱动结构:Linux USB主机驱动由三部分组成:
1) USB主机控制器驱动(HCD):是USB主机驱动程序中直接与硬件交互的软件模块,其主要功能有:主机控制器硬件初始化;为USBD层提供相应的接口函数;提供根HUB(ROOT HUB)设备配置、控制功能;完成4种类型的数据传输等。
2) USB驱动(USBD):是整个USB主机驱动的核心,其主要实现的功能有:USB总线管理、USB总线设备、USB总线带宽管理、USB的4种类型数据传输、USB HUB驱动、为USB设备类驱动提供相关接口、提供应用程序访问的USB系统的文件接口等。
3) USB设备类驱动:是最终与应用程序交互的软件模块,其主要实现的功能有:访问特定的USB设备、为应用程序提供访问接口等。
应用程序首先通过文件系统(POSIX)接口来访问相应的USB设备类驱动程序和USBD;USB设备类驱动程序则通过USBD提供的相关接口将数据请求包传递给USBD;USBD通过HCD提供的接口,进一步将数据包传递给HCD;HCD最终将数据发送到USB总线上。Linux定义了通用的数据结构URB用来在USB设备类驱动和USBD,USBD和HCD间进行数据传输。统一的URB(Universal Request Block)结构为usb主机驱动程序的开发带来了很大方便。
4、 USB时序:
1) 数据传输时序:在USB总线上,所有的数据传输都是由USB HOST发起的。每个USB设备通过地址过滤出自己要接受的数据包,并根据数据包请求的类型与USB HOST进行数据传输。由于数据传输的时序和总线带宽问题,当应用程序通过设备类提供一个URB时,该数据包并不能立即被送到USB总线上,而只能在USB总线上有足够带宽的情况下,该数据请求才会被传输。因而,HCD层为不同类型的数据传输维护了相应的数据链,当数据链上的数据包传输结束后,HCD通过调用与该数据包相关联的回调函数来通知设备类驱动程序。
2) 设备连接:当一个USB设备连接到USB总线上时,USB HUB驱动程序首先通过中断数据传输获得设备连接信息,然后通过调用USB内核模块所提供的相关函数来完成对USB设备的配置工作。同时USBD不但要为新设备分配在USBD层所需要的资源,同时也要为USB设备分配在HCD层所需要的资源。接着,USBD通过调用所有USB设备类驱动程序提供的Probe函数来查找适合该USB设备的驱动程序。
3) 设备断开:当一个USB设备断开时,USB HUB驱动程序首先通过中断数据传输获得设备断开信息,然后通过调用USB内核模块所提供的相关函数来释放USBD层和HCD层为该设备分配资源。同时通过调用该设备的相关驱动程序提供的Disconnect函数来通知设备驱动程序该设备已经断开。
第十章 图形用户接口
1、 嵌入式系统中几种流行的GUI:
1) MicroWindows;
2) MiniGUI:是我国为数不多的在国际比较知名的自由软件之一。几乎所有的MiniGUI代码都采用C语言开发,提供了完备的多窗口机制和消息传递机制以及众多空间和其他GUI元素。其引人注目的特性和技术创新主要有:
是一个轻量级的图形系统;
完善的对中日韩文字、输入法的多字体和多字符集支持
提供图形抽象层(GAL)以及输入抽象层(IAL)以适应嵌入式系统各种显示和输入设备
提供MiniGUI-Threads、MiniGUI-Lite、MiniGUI-standone三种不同架构的版本以满足不同的嵌入式系统
提供了丰富的应用软件,其商业版本提供了手机、PDA类产品、媒体及机顶盒类产品以及工业控制方面的诸多程序;
3) Qt/Embedded:是挪威Trolltech软件公司的产品,Linux桌面系统的KDE就是基于Qt库(不是QtE)开发的。使用QtE苦,开发者可以:
当移植QtE程序到不同平台时,只需要重新编译代码,而不需要对代码进行修改;
随意设置程序界面的外观;
方便地为程序连接数据库;
使程序本地化;
将程序与Java集成
同Qt一样,QtE也是用C++写的,虽然这样会增加系统资源消耗,但是却为开发者提供了清晰的程序框架,使开发者能够迅速上手,并且可以方便的编写自定义的用户界面程序。
2、 MiniGUI编程:
它采用类似WINDOWS SDK的窗口和事件驱动机制,MiniGUI的存储空间占用情况如下:
项目
容量
备注
Linux内核
300~500KB
由系统决定
MiniGUI支持库
500~700KB
由编译选项确定
MiniGUI字体、位图等资源
400KB
由应用程序确定,可缩小到200KB以内
GB2312输入法码表
200KB
不是必需的,由应用程序确定
应用程序
1~2MB
由应用程序决定
MiniGUI层次:
FrameWork, MMI, Key Apps
MiniGUI
ANSIC Library
Portable Layer
Devices
Linux/uCLinux, eCos, uC/OS-II, VxWorks, Psos
ix86, ARM, MIPS, PowerPC, M68K
MiniGUI是一个典型的消息驱动的GUI系统,每一个MiniGUI程序都从MiniGUIMain函数开始。应用程序先以CreatMainWindow函数创建一个主窗口,由于窗口创建的时候默认是不可见的,所以我们通过ShowWindow函数显现该窗口,最后通过GetMessage(&Msg, hMainWnd)进入消息循环。
3、 Qt/Embedded编程:
Qt是以工具开发包的形式提供给开发者的,这些工具开发包包括了图形设计器、Makefile制作工具、字体国际化工具和Qr的C++类库等。如果不考虑X窗口系统的需要,基于QrE的应用程序可以直接对缓冲帧进行写操作。QtE提供了大约200个可配置的特征,由此再Intel X86平台上库的大小范围为700KB~5MB。大部分客户选择的配置使得库的大小为1.5~4MB。Qt/Embedded与Qt/X11的Linux版本架构比较如下:
应用源代码
Qt API
Qt/Embedded
Qt/X11
Qt/XLib
X Window sever
帧缓冲
Linux内核
信号与插槽机制提供了对象间的通信机制。Qt的窗口在事件发生后会激发信号,程序员通过建立一个函数(称作插槽)然后调用connect()函数把这个插槽和一个信号连接起来,这样就完成了一个事件和响应代码的连接。信号与插槽机制不需要类之间互相知道细节,这样就可以相对容易地开发出代码可高度重用的类。信号与插槽机制是类型安全的,它以警告的方式报告类型错误,而不会使系统产生崩溃。
Qt拥有丰富的满足不同需求的窗体(如按钮、滚动条等)。窗体是QWidget类或它子类的实例,客户自己的窗体类需要从QWidget的子类继承。一个窗体可以包含任意数量的子窗体,子窗体可以显示在父窗体的客户区,一个没有父窗体的窗体称为顶级窗体(一个“窗口”),一个窗体通常有一个边框和标题栏作为装饰。在父窗体无效、隐藏或被删除后,它的子窗体都会进行同样的操作。
第十一章 系统设计开发
1、 按照嵌入式系统的工程设计方法,嵌入式系统的设计可以分成7个阶段:产品定义、硬件与软件划分、迭代与实现、详细的硬件与软件设计、硬件与软件集成、接受测试和维护与升级。前三个阶段确定要解决的问题及需要完成的目标,也常被称为“需求阶段”;第四阶段主要解决如何在给定的约束条件下完成用户的需求;后三个阶段主要解决如何在所选择的硬件和软件基础上进行整个软/硬件系统的协调实现。
2、 硬件设计主要有以下五个关键步骤:
1) 功能定义;
2) 原理图设计:应尽量做到标准化、通用化、模块化、可扩展化;
3) PCB设计:是硬件设计的难点;
4) 硬件调试;
5) 产品化调整:一个系统的设计样机到产品化是个漫长的过程,单从硬件角度讲,至少要满足功能性、稳定性、可靠性、工艺性等最低要求。
3、 原理图设计中的几点建议:
1) 开关电源和线性电源的优缺点:
优点
缺点
开关电源
输入电压范围宽
效率高
输出功率大
应用比较灵活
电路相对复杂,外围器件较多
对电容电感的要求很高,布线也很讲究
开关频率会给系统带来干扰
纹波比较难控制
线性电源
电路简单,外围器件很少
输出精度高,有很好的负载曲线
工作在低频状态,不会给系统带来麻烦
输入范围比较受限制
效率低,这是由于线性电源自身的损耗造成的
输出功率相对较小
2) NAND和NOR Flash的速度差异:
NOR得读速度比NAND稍快一些;
NAND的写入速度比NOR快很多;
NAND的4ms擦除速度远比NOR的5s快;
大多数写入操作需要先进行擦除操作;
NAND的擦除单元更小,相应的擦除电路更少。
4、 PCB设计要点:
1) 元件封装的准备: 尽量调用标准封装库中的文件;
严密按照所选器件的数据手册上的规范制作封装,不能忽略累计误差;
注意二极管、三极管等极性元件及一些非对称元件的引脚定义不能搞错。
2) 合理布局:
尽量依照参考板的模式进行布局;
模块化布局;
要求模拟电路与数字电路分开;
输入模块与输出模块隔离;
去隅电容尽量靠近元件的电源/地;
电源等发热元件要考虑散热,主发热元件靠近出风口,大体积元件的放置避开风路;
元件分布均匀,避免电流过于密集;
板上的跳线或按键考虑易操作性;
元件的排列尽量整齐美观;
考虑机械尺寸,不要超过结构所允许的范围。
3) PCB分层:
如果有参考板,按照参考板进行分层;
多层板安排:顶层和底层为元件面,第二层为地平面,倒数第二层为电源层;
在不影响性能的情况下,减少PCB层数,降低成本。
4) 电源考虑:
系统电源入口做高频和低频滤波处理;
功率较高的器件配备大容量电容去除低频干扰;
每个器件配备0.1Uf电容过滤高频干扰;
高频器件电源管脚和电容之间串联磁珠达到更好的效果;
去耦电容的引线不能过长,特别是高频旁路电容不能带引线。
5) 时钟考虑:
时钟电路尽量靠近芯片;
晶体下方不要走线;
晶体外科接地,增加抗电磁干扰能力;
频率大于20MHz的时钟信号有地线护送;
时钟线宽大于10mil;
时钟输出端串联22~220Ω的阻尼电阻。
6) 高速信号:
采用手工布线;
高速总线走线尽量等长,并且在靠近数据输出端串联22~300Ω的阻尼电阻;
高速信号远离时钟芯片和晶体;
高速信号远离外部输入输出端口,或地线隔离。
7) 差分信号:
差分信号要平行等长;
信号之间不能走其他信号线;
信号要求在同一层上。
8) 走线规范:
不同层的信号垂直走线;
地线和电源层不要走线,否则要保证平面的完整性;
导线宽度不要突变;
导线变向时导角要大于90度;
定位孔周围0.5mm范围不要走线。
5、 硬件调试:
优先调试电源:保证系统可靠地供电;
分模块调试:可以分清模块间的问题,不至于混淆;
结合软件调试:对于复杂的接口,单纯硬件角度不易调试,结合软件从不同的角度测试,能起到更好的效果。
正确、合理的使用示波器,提高工作效率;
对比调试:有条件用评估板或功能相似的电路板作参考,比较差异并找出问题所在;
系统时钟受干扰或晶体震荡不正常导致系统工作故障;
复位不可靠,造成个单元未进入预期状态而出现问题;
因焊接问题引发的各种问题,如方向焊错、虚焊、错焊等;
因时序不匹配引发的通信故障,如时钟信号通过逻辑器件后产生延时,与读写信号时序搭配不上导致读写错误。
6、 嵌入式文件系统:
目前支持闪存的文件系统技术有以下几种:
JFFS2和Yaffs。这些文件系统可以使用在没有初始化的NAND Flash和有CFI接口的NOR Flash中。JFFS2的特点包括:
支持数据压缩;
提供了“写平衡”支持;
支持多种结点类型;
提高了对闪存的利用率,降低了闪存的消耗。
JFFS2中最重要的数据结构是jffs2_sb_info,这个数据结构用来管理所有的结点链表和闪存块。它在/src/include/Linux/jffs2_fs_sb.h中定义。
Yaffs/Yaffs2(Yet Another Flash File System)和JFFS相比,它减少了一些功能,因此速度更快、占用内存更少。
TrueFFS。该文件系统相当于Linux中的MTD层,必须配合其他文件系统。
FTL/NTFL。它是一种中间层解决方案的统称,为上层文件系统提供接口。
RAMFS、CRAMFS和ROMFS。这些文件系统用于早期的小容量闪存设备,系统功能比较简单,仅提供基本接口,只属于只读的闪存文件系统。适合存储空间小的系统。
7、 MTD简介:
无论JFFS2还是Yaffs,都需要MTD(Memory Technology Devices,内存技术设备)的支持。MTD是对Flash操作的接口,提供了一系列的标准函数,将硬件驱动设计和系统程序设计分开,硬件驱动人员不用了解存储设备的组织方法,只需提供标准的函数调用,如读、写等。
一个MTD原始设备可以通过mtd_part分割成数个MTD原始设备注册进mtd_table,mtd_table中的每个MTD原始设备都可以被注册成一个MTD设备,
1、 嵌入式系统定义:
“嵌入式系统是用来控制或者监视机器、装置、工厂等大规模系统的设备。”
——电气工程师协会
“嵌入到对象体系中的专用计算机系统”
——北京航空航天大学 何立民教授
“嵌入性”、“专用性”与“计算机系统”是嵌入式系统的三个基本要素。
2、 嵌入式操作系统:
硬实时系统有一个刚性的、不可改变的时间限制,它不允许任何超出时限的错误超时错误会带来损害甚至导致系统失败、或者导致系统不能实现它的预期目标。
软实时系统的时限是柔性灵活的,它可以容忍偶然的超时错误。失败造成的后果并不严重,仅仅是轻微地降低了系统的吞吐量。
我们可以认为至少嵌入式系统都是软实时系统,所有的嵌入式系统都是实时系统,但并不是所有的实时系统都是嵌入式系统。
常用的嵌入式操作系统有:Linux, uC/OS, Windows CE, VxWorks, Palm OS, QNX等。
3、 选择Embedded OS的原则:
系统成本;
市场进入时间及技术支持;
可移植性;
可利用资源;
系统定制能力。
第二章 基于ARM9处理器的硬件开发平台
1、 ARM的历史:
ARM(Advanced RISC Machine)公司于1990年11月在英国剑桥成立。
1991年,ARM推出第一个嵌入式RISC核心——ARM6系列处理器,VLSI、夏普、GEC Plessey、德州仪器、Cirrus Logic等公司相继同ARM公司签署了授权协议。
1998年4月,ARM在伦敦证券交易所和纳斯达克交易所上市。
ARM中国安谋咨询上海有限公司于2002年7月在中国上海成立。
目前基于ARM核的处理器有以下几类:
ARM7家族;
ARM9家族;
ARM9E家族;
ARM10E家族;
ARM11家族;
SecurCore家族;
OptimoDE数据引擎内核;
MPCore多处理器家族;
Intel公司的StrongARM/XScale。
2、ARM7和ARM9处理器的主要区别:
指令流水线:
ARM7:三级,(取指令,译码,执行);
ARM9:五级,(取指,译码,执行,缓冲/数据,回写)。
3、三星S3C2410X处理器:
基于ARM920T核(由ARM9TDMI、存储管理单元MMU和高速缓存三部分组成),片上资源包括:
1个LCD控制器(支持STN和TFT带有触摸屏的液晶显示屏);
SDRAM控制器;
3个通道的UART;
4个通道的DMA;
4个具有PWM功能的计时器和1个内部时钟;
8通道的10位ADC;
触摸屏接口;
I2S总线接口;
2个USB主机接口,1个USB设备接口;
2个SPI接口;
SD接口和MMC卡接口;
看门狗计数器;
117位通用I/O口和24位外部中断源;
第三章 调试嵌入式系统程序
1、 嵌入式系统调试方法:
1) 实时在线仿真(In-Circuit Emulator, ICE)
优点:功能非常强大,软硬件均可做到完全实时在线调试。
缺点:价格昂贵。
2) 模拟调试
优点:简单方便,不需要目标板,成本低。
缺点:功能非常有限,无法实时调试。
3) 软件调试
优点:纯软件,价格较低,简单,软件调试能力较强。
缺点:需要事先烧制监控程序(Monitor)(往往需多次实验才能成功)且目标板工作正常,功能有限,特别是硬件调试能力较差。
4) JTAG调试
优点:方便、简单,无需制作Monitor,软硬件均可调适。
缺点:需要工作基本正常(至少CPU工作正常)的目标板,仅适用于有调试接口的芯片。
2、 ARM仿真器工作原理:
利用高速JTAG(Joint Test Action Group)串行扫描链,通过调试通信通道(Debug Communication Channel, DCC)连接ARM核心内嵌的名为“Embedded-ICE”的调试逻辑,调试逻辑实时监测ARM核心的寄存器、数据总线和地址总线。调试器设置Breakpoint及Watchpoint后,程序在ARM内核全速运行,调试程序实时监测地址与数据总线并与预设值比较,在吻合时产生异常中断通知内核并把控制权交给调试器。这样,在程序全速运行时,可以在断点处停止,可以设置条件断点、条件观测断点等,而又不占用CPU时间及内存资源。
3、 JTAG接口:
1985年制定的检测PCB和IC的一个标准,1990年被修改后成为IEEE的一个标准,及IEEE1149.1-1990。通过这个标准,可对具有JTAG接口芯片的电路进行边界扫描和故障检测。
第四章 创建嵌入式系统开发环境
1、交叉编译步骤:
(1) 创建编译环境。在这个过程中,将设置一些环境变量,创建安装目录,安装内核源代码和头文件等。
(2) 创建binutils。这个过程结束后,会创建类似arm-linux-ld等工具。
Binutils是一组开发工具,包括链接器、汇编器以及其他用于目标文件和档案的工具
首先要安装的软件包使binutils。这非常重要,因为glibc和gcc会针对可用的连接器和汇编器进行多种测试,以决定打开某些特性。
(3) 创建一个交叉编译版本的gcc。注意:在这个过程中只能编译C程序,而不能编译C++程序。
创建交叉编译版本的gcc,需要交叉编译版本的glibc及其头文件,而交叉编译版本的glibc是通过交叉编译版本的gcc创建的。面对这个先有鸡还是先有蛋的问题,解决办法是先只编译对C语言的支持,并禁止支持线程。
(4) 创建一个交叉编译版本的glibc。这里最容易出现问题。
glibc是一个提供系统调用和基本函数的C语言库,比如open,malloc和printf等,所有动态链接的程序都要用到它。创建glibc需要的时间更长。
(5) 创建一个交叉编译版本的gdb。在这个过程结束后,会创建ARM-Linux-gdb。
(6) 重新创建gcc。前面创建gcc的过程没有编译C++编译器,现在glibc已经准备好了,所以这个步骤将完善gcc的交叉编译。
(7) 重新创建glibc。如果成功执行了这个过程,那么你就拥有了一套属于自己的交叉编译工具链。
2、如果在交叉编译过程中出现错误,那么请检查:
版本选择是否正确,以及是否安装了相应的补丁;
库文件路径是否正确;
系统环境变量是否设置正确。
第五章 Bootloader
1、 Bootloader(引导加载程序)是系统加电后运行的第一段代码。一般它只在系统启动时运行非常短的一段时间,但对于嵌入式系统来说,这是一个非常重要的系统组成部分。
2、 嵌入式Linux系统从软件的角度看通常可以分成4个层次:
(1) 引导加载程序。包括固化在固件(Firmware)中的启动代码(可选)和Bootloader两大部分。
(2) 内核。特定于嵌入式板子的定制内核以及控制内核引导系统的参数。
(3) 文件系统。包括根文件系统和建立于Flash内存设备之上的文件系统。通常用Ramdisk作为根文件系统。它是提供管理系统的各种配置文件以及系统执行用户应用程序的良好的运行环境的载体。
(4) 用户应用程序。特定于用户的应用程序。有时在用户应用程序和内核层之间可能还会包括一个嵌入式图形用户界面。
3、 Bootloader包含两种不同的操作模式:
1) 启动加载(Bootloading)模式;
2) 下载(Downloading)模式。
4、 Bootloader的启动流程:
第一阶段:主要包含依赖于CPU的体系结构硬件初始化的代码,通常都用汇编实现。这个阶段的任务有:
基本的硬件设备初始化(屏蔽所有的中断、关闭处理器内部指令/数据Cache等)
为第二阶段准备RAM空间
如果是从某个固态存储媒质中,则复制Bootloader的第二段代码到RAM
设置堆栈
跳转到第二阶段的程序入口点
第二阶段:通常用C语言完成,以便实现更复杂的功能,也使程序有更好的可读性和可移植性。这个阶段的任务有:
初始化本阶段要使用到的硬件设备
检测系统内存映射
将内核映像和根文件系统映像从FLASH读到RAM
为内核设置启动参数
调用内核
5、 常用U-Boot命令:
命令名
功能
help / ?
帮助命令。用于查询U-Boot支持的命令并列出简单说明,和“?”是同一个命令
bdinfo
察看目标系统参数和变量、目标板的硬件配置、各种变量参数
setenv
设置环境变量。比较常用的有:
setenv ipaddr *.*.*.*
setenv severip *.*.*.*
setenv gatewayip *.*.*.*
setenv ethaddr *.*.*.*.*.*
printenv
查看环境变量
saveenv
保存设置的环境变量到Flash
mw
写内存
md
察看内存
mm
修改内存
flinfo
察看Flash的信息
erase [起始地址 结束地址]
搽除Flash内容,必须以扇区为单位进行搽除
cp [源地址 目标地址 大小]
内存复制,可以在Flash和ram中交换数据
imi [起始地址]
察看内核映像文件
bootm [起始地址]
从某个地址启动内核
tftpboot [起始地址 镜像名]
通过ftp从主机系统下载内核映像文件
reset
复位
第六章 Linux系统在ARM平台的移植
1、 使某一个平台的代码运行在其它平台上的过程就叫做移植。
2、 Linux内核结构:
/arch包含了所有硬件结构特定的内核代码。
Linux系统能支持如此多平台的部分原因是因为内河把原程序代码清晰的划分为体系结构无关部分和体系结构相关部分。对于任何平台,都必须包含以下几个目录:
u boot:包括启动内核所使用的部分或全部平台特有代码。
u kernel:存放支持体系结构特有的(如信号处理和SMP)特征的实现。
u lib:存放高速体系结构特有的(如strlen和memcpy)同用函数的实现。
u mm:存放体系结构特有的内存管理程序的实现。
u math-emu:模拟FPU的代码。对于ARM处理器来说,此目录用mach-xxx代替。
显然,移植工作的重点就是移植arch目录下的文件。
/drivers包含了内核中所有的设备驱动程序。
/fs包含了所有的文件系统的代码。
/include包含了建立内核代码时所需的大部分库文件,这个模块利用其他模块重建内核。该目录也包含了不同平台需要的库文件。比如,asm-arm是arm平台需要的库文件。
/init包含了内核的初始化代码,内核从此处工作。
不是系统的引导代码,由main.c和version.c两个文件。这是研究核心如何工作的好起点。
/ipc包含了进程间通信代码。
/kernel包含了主内核代码。
/mm包含了所有内存管理代码。
/net包含了和网络相关的代码。
3、 在移植过程中,定时器、中断、CACHE管理、MMU等和硬件密切相关的地方都是要相关平台的底层代码支持的,要特别注意。
4、 编译内核:
1) 配置内核
make ARCH=arm CROSS_COMPILE=arm-Linux- menuconfig
2) 创建内核依赖关系
make dep
3) 创建内核镜像文件
make zImage
4) 创建内核模块
make modules
make modules_install
对每一个配置来说,内核生成以后包括4个文件:没有压缩的内核镜像(zImage或bzImage),压缩的内核镜像(vmlinux),内核符号映射文件(System.map)以及配置文件(config)。
第七章 Linux设备驱动程序开发
1、 设备驱动的任务包括:
1) 自动配置和初始化子程序。这部分程序仅在初始化的时候被调用一次。
2) 服务于I/O请求的子程序。这部分是系统调用的结果。在执行这部分程序的时候,系统仍认为和进行调用的进程属于同一个进程,只是由用户态变成了核心态,并具有进行此系统调用的用户程序的运行环境,所以可以在其中调用sleep()等与进程运行环境有关的函数。
2、 设备类型分类:
1) 字符设备(char device)。字符设备是Linux最简单的设备,可以向文件一样访问。
初始化字符设备时,它的设备驱动程序向Linux登记,并在字符设备向量表中增加一个device_struct数据结构条目,这个设备的主设备标识符用作这个向量表的索引。一个设备的主设备标识符是固定的。chrdevs向量表中的每一个条目,一个device_struct数据结构,包括两个元素:一个登记的设备驱动程序的名称的指针和一个指向一组文件操作的指针。参见include/linux/major.h。
2) 块设备(block device)。是文件系统的物质基础,它也支持像文件一样被访问。
Linux用blkdevs向量表维护已经登记的块设备文件。它像chrdevs向量表一样,使用设备的主设备号作为索引。它的条目也是device_struct数据结构。与字符设备不同的是,块设备分为SCSI类和IDE类。类向Linux内核登记并向河心提供文件操作。一种块设备类的设备驱动程序向这种类提供和类相关的接口。参见fs/devices.c。
每一个块设备驱动程序必须提供普通的文件操作接口和对于buffer cache的接口。每一个块设备驱动程序填充blk_dev向量表中的blk_dev_struct数据结构。这个向量表的索引还是设备的主设备号。这个blk_dev_struct数据结构包括一个请求例程的地址和一个指针,指向一个request数据结构的列表,每一个都表达buffer cache向设备读/写一块数据的一个请求。参见drivers/block/ll_rw_blk.c和include/linux/blkdev.h。
当buffer_cache从一个已登记的设备读/写一块数据,或者希望读写一块数据到其他位置时,他救灾blk_dev_struct中增加一个request数据结构。每个request数据结构都有一个指向一个或多个buffer_head数据结构的指针,每一个都是读/写一块数据的请求。如果buffer_head数据结构被锁定(buffer_cache),可能会有一个进程在等待这个缓冲区的阻塞进成完成。每一个request数据结构都是从all_request表中分配的。如果request增加到空的request列表,就调用驱动程序的request函数处理这个request队列,否则驱动程序只是简单的处理request队列中的每一个请求。
块设备驱动程序和字符设备驱动程序的主要区别是:在对字符设备发出读写请求时,实际的硬件I/O一般紧接着就发生了,块设备则不然,它利用一块系统内存作为缓冲区,当用户进程对设备请求能满足用户的要求时,就返回请求的数据,如果不能就调用请求函数来进行实际的I/O操作。块设备是主要针对磁盘等慢速设备的,以免耗费过多地CPU时间来等待。
3) 网络设备。
3、 设备驱动中关键数据结构:
1) file_operations:内核内部通过file结构识别设备,通过file_operations数据结构提供文件系统的入口点函数。file_operations定义在
2) inode:文件系统处理的文件所需要的信息在inode(索引节点)中。
3) file:主要用于与文件系统对应的设备驱动程序使用。
第八章 网络设备驱动程序开发
1、 网络设备使用网络接口管理表dev_base。dev_base是一个指向device结构的指针,因为网络设备是通过device数据结构来表示的。
2、 网络驱动程序必须解决的两个问题:
1) 不是所有建立在Linux内核的网络设备驱动程序都会有控制的设备。
2) 系统中的以太网设备总是叫做/dev/eth0到/dev/eth7,而不管底层的设备驱动程序是什么。当驱动程序找到它的以太网设备时,它就填充它现在拥有的ethn的device数据结构。这时网络驱动程序也要初始化它控制的物理硬件,并找出它使用的IRQ、DMA等。一旦所有的8个标准的/dev/ethn都分配了,就不会再探测更多的以太网设备。
3、 两个重要的数据结构:
1) device:这个数据结构是在系统中每一个设备的代表,它提供了多个设备方法,供操作系统或协议层使用。这其中就包括设备初始化时调用的init函数、打开和关闭设备的open和stop函数、处理数据包发送的hard_start_xmit函数等。
2) sk_buff:Linux网络各层之间的数据传送都是通过sk_buff(套接字缓冲区)完成的。每个sk_buff包括一些控制方法和一块数据缓冲区,这个区域存放了网络传输的数据包。控制方法按功能分为两种类型,一种是控制整个buffer链的方法,另一种是控制数据缓冲区的方法。sk_buff组成双向链表的形式,对链表的操作主要是删除链表头的元素和添加到链表尾。sk_buff数据结构再include/linux/skbuff.h文件中定义。
4、 内核的驱动程序接口:
1) 打开函数net_open:执行“ifconfig eth0 up”命令时网络层调用它,把设备连到线路上并启用来接受/发送数据。open函数在网络设备驱动程序里是网络设备被激活的时候被调用的,即设备状态由down至up。所以实际上很多在初始化的工作可以放到这里来做。
2) 关闭函数net_close:执行“ifconfig eth0 down”时使网卡进入一个清醒的状态。如果硬件许可那么它会释放中断和DMA通道,并完全关闭以节约能源。
3) 探测函数probe/probe1:在启动时调用以检测网卡存在与否。如果可以通过读取内存等非强制手段进行检查最好,也可以从I/O端口读取。在探测函数的最后它会填充device结构各域。
4) 发送函数:它与dev->hard_start_xmit()连接,在内核想通过设备传送数据时调用它。如果发送成功,hard_start_xmit()方法里释放sk_buff,返回0,否则返回1。如果dev->tbusy置为非0,则系统认为硬件忙,要等到dev->tbusy置0后才会再次发送;也可以不置dev->tbusy为非0,这样系统会不断尝试重发。tbusy的置0任务一般由中断完成,然后用mark_bh()调用通知系统可以再次发送。
5) 接收函数:它把数据从硬件移出,放在sk_buff结构中,执行netif_rx(sk_buff)告诉内核数据所在位置。真正的处理是在中断返回之后,这样可以减少中断时间。在协议层,接收数据包的流程控制分两个层次:首先,netif_rx()函数限制了从物理层到协议层的数据帧的数量;然后,每一个套接字都有一个队列,限制从协议层到套接字层的数据帧的数量。
6) 中断处理函数:需要了解相关的中断状态位以进行相应的操作。
7) 其他函数。
第九章 USB驱动程序开发
1、 OHCI简介:
OHCI规范定义了两个主机控制器(HC)与主机控制器驱动(HDC)的通信通道(Communication Channel)。第一个为主机控制器操作寄存器,第二个为主机控制器通信域(Host Controller Communications Area, HCCA)。
OHCI规范支持USB四种数据通信方式,并根据数据传输特定,将中断数据传输和等时数据传输归为同一类周期性数据传输方式。在HCCA中定义了4个链表。其中除完成数据链表外,其他的周期性数据链表、控制传输数据链表和批量传输数据链表都是二维链表。每个ED(Endpoint Descriptor)描述USB设备的一个端点的所有的数据传输,所有的ED被连接在一起,而TD(Transfer Descriptor)描述的才是最终要在USB总线上传输的数据包。属于同一个USB设备的端点的TD被连接在一起,并挂在相应的ED上。
主机控制器硬件通过寄存器访问该链表来得到相关的USB传输数据包,并将其发送到USB总线上。主机控制器驱动程序则根据实际的数据传输需要,将要发送的数据包添加到相应的链表上。
OHCI定义了两类TD:通用TD(General TD)和等时TD(Isochronous TD)。通用TD被用来支持USB中断、批量和控制三类传输方式,而等时TD被用来支持USB等时数据传输。用一个单独类型的TD来实现USB等时数据传输的目的是为了方便实现DMA数据传输功能。
2、 Linux下USB系统文件节点:同其他外设一样,上层应用软件对连接在系统地USB设备访问是通过文件系统的形式进行的。每个连接到系统总线上的USB设备可以同时对应有一个或者多个驱动程序。即一个USB设备可以在Linux系统上形成一个或多个设备节点,以供应用程序使用。在Linux系统上,每个设备节点都有其相关的主设备号和次设备号。
3、 USB主机驱动结构:Linux USB主机驱动由三部分组成:
1) USB主机控制器驱动(HCD):是USB主机驱动程序中直接与硬件交互的软件模块,其主要功能有:主机控制器硬件初始化;为USBD层提供相应的接口函数;提供根HUB(ROOT HUB)设备配置、控制功能;完成4种类型的数据传输等。
2) USB驱动(USBD):是整个USB主机驱动的核心,其主要实现的功能有:USB总线管理、USB总线设备、USB总线带宽管理、USB的4种类型数据传输、USB HUB驱动、为USB设备类驱动提供相关接口、提供应用程序访问的USB系统的文件接口等。
3) USB设备类驱动:是最终与应用程序交互的软件模块,其主要实现的功能有:访问特定的USB设备、为应用程序提供访问接口等。
应用程序首先通过文件系统(POSIX)接口来访问相应的USB设备类驱动程序和USBD;USB设备类驱动程序则通过USBD提供的相关接口将数据请求包传递给USBD;USBD通过HCD提供的接口,进一步将数据包传递给HCD;HCD最终将数据发送到USB总线上。Linux定义了通用的数据结构URB用来在USB设备类驱动和USBD,USBD和HCD间进行数据传输。统一的URB(Universal Request Block)结构为usb主机驱动程序的开发带来了很大方便。
4、 USB时序:
1) 数据传输时序:在USB总线上,所有的数据传输都是由USB HOST发起的。每个USB设备通过地址过滤出自己要接受的数据包,并根据数据包请求的类型与USB HOST进行数据传输。由于数据传输的时序和总线带宽问题,当应用程序通过设备类提供一个URB时,该数据包并不能立即被送到USB总线上,而只能在USB总线上有足够带宽的情况下,该数据请求才会被传输。因而,HCD层为不同类型的数据传输维护了相应的数据链,当数据链上的数据包传输结束后,HCD通过调用与该数据包相关联的回调函数来通知设备类驱动程序。
2) 设备连接:当一个USB设备连接到USB总线上时,USB HUB驱动程序首先通过中断数据传输获得设备连接信息,然后通过调用USB内核模块所提供的相关函数来完成对USB设备的配置工作。同时USBD不但要为新设备分配在USBD层所需要的资源,同时也要为USB设备分配在HCD层所需要的资源。接着,USBD通过调用所有USB设备类驱动程序提供的Probe函数来查找适合该USB设备的驱动程序。
3) 设备断开:当一个USB设备断开时,USB HUB驱动程序首先通过中断数据传输获得设备断开信息,然后通过调用USB内核模块所提供的相关函数来释放USBD层和HCD层为该设备分配资源。同时通过调用该设备的相关驱动程序提供的Disconnect函数来通知设备驱动程序该设备已经断开。
第十章 图形用户接口
1、 嵌入式系统中几种流行的GUI:
1) MicroWindows;
2) MiniGUI:是我国为数不多的在国际比较知名的自由软件之一。几乎所有的MiniGUI代码都采用C语言开发,提供了完备的多窗口机制和消息传递机制以及众多空间和其他GUI元素。其引人注目的特性和技术创新主要有:
是一个轻量级的图形系统;
完善的对中日韩文字、输入法的多字体和多字符集支持
提供图形抽象层(GAL)以及输入抽象层(IAL)以适应嵌入式系统各种显示和输入设备
提供MiniGUI-Threads、MiniGUI-Lite、MiniGUI-standone三种不同架构的版本以满足不同的嵌入式系统
提供了丰富的应用软件,其商业版本提供了手机、PDA类产品、媒体及机顶盒类产品以及工业控制方面的诸多程序;
3) Qt/Embedded:是挪威Trolltech软件公司的产品,Linux桌面系统的KDE就是基于Qt库(不是QtE)开发的。使用QtE苦,开发者可以:
当移植QtE程序到不同平台时,只需要重新编译代码,而不需要对代码进行修改;
随意设置程序界面的外观;
方便地为程序连接数据库;
使程序本地化;
将程序与Java集成
同Qt一样,QtE也是用C++写的,虽然这样会增加系统资源消耗,但是却为开发者提供了清晰的程序框架,使开发者能够迅速上手,并且可以方便的编写自定义的用户界面程序。
2、 MiniGUI编程:
它采用类似WINDOWS SDK的窗口和事件驱动机制,MiniGUI的存储空间占用情况如下:
项目
容量
备注
Linux内核
300~500KB
由系统决定
MiniGUI支持库
500~700KB
由编译选项确定
MiniGUI字体、位图等资源
400KB
由应用程序确定,可缩小到200KB以内
GB2312输入法码表
200KB
不是必需的,由应用程序确定
应用程序
1~2MB
由应用程序决定
MiniGUI层次:
FrameWork, MMI, Key Apps
MiniGUI
ANSIC Library
Portable Layer
Devices
Linux/uCLinux, eCos, uC/OS-II, VxWorks, Psos
ix86, ARM, MIPS, PowerPC, M68K
MiniGUI是一个典型的消息驱动的GUI系统,每一个MiniGUI程序都从MiniGUIMain函数开始。应用程序先以CreatMainWindow函数创建一个主窗口,由于窗口创建的时候默认是不可见的,所以我们通过ShowWindow函数显现该窗口,最后通过GetMessage(&Msg, hMainWnd)进入消息循环。
3、 Qt/Embedded编程:
Qt是以工具开发包的形式提供给开发者的,这些工具开发包包括了图形设计器、Makefile制作工具、字体国际化工具和Qr的C++类库等。如果不考虑X窗口系统的需要,基于QrE的应用程序可以直接对缓冲帧进行写操作。QtE提供了大约200个可配置的特征,由此再Intel X86平台上库的大小范围为700KB~5MB。大部分客户选择的配置使得库的大小为1.5~4MB。Qt/Embedded与Qt/X11的Linux版本架构比较如下:
应用源代码
Qt API
Qt/Embedded
Qt/X11
Qt/XLib
X Window sever
帧缓冲
Linux内核
信号与插槽机制提供了对象间的通信机制。Qt的窗口在事件发生后会激发信号,程序员通过建立一个函数(称作插槽)然后调用connect()函数把这个插槽和一个信号连接起来,这样就完成了一个事件和响应代码的连接。信号与插槽机制不需要类之间互相知道细节,这样就可以相对容易地开发出代码可高度重用的类。信号与插槽机制是类型安全的,它以警告的方式报告类型错误,而不会使系统产生崩溃。
Qt拥有丰富的满足不同需求的窗体(如按钮、滚动条等)。窗体是QWidget类或它子类的实例,客户自己的窗体类需要从QWidget的子类继承。一个窗体可以包含任意数量的子窗体,子窗体可以显示在父窗体的客户区,一个没有父窗体的窗体称为顶级窗体(一个“窗口”),一个窗体通常有一个边框和标题栏作为装饰。在父窗体无效、隐藏或被删除后,它的子窗体都会进行同样的操作。
第十一章 系统设计开发
1、 按照嵌入式系统的工程设计方法,嵌入式系统的设计可以分成7个阶段:产品定义、硬件与软件划分、迭代与实现、详细的硬件与软件设计、硬件与软件集成、接受测试和维护与升级。前三个阶段确定要解决的问题及需要完成的目标,也常被称为“需求阶段”;第四阶段主要解决如何在给定的约束条件下完成用户的需求;后三个阶段主要解决如何在所选择的硬件和软件基础上进行整个软/硬件系统的协调实现。
2、 硬件设计主要有以下五个关键步骤:
1) 功能定义;
2) 原理图设计:应尽量做到标准化、通用化、模块化、可扩展化;
3) PCB设计:是硬件设计的难点;
4) 硬件调试;
5) 产品化调整:一个系统的设计样机到产品化是个漫长的过程,单从硬件角度讲,至少要满足功能性、稳定性、可靠性、工艺性等最低要求。
3、 原理图设计中的几点建议:
1) 开关电源和线性电源的优缺点:
优点
缺点
开关电源
输入电压范围宽
效率高
输出功率大
应用比较灵活
电路相对复杂,外围器件较多
对电容电感的要求很高,布线也很讲究
开关频率会给系统带来干扰
纹波比较难控制
线性电源
电路简单,外围器件很少
输出精度高,有很好的负载曲线
工作在低频状态,不会给系统带来麻烦
输入范围比较受限制
效率低,这是由于线性电源自身的损耗造成的
输出功率相对较小
2) NAND和NOR Flash的速度差异:
NOR得读速度比NAND稍快一些;
NAND的写入速度比NOR快很多;
NAND的4ms擦除速度远比NOR的5s快;
大多数写入操作需要先进行擦除操作;
NAND的擦除单元更小,相应的擦除电路更少。
4、 PCB设计要点:
1) 元件封装的准备: 尽量调用标准封装库中的文件;
严密按照所选器件的数据手册上的规范制作封装,不能忽略累计误差;
注意二极管、三极管等极性元件及一些非对称元件的引脚定义不能搞错。
2) 合理布局:
尽量依照参考板的模式进行布局;
模块化布局;
要求模拟电路与数字电路分开;
输入模块与输出模块隔离;
去隅电容尽量靠近元件的电源/地;
电源等发热元件要考虑散热,主发热元件靠近出风口,大体积元件的放置避开风路;
元件分布均匀,避免电流过于密集;
板上的跳线或按键考虑易操作性;
元件的排列尽量整齐美观;
考虑机械尺寸,不要超过结构所允许的范围。
3) PCB分层:
如果有参考板,按照参考板进行分层;
多层板安排:顶层和底层为元件面,第二层为地平面,倒数第二层为电源层;
在不影响性能的情况下,减少PCB层数,降低成本。
4) 电源考虑:
系统电源入口做高频和低频滤波处理;
功率较高的器件配备大容量电容去除低频干扰;
每个器件配备0.1Uf电容过滤高频干扰;
高频器件电源管脚和电容之间串联磁珠达到更好的效果;
去耦电容的引线不能过长,特别是高频旁路电容不能带引线。
5) 时钟考虑:
时钟电路尽量靠近芯片;
晶体下方不要走线;
晶体外科接地,增加抗电磁干扰能力;
频率大于20MHz的时钟信号有地线护送;
时钟线宽大于10mil;
时钟输出端串联22~220Ω的阻尼电阻。
6) 高速信号:
采用手工布线;
高速总线走线尽量等长,并且在靠近数据输出端串联22~300Ω的阻尼电阻;
高速信号远离时钟芯片和晶体;
高速信号远离外部输入输出端口,或地线隔离。
7) 差分信号:
差分信号要平行等长;
信号之间不能走其他信号线;
信号要求在同一层上。
8) 走线规范:
不同层的信号垂直走线;
地线和电源层不要走线,否则要保证平面的完整性;
导线宽度不要突变;
导线变向时导角要大于90度;
定位孔周围0.5mm范围不要走线。
5、 硬件调试:
优先调试电源:保证系统可靠地供电;
分模块调试:可以分清模块间的问题,不至于混淆;
结合软件调试:对于复杂的接口,单纯硬件角度不易调试,结合软件从不同的角度测试,能起到更好的效果。
正确、合理的使用示波器,提高工作效率;
对比调试:有条件用评估板或功能相似的电路板作参考,比较差异并找出问题所在;
系统时钟受干扰或晶体震荡不正常导致系统工作故障;
复位不可靠,造成个单元未进入预期状态而出现问题;
因焊接问题引发的各种问题,如方向焊错、虚焊、错焊等;
因时序不匹配引发的通信故障,如时钟信号通过逻辑器件后产生延时,与读写信号时序搭配不上导致读写错误。
6、 嵌入式文件系统:
目前支持闪存的文件系统技术有以下几种:
JFFS2和Yaffs。这些文件系统可以使用在没有初始化的NAND Flash和有CFI接口的NOR Flash中。JFFS2的特点包括:
支持数据压缩;
提供了“写平衡”支持;
支持多种结点类型;
提高了对闪存的利用率,降低了闪存的消耗。
JFFS2中最重要的数据结构是jffs2_sb_info,这个数据结构用来管理所有的结点链表和闪存块。它在/src/include/Linux/jffs2_fs_sb.h中定义。
Yaffs/Yaffs2(Yet Another Flash File System)和JFFS相比,它减少了一些功能,因此速度更快、占用内存更少。
TrueFFS。该文件系统相当于Linux中的MTD层,必须配合其他文件系统。
FTL/NTFL。它是一种中间层解决方案的统称,为上层文件系统提供接口。
RAMFS、CRAMFS和ROMFS。这些文件系统用于早期的小容量闪存设备,系统功能比较简单,仅提供基本接口,只属于只读的闪存文件系统。适合存储空间小的系统。
7、 MTD简介:
无论JFFS2还是Yaffs,都需要MTD(Memory Technology Devices,内存技术设备)的支持。MTD是对Flash操作的接口,提供了一系列的标准函数,将硬件驱动设计和系统程序设计分开,硬件驱动人员不用了解存储设备的组织方法,只需提供标准的函数调用,如读、写等。
一个MTD原始设备可以通过mtd_part分割成数个MTD原始设备注册进mtd_table,mtd_table中的每个MTD原始设备都可以被注册成一个MTD设备,
2007年7月29日星期日
2007年7月28日星期六
在上海
来上海整一周了!上海的天气实在太热,习惯了北方的凉爽所以现在几乎不敢出门,这一周似乎自己一直在流汗。网站今天开始准备了,搜索了一天的时间,逐渐的网站的轮廓出来了,感觉老板的思路不是很清楚,可能是外行的原因吧。有预感自己这一假期的工作不会创造出什么价值(老板不像是思路清楚到能赚钱的人)当然除了我的千元工资,也许我该跳槽?......
2007年6月13日星期三
After lost my computer five days.
Before five days I lost my computer,this blog I writed with my friend's computer.In that five days I feeling bad,Suddenly found without computer I have nothing to do.
I am boring after lost my computer.
I am boring after lost my computer.
2007年6月6日星期三
The second Hard Disk.
几天来一直在做一些系统,转接器早已搞定,拆下了原来的笔记本硬盘接上Maxtor的台机盘,外接电源通过短接二十线主板电源线的绿线和黑色地线来唤醒。开机前先接通外接电源使硬盘转起来,开机Bios显示找到硬盘,可名字出现乱码,并且往下就不能启动了。于是关掉一切电源设置了一下硬盘的主从盘跳线,第二次开机硬盘显示正常。接下来重启并用FREEBSD光盘把硬盘分区并一步步装上系统,FREEBSD我没用gnone和kde而是安装了fvwm,感觉fvwm才应该是挑剔的人的选择。装好后重新编译了一遍内核并设置了控制台的分辨率,小字体看起来更舒服,然后用ports安装了apache,php,mysql,lynx等常用的软件。最后设置fvwm,在命令行下用lynx上网,由于还没设置中文显示所以只有翻些英文网站,加上英语不好就随便找了一个自己感觉还不错的.fvwm2rc 文件,修改了一些东西,加上了一些自己的设置就放到了~/.fvwm目录下作配置文件,最后在~/目录下vi .xinitrc文件,并加入exec fvwm一句,保存并退出。startx后fvwm 就启动了,其他设置都起作用,就是桌面背景图片没有显示,检查.fvwm2rc没有错误,配置是中用xloadimage加载背景图片,于是用sysinstall检查发现没有安装xloadimage,然后用ports安装好后桌面背景图片就正常显示了,告一段落。
换回笔记本的硬盘,突然感觉应该再装个vista,于是就下了个vista,2.43G只用了20分钟,迅雷还是挺让人满意的。没办法,系统要求15G作系统盘,而且还要把安装文件拷上硬盘安装,看来只有把刚弄好的FREEBSD格掉了,也罢,以后再装就是,反正现在心情很好。在同学台机上接上了Maxtor台机硬盘,用深山红叶分了两个区,因为vista的要求于是就用了NTFS格式,重启后把笔记本上的安装文件拷到了第二个分区,安装前准备工作算是就绪,接下来换硬盘开始安装。由于用的是NTFS,所以就不能用深山红叶DOS启动安装,于是决定先用Ghost做一个xp系统,然后升级安装vista。一切都很顺利,经过多次重启,傻瓜式的安装终于完成,界面确实挺华丽,而且也不像传说中的那么占资源,我的机器感觉和用小硬盘时的xp速度差不多,而且ttplayer,cs等软件也都可以正常运行,只是局域网共享是找不到其它机器,但用通过ip却可以共享,cs也不能联机,也可能是设置不对,继续摸索......
换回笔记本的硬盘,突然感觉应该再装个vista,于是就下了个vista,2.43G只用了20分钟,迅雷还是挺让人满意的。没办法,系统要求15G作系统盘,而且还要把安装文件拷上硬盘安装,看来只有把刚弄好的FREEBSD格掉了,也罢,以后再装就是,反正现在心情很好。在同学台机上接上了Maxtor台机硬盘,用深山红叶分了两个区,因为vista的要求于是就用了NTFS格式,重启后把笔记本上的安装文件拷到了第二个分区,安装前准备工作算是就绪,接下来换硬盘开始安装。由于用的是NTFS,所以就不能用深山红叶DOS启动安装,于是决定先用Ghost做一个xp系统,然后升级安装vista。一切都很顺利,经过多次重启,傻瓜式的安装终于完成,界面确实挺华丽,而且也不像传说中的那么占资源,我的机器感觉和用小硬盘时的xp速度差不多,而且ttplayer,cs等软件也都可以正常运行,只是局域网共享是找不到其它机器,但用通过ip却可以共享,cs也不能联机,也可能是设置不对,继续摸索......
2007年6月1日星期五
硬盘到了。
淘宝上买的硬盘来的很快,不到两天就收到了。回小屋测了一下,除了突发传输速度有点低其他的都要比笔记本的要高很多,总体参数显示是块不错的盘。马上去科技广场买转接器把它接到笔记本上,可转遍了都说没有,从网上订还不够快递费,看来只有自己做一个了,没办法只买了个电源回来。晚上查了查有关台机和笔记本硬盘接口的相关数据,做了一些理论准备,明天再跑一趟科技城去把那些接口针买回来把转接器做好可以搞深一步的操作系统研究了!......
兴奋......
兴奋......
2007年5月30日星期三
2007年5月27日星期日
2007年5月10日星期四
图书馆借书......
图书馆天天换人,以前的老师都换掉了,他们多好啊......
借书的制度天天变,现在借碟要用学生证,给他一卡通还不要,好在我软磨硬泡功夫还行,其实老太太只是负责而已,况且人也不错,像我的大姨.她说以后只能借一本了,真要是那样的话学校就太差劲了......
不管怎样我下午还是找到了几本好书,好在一本的限制还没实施,我借到了三本,加上现在手里的共十本,都是很经典的书.我找到了"linux内核完全注解""PHP and MySQL Development"还有一本是关于思科路由配置的书,都是好书,一定要好好研究.
走出小屋感觉挺不错的,就像是回到了久违的光明的世界(小屋其实真的就像是地下室,见不到阳光的)......
借书的制度天天变,现在借碟要用学生证,给他一卡通还不要,好在我软磨硬泡功夫还行,其实老太太只是负责而已,况且人也不错,像我的大姨.她说以后只能借一本了,真要是那样的话学校就太差劲了......
不管怎样我下午还是找到了几本好书,好在一本的限制还没实施,我借到了三本,加上现在手里的共十本,都是很经典的书.我找到了"linux内核完全注解""PHP and MySQL Development"还有一本是关于思科路由配置的书,都是好书,一定要好好研究.
走出小屋感觉挺不错的,就像是回到了久违的光明的世界(小屋其实真的就像是地下室,见不到阳光的)......
2007年5月8日星期二
前一段时间腾讯更改QQ的登录协议,我的GAIM不能登录QQ。用了一段时间的GMAIL,可我的朋友们还是继续在windows下使用QQ,无奈干脆就消失不让他们找到我。
前几天上GAIM的官网,发现改名了,改成PIDGIN。搜索一番找到了UBUNTU的安装包,但要升级系统,两个小时后升级完成,删除多余内核后安装PIDGIN包,提示错误,原来升级系统时安装了GAIM的老版本,卸载了GAIM后安装成功,登录速度很快。
系统升级后新增了一些功能,其中的桌面效果不错,还有3D效果,不过感觉不实用,玩玩还行。
以前安装的编程工具卸载后还有残留项目,选择文件的打开程序时会出现他们的残留项目。在用户目录下显示隐藏文件进入.local---share---applications删除多余项目即可。
前几天上GAIM的官网,发现改名了,改成PIDGIN。搜索一番找到了UBUNTU的安装包,但要升级系统,两个小时后升级完成,删除多余内核后安装PIDGIN包,提示错误,原来升级系统时安装了GAIM的老版本,卸载了GAIM后安装成功,登录速度很快。
系统升级后新增了一些功能,其中的桌面效果不错,还有3D效果,不过感觉不实用,玩玩还行。
以前安装的编程工具卸载后还有残留项目,选择文件的打开程序时会出现他们的残留项目。在用户目录下显示隐藏文件进入.local---share---applications删除多余项目即可。
2007年5月7日星期一
破解成功!
一直答应要给深圳的王哥把他的网站弄起来可加上天天状态不好还有就是这样那样的事情结果就拖了两个月,前天终于给他交稿了,但他最后选了另一套商业的网站程序,因为我的程序虽然功能还行但美工不是很好,而他又是做美容类产品的,所以最好漂亮一些。这样,破解工作就落在了我身上,奋战一晚加一个上午终于把他的后台限制搞定,论坛也已经可以正常工作,还算是做了件“大事"吧。
最近在windows下也可以正常访问bolgger了,可能这次blogger真的已经解禁了吧!
最近在windows下也可以正常访问bolgger了,可能这次blogger真的已经解禁了吧!
2007年5月5日星期六
我回来了
我回来了,又是一个新的开始.我喜欢开始,满怀激情,雄心壮志......
每次回家都有新的收获,这次我调整好了活动与学习的心态,学累了就要活动一下.家里都挺好的,去年在老爸的事件中出力不少的亲戚都走了走,起码在我的心里我不会忘记他们的帮助.
二姨给出了一个非常经典的词语,而且我感觉恰好总结了我门一代八〇后的成功路线,这个词语就是"借鸡下蛋"......
CCNA已经开始看了一些,以前看得linux网络与安全指南起了不小的帮助.思科路由的操作系统好像是基于freebsd开发的,我想应该也是可以研究一下的.
总之心情不错,没有了迷茫,感觉一切都不是那么难实现,只要努力,只要动脑筋......
每次回家都有新的收获,这次我调整好了活动与学习的心态,学累了就要活动一下.家里都挺好的,去年在老爸的事件中出力不少的亲戚都走了走,起码在我的心里我不会忘记他们的帮助.
二姨给出了一个非常经典的词语,而且我感觉恰好总结了我门一代八〇后的成功路线,这个词语就是"借鸡下蛋"......
CCNA已经开始看了一些,以前看得linux网络与安全指南起了不小的帮助.思科路由的操作系统好像是基于freebsd开发的,我想应该也是可以研究一下的.
总之心情不错,没有了迷茫,感觉一切都不是那么难实现,只要努力,只要动脑筋......
2007年4月30日星期一
今天下雨了
昨晚开始下雨一直下到今天早晨,运动会结束了,感觉很累但是挺开心的,因为不用在小屋憋着.明天准备回家,越来越感觉回家是必须的,就像是对老人的一种责任,不管自己是怎样的状态都尽量不要让老人失望......
今天和王哥一起装的机器,配置不错都是好料子,他应该可以舒舒服服的玩游戏了.作系统时店里一个打工的给作系统,和他说话不搭理人,问他三遍要怎么分区也不说话,最后朋友火了,找到老板,结果那小子被老板痛骂一顿.作技术也应该和气,更何况做的又不是高端技术......
今天和王哥一起装的机器,配置不错都是好料子,他应该可以舒舒服服的玩游戏了.作系统时店里一个打工的给作系统,和他说话不搭理人,问他三遍要怎么分区也不说话,最后朋友火了,找到老板,结果那小子被老板痛骂一顿.作技术也应该和气,更何况做的又不是高端技术......
2007年4月28日星期六
I am a referees
校运会系里安排当裁判,感觉还行.首先中午饭还行---宁宇盒饭+包子+酸奶(很不容易了,似乎破了建校以来运动会裁判员午饭标准的纪录!haha!)
感觉有几个趣味比赛不错,全民参与.其他的就好像来回那么几个人在表现一样......
关于ISO刻碟的问题已解决,nero程序内部还有一层验证,我从网上找了查看的办法,查看发现我的软件已经通过验证,可不知为什么还是随机的刻坏碟. 最后找到了一个不错的软件Alcohol 120%,感觉挺好用,起码我用它还没刻坏碟.
感觉有几个趣味比赛不错,全民参与.其他的就好像来回那么几个人在表现一样......
关于ISO刻碟的问题已解决,nero程序内部还有一层验证,我从网上找了查看的办法,查看发现我的软件已经通过验证,可不知为什么还是随机的刻坏碟. 最后找到了一个不错的软件Alcohol 120%,感觉挺好用,起码我用它还没刻坏碟.
2007年4月24日星期二
I'am Speechless.
Further modest people do not want to accept other people's opinions,so the word also lost an honest people !
2007年4月22日星期日
一觉醒后
几天一来一直是昏昏沉沉的状态,突然感觉自己是不是在小屋憋出病来了,好多事情都找不到答案......
几天的昏沉让我无意中在考虑人生的意义,人生到底是什么?生老病死?或许是其他一些烦心高兴的事组合起来的一个过程,人们都在寻找人生意义的解脱.
有感于GENTOO字符界面的高分辨率就像把FREEBSD的字符界面也搞一稿,网上搜索了一翻最后成功,步骤如下:
# cd /sys/i386/conf
# ls
会看到有GENERIC文件,
# cp GENERIC graph
# ls
会看到多了graph文件
# vi graph
用vi编辑graph文件,向下翻会看到很多options行,输入A在其中options行后插入
options VESA
options SC_PIXEL_MODE
按Esc退出编辑模式,输入:x保存并退出
# config graph
# cd ../compile/graph
# make cleandepend
# make depend
# make
# make install
编译内核,可能要挺常时间
# reboot
重启
# vidcontrol -i mode
输出支持的模式,需要记住前面的号码,我的是321 1024*768*32
# vidcontrol MODE_321
这样就到了1024*768分辨率
# vi /etc/rc.conf
在其中插入
allscreens_flags="MODE_321"
启动时就会自动到设置的分辨率,不过启动前半部分的界面还是低分辨率,正在积极寻找改变方法......
其他在字符界面用背景图片等都可以实现,只要google就可以解决.
几天的昏沉让我无意中在考虑人生的意义,人生到底是什么?生老病死?或许是其他一些烦心高兴的事组合起来的一个过程,人们都在寻找人生意义的解脱.
有感于GENTOO字符界面的高分辨率就像把FREEBSD的字符界面也搞一稿,网上搜索了一翻最后成功,步骤如下:
# cd /sys/i386/conf
# ls
会看到有GENERIC文件,
# cp GENERIC graph
# ls
会看到多了graph文件
# vi graph
用vi编辑graph文件,向下翻会看到很多options行,输入A在其中options行后插入
options VESA
options SC_PIXEL_MODE
按Esc退出编辑模式,输入:x保存并退出
# config graph
# cd ../compile/graph
# make cleandepend
# make depend
# make
# make install
编译内核,可能要挺常时间
# reboot
重启
# vidcontrol -i mode
输出支持的模式,需要记住前面的号码,我的是321 1024*768*32
# vidcontrol MODE_321
这样就到了1024*768分辨率
# vi /etc/rc.conf
在其中插入
allscreens_flags="MODE_321"
启动时就会自动到设置的分辨率,不过启动前半部分的界面还是低分辨率,正在积极寻找改变方法......
其他在字符界面用背景图片等都可以实现,只要google就可以解决.
2007年4月11日星期三
今天又会很忙的
昨天还是那种状态:晕晕的,从来不睡午觉的我昨天下午竟然睡了一下午,而且感觉越睡越累。好在晚上训练教练教的挺多自己学的也挺开心,一直练到大汗淋漓。
昨天qd开了wap上网,哈哈那小家伙还挺厉害,除了游戏在线听音乐看电视都行。越来越感到它就是一台“掌上电脑"。看了很多手机网站,突然感到可以做一个手机wap网站,因为现在的手机网站还是不多,而且除了几个大站其它的或多或少都存在一些错误。做一个无错资源性的wap下载网站应该会受欢迎,只是想法,哈哈。这几天写个可行性报告,然后找人投资......
今天上午又是那些无用的研究生讲得那些无用的课(只是现在的感觉)。也好,课上的时间可以安静的看看借的那些书,前几天的《恶意传播代码》才只看了开头来。
下午的兼职网管过去看看,朋友介绍的,应该待遇不错。况且最经手头也比较紧......
对了,解决国内访问bloger的问题:
function FindProxyForURL(url,host){
if(dnsDomainIs(host, ".blogspot.com")){
return "PROXY 72.14.219.190:80";
}
}
上面的代码存为proxy.pac文件,然后在firefox里找到自动代理配置URL并且用我们保存的这个文件.linux和windows下都适用.我把文件放在我的用户主文件夹下,感觉孤零零的一个文件不好看,所以就设成了隐藏。linux下设隐藏只需将文件重命名,在前面加一个"."。
昨天qd开了wap上网,哈哈那小家伙还挺厉害,除了游戏在线听音乐看电视都行。越来越感到它就是一台“掌上电脑"。看了很多手机网站,突然感到可以做一个手机wap网站,因为现在的手机网站还是不多,而且除了几个大站其它的或多或少都存在一些错误。做一个无错资源性的wap下载网站应该会受欢迎,只是想法,哈哈。这几天写个可行性报告,然后找人投资......
今天上午又是那些无用的研究生讲得那些无用的课(只是现在的感觉)。也好,课上的时间可以安静的看看借的那些书,前几天的《恶意传播代码》才只看了开头来。
下午的兼职网管过去看看,朋友介绍的,应该待遇不错。况且最经手头也比较紧......
对了,解决国内访问bloger的问题:
function FindProxyForURL(url,host){
if(dnsDomainIs(host, ".blogspot.com")){
return "PROXY 72.14.219.190:80";
}
}
上面的代码存为proxy.pac文件,然后在firefox里找到自动代理配置URL并且用我们保存的这个文件.linux和windows下都适用.我把文件放在我的用户主文件夹下,感觉孤零零的一个文件不好看,所以就设成了隐藏。linux下设隐藏只需将文件重命名,在前面加一个"."。
2007年4月10日星期二
一天又结束了!
今天就这样过去了,上午买的空碟,自己实在是急脾气,没完成的事不干完就睡不好。碟片是传说中的lenovo的,而且挺贵的,可回来还是刻坏了两张,本来还有一套fc4的要刻的,害怕再刻坏了就只刻的centos。可能是nero软件盗版的问题,原因我会继续寻找......
到现在只差mac没收集了,哪天有心情了会找找。
一天下来感觉没什么收获,而且头晕,这几天一直没能六点起,总是很累的样子......
明早六点起,好多东西要学!
到现在只差mac没收集了,哪天有心情了会找找。
一天下来感觉没什么收获,而且头晕,这几天一直没能六点起,总是很累的样子......
明早六点起,好多东西要学!
订阅:
博文 (Atom)










