Chinaunix首页 | 论坛 | 博客
  • 博客访问: 64083
  • 博文数量: 31
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 15
  • 用 户 组: 普通用户
  • 注册时间: 2016-08-29 12:33
文章分类

全部博文(31)

文章存档

2017年(1)

2015年(1)

2014年(2)

2013年(2)

2011年(1)

2010年(6)

2009年(18)

我的朋友

分类: C/C++

2009-09-27 09:35:28

---------计算机体系结构基础-----------
32位就是指地址是32位的,从0x0000 0000到0xffff ffff。所以也可以说处理器的位数是指它的寄存器的位数(寄存器位数和地址位数
一致)。处理器的位数也叫做字长,
CPU总是周而复始地做同一件事:从内存取指令,然后解释执行它,然后再取下一条指令,再解释执行。
寄存器(Register),
程序计数器(PC,Program Counter),保存着CPU取指令的地址,CPU取指令后程序计数器保存的地址会自动加上该指令的长度,指向内
存中的下一条指令
指令解码器(Instruction Decoder)。解释指令的语义
地址和数据总线(Bus)。CPU和内存之间用地址总线、数据总线和控制线连接起来,32位处理器有32条地址线和32条数据线[24],
CPU指令执行过程为:
CPU内部将寄存器对接到数据总线上,使寄存器的每一位对接到一条数据线,等待接收数据。
CPU将内存地址通过地址线发给内存,然后通过另外一条控制线发一个读请求。
内存收到地址和读请求之后,将相应的存储单元对接到数据总线的另一端,这样,存储单元每一位的1或0状态通过一条数据线到达CPU寄
存器中相应的位,就完成了数据传送。
有些设备像内存芯片一样连接到处理器的地址总线和数据总线,正因为地址线和数据线上可以挂多个设备和内存芯片所以才叫“总线”
,但不同的设备和内存应该占不同的地址范围。访问这种设备就像访问内存一样,按地址读写即可,和访问内存不同的是,往一个地址
写数据只是给设备发一个命令,数据不一定要保存,从一个地址读出的数据也不一定是先前保存在这个地址的数据,而是设备的某个状
态。设备中可供读写访问的单元通常称为设备寄存器(注意和CPU的寄存器不是一回事),操作设备的过程就是对这些设备寄存器做读写
操作的过程,比如向串口发送寄存器里写数据,串口设备就会把数据发送出去,读串口接收寄存器的值,就可以读取串口设备接收到的
数据。
CPU核引出的地址和数据总线有一端接到芯片内部集成的设备上,这些设备都有各自的内存地址范围,称为内存映射I/O(Memory-mapped
I/O)。但是x86比较特殊,引出额外的地址线来连接片内设备,访问设备寄存器时用特殊的in/out指令,而不是和访问内存用同样的指
令,这种方式称为端口I/O(Port I/O)。
由于外设五花八门,于是出现了各种适应不同要求的设备总线,比如PCI、AGP、USB、1394、SATA等等,这些设备总线并不直接和CPU相
连,CPU通过内存映射I/O或端口I/O访问相应的总线控制器,通过它再去访问挂在总线上的设备。
硬盘是ATA、SATA或SCSI总线上的设备,保存在硬盘上的程序是不能被CPU直接取指令执行的,操作系统在执行程序时会把它从硬盘拷到
内存,这样CPU才可以取指令执行,这个过程称为加载(Load)。程序加载到内存之后,成为操作系统调度执行的一个任务,就称为进程
(Process)。
所以内存总是被动地等待被读或被写。而设备往往会自己产生数据,并且需要主动通知CPU来读这些数据,这是由中断(Interrupt)机
制实现的,每个设备都有一条中断线,通过中断控制器连接到CPU,当设备需要主动通知CPU时就引发一个中断信号,CPU正在执行的指令
将被打断,程序计数器会设置成某个固定的地址(这个地址由体系结构定义),于是CPU从这个地址开始取指令执行中断服务程序(ISR
,Interrupt Service Routine),
如果处理器启用了MMU(Memory Management Unit,内存管理单元),CPU执行单元发出的内存地址将被MMU截获,从CPU到MMU的地址称为
虚拟地址(Virtual Address,以下简称VA),而MMU将这个地址翻译成另一个地址发到CPU芯片的外部地址引脚上,也就是将虚拟地址映
射成物理地址。
在启用MMU的情况下虚拟地址空间和物理地址空间是完全独立的,物理地址空间既可以小于也可以大于虚拟地址空间,例如有些32位的服
务器可以配置大于4GB的物理内存。我们说32位的CPU,是指CPU寄存器是32位的,数据总线是32位的,虚拟地址空间是32位的,而物理地
址空间则不一定是32位的。物理地址的范围是多少,取决于处理器引脚上有多少条地址线,
MMU将虚拟地址映射到物理地址是以页(Page)为单位的,对于32位CPU通常一页为4KB。例如,MMU可以通过一个映射项将虚拟地址的一
页0xb7001000~0xb7001fff映射到物理地址的一页0x2000~0x2fff,物理内存中的页称为物理页面或页帧(Page Frame)。至于虚拟内存
的哪个页面映射到物理内存的哪个页帧,这是通过页表(Page Table)来描述的,页表保存在物理内存中,MMU会查找页表来确定一个虚
拟地址应该映射到什么物理地址
在操作系统初始化或者分配、释放内存时,会执行一些指令在物理内存中填写页表,然后用指令设置MMU,告诉MMU页表在物理内存中的
什么位置
有了虚拟内存管理机制,各进程不必担心自己使用的地址范围会不会和别的进程冲突,比如两个进程都使用了虚拟地址0x0804 8000,操
作系统可以设置MMU的映射项把它们映射到不同的物理地址。
MMU除了做地址转换之外,还提供内存保护机制,运行有权限的用户才能操作某个页面,否则不允许访问,产生一个异常(Exception)
。异常的处理过程和中断类似,只不过中断是由外部设备产生的,而异常是由CPU内部产生的
通常操作系统把虚拟地址空间划分为用户空间和内核空间,例如x86平台的虚拟地址空间是0x0000 0000~0xffff ffff,大致上前3GB
(0x0000 0000~0xbfff ffff)是用户空间,后1GB(0xc000 0000~0xffff ffff)是内核空间。用户程序在用户模式下执行,不能访问内
核中的数据,也不能跳转到内核代码中执行。除了中断处理。
现代计算机都把存储器分成若干级,称为Memory Hierarchy,按照离CPU由近到远的顺序依次是CPU寄存器、Cache、内存、硬盘,越靠近
CPU的存储器容量越小但访问速度越快,
Cache和内存都是由RAM(Random Access Memory)组成的,可以根据地址随机访问,计算机掉电时RAM中保存的数据会丢失。不同的是,
Cache通常由SRAM(Static RAM,静态RAM)组成,而内存通常由DRAM(Dynamic RAM,动态RAM)组成。
一级缓存是用VA寻址的,二级缓存是用PA寻址的,这是它们的区别。Cache所做的工作是由硬件自动完成的,而不是像寄存器一样由指令
决定先做什么后做什么。
大多数程序的行为都具有局部性(Locality)的特点:它们会花费大量的时间反复执行一小段代码(例如循环),或者反复访问一个很
小的地址范围中的数据(例如访问一个数组)。所以预读缓存的办法是很有效的,
--------汇编 编译 链接 Makefile--------
汇编程序中以.开头的名称并不是指令的助记符,不会被翻译成机器指令,而是给汇编器一些特殊的指示,称为汇编指示(Assembler
Directive)或伪操作(Pseudo-operation),由于它不是真正的指令所以加个“伪”字。.section指示把代码划分成若干个段
(Section),程序被操作系统加载执行时,每个段被加载到不同的地址,具有不同的读、写、执行权限。.data段保存程序的数据,是
可读可写的
_start是一个符号(Symbol),符号在汇编程序中代表一个地址,可以用在指令中,汇编程序经过汇编器的处理之后,所有的符号都被
替换成它所代表的地址值,在C语言中,变量名和函数名都是符号,本质上是代表内存地址的。
_start就像C程序的main函数一样特殊,是整个程序的入口,所以每个汇编程序都要提供一个_start符号并且用.globl声明。如果一个符
号没有用.globl指示声明,就表示这个符号不会被链接器用到。
movl $1, %eax
这是一条数据传送指令,CPU内部产生一个数字1,然后传送到eax寄存器中。mov后面的l表示long,说明是32位的传送指令。CPU内部产
生的数称为立即数(Immediate),在汇编程序中,立即数前面要加$,寄存器名前面要加%,以便跟符号名区分开。
 int $0x80
