第3章 内核编程语言与环境(2)
第3章 内核编程语言与环境(1)
目录
- 第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 本章小结
3.4 C与汇编程序的相互调用
3.4.1 C函数调用机制
3.4.1.1 栈帧结构和控制转移权方式
栈:传递函数参数、存储返回信息、临时保存寄存器原有值以备恢复以及用于存储局部数据。
栈帧(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 <stdio.h>
//交换两个变量中的值,并返回其差值
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_startprocpushl %ebp #保存原ebp的值.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp #设置当前函数的帧指针.cfi_def_cfa_register 5subl $16, %esp #为局部变量分配空间call __x86.get_pc_thunk.axaddl $_GLOBAL_OFFSET_TABLE_, %eaxmovl 8(%ebp), %eax #取函数的第一个参数,该参数是一个整数类型值的指针,这边是加8,因为+4是返回地址。movl (%eax), %eax #取该地址指针所指位置的内容movl %eax, -4(%ebp) #保存到局部变量c中movl 12(%ebp), %eax #把第2个参数所指内容保存到第1个参数所指的位置movl (%eax), %edxmovl 8(%ebp), %eax movl %edx, (%eax)movl 12(%ebp), %eax #再次取第2个参数movl -4(%ebp), %edx #把局部变量c中的内容放到这个指针所指位置movl %edx, (%eax)nopleave #恢复原ebp、esp的值(即movl %ebp,%esp;popl %ebp;) .cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc
.LFE0:.size swap, .-swap.globl main.type main, @function
main:
.LFB1:.cfi_startprocleal 4(%esp), %ecx.cfi_def_cfa 1, 0andl $-16, %esppushl -4(%ecx)pushl %ebp #保存原ebp值.cfi_escape 0x10,0x5,0x2,0x75,0movl %esp, %ebp #设置当前函数的帧指针pushl %ecx.cfi_escape 0xf,0x3,0x75,0x7c,0x6subl $20, %esp #为局部变量在栈中分配空间call __x86.get_pc_thunk.axaddl $_GLOBAL_OFFSET_TABLE_, %eax movl %gs:20, %eaxmovl %eax, -12(%ebp)xorl %eax, %eaxmovl $16, -20(%ebp) #为整型变量a赋初值16movl $32, -16(%ebp) #为整型白能量b赋初值32leal -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), %ecxxorl %gs:20, %ecxje .L4call __stack_chk_fail_local
.L4:movl -4(%ebp), %ecx.cfi_def_cfa 1, 0leave.cfi_restore 5leal -4(%ecx), %esp.cfi_def_cfa 4, 4ret.cfi_endproc
这段代码和书上的很不一样,关于语句
call __x86.get_pc_thunk.axaddl $_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_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5subl $16, %espmovl 8(%ebp), %eaxmovl (%eax), %eaxmovl %eax, -4(%ebp)movl 12(%ebp), %eaxmovl (%eax), %edxmovl 8(%ebp), %eaxmovl %edx, (%eax)movl 12(%ebp), %eaxmovl -4(%ebp), %edxmovl %edx, (%eax)nopleave.cfi_restore 5.cfi_def_cfa 4, 4ret.cfi_endproc
.LFE0:.size swap, .-swap.globl xmain.type xmain, @function
xmain:
.LFB1:.cfi_startprocpushl %ebp.cfi_def_cfa_offset 8.cfi_offset 5, -8movl %esp, %ebp.cfi_def_cfa_register 5subl $24, %espmovl %gs:20, %eax #Stack canaries, 堆栈金丝雀,校验作用#参见 https://stackoverflow.com/questions/12234817/what-does-this-instruction-do-mov-gs0x14-eaxmovl %eax, -12(%ebp)xorl %eax, %eaxmovl $16, -20(%ebp)movl $32, -16(%ebp)leal -16(%ebp), %eaxpushl %eaxleal -20(%ebp), %eaxpushl %eaxcall swapaddl $8, %espmovl -20(%ebp), %edxmovl -16(%ebp), %eaxsubl %eax, %edxmovl %edx, %eaxmovl -12(%ebp), %ecxxorl %gs:20, %ecxje .L4call __stack_chk_fail
.L4:leave.cfi_restore 5.cfi_def_cfa 4, 4ret.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语言是传值的语言
3.4.1.3 main()也是一个函数
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 _exitjmp 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文件格式
这块相对东西比较多,还得再学习中慢慢的深入了解。
3.4.2 在汇编程序中调用C函数
调用流程:1、按照逆向顺序将函数参数压入栈;2.执行CALL指令执行被调用的函数;3.在调用函数返回后,将先前压入栈中的函数参数清除。
图中EIP:CALL指令下一条指令的地址。 Linux内核中使用中断门和陷阱门的方式处理特权级变化时的调用情况(这是什么?)
如果没有专门为调用函数func()压入参数就直接调用它的话,func()函数会把存放在EIP位置以上的栈中其他内容作为自己的参数使用。
汇编调用C函数示例,_sys_fork函数:
//kernel/system_call.s汇编程序_sys_fork部分push %gspushl %esipushl %edipushl %ebppushl %eaxcall _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程序中调用汇编函数
这种方式不常使用。重点:对函数参数在栈中位置的确定。
例子:
/*本汇编程序利用系统调用sys_write()实现显示函数int mywrite(int fd,char * buf, int count)。函数int myadd(int a, int b,int *res) 用于执行a+b=res运算。若函数返回0,则说明溢出。注意:如果在现在的Linux系统下编译,则请去掉函数名前的下划线'_'
*/
SYSWRITE = 4 #sys_write()系统调用号
.global _mywrite,_myadd
.text
_mywrite:pushl %ebpmovl %esp,%ebppushl %ebxmovl 8(%ebp),%ebx #取调用者第1个参数:文件描述符fdmovl 12(%ebp),%ecx #取第2个参数:缓冲区指针。movl 16(%ebp),%edx #取第3个参数:显示字符数movl $SYSWRITE,%eax #%eax中放入系统调用号4.int $0x80 #执行系统调用popl %ebx movl %ebp,%esppopl %ebpret
_myadd:pushl %ebpmovl %esp,%ebpmovl 8(%ebp),%eax #取第1个参数amovl 12(%ebp),%edx #取第2个参数bxorl %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,%esppopl %ebpret
调用这两个函数的C程序caller.c 如下所示:
#include <string.h>
#include <stdio.h>
/*调用汇编函数mywrite(fd,buf,count)显示信息;调用myadd(a,b,result)执行加运算如果myadd()返回0,则表示加函数发生溢出。首先显示开始计算信息,然后显示运算结果。
*/
int main()
{char buf[1024];int a,b,res;char *mystr = "Calculating...\n";char *emsg = "Error in adding\n";a=5;b=10;mywrite(1,mystr,strlen(mystr));if(myadd(a,b,&res)){sprintf(buf,"The result is %d\n",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。
3.5 Linux 0.11 目标格式文件
Linux 0.11使用了两种编译器:
汇编编译器 as86和响应的链接器ld86,用于编译和链接实地址模式下16位内核引导扇区程序bootsect.s和设置程序setup.s;
GNU的汇编器as(gas)和C语言编译器gcc以及相应的链接程序gld。
这两者的具体使用及说明见第3章 内核编程语言与环境(1)以及其对应的原书内容。
编译器:为源程序文件产生对应的二进制代码和数据目标文件。
链接器:对相关的所有目标文件进行组合处理,形成一个可被内核加载执行的目标文件,即可执行文件。
目标文件和链接程序的基本工作原理参考书:Linkers & Loaders
在下文中 编译器生成的目标文件称为目标模块文件(简称模块文件)
链接程序生成的可执行目标文件称为可执行文件。二者统称为目标文件。
3.5.1 目标文件格式
使用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)。该部分含有与符号名相对应的字符串。用于调试程序调试目标代码,与链接过程无关。
3.5.1.1 执行头部分
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段:用于分配进程在执行过程中动态申请的内存空间。
3.5.3 连接程序输出
对具有两个输入模块文件和需要连接一个库函数模块的情况,其存储分配情况为:
3.5.4 连接程序预定义变量
连接器预定义外部变量包括: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用于显示几个变量的地址:
/*print the symbols predefined by linker.
*/
extern int end,etext,edata;
extern int _etext,_edata,_end;
int main()
{printf("&etext=%p, &edata=%p, &end=%p\n",&etext,&edata,&end);printf("&_etext=%p, &_edata=%p, &_end=%p\n",&_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位。这边还需要搞清楚。
3.5.5 System.map 文件
运行GNU链接器gld(ld)使用了-M选项,或者使用nm命令,则会在标准输出设备打印出链接映像(link map)信息,包括:
将这些信息重定向到一个文件中(如System.map)。
符号表样例:
第一栏表示符号值(地址);
第2栏是符号类型,指明符号位于目标文件的哪个区(sections)或其属性
第3栏是对应的符号名称
对第2栏内容,如果是小写,则说明是局部;大写表示全局(外部),参见文件include/a.out.h中nlist{}结构n_type字段的定义(l110-185)。
3.6 make程序和Makefile文件
Makefile是文件是make工具程序的配置文件。其详细说明参考GNU make使用手册。
Makefile文件规则的形式:
目标(target)... : 先决条件(prerequisites)...命令(command)......
自动变量示例:
foo.o:foo.c defs.h hack.hcc -c $(CFLAGS) $< -o $@
其中$<表示第一个先决条件,在上面的例子中为 foo.c;
'$@'代表目标对象,在上面的例子中foo.o
双后缀规则示例:
.c.s:$(CC) $(CFLAGS) \-nostdinc -Iinclude -S -o $*.s $<
其中 .c.s 分别表示源后缀和目标后缀。
'$<‘值是‘*.c’文件名
这条规则的含义是将‘*.c’程序编译成’*.s’代码
3.7 本章小结
第3章 内核编程语言与环境(2)相关推荐
- 第3章 内核编程语言与环境(1)
第3章 内核编程语言与环境(1) 这一章都是基础内容,但是感觉非常重要,填补了我之前好多的知识空白. 可先大致浏览原书内容,然后碰到问题回过头来再看. 目录 第3章 内核编程语言与环境(1) 3.1 ...
- LINUX 0.11内核完全剖析学习笔记-第三章内核编程语言和环境
一.编译器 linux 0.11 集成了两种汇编器.一种是能产生16位代码的as86汇编器,使用配套的ld86链接器:另一种是GUN汇编器gas,使用GNU ld链接器俩链接产生的目标文件. 1.1 ...
- Windows核心编程 第三章 内核对象
第3章内核对象 在介绍Windows API的时候,首先要讲述内核对象以及它们的句柄.本章将要介绍一些比较抽象的概念,在此并不讨论某个特定内核对象的特性,相反只是介绍适用于所有内核对象的特性. 首先介 ...
- 软件测试之第一章 软件测试和测试环境
第一章 软件测试和测试环境 一. 软件的含义和分类 1 软件的含义 软件是程序.数据和文档的集合. 程序:编程语言:C.C++.Java.php 等. 数据:使用文件或数据库来存储数据. 文档:安装说 ...
- xcode w情ndows版,第 1 章 简介和环境搭建
第 1 章 简介和环境搭建 1.1 什么是编程语言 如果想控制计算机,你需要一种可以和计算机对话的方法.不像猫或狗那样有一套自己的神秘语言,计算机的语言是人类创造的.计算机程序是一段文本,就像一本书或 ...
- 2022 最新 Android 基础教程,从开发入门到项目实战【b站动脑学院】学习笔记——第一章:Android开发环境搭建
第 1 章 Android开发环境搭建 本章介绍了如何在个人电脑上搭建Android开发环境,主要包括:Android开发的发展历史是怎样的.Android Studio的开发环境是如何搭建的.如何创 ...
- 学内核之二:基于QEMU搭建Linux内核运行调试环境
目录 一 接续上文 二 编译根文件系统 三 构建完善根文件系统 四 内核中指定根文件系统 五 带根文件系统启动内核 一 接续上文 在上一篇文章中,我们展示了通过QEMU仿真软件来运行Linux内核的过 ...
- Linux内核源代码分析-第三章 内核体系结构概述-1
第3章 内核体系结构概述 本章从较高层次上对内核进行说明.从顺序上来说,本章首先介绍内核设计目标,接下来 介绍内核体系结构,最后介绍内核源程序目录结构. 3.1 内核设计目标 Linux 的内核展现出 ...
- 《我和PIC单片机:基于PIC18》——第2章 PIC的开发环境 2.1 PIC开发的硬件资源...
第2章 PIC的开发环境 前面我们学习了PIC的内部资源配置和I/O口的基本结构,这一章我们重点要实现对I/O口的控制.单片机是软硬件结合的统一体,因此本章先介绍如何用简单的材料搭建起供学习使用的最小 ...
最新文章
- 高维、相依和不完全数据的统计分析(二)
- 剑指offer之11-15题解
- .NET手撸绘制TypeScript类图——上篇
- python脚本式编程_Python编程入门(一)
- 一起学《Troubleshooting Oracle Performance》吧
- linux查看系统版本_Win8系统查看directx版本的操作方法是什么?
- 2021年最新UI/UE设计学习路线图
- python基础教程电子版-Python基础教程(第2版)PDF文档下载
- 工具 - 怎么看微信h5的源码?
- vs2010等宽字体设置
- Mac下通过virtualbox安装windows系统
- 有关初始位置检测,死区补偿,弱磁,MTPA,Foc保护措施,参数辨别的一些文档,和参考代码。
- Noiseware 5 降噪滤镜
- Mac快速录制音频工具:Recordia
- 安装软件时显示无法定位程序输入点xxx于动态链接库KERNEL32.dll上
- [pwn]堆:fastbin attack详解
- Win10开了热点之后,电脑不能上网怎么解决?
- 域控服务器怎么开策略,组策略(域和域服务的搭建)
- DIY个人智能家庭网关—— 路由器篇之申请公网IP
- CabloyJS微信模块、企业微信模块已出齐
热门文章
- mysql复杂查询的书_mysql 复杂查询
- vue实现动态css,巧用 CSS 动画实现动态气泡背景__Vue.js__CSS__前端
- 智能窗帘传感器c语言程序,基于单片机的智能窗帘控制系统设计(附程序代码)
- 索尼笔记本E系列,关闭触摸板
- sspanel php,sspanelv3魔改版邮件设置指南及常用配置
- 计算机管理员权限粘贴文件,高手分析win10往c盘粘贴文件需要权限的详细解决对策...
- 基于HC-05蓝牙模块的STM32无线控制智能系统硬件开发
- 小白如何装重装操作系统(使用PE辅助)
- 自定义View-仿QQ运动步数进度效果(完整代码)
- 掌握这节JMeter性能测试:并发测试、压力测试,年薪30万不是梦