• 第0章Linux环境到内核基础知识


    1. #include<stdio.h>
    2. int main(void)
    3. {
    4. printf("hello world ");
    5. return 0;
    6. }
    gcc -g -wall helloworld.c -o hello_world 生成可执行文件,其过程 涉及预处理,编译,汇编,链接等多个步骤

    预处理:用于处理预处理命令,上面helloworld代码的预处理就是#include,该头文件所有源码将在第一行展开,可使用 gcc -E helloworld.c > helloworld.i ,生成预处理文件。理解了预处理,在出现一些常见的错误时,才能明白其中的原因。比如,为什么不能在头文件中定义全局变量?这是因为定义全局变量的代码会存在于所有以#include包含该头文件的文件中,也就是说所有的这些文件,都会定义一个同样的全局变量,这样就不可避免地造成了冲突

     编译环节指的是对源代码进行语法分析,并优化产生汇编代码(而不是二进制代码)
    gcc -S helloworld.c -o helloworld.s

    接下来汇编阶段,就是将汇编代码翻译成可执行的指令 gcc -c helloworld.c -o hellowrold.o

    链接阶段是生成可执行文件的最后一个步骤,其工作是将各个目标文件--包括库文件,链接生成一可执行文件。这个过程中,涉及的概念比较多,比如地址和空间分配,符号解析,重定位等在Linux环境下由GNU的连接器ld完成的
    gcc -g -Wall -v helloworld.c -o helloworld
    --------------------------------------------------程序的构成----------------------------------
    Linux下可执行文件的格式为elf格式,下面使用readelf查看helloworld格式
    1. ELF Header:
    2. Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
    3. Class: ELF64
    4. Data: 2's complement, little endian
    5. Version: 1 (current)
    6. OS/ABI: UNIX - System V
    7. ABI Version: 0
    8. Type: EXEC (Executable file)
    9. Machine: Advanced Micro Devices X86-64
    10. Version: 0x1
    11. Entry point address: 0x4003c0
    12. Start of program headers: 64 (bytes into file)
    13. Start of section headers: 2560 (bytes into file)
    14. Flags: 0x0
    15. Size of this header: 64 (bytes)
    16. Size of program headers: 56 (bytes)
    17. Number of program headers: 8
    18. Size of section headers: 64 (bytes)
    19. Number of section headers: 29
    20. Section header string table index: 26
    21. Section Headers:
    22. [Nr] Name Type Address Offset
    23. Size EntSize Flags Link Info Align
    24. [ 0] NULL 0000000000000000 00000000
    25. 0000000000000000 0000000000000000 0 0 0
    26. [ 1] .interp PROGBITS 0000000000400200 00000200
    27. 000000000000001c 0000000000000000 A 0 0 1
    28. [ 2] .note.ABI-tag NOTE 000000000040021c 0000021c
    29. 0000000000000020 0000000000000000 A 0 0 4
    30. [ 3] .hash HASH 0000000000400240 00000240
    31. 0000000000000024 0000000000000004 A 4 0 8
    32. [ 4] .dynsym DYNSYM 0000000000400268 00000268
    33. 0000000000000060 0000000000000018 A 5 1 8
    34. [ 5] .dynstr STRTAB 00000000004002c8 000002c8
    35. 000000000000003d 0000000000000000 A 0 0 1
    36. [ 6] .gnu.version VERSYM 0000000000400306 00000306
    37. 0000000000000008 0000000000000002 A 4 0 2
    38. [ 7] .gnu.version_r VERNEED 0000000000400310 00000310
    39. 0000000000000020 0000000000000000 A 5 1 8
    40. [ 8] .rela.dyn RELA 0000000000400330 00000330
    41. 0000000000000018 0000000000000018 A 4 0 8
    42. [ 9] .rela.plt RELA 0000000000400348 00000348
    43. 0000000000000030 0000000000000018 A 4 11 8
    44. [10] .init PROGBITS 0000000000400378 00000378
    45. 0000000000000018 0000000000000000 AX 0 0 4
    46. [11] .plt PROGBITS 0000000000400390 00000390
    47. 0000000000000030 0000000000000010 AX 0 0 4
    48. [12] .text PROGBITS 00000000004003c0 000003c0
    49. 0000000000000258 0000000000000000 AX 0 0 16
    50. [13] .fini PROGBITS 0000000000400618 00000618
    51. 000000000000000e 0000000000000000 AX 0 0 4
    52. [14] .rodata PROGBITS 0000000000400628 00000628
    53. 0000000000000010 0000000000000000 A 0 0 4
    54. [15] .eh_frame_hdr PROGBITS 0000000000400638 00000638
    55. 0000000000000024 0000000000000000 A 0 0 4
    56. [16] .eh_frame PROGBITS 0000000000400660 00000660
    57. 000000000000007c 0000000000000000 A 0 0 8
    58. [17] .ctors PROGBITS 00000000006006e0 000006e0
    59. 0000000000000010 0000000000000000 WA 0 0 8
    60. [18] .dtors PROGBITS 00000000006006f0 000006f0
    61. 0000000000000010 0000000000000000 WA 0 0 8
    62. [19] .jcr PROGBITS 0000000000600700 00000700
    63. 0000000000000008 0000000000000000 WA 0 0 8
    64. [20] .dynamic DYNAMIC 0000000000600708 00000708
    65. 0000000000000190 0000000000000010 WA 5 0 8
    66. [21] .got PROGBITS 0000000000600898 00000898
    67. 0000000000000008 0000000000000008 WA 0 0 8
    68. [22] .got.plt PROGBITS 00000000006008a0 000008a0
    69. 0000000000000028 0000000000000008 WA 0 0 8
    70. [23] .data PROGBITS 00000000006008c8 000008c8
    71. 0000000000000010 0000000000000000 WA 0 0 8
    72. [24] .bss NOBITS 00000000006008d8 000008d8
    73. 0000000000000010 0000000000000000 WA 0 0 8
    74. [25] .comment PROGBITS 0000000000000000 000008d8
    75. 000000000000003e 0000000000000001 MS 0 0 1
    76. [26] .shstrtab STRTAB 0000000000000000 00000916
    77. 00000000000000e7 0000000000000000 0 0 1
    78. [27] .symtab SYMTAB 0000000000000000 00001140
    79. 0000000000000660 0000000000000018 28 47 8
    80. [28] .strtab STRTAB 0000000000000000 000017a0
    81. 000000000000025b 0000000000000000 0 0 1
    由于输出过多,后面的结果并没有完全展示出来。ELF文件的主要内容就是由各个section及symbol表组成的。在上面的section列表中,大家最熟悉的应该是text段、data段和bss段。text段为代码段,用于保存可执行指令。data段为数据段,用于保存有非0初始值的全局变量和静态变量。bss段用于保存没有初始值或初值为0的全局变量和静态变量,当程序加载时,bss段中的变量会被初始化为0。这个段并不占用物理空间——因为完全没有必要,这些变量的值固定初始化为0,因此何必占用宝贵的物理空间?
    其他段没有这三个段有名,下面来介绍一下其中一些比较常见的段:
    ·debug段:顾名思义,用于保存调试信息。
    ·dynamic段:用于保存动态链接信息。
    ·fini段:用于保存进程退出时的执行程序。当进程结束时,系统会自动执行这部分代码。
    ·init段:用于保存进程启动时的执行程序。当进程启动时,系统会自动执行这部分代码。
    ·rodata段:用于保存只读数据,如const修饰的全局变量、字符串常量。
    ·symtab段:用于保存符号表。

    -------------------------------------程序是如何跑起来的--------------------
    在Linux环境下,可以使用strace跟踪系统调用,此处以helloworld为例
    1. execve("./hello", ["./hello"], [/* 41 vars */]) = 0
    2. brk(0) = 0x151b000
    3. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f30733ef000

    4. access("/etc/ld.so.preload", R_OK) = -1 ENOENT (No such file or directory)
    5. open("/etc/ld.so.cache", O_RDONLY) = 3
    6. fstat(3, {st_mode=S_IFREG|0644, st_size=62458, ...}) = 0
    7. mmap(NULL, 62458, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7f30733df000
    8. close(3)= 0
    9. open("/lib64/libc.so.6", O_RDONLY) = 3 //加载c语言库
    10. read(3, "177ELF21133>1000356!2478"..., 832) = 832
    11. fstat(3, {st_mode=S_IFREG|0755, st_size=1928936, ...}) = 0
    12. mmap(0x38a7200000, 3750184, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x38a7200000
    13. mprotect(0x38a738a000, 2097152, PROT_NONE) = 0
    14. mmap(0x38a758a000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x18a000) = 0x38a758a000
    15. mmap(0x38a7590000, 14632, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x38a7590000
    16. close(3) = 0
    17. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f30733de000
    18. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f30733dd000
    19. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f30733dc000
    20. arch_prctl(ARCH_SET_FS, 0x7f30733dd700) = 0
    21. mprotect(0x38a758a000, 16384, PROT_READ) = 0
    22. mprotect(0x38a701f000, 4096, PROT_READ) = 0
    23. munmap(0x7f30733df000, 62458) = 0
    24. fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
    25. mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f30733ee000
    26. write(1, "hello world ", 12hello world
    27. ) = 12
    28. exit_group(0) = ?
    29. +++ exited with 0 +++
    下面就针对strace输出说明其含义。在Linux环境中,执行一个命令时,首先是由shell调用fork,然后在子进程中来真正执行这个命令(这一过程在strace输出中无法体现)。strace是hello_world开始执行后的输出。首先是调用execve来加载hello_world,然后ld会分别检查ld.so.nohwcap和ld.so.preload。其中,如果ld.so.nohwcap存在,则ld会加载其中未优化版本的库。如果ld.so.preload存在,则ld会加载其中的库——在一些项目中,我们需要拦截或替换系统调用或C库,此时就会利用这个机制,使用LD_PRELOAD来实现。之后利用mmap将ld.so.cache映射到内存中,ld.so.cache中保存了库的路径,这样就完成了所有的准备工作。接着ld加载c库——libc.so.6,利用mmap及mprotect设置程序的各个内存区域,到这里,程序运行的环境已经完成。后面的write会向文件描述符1(即标准输出)输出"Hello world! ",返回值为13,它表示write成功的字符个数。最后调用exit_group退出程序,此时参数为0,表示程序退出的状态——此例中hello-world程序返回0。

    --------------------------------------系统调用----------------------
    系统调用是操作系统提供的服务,是应用程序与内核通信的接口,在早期Linux系统中,使用int 0x80陷入内核,相对于普通的函数调用来说,系统调用的性能消耗巨大。另外用户控件的程序默认是通过栈来传递参数,对于系统调用来说,内核态跟用户态使用的是不同的栈,因此,系统调用的参数只能通过寄存器的方式进行传递
    ------------------------------------C库函数---------------------------
    Linux下,一般使用的C库是glibc,它封装了几乎所有的系统调用,下面以具体的系统调用open来看看glibc库是如何封装系统调用的。open在glibc中对应的实现函数是__open_nocancel
    1. int __open_nocancel(const char *file,int oflag,...)
    2. { int mode=0;
    3. if(oflagO_CREAT) {
    4. va_list arg;
    5. va_start(arg,oflag);
    6. mode=va_arg(arg,int);
    7. va_end(arg);
    8. }
    9. //系统调用编号
    10. return INLINE_SYSCALL(openat,4,AT_FDCWD,file,oflag,mode);
    11. }
    其中INLINE_SYSCALL是我们关心的内容,这个宏完成了对真正系统调用的封装:INLINE_SYSCALL->INTERNAL_SYSCALL。实现INTERNAL_SYSCALL的一个实例为
    1. # define INTERNAL_SYSCALL(name, err, nr, args...)
    2. ({
    3. register unsigned int resultvar;  
    4. EXTRAVAR_##nr
    5. asm volatile (  
    6. LOADARGS_##nr
    7. "movl %1, %%eax "
    8. "int $0x80 "  
    9. RESTOREARGS_##nr
    10. : "=a" (resultvar)
    11. : "i" (__NR_##name) ASMFMT_##nr(args) : "memory", "cc");
    12. (int) resultvar; })

     「其中,关键的代码是用嵌入式汇编写的,在此只做简单说明。“move%1,%%eax”表示将第一个参数(即__NR_##name)赋给寄存器eax。__NR_##name为对应的系统调用号,对于本例中的open来说,其为__NR_openat。系统调用号在文件/usr/include/asm/unitstd_32(64).h中定义,「也就是说,在Linux平台下,系统调用的约定是使用寄存器eax来传递系统调用号的。至于参数的传递,在glibc中也有详细的说明,参见文件sysdeps/unix/sysv/linux/i386/sysdep.h。」

    ----------------------------------------可重入函数-------------------
    「从字面上理解,可重入就是可重复进入。在编程领域,它不仅仅意味着可以重复进入,还要求在进入后能成功执行。这里的重复进入,是指当前进程已经处于该函数中,这时程序会允许当前进程的某个执行流程再次进入该函数,而不会引发问题。这里的执行流程不仅仅包括多线程,还包括信号处理、longjump等执行流程。所以,可重入函数一定是线程安全的,而线程安全函数则不一定是可重入函数。
    从以上定义来看,很难说出哪些函数是可重入函数,但是可以很明显看出哪些函数是不可以重入的函数。当函数使用锁的时候,尤其是互斥锁的时候,该函数是不可重入的,否则会造成死锁。若函数使用了静态变量,并且其工作依赖于这个静态变量时,该函数也是不 可重入









  • 相关阅读:
    项目部署
    nginx
    IDEA中Lombok插件的安装与使用
    Git常用命令总结
    CentOS 7 NAT模式上网配置
    一名3年工作经验的java程序员应该具备的技能
    maven 项目加载本地JAR
    linux压缩(解压缩)命令详解
    jdk7与jdk8环境共存与切换
    linux服务器卸载本机默认安装的jdk
  • 原文地址:https://www.cnblogs.com/zengyiwen/p/5755185.html
Copyright © 2020-2023  润新知