int指令称为软中断指令,可以用这条指令故意产生一个异常,CPU从用户模式切换到特权模式,然后跳转到内核代码中执行异常处理程
序。
int指令中的立即数0x80是一个参数,在异常处理程序中要根据这个参数决定如何处理,在Linux内核中,int $0x80这种异常称为系统调
用(System Call)。
通过异常处理程序进入内核,用户程序只能通过寄存器传几个参数,之后就要按内核设计好的代码路线走,而不能由用户程序随心所欲

eax和ebx寄存器的值是传递给系统调用的两个参数,eax的值是系统调用号,1表示_exit系统调用,ebx的值则是传给_exit系统调用的参
数,也就是退出状态。但_exit这个系统调用有点特殊,它会终止掉当前进程,而不会返回它继续执行
x86的通用寄存器有eax、ebx、ecx、edx、edi、esi。这些寄存器在大多数指令中是可以任意选用的,但对于某些指令而言不是通用的。
x86的特殊寄存器有ebp、esp、eip、eflags。eip是程序计数器,eflags保存着计算过程中产生的标志位,进位、溢出、零、负数四个标
志位在x86的文档中分别称为CF、OF、ZF、SF。ebp和esp用于维护函数调用的栈帧。
data_items .long 1,2     #说明一个数组
movl data_items(,%edi,4),%eax  #取data_items中的edi个元素给eax,4表示long型的长度
cmpl $0,%eax  #比较eax中值个0的大小,结构存于eflag中
je loop_exit  #如果相等,则跳至loop_exit,cmpl一般和je一起用,另外jmp是无条件跳转
incl %edi   #edi加1

