- 第3章 内核编程语言与环境(1)
- 3.4 C与汇编程序的相互调用
- 3.4.1 C函数调用机制
- 3.4.1.1 栈帧结构和控制转移权方式
- 3.4.1.2 函数调用举例
- 3.4.1.3 main()也是一个函数
- 3.4.2 在汇编程序中调用C函数
- 3.4.3 在C程序中调用汇编函数
- 3.5 Linux 0.11 目标格式文件
- 3.5.1 目标文件格式
- 3.5.1.1 执行头部分
- 3.5.1.2 重定位信息部分
- 3.5.1.3 符号表和字符串部分
- 3.5.2 Linux 0.11中的目标文件格式
- 3.5.3 连接程序输出
- 3.5.4 连接程序预定义变量
- 3.5.5 System.map 文件
- 3.6 make程序和Makefile文件
- 3.7 本章小结
栈:传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用于存储局部数据。
栈帧(Stack frame):单个函数调用操作所使用的栈部分
ebp(frame pointer):帧指针,指向栈低(高地址)
esp(stack pointer):栈指针,指向栈顶(低地址)
通过push 和pop指令来入栈和出栈
栈指针递减以扩展空间,栈指针增加以回收空间。
CALL 和 RET用于处理函数调用和返回。
一个程序只有一个连续增长的栈,但是里面的数据分属于不同的具有调用关系的函数。如函数1 调用函数2,函数2调用函数3, 则栈中的数据从高到低分别为函数1->函数2->函数3。在函数返回时,被调用函数需要将栈中自己的数据全部清理掉。
栈是用于临时保存数据的。C语言程序内存分布如下:
上面的栈结果是图中的stack的细节表示。而保存的返回地址是指代码区中指令的地址。
8个通用寄存器的保存法则:
假设A调用B
eax、ecx、edx:调用者A自己负责保存,即调用者A在调用函数B时,首先将这三个寄存器的值压栈(当然如果A后面用不到可能不压也没有影响)。在被调用者函数中使用这三个寄存器时不用提前保存其数值;在B结束返回A之后,A对其进行恢复然后使用;
ebx,esi,edi,ebp、esp:被调用者B保存。即B在使用这些寄存器之前需要首先保存这些寄存器的值,在函数结束之前对其进行恢复。
可能多写写汇编这些就变得非常正常了,还是汇编写的少了。
3.4.1.2 函数调用举例C程序例子 exch.c
#include//交换两个变量中的值,并返回其差值 void swap(int *a,int *b) { int c; c = *a;*a=*b;*b=c; } int main() { int a,b; a=16;b=32; swap(&a,&b); return (a-b); }
上述函数的栈帧结构:
使用指令
gcc -m32 -S -o exch.s exch.c
得到其exch.s代码(也进行了一些删除)
swap: .LFB0: .cfi_startproc pushl %ebp #保存原ebp的值 .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp #设置当前函数的帧指针 .cfi_def_cfa_register 5 subl $16, %esp #为局部变量分配空间 call __x86.get_pc_thunk.ax addl $_GLOBAL_OFFSET_TABLE_, %eax movl 8(%ebp), %eax #取函数的第一个参数,该参数是一个整数类型值的指针,这边是加8,因为+4是返回地址。 movl (%eax), %eax #取该地址指针所指位置的内容 movl %eax, -4(%ebp) #保存到局部变量c中 movl 12(%ebp), %eax #把第2个参数所指内容保存到第1个参数所指的位置 movl (%eax), %edx movl 8(%ebp), %eax movl %edx, (%eax) movl 12(%ebp), %eax #再次取第2个参数 movl -4(%ebp), %edx #把局部变量c中的内容放到这个指针所指位置 movl %edx, (%eax) nop leave #恢复原ebp、esp的值(即movl %ebp,%esp;popl %ebp;) .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size swap, .-swap .globl main .type main, @function main: .LFB1: .cfi_startproc leal 4(%esp), %ecx .cfi_def_cfa 1, 0 andl $-16, %esp pushl -4(%ecx) pushl %ebp #保存原ebp值 .cfi_escape 0x10,0x5,0x2,0x75,0 movl %esp, %ebp #设置当前函数的帧指针 pushl %ecx .cfi_escape 0xf,0x3,0x75,0x7c,0x6 subl $20, %esp #为局部变量在栈中分配空间 call __x86.get_pc_thunk.ax addl $_GLOBAL_OFFSET_TABLE_, %eax movl %gs:20, %eax movl %eax, -12(%ebp) xorl %eax, %eax movl $16, -20(%ebp) #为整型变量a赋初值16 movl $32, -16(%ebp) #为整型白能量b赋初值32 leal -16(%ebp), %eax #为调用swap()作准备,取局部变量b的地址 pushl %eax #作为调用的参数压入栈中,即先压第2个参数 leal -20(%ebp), %eax #再取局部变量a的值,作为第一个参数压栈 pushl %eax call swap #调用函数swap() addl $8, %esp #这句话的作用应该是清除swap的两个输入参数占用的栈空间。即变量a的指针和变量b的指针 movl -20(%ebp), %edx #取第一个局部变量a的值 movl -16(%ebp), %eax #取第2个局部变量b的值 subl %eax, %edx #相减 movl %edx, %eax #结果保存在eax中 movl -12(%ebp), %ecx xorl %gs:20, %ecx je .L4 call __stack_chk_fail_local .L4: movl -4(%ebp), %ecx .cfi_def_cfa 1, 0 leave .cfi_restore 5 leal -4(%ecx), %esp .cfi_def_cfa 4, 4 ret .cfi_endproc
这段代码和书上的很不一样,关于语句
call __x86.get_pc_thunk.ax addl $_GLOBAL_OFFSET_TABLE_, %eax
可以参考How do i get rid of call __x86.get_pc_thunk.ax了解其产生原因,可以使用两种策略尝试去掉这些代码:1)编译时加入选项 -fno-pie;2)修改函数名称main,如为xmain(但是这样做的话,最后生成的可执行文件会无法执行)。
其实上面代码里面很多的内容都是为了主函数服务的,而这边重点是介绍函数调用的例子,因此我们两种策略都用上,最终生成的.s代码(删除一些伪指令)为:
swap: .LFB0: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $16, %esp movl 8(%ebp), %eax movl (%eax), %eax movl %eax, -4(%ebp) movl 12(%ebp), %eax movl (%eax), %edx movl 8(%ebp), %eax movl %edx, (%eax) movl 12(%ebp), %eax movl -4(%ebp), %edx movl %edx, (%eax) nop leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE0: .size swap, .-swap .globl xmain .type xmain, @function xmain: .LFB1: .cfi_startproc pushl %ebp .cfi_def_cfa_offset 8 .cfi_offset 5, -8 movl %esp, %ebp .cfi_def_cfa_register 5 subl $24, %esp movl %gs:20, %eax #Stack canaries, 堆栈金丝雀,校验作用 #参见 https://stackoverflow.com/questions/12234817/what-does-this-instruction-do-mov-gs0x14-eax movl %eax, -12(%ebp) xorl %eax, %eax movl $16, -20(%ebp) movl $32, -16(%ebp) leal -16(%ebp), %eax pushl %eax leal -20(%ebp), %eax pushl %eax call swap addl $8, %esp movl -20(%ebp), %edx movl -16(%ebp), %eax subl %eax, %edx movl %edx, %eax movl -12(%ebp), %ecx xorl %gs:20, %ecx je .L4 call __stack_chk_fail .L4: leave .cfi_restore 5 .cfi_def_cfa 4, 4 ret .cfi_endproc .LFE1: .size xmain, .-xmain .ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0" .section .note.GNU-stack,"",@progbits
相较于书中给出的代码,这边代码在进行操作时较为繁琐,使用了更多的寄存器。如果开启编译优化即在编译时加入 -O1到-O3优化,代码会变得很简单。
上面的两个函数都可以大致分为三个部分:
“设置”:初始化栈帧结构
“主体”:执行函数的实际计算操作
“结束”:恢复栈状态并从函数中返回。
leave等价于
movl %ebp,%esp #恢复原esp的值(指向栈帧开始处) popl %ebp #恢复原ebp的值(通常是调用者的帧指针)
注意,在swap参数压栈的时候是先把&b压入,然后再把&a压入,即函数调用之前其参数是从右向左压栈的。即变量压栈的顺序与函数声明的参数顺序正好相反。
C语言是传值的语言
main()函数在编译链接时会作为crt0.汇编程序的函数被调用。crt0.s是一个桩(stub)程序,crt:“C run-time”。
linux 0.11中crt0.汇编程序:
.text .global _environ #声明全局变量 _environ (对应C程序中的environ变量) __entry: #代码入口标号 movl 8(%esp),%eax #取程序的环境变量指针envp并保存在_environ中 movl %eax,_environ #envp是execve()函数在加载执行文件时设置的 call _main #调用我们的主程序。其返回状态值在eax寄存器中 pushl %eax #压入返回值作为exit()函数的参数并调用该函数 1: call _exit jmp 1b #控制应该不会到达这里。若到达这里则继续执行exit() .data _environ: #定义变量_environ,为其分配一个长字空间 .long 0
上面的程序执行
gcc -m32 -v -o exch exch.s
结果为:
Using built-in specs. COLLECT_GCC=gcc COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper OFFLOAD_TARGET_NAMES=nvptx-none OFFLOAD_TARGET_DEFAULT=1 Target: x86_64-linux-gnu Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.5.0-3ubuntu1~18.04' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu Thread model: posix gcc version 7.5.0 (Ubuntu 7.5.0-3ubuntu1~18.04) COLLECT_GCC_OPTIONS='-m32' '-v' '-o' 'exch' '-mtune=generic' '-march=i686' as -v --32 -o /tmp/ccvVEOef.o exch.s GNU assembler version 2.30 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.30 COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/ LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/7/32/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../i386-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib32/:/lib/i386-linux-gnu/:/lib/../lib32/:/usr/lib/i386-linux-gnu/:/usr/lib/../lib32/:/usr/lib/gcc/x86_64-linux-gnu/7/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../i386-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/7/../../../:/lib/i386-linux-gnu/:/lib/:/usr/lib/i386-linux-gnu/:/usr/lib/ COLLECT_GCC_OPTIONS='-m32' '-v' '-o' 'exch' '-mtune=generic' '-march=i686' /usr/lib/gcc/x86_64-linux-gnu/7/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/7/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper -plugin-opt=-fresolution=/tmp/ccvjdtBG.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_i386 --hash-style=gnu --as-needed -dynamic-linker /lib/ld-linux.so.2 -pie -z now -z relro -o exch /usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib32/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib32/crti.o /usr/lib/gcc/x86_64-linux-gnu/7/32/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/7/32 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../i386-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib32 -L/lib/i386-linux-gnu -L/lib/../lib32 -L/usr/lib/i386-linux-gnu -L/usr/lib/../lib32 -L/usr/lib/gcc/x86_64-linux-gnu/7 -L/usr/lib/gcc/x86_64-linux-gnu/7/../../../i386-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/7/../../.. -L/lib/i386-linux-gnu -L/usr/lib/i386-linux-gnu /tmp/ccvVEOef.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/7/32/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/7/../../../../lib32/crtn.o COLLECT_GCC_OPTIONS='-m32' '-v' '-o' 'exch' '-mtune=generic' '-march=i686'
相较于书中的例子要复杂很多。
ELF文件格式
这块相对东西比较多,还得再学习中慢慢的深入了解。
调用流程:1、按照逆向顺序将函数参数压入栈;2.执行CALL指令执行被调用的函数;3.在调用函数返回后,将先前压入栈中的函数参数清除。
图中EIP:CALL指令下一条指令的地址。 Linux内核中使用中断门和陷阱门的方式处理特权级变化时的调用情况(这是什么?)
如果没有专门为调用函数func()压入参数就直接调用它的话,func()函数会把存放在EIP位置以上的栈中其他内容作为自己的参数使用。
汇编调用C函数示例,_sys_fork函数:
//kernel/system_call.s汇编程序_sys_fork部分 push %gs pushl %esi pushl %edi pushl %ebp pushl %eax call _copy_process # 调用C函数copy_process() (kernal/fork.c,68). addl $20,%esp #丢弃这里所有压栈数据 1: ret
//kernel/fork.c程序 int copy_process(int nr,long ebp,long edi,long esi,long gs,long none, long ebx,long ecx,long edx, long fs,long es,long ds, long eip,long cs,long eflags,long esp,long ss)
可以不用CALL指令而使用JMP指令实现汇编对函数的调用:首先将要执行的下一条指令地址入栈,然后跳转到被调用函数的开始地址去执行函数。在kernel/asm.s程序第62行调用执行traps.c中的do_int3()函数时即是如此。
3.4.3 在C程序中调用汇编函数这种方式不常使用。重点:对函数参数在栈中位置的确定。
例子:
SYSWRITE = 4 #sys_write()系统调用号 .global _mywrite,_myadd .text _mywrite: pushl %ebp movl %esp,%ebp pushl %ebx movl 8(%ebp),%ebx #取调用者第1个参数:文件描述符fd movl 12(%ebp),%ecx #取第2个参数:缓冲区指针。 movl 16(%ebp),%edx #取第3个参数:显示字符数 movl $SYSWRITE,%eax #%eax中放入系统调用号4. int $0x80 #执行系统调用 popl %ebx movl %ebp,%esp popl %ebp ret _myadd: pushl %ebp movl %esp,%ebp movl 8(%ebp),%eax #取第1个参数a movl 12(%ebp),%edx #取第2个参数b xorl %ecx,%ecx #ecx为0表示计算溢出。 addl %eax,%edx #执行加法运算 jo 1f #若溢出则跳转 jo:溢出跳转 movl 16(%ebp),%eax #取第3个参数的指针 movl %edx,(%eax) #把计算结果放入指针所指位置处 incl %ecx #没有发生溢出,于是设置无溢出返回值 1: movl %ecx,%eax #%eax中是函数返回值 movl %ebp,%esp popl %ebp ret
调用这两个函数的C程序caller.c 如下所示:
#include#include int main() { char buf[1024]; int a,b,res; char *mystr = "Calculating...n"; char *emsg = "Error in addingn"; a=5;b=10; mywrite(1,mystr,strlen(mystr)); if(myadd(a,b,&res)) { sprintf(buf,"The result is %dn",res); mywrite(1,buf,strlen(buf)); } else { mywrite(1,emsg,strlen(emsg)); } return 0; }
编译指令:
as -o callee.o callee.s -32 gcc -o caller caller.c callee.o -m32
执行结果:
编译会有两个warning。
Linux 0.11使用了两种编译器:
汇编编译器 as86和响应的链接器ld86,用于编译和链接实地址模式下16位内核引导扇区程序bootsect.s和设置程序setup.s;
GNU的汇编器as(gas)和C语言编译器gcc以及相应的链接程序gld。
这两者的具体使用及说明见第3章 内核编程语言与环境(1)以及其对应的原书内容。
编译器:为源程序文件产生对应的二进制代码和数据目标文件。
链接器:对相关的所有目标文件进行组合处理,形成一个可被内核加载执行的目标文件,即可执行文件。
目标文件和链接程序的基本工作原理参考书:Linkers & Loaders
在下文中 编译器生成的目标文件称为目标模块文件(简称模块文件)
链接程序生成的可执行目标文件称为可执行文件。二者统称为目标文件。
使用a.out形式:汇编与链接输出(Assembly & linker editor output)
组成:文件头、代码区(Text section,正文段、代码段)、已初始化数据区(Data section,数据段)、重定位信息区、符号表以及符号名字符串构成。
图中7个区的基本定义和用途是:
- 执行头部分(exec header)。执行文件头部分,含有关于目标文件整体结构信息的参数。唯一的必要组成部分。
- 代码区(text segment)。由编译器或汇编器生成的二进制指令代码和数据信息。
- 数据区(data segment)。由编译器或汇编器生成的二进制指令代码数据信息,已经初始化了的。
- 代码重定位部分(text relocations)。这部分含有供链接程序使用的记录数据。
- 数据重定位部分(data relocations)。类似于代码重定位部分的作用,但是是用于数据段中指针的重定位。
- 符号表部分(symbol table)。这部分含有供链接程序使用的记录数据,保存着文件中定义的全局符号以及需要从其他模块文件中输入的符号,或者是由连接器定义的符号。
- 字符串表部分(string tablel)。该部分含有与符号名相对应的字符串。用于调试程序调试目标代码,与链接过程无关。
struct exec
{
unsigned long a_magic //执行文件魔数,使用N_MAGIC等宏访问
unsigned a_text //代码长度,字节数
unsigned a_data //数据长度,字节数
unsigned a_bss //文件中的未初始化数据区长度,字节数。
unsigned a_syms //文件中的符号表长度,字节数。
unsigned a_entry //执行开始地址
unsigned a_trsize //代码重定位信息长度,字节数
unsigned a_drsize //数据重定位信息长度,字节数
}
3.5.1.2 重定位信息部分
struct relocation_info
{
int r_address; //段内需要重定位的地址
unsigned int r_symbolnum:24; //含义与r_extern有关。指定符号表中一个符号或者一个段
unsigned int r_pcrel:1; //1比特。PC相关标志
unsigned int r_length:2; //2比特。指定要被重定位字段长度(2的次方)
unsigned int r_extern:1; //外部标志位。1 - 以符号的值重定位,0 - 以段的地址重定位
unsigned int r_pad:4; //没有使用的4个比特位,但最好将它们复位掉
}
3.5.1.3 符号表和字符串部分
struct nlist
{
union
{
char *n_name; //字符串指针
struct nlist *n_next; //或者是指向另一个符号项结构的指针
long n_strx; //或者是符号名称在字符串表中的字节偏移值
} n_un;
unsigned char n_type; //该字节分成3个字段,参见a.out.h文件146-154行
char n_other; //通常不用。
short n_desc; //
unsigned long n_value; //符号的值。
}
3.5.2 Linux 0.11中的目标文件格式
源代码
用到的指令
gcc -c -o hello.o hello.c gcc -o hello hello.o hexdump -x hello.o objdump -h hello.o hexdump -x hello | more objdump -h hello
运行结果:
用strip文件删除执行文件中的符号表信息:
用到的指令:
ll hello objdump -h hello strip hello ll hello objdump -h hello
符号表信息删除了,且文件大小也变小了。
磁盘上的a.out执行文件的各区在进程逻辑地址空间中的对应关系见下图。
使用需求页技术。
图中bss段:进程的未初始化数据区,用于存放静态的未初始化的数据。注意是静态的。
heap段:用于分配进程在执行过程中动态申请的内存空间。
对具有两个输入模块文件和需要连接一个库函数模块的情况,其存储分配情况为:
连接器预定义外部变量包括:etext, _etext、edata、_edata、end和_end
作用:获得程序中段的位置
_etext和etext的地址是程序正文段结束后的第1个地址;
_edata和edata的地址是初始化数据区后面的第1个地址;
_end和end的地址是未初始化数据区(bss)后的第1个地址位置。
带下划线和不带下划线等同,其区别在于ANSI、POSIX等标准中没有定义不带下划线的符号。
brk:参考brk(2) — Linux manual page以及Linux进程分配内存的两种方式–brk() 和mmap()。用于分配内存空间的函数。
例子:
extern int _etext; int et; (int *)et=&_etext; //此时et含有正文段结束处后面的地址。
程序predef.c用于显示几个变量的地址:
extern int end,etext,edata;
extern int _etext,_edata,_end;
int main()
{
printf("&etext=%p, &edata=%p, &end=%pn",
&etext,&edata,&end);
printf("&_etext=%p, &_edata=%p, &_end=%pn",
&_etext,&_edata,&_end);
return 0;
}
在linux 0.11下运行的编译指令:
gcc -o predef predef.c
运行结果(注意结果中给出的都是逻辑地址):
代码段起始地址为0,则由结果可知,代码段长度为16kb,数据段长度为1216b,bss段长度为:1048b。
在unbuntu18.04中,以32位格式编译,指令为
gcc -o predef predef.c -m32
运行结果为:
在unbuntu18.04中,以64位格式编译,指令为
gcc -o predef predef.c
结果为
关于linux下程序内存分布可以看看参考1。
以及参考2.
但是感觉为什么显示上面的地址值我还是不太清楚。到底代码段开始的逻辑地址从哪里开始呢?
不过确实64位程序逻辑地址是48位。这边还需要搞清楚。
运行GNU链接器gld(ld)使用了-M选项,或者使用nm命令,则会在标准输出设备打印出链接映像(link map)信息,包括:
将这些信息重定向到一个文件中(如System.map)。
符号表样例:
第一栏表示符号值(地址);
第2栏是符号类型,指明符号位于目标文件的哪个区(sections)或其属性
第3栏是对应的符号名称
对第2栏内容,如果是小写,则说明是局部;大写表示全局(外部),参见文件include/a.out.h中nlist{}结构n_type字段的定义(l110-185)。
Makefile是文件是make工具程序的配置文件。其详细说明参考GNU make使用手册。
Makefile文件规则的形式:
目标(target)... : 先决条件(prerequisites)... 命令(command) ... ...
自动变量示例:
foo.o:foo.c defs.h hack.h cc -c $(CFLAGS) $< -o $@
其中$<表示第一个先决条件,在上面的例子中为 foo.c;
'$@'代表目标对象,在上面的例子中foo.o
双后缀规则示例:
.c.s: $(CC) $(CFLAGS) -nostdinc -Iinclude -S -o $*.s $<
其中 .c.s 分别表示源后缀和目标后缀。
'$<‘值是‘*.c’文件名
这条规则的含义是将‘*.c’程序编译成’*.s’代码