寻址方式:ADDRESS_OR_OFFSET(%BASE_OR_OFFSET,%INDEX,MULTIPLIER)
直接寻址(Direct Addressing Mode)。movl ADDRESS, %eax
变址寻址(Indexed Addressing Mode) movl data_items(,%edi,4), %eax
间接寻址(Indirect Addressing Mode)。movl (%eax), %ebx,
基址寻址(Base Pointer Addressing Mode)。movl 4(%eax), %ebx
立即数寻址(Immediate Mode)
寄存器寻址(Register Addressing Mode)。
ELF文件格式是一个开放标准,各种UNIX系统的可执行文件都采用ELF格式,包括:Relocatable,Executable,Shared Object
在汇编器和链接器看来,ELF文件是由Section Header Table描述的一系列Section的集合;在加载器(Loader)看来它是由Program
Header Table描述的一系列Segment的集合
我们在汇编程序中用.section声明的Section会成为目标文件中的Section,此外汇编器还会自动添加一些Section(比如符号表)。
Segment是指在程序运行时加载到内存的具有相同属性的区域,由一个或多个Section组成,只对汇编器和连接器有用的,不加载到内存
目标文件需要链接器做进一步处理,所以一定有Section Header Table;可执行文件需要加载运行,所以一定有Program Header Table
;而共享库既要加载运行,又要在加载时做动态链接,所以既有Section Header Table又有Program Header Table。
$ readelf -a max.o
$ hexdump -C max.o
低地址保存的是整数的低位,这种字节序(Byte Order)称为小端(Little Endian)。但是有些平台是大端(Big Endian)
.shstrtab中保存着各Section的名字,.strtab中保存着程序中用到的符号的名字。每个名字都是以'\0'结尾的字符串。
.symtab是符号表。Ndx列是每个符号所在的Section编号,Value列是每个符号所代表的地址,在目标文件中,符号地址都是相对于该符
号所在Section的相对地址
.data段需要占用一部分空间保存初始值,而.bss段则不需要。也就是说,.bss段在文件中只占一个Section Header而没有对应的
Section,
反汇编命令:$ objdump -d max.o
左边是机器指令的字节,右边是反汇编结果。显然,所有的符号都被替换成地址了,比如je 23,注意没有加$的数表示内存地址,而不
表示立即数。
查看C对应的汇编指令:$ objdump -dS a.out
gcc -S main.c,这样只生成汇编代码main.s,而不生成二进制的目标文件。
gdb命令:disassemble可以反汇编当前函数或者指定的函数,si命令可以一条指令一条指令地单步调试。info registers可以显示所有寄
存器的当前值。在gdb中表示寄存器名时前面要加个$,例如p $esp可以打印esp寄存器的值,如:x/20 $esp命令查看内存中从$esp地址
开始的20个32位数。
在x86平台上这个栈是从高地址向低地址增长的,
第二个参数保存在esp+4所指向的内存位置,第一个参数保存在esp所指向的内存位置,可见参数是从右向左依次压栈的。
ebp指向栈底,而esp指向栈顶,在函数执行过程中esp随着压栈和出栈操作随时变化,而ebp是不动的,函数的参数和局部变量都是通过
ebp的值加上一个偏移量来访问的
call:把call的下一条指令压栈,同时esp减4,修改eip跳到call参数指的位置。
reset:call的逆操作,把esp指定的返回地址赋给eip,esp加4,
leave:是push %ebp move %esp,%ebp 的逆操作,
gcc -E 预处理
gcc -S 生成汇编
gcc -c 生成目标文件
gcc    从现在一直到可执行文件
因此那些简短并且被频繁调用的函数经常用函数式宏定义来代替实现。
#define MAX(a, b) ((a)>(b)?(a):(b))
k = MAX(i&0x0f, j&0x0f)
#define device_init_wakeup(dev,val) \
        do { \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); \
        } while(0)
问题出在device_init_wakeup(d, v);末尾的;号,如果不允许写这个;号,看起来不像个函数调用,可如果写了这个;号,宏展开之后就
有语法错误,if语句被这个;号结束掉了,没法跟else配对。
用gcc的-v选项可以了解详细的编译过程
libc并不像其它目标文件一样链接到可执行文件main中,而是在运行时做动态链接:
操作系统在加载执行main这个程序时,首先查看它有没有需要动态链接的未定义符号。
1 如果需要做动态链接,就查看这个程序指定了哪些共享库(我们用-lc指定了libc)以及用什么动态链接器来做动态链接(我们用-2
dynamic-linker /lib/ld-linux.so.2指定了动态链接器)。
3 动态链接器在共享库中查找这些符号的定义,完成链接过程。
__libc_start_main需要动态链接,所以这个库函数的指令在可执行文件main的反汇编中肯定是找不到的
int main(int argc, char *argv[]),多传了参数而不用是没有问题的,少传了参数却用了则会出问题。
main函数的返回值最终被传给_exit系统调用,成为进程的退出状态。我们也可以在main函数中直接调用exit函数终止进程而不返回到启
动例程,例如:exit(4);
退出状态只有8位,而且被Shell解释成无符号数,如果将上面的代码改为exit(-1);或return -1;,则运行结果为:255
但如果某个分支控制流程调用了exit或_exit而不写return,编译器是允许的,因为它都没有机会返回了,指不指定返回值也就无所谓了
。使用exit函数需要包含头文件stdlib.h,而使用_exit函数需要包含头文件unistd.h,
.rodata段和.text段通常合并到一个Segment中,操作系统将这个Segment的页面只读保护起来,
static在这里的作用是声明b这个符号为LOCAL的,不被链接器处理,LOCAL的符号只能在某一个目标文件中定义和使用,而不能定义在一
个目标文件中却在另一个目标文件中使用。
.bss段在文件中不占存储空间,在加载时这个段用0填充。所以我们在第 4 节 “局部变量与全局变量”讲过,全局变量如果不初始化则
初值为0,
函数的参数和局部变量是分配在栈上的
虽然栈是从高地址向低地址增长的,但数组内元素总是从低地址向高地址排列的,
register,不压栈直接放入寄存器中
如果有文件a.c包含了b.h和c.h,那么我所说的“程序文件”指的是经过预处理把b.h和c.h在a.c中展开之后生成的代码,在C标准中称为
编译单元(Translation Unit)。每个编译单元可以分别编译成一个.o目标文件,最后这些目标文件用链接器链接到一起,成为一个可
执行文件。
栈是从高地址向低地址增长的,但结构体成员也是从低地址向高地址排列的,这一点和数组类似。不同的是元素之间有空隙,成为填充
(Padding)
%u转换说明表示无符号数,sizeof的值是size_t类型的,是某种无符号整型。
在32位平台上,访问4字节的指令(比如上面的movl)所访问的内存地址应该是4的整数倍,访问两字节的指令(比如上面的movw)所访
问的内存地址应该是两字节的整数倍,这称为对齐(Alignment)。
合理设计结构体各成员的排列顺序可以节省存储空间,例如上例中的结构体改成这样就可以避免产生填充字节:
gcc提供了一种扩展语法可以消除结构体中的填充字节:
struct {
 char a;
 short b;
 int c;
 char d;
} __attribute__((packed)) s;
这样就不能保证结构体成员的对齐了,在访问b和c的时候可能会有效率问题
结构体中还可以使用Bit Field语法定义只占几个Bit的成员。字节中的Bit Order也是小端的
关于如何排列Bit Field在C标准中没有详细的规定,这跟Byte Order、Bit Order、对齐等问题都有关,不同的平台和编译器可能会排列
得很不一样,要编写可移植的代码就不能假定Bit Field是按某一种固定方式排列的。
一个联合体的各个成员占用相同的内存空间,联合体的长度等于其中最长成员的长度。如果用Initializer初始化,则只初始化它的第一
个成员,
需要用到完整的内联汇编格式:
__asm__(assembler template
 : output operands                  /* optional */
 : input operands                   /* optional */
 : list of clobbered registers      /* optional */
 );
这种格式由四部分组成,第一部分是汇编指令,和上面的例子一样,第二部分和第三部分是约束条件,第二部分指示汇编指令的运算结
果要输出到哪些C操作数中,C操作数应该是左值表达式,第三部分指示汇编指令需要从哪些C操作数获得输入,第四部分是在汇编指令中
被修改过的寄存器列表,指示编译器哪些寄存器的值在执行这条__asm__语句时会改变。后三个部分都是可选的,如果有就填写,没有就
空着只写个:号。
有了volatile限定符,是可以防止编译器优化对设备寄存器的访问,volatile限定符修饰变量,就是告诉编译器,即使在编译时指定了优
化选项,每次读这个变量仍然要老老实实从内存读取,每次写这个变量也仍然要老老实实写回内存,不能省略任何步骤。但还是无法防
止Cache优化对设备寄存器的访问。
通常,有Cache的平台都有办法对某一段地址范围禁用Cache,一般是在页表中设置的,可以设定哪些页面允许Cache缓存,哪些页面不允
许Cache缓存,MMU不仅要做地址转换和访问权限检查,也要和Cache协同工作。
除了设备寄存器需要用volatile限定之外,当一个全局变量被同一进程中的多个控制流程访问时也要用volatile限定,比如信号处理函
数和多线程。
C语言不允许嵌套定义函数[29],但如果只是声明而不定义,这种声明是允许写在函数体里面的,这样声明的标识符具有块作用域,
变量声明和函数声明有一点不同,函数声明的extern可写可不写,而变量声明如果不写extern意思就完全变了,extern声明也不能初始
化,因为声明带上初始化表示定义
static实现的安全的访问保护机制,extern对于static变量作用无效。static是具有Internal Linkage
include 对于用角括号包含的头文件,gcc首先查找-I选项指定的目录,然后查找系统的头文件目录(通常是/usr/include,在我的系统
上还包括/usr/lib/gcc/i486-linux-gnu/4.3.2/include);而对于用引号包含的头文件,gcc首先查找包含头文件的.c文件所在的目录
,然后查找-I选项指定的目录,然后查找系统的头文件目录。
#infdef #endif是必须的,否则错误很严重
头文件中的变量和函数声明一定不能是定义。如果头文件中出现变量或函数定义,这个头文件又被多个.c文件包含,那么这些.c文件就
不能链接在一起了。
extern的准确定义:这次声明的标识符具有什么样的Linkage取决于前一次声明,
初始化有Static Initializer和Dynamic Initializer两种情况,前者表示Initializer中只能使用常量表达式,表达式的值必须在编译
时就能确定,后者表示Initializer中可以使用任意的右值表达式,
创建静态库:
$ gcc -c stack/stack.c stack/push.c stack/pop.c stack/is_empty.c
ar rs libstack.a stack.o push.o pop.o is_empty.o
$ gcc main.c -L. -lstack -Istack -o main
编译器默认会找的目录可以用-print-search-dirs选项查看:
编译器是优先考虑共享库的,如果希望编译器只链接静态库,可以指定-static选项。
在链接libc共享库时只是指定了动态链接器和该程序所需要的库文件,并没有真的做链接,可执行文件main中调用的libc库函数仍然是
未定义符号,要在运行时做动态链接。而在链接静态库时,链接器会把静态库中的目标文件取出来和可执行文件真正链接在一起。
$ gcc -shared -o libstack.so stack.o push.o pop.o is_empty.o
0x0(%ebx)被修改成-0xc(%ebx)和-0x8(%ebx),而不是修改成绝对地址。所以共享库各段的加载地址并没有定死,可以加载到任意位置,
因为指令中没有使用绝对地址,因此称为位置无关代码。
(%eax)表示%eax寄存器中地址对应单元中的值
FHS(Filesystem Hierarchy Standard)标准规定了Man Page各Section的含义如下:
Section 描述
1 用户命令,例如ls(1)
2 系统调用,例如_exit(2)
3 库函数,例如printf(3)
4 特殊文件,例如null(4)描述了设备文件/dev/null、/dev/zero的作用
5 系统配置文件的格式,例如passwd(5)描述了系统配置文件/etc/passwd的格式
6 游戏
7 其它杂项,例如bash-builtins(7)描述了bash的各种内建命令
8 系统管理命令,例如ifconfig(8)
注意区分用户命令和系统管理命令,用户命令通常位于/bin和/usr/bin,系统管理命令通常位于/sbin和/usr/sbin,任何用户都可以执
行用户命令,而执行系统管理命令经常需要root权限。
C编译器做语法解析之前的预处理步骤:
1)三连符替换成相应的单字符,\r\n(windows上)和\n(linux)上通改为换行符
2)把用\字符续行的多行代码接成一行
3)把注释(不管是单行注释还是多行注释)都替换成一个空格。
4)预处理器把逻辑代码行划分成Token和空白字符,
5)遇到#include预处理指示,则把相应的源文件包含进来,并对源文件做以上1-4步预处理。如果遇到宏定义则做宏展开。
6)找出字符常量或字符串中的转义序列,用相应的字节来替换它,比如把\n替换成字节0x0a。
7)把相邻的字符串连接起来。
8)把Token交给C编译器做语法解析,这时就不再是预处理Token,而称为C Token了。
函数式宏定义(Function-like Macro):就像函数调用一样,把两个实参分别替换到宏定义中形参a和b的位置
#define MAX(a, b) ((a)>(b)?(a):(b))
k = MAX(i&0x0f, j&0x0f)
宏定义本身倒不必编译生成指令,但是代码中出现的每次调用编译生成的指令都相当于一个函数体,而不是简单的几条传参指令和call
指令。所以,使用函数式宏定义编译生成的目标文件会比较大。
函数调用时如果实参表达式有Side Effect,那么这些Side Effect只发生一次。例如MAX(++a, ++b),如果MAX是个真正的函数,a和b只
增加一次。但如果MAX是上宏定义,则要展开成k = ((++a)>(++b)?(++a):(++b)),a和b就不一定是增加一次还是两次了。
即使实参没有Side Effect,使用函数式宏定义也往往会导致较低的代码执行效率。甚至是死循环。只要小心使用还是会显著提高代码的
执行效率,毕竟省去了分配和释放栈帧、传参、传返回值等一系列工作,因此那些简短并且被频繁调用的函数经常用函数式宏定义来代
替实现。
函数式宏定义经常写成这样的形式:
#define device_init_wakeup(dev,val) \
        do { \
                device_can_wakeup(dev) = !!(val); \
                device_set_wakeup_enable(dev,val); \
        } while(0)
以防止if后的逗号断开
在定义之中有空白和没有空白被认为是不同的,所以这样的重复定义是不允许的:
#define OBJ_LIKE (1 - 1)
#define OBJ_LIKE (1-1)
如果需要重新定义一个宏,和原来的定义不同,可以先用#undef取消原来的定义,再重新定义
inline关键字告诉编译器,这个函数的调用要尽可能快,可以当普通的函数调用实现,也可以用宏展开的办法实现(gcc -O优化编译时
)。
#运算符用于创建字符串,#运算符后面应该跟一个形参,#define STR(s) # s
其中多空格改为单个空格,"改为\",\改为\\
宏定义中可以用##运算符把前后两个预处理Token连接成一个预处理Token:#define CONCAT(a, b) a##b
函数式宏定义也可以带可变参数,同样是在参数列表中用...表示可变参数。可变参数的部分用__VA_ARGS__表示,实参中对应...的几个
参数可以看成一个参数替换到宏定义中__VA_ARGS__所在的地方。
#define FOO(a, b, c) a##b##c
FOO(1,2,)
FOO在定义时带一个参数,在调用时必须传一个参数给它,如果不传参数则表示传了一个空参数。
和##一起使用时,当__VA_ARGS是空参数时,##运算符把它前面的,号“吃”掉了。
#error UNKNOWN TARGET MACHINE,编译器遇到这个预处理指示就报错退出,错误信息就是UNKNOWN TARGET MACHINE。
通常这个头文件由配置工具生成,比如在Linux内核源代码的目录下运行make menuconfig命令可以出来一个配置菜单,在其中配置的选
项会自动转换成头文件include/linux/autoconf.h中的宏定义。
#pragma预处理指示供编译器实现一些非标准的特性,
C标准规定了几个特殊的宏,在不同的地方使用可以自动展开成不同的值,常用的有__FILE__和__LINE__,__FILE__
关于Makefile:main是这条规则的目标(Target),main.o、stack.o和maze.o是这条规则的条件(Prerequisite)。目标和条件之间的
关系是:欲更新目标,必须首先更新它的所有条件;所有条件中只要有一个条件被更新了,目标也必须随之被更新。所谓“更新”就是
执行一遍规则中的命令列表,命令列表中的每条命令必须以一个Tab开头,注意不能是空格。
现在总结一下Makefile的规则,请读者结合上面的例子理解。如果一条规则的目标属于以下情况之一,就称为需要更新:
目标没有生成。
某个条件需要更新。
某个条件的修改时间比目标晚。
执行一条规则A的步骤如下:
检查它的每个条件P:
如果P需要更新,就执行以P为目标的规则B。之后,无论是否生成文件P,都认为P已被更新。
如果找不到规则B,并且文件P已存在,表示P不需要更新。
如果找不到规则B,并且文件P不存在,则报错退出。
在检查完规则A的所有条件后,检查它的目标T,如果属于以下情况之一,就执行它的命令列表:
文件T不存在。
文件T存在,但是某个条件的修改时间比它晚。
某个条件P已被更新(并不一定生成文件P)。
Makefile都会有一个clean规则,用于清除编译过程中产生的二进制文件:
如果在make的命令行中指定一个目标(例如clean),则更新这个目标,如果不指定目标则更新Makefile中第一条规则的目标(缺省目标
)。
clean目标不依赖于任何条件,并且执行它的命令列表不会生成clean这个文件,刚才说过,只要执行了命令列表就算更新了目标,即使
目标并没有生成也算。在这个例子还演示了命令前面加@和-字符的效果:如果make执行的命令前面加了@字符,则不显示命令本身而只显
示它的结果;通常make执行的命令如果出错(该命令的退出状态非0)就立刻终止,不再执行后续命令,但如果命令前面加了-号,即使
这条命令出错,make也会继续执行后续命令。
而我们希望把clean当作一个特殊的名字使用,不管它存在不存在都要更新,可以添一条特殊规则,把clean声明为一个伪目标:
.PHONY: clean

clean目标是一个约定俗成的名字,在所有软件项目的Makefile中都表示清除编译生成的文件,类似这样的约定俗成的目标名字有:
all,执行主要的编译工作,通常用作缺省目标。
install,执行编译后的安装工作,把可执行文件、配置文件、文档等分别拷到不同的安装目录。
clean,删除编译生成的二进制文件。
distclean,不仅删除编译生成的二进制文件,也删除其它生成的文件,例如配置文件和格式转换后的文档,执行make distclean之后应
该清除所有这些文件,只留下源文件。
:=运算符,make在遇到变量定义时立即展开,
make常用的特殊变量有:
$@,表示规则中的目标。
$<,表示规则中的第一个条件。
$?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔。
$^,表示规则中的所有条件,组成一个列表,以空格分隔。
main: main.o stack.o maze.o
 gcc $^ -o $@
gcc的-M选项自动生成目标文件和源文件
-n选项只打印要执行的命令,而不会真的执行命令,这个选项有助于我们检查Makefile写得是否正确,
一些规模较大的项目会把不同的模块或子系统的源代码放在不同的子目录中,然后在每个子目录下都写一个该目录的Makefile,然后在
一个总的Makefile中用make -C命令执行每个子目录下的Makefile。
如果在Makefile中也定义了CFLAGS变量,则命令行的值覆盖Makefile中的值。
自动处理头文件的依赖关系:
$ gcc -MM *.c
all: main
main: main.o stack.o maze.o
 gcc $^ -o $@
clean:
 -rm main *.o
.PHONY: clean
sources = main.c stack.c maze.c
include $(sources:.c=.d)
%.d: %.c
 set -e; rm -f $@; \
 $(CC) -MM $(CPPFLAGS) $< > ; \
 sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < > $@; \
 rm -f
----------指针和库函数--------------
为避免出现野指针,在定义指针变量时就应该给它明确的初值,或者把它初始化为NULL:
ANSI在将C语言标准化时引入了void *类型,void *指针与其它类型的指针之间可以隐式转换,而不必用类型转换运算符。不能定义void
型的变量,
只有指向同一个数组中元素的指针之间相互比较才有意义,否则没有意义。指针不能相加
数组名做右值时转换成指向首元素的指针,但做左值仍然表示整个数组的存储单元,而不是首元素的存储单元,
const int *a;
int const *a;
a是一个指向const int型的指针,a所指向的内存单元不可改写,所以(*a)++是不允许的,但a可以改写,
int * const a;
a是一个指向int型的const指针,*a是可以改写的,但a不允许改写。
指向非const变量的指针或者非const变量的地址可以传给指向const变量的指针,编译器可以做隐式类型转换,
指向const变量的指针或者const变量的地址不可以传给指向非const变量的指针,
但良好的编程习惯应该尽可能多地使用const
const char *p = "abcd";
printf(p);
如果要定义一个指针指向字符串字面值,这个指针应该是const char *型,如果写成char *p = "abcd";就不好了,有隐患,例如:
int *a[10] 等价:
typedef int *t;
t a[10];
int (*a)[10] 等价:
typedef int t[10];
t *a;
&a[0]表示数组a的首元素的首地址,而&a表示数组a的首地址,显然这两个地址的数值相同,但这两个表达式的类型是两种不同的指针类
型,

函数也是一种类型,可以定义指向函数的指针。
函数指针存放的就是函数的入口地址(位于.text段)。
void (*f)(const char *) = say_hello; 等价于void (*f)(const char *) = &say_hello;
say_hello是一种函数类型,而函数类型和数组类型类似,做右值使用时自动转换成函数指针类型,所以可以直接赋给f,
函数调用运算符()要求操作数是函数指针,所以f("Guys")是最直接的写法,而say_hello("Guys")或(*f)("Guys")则是把函数类型自动
转换成函数指针然后做函数调用。
函数可以返回void类型、标量类型、结构体、联合体,但不能返回函数类型,也不能返回数组类型。
具有不完全类型的变量可以通过多次声明组合成一个完全类型,比如数组str声明两次:
char str[];
char str[10];
在分析复杂声明时,要借助typedef把复杂声明分解成几种基本形式:
malloc返回的指针一定要保存好,只有把它传给free才能释放这块内存,如果这个指针丢失了,就没有办法free这块内存了,也会造成
内存泄漏。
如果参数是一个函数指针,调用者可以传递一个函数的地址给实现者,让实现者去调用它,这称为回调函数(Callback Function)。
异步调用也是回调函数的一种典型用法,调用者首先将回调函数传给实现者,实现者记住这个函数,这称为注册一个回调函数,然后当
某个事件发生时实现者再调用先前注册的函数
既然参数可以是函数指针,返回值同样也可以是函数指针,因此可以有func()();这样的调用。
$ od -tx1 -tc -Ax textfile
-tx1选项表示将文件中的字节以十六进制的形式列出来,每组一个字节,-tc选项表示将文件中的ASCII码以字符形式列出来。和hexdump
类似,输出结果最左边的一列是文件中的地址,默认以八进制显示,-Ax选项要求以十六进制显示文件中的地址。
crw-rw-rw- 1 root dialout 5, 0 2009-03-20 19:31 /dev/tty
开头的c表示文件类型是字符设备。中间的5, 0是它的设备号,主设备号5,次设备号0,主设备号标识内核中的一个设备驱动程序,次设
备号标识该设备驱动程序管理的一个设备。
程序启动时(在main函数还没开始执行之前)会自动把终端设备打开三次,分别赋给三个FILE *指针stdin、stdout和stderr,这三个文
件指针是libc中定义的全局变量
一个系统函数错误返回后应该马上检查errno,在检查errno之前不能再调用其它系统函数。
fgetc函数从指定的文件中读一个字节,getchar从标准输入读一个字节,调用getchar()相当于调用fgetc(stdin)。
fputc函数向指定的文件写一个字节,putchar向标准输出写一个字节,调用putchar(c)相当于调用fputc(c, stdout)。
调用rewind函数把读写位置移到文件开头
gets函数的接口设计得很有问题,就像strcpy一样,用户提供一个缓冲区,却不能指定缓冲区的大小,很可能导致缓冲区溢出错误
fscanf从指定的文件stream中读字符,而sscanf从指定的字符串str中读字符。后面三个以v开头的函数的可变参数不是以...的形式传进
来,而是以va_list类型传进来。
使用realloc函数简化了这些步骤,把原内存空间的指针ptr传给realloc,通过参数size指定新的大小(字节数),realloc返回新内存
空间的首地址,并释放原内存空间。malloc realloc不负责清零,calloc负责清零。
alloca函数不是在堆上分配空间,而是在调用者函数的栈帧上分配空间,当调用者函数返回时自动释放栈帧,所以不需要free。这个函数不属于C标准库,而是在POSIX标准中定义的。
------------------------------------------------------------------------
阅读(3113) | 评论(0) | 转发(0) |
0

上一篇:租房,存储

下一篇:信号量分析

给主人留下些什么吧!~~