💬

7-gcc,make,git

gcc的编译流程:
预处理→编译→汇编→链接
gcc常用选项及其意义:
-I,-L,-l
readelf/objdump/strings/ldd等命令的辅助使用
ELF文件的头部信息的意义
静态库 动态共享库编译与使用
makefile中的信息与写法
autotools的使用
git多分支与本地仓库、远程仓库

第1讲gcc

1、gcc

  • C语言标准库文件:/usr/lib64/libc.so.6
    • [root@bioinfo ~]# rpm -qa | grep glibc glibc-headers-2.17-307.el7.1.x86_64 glibc-devel-2.17-307.el7.1.x86_64 glibc-common-2.17-307.el7.1.x86_64 glibc-2.17-307.el7.1.x86_64
      🍉rpm:软件包的管理工具
  • C++语言标准库文件:/usr/lib64/libstdc++.so.6
  • Linux下的目标文件格式为ELF(Executable and Linkable Format)有三种类型:
    • 可重定位(relocatable)目标文件:包含二进制代码与数据,可在编译时与其他可重定位目标文件合并,创建一个可执行目标文件。
    • 可执行目标文件:包含二进制代码与数据,可直接加载到内存中运行。
    • 共享目标文件:一种特殊的可重定位目标文件,可在加载时或者运行时被动态的加载进内存并链接,在linux中为.so文件。
  • 一个典型的ELF头以16字节的序列开始,序列描述了生成该文件系统
    • 字的大小(Word size)
    • 字节顺序(Byte-order)
    • ELF头的大小(ELF Header size)
    • 目标文件的类型(File Type)
    • 机器类型(Machine type)
    • 节头部表的文件偏移(offset)
    • 节头部表的大小和数量
  • 字节序:什么是LSB
    • LSB stands for least-significant byte first, or we can call it little-endian.
      假设存在第一个无符号十六进制数值0x1234,内存中至少需要两个字节来表示。内存中是以字节为单位进行编址的,假设也就是存放在地址a和地址a+1中。这时候就有两种存放的策略
    • 如果采用大尾方式存储,则a存储12a+1存储34;
    • 如果采用小尾策略存储,则a存储34a+1存储12
  • 夹在ELF头和节头部表之间的节:
    • .text:已编译的机器代码
    • .rodata:只读数据,如printf的格式化字符串
    • .data:已初始化的全局和静态变量。局部变量在运行的栈中,不出现在节中。
    • .bss:未初始化的全局和静态变量,以及所有被初始化为0的全局和静态变量。目标文件这个节不占据实际的空间,仅作为一个占位符。
    • .symtab:一个符号表,存放程序中定义和应用的函数以及全局变量的信息。
    • .rel.text:一个在.text节中位置的列表,当链接器把这个目标文件与其他目标文件结合时,需要修改这些位置。
    • .rel.data:被模块引用或者定义的所有全局变量的重定位信息。
    • .debug:一个调试符号表,其条目是程序中定义的局部变量和类型定义,程序中定义和应用的全局变量,以及初识的C源程序。只有使用g选项编译才会得到这张表。
    • .strtab:一个字符串表,其内容包含.symtab.debug中的符号表,以及节头部表中的节名称。
  • 什么是重定位
    • 重定位将每个符号引用和符号定义关联起来,并且为每个符号分配运行时地址。

(1)GCC编译过程

一般来说,gcc的编译过程分为下面几个主要步骤 :
cpp -> cc1 -> as -> ld
  1. 预处理(Preprocessing)
    1. gcc -E test.c -o test.i
  1. 编译(Compiling)
    1. gcc -S test.i -o test.s
  1. 汇编(Assembling)
    1. gcc -c test.s -o test.o
      怎么理解下面这个输出结果?
      $ file test.o test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
      • ELF文件
      • 64-bit
      • LSB:字节序
      • relocatable:可重定位目标文件,与可执行目标文件不同
  1. 链接(Linking)
    1. gcc test.o -o test
      链接需要完成的两个任务为:
      符号解析:将每个符号的引用 与每个符号的定义关联起来。重定位:编译和汇编生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向相应的内存位置。
      这里所说的符号对应汇编文件中的一个个标签(label),包含
      • 一个函数
      • 一个全局变量
      • 一个静态变量

(2)GCC常用编译选项

GCC常用编译选项
选项
含义
链接静态库,禁止使用动态共享库
进行动态库编译,链接动态库
在动态库的搜索路径中增加dir目录
链接静态库(libname.a)或动态库(libname.so)的库文件
生成使用相对地址无关的目标代码

注解

  1. PIC是位置无关代码(Position-independent code)的缩写;
  1. static的话,势必禁止使用动态共享库,但这往往是有副作用的,因为某些库往往都是动态共享库而没有静态库,这时候要注意。

(3)静态库与动态共享库

  • 静态库的文件名一般是libname.a
  • 动态共享库的文件名一般是libname.so
    • // test.c #include "test.h" void test() {   printf("hello, test!\n"); } // test.h #ifndef __TEST__ #define __TEST__ #include <stdio.h> void test(); #endif
  • 如何制作静态库:
    • $ gcc -c test.c $ ar rcs libtest.a test.o
  • 如何编译共享库:
    • $ gcc -fPIC -c test.c $ gcc -shared -o libtest.so test.o
      // hello.c #include <test.h> int main() {   test();   return 0; }
  • 如何利用动态共享库
    • $ gcc -o hello hello.c -L. -ltest -I. $ ./hello hello, test!
      可以看看这时候的文件大小:
      $ ls -lh -rwxrwxr-x 1 bio bio 8.3K 5月  17 23:01 hello
      但是,动态共享库的一个问题是一旦共享库丢失或者版本变换,可能会出现问题:
      $ mv libtest.o libtest.so.bak $ ./hello ./hello: error while loading shared libraries: libtest.so: cannot open shared object file: No such file or directory
  • 如何利用静态库
    • $ gcc -static -o hello hello.c -L. -ltest -I.
      如果出现下面的错误
      /bin/ld: cannot find -lc
      则说明缺少C的标准静态库,则需要安装:
      $ yum install glibc-static
      在动态共享库与静态库都存在的情况下,如果不指定-static的话,则优先使用动态共享库;如果要强制使用静态库,则需要指定-static;一旦只存在静态库或者动态共享库,则使用存在的一种。
  • 静态编译将静态库整合入可执行文件,因此,其文件也通常会更大一些
    • $ ls -lh hello -rwxrwxr-x 1 bio bio 845K 5月  17 23:24 hello
      但这也有个好处,即使库文件丢失也不影响可执行目标文件:
      $ mv libtest.a libtest.a.bak $ ./hello hello, test!
      但是,静态库也有两个非常大的缺点:
      每次程序运行时都会在内存中加载一个静态库的副本每次更新静态库都需要重新链接程序与静态库

2、 objdump的使用

objdumpbinutils包的重要组成部分,其目的是可以进行反汇编,将二进制可执行目标文件或者可重定位目标(.o)文件反汇编为汇编代码的工具。
常规上将可执行文件反汇编为汇编语言和对应的源代码部分。这里以一个二进制可执行文件反汇编为例。
这是源代码:
/* test.c */ #include <stdio.h> int main() { int x, y, z; x = 3; y = 5; z = x + y; printf("%d\n", z); return 0; }
进行编译:
gcc -g -o test test.c
查看符号信息:
readelf --sym test
得到
Symbol table '.dynsym' contains 4 entries:   Num:   Value         Size Type   Bind   Vis     Ndx Name     0: 0000000000000000     0 NOTYPE LOCAL DEFAULT UND     1: 0000000000000000     0 FUNC   GLOBAL DEFAULT UND printf@GLIBC_2.2.5 (2)     2: 0000000000000000     0 FUNC   GLOBAL DEFAULT UND __libc_start_main@GLIBC_2.2.5 (2)     3: 0000000000000000     0 NOTYPE WEAK   DEFAULT UND __gmon_start__ Symbol table '.symtab' contains 67 entries:   Num:   Value         Size Type   Bind   Vis     Ndx Name     0: 0000000000000000     0 NOTYPE LOCAL DEFAULT UND     ...   27: 0000000000000000     0 FILE   LOCAL DEFAULT ABS crtstuff.c   ...   36: 0000000000000000     0 FILE   LOCAL DEFAULT ABS test2.c   ...   48: 0000000000601030     0 NOTYPE WEAK   DEFAULT   24 data_start   49: 0000000000601038     0 NOTYPE GLOBAL DEFAULT   24 _edata   50: 00000000004005e4     0 FUNC   GLOBAL DEFAULT   14 _fini   51: 0000000000000000     0 FUNC   GLOBAL DEFAULT UND printf@@GLIBC_2.2.5   52: 0000000000000000     0 FUNC   GLOBAL DEFAULT UND __libc_start_main@@GLIBC_   53: 0000000000601030     0 NOTYPE GLOBAL DEFAULT   24 __data_start   54: 0000000000000000     0 NOTYPE WEAK   DEFAULT UND __gmon_start__   55: 00000000004005f8     0 OBJECT GLOBAL HIDDEN   15 __dso_handle   56: 00000000004005f0     4 OBJECT GLOBAL DEFAULT   15 _IO_stdin_used   57: 0000000000400570   101 FUNC   GLOBAL DEFAULT   13 __libc_csu_init   58: 0000000000601040     0 NOTYPE GLOBAL DEFAULT   25 _end   59: 0000000000400440     0 FUNC   GLOBAL DEFAULT   13 _start   60: 000000000060103c     4 OBJECT GLOBAL DEFAULT   25 a   61: 0000000000601038     0 NOTYPE GLOBAL DEFAULT   25 __bss_start   62: 0000000000400530   56 FUNC   GLOBAL DEFAULT   13 main   63: 0000000000000000     0 NOTYPE WEAK   DEFAULT UND _Jv_RegisterClasses   64: 0000000000601038     0 OBJECT GLOBAL HIDDEN   24 __TMC_END__   65: 0000000000000000     0 NOTYPE WEAK   DEFAULT UND _ITM_registerTMCloneTable   66: 00000000004003e0     0 FUNC   GLOBAL DEFAULT   11 _init
可以看到main的符号,其运行时在内存中的相对位移地址是400530,main的代码大小为56字节,类型为函数。
然后反汇编:
objdump -S test >testcode
查看反汇编的结果:
0000000000400530 <main>: #include <stdio.h> int main() {  400530:       55                     push   %rbp  400531:       48 89 e5               mov   %rsp,%rbp  400534:       48 83 ec 10             sub   $0x10,%rsp        int x, y, z;        x = 3;  400538:       c7 45 fc 03 00 00 00   movl   $0x3,-0x4(%rbp)        y = 5;  40053f:       c7 45 f8 05 00 00 00   movl   $0x5,-0x8(%rbp)        z = x + y;  400546:       8b 45 f8               mov   -0x8(%rbp),%eax  400549:       8b 55 fc               mov   -0x4(%rbp),%edx  40054c:       01 d0                   add   %edx,%eax  40054e:       89 45 f4               mov   %eax,-0xc(%rbp)       printf("%d\n", z);  400551:       8b 45 f4               mov   -0xc(%rbp),%eax  400554:       89 c6                   mov   %eax,%esi  400556:       bf 00 06 40 00         mov   $0x400600,%edi  40055b:       b8 00 00 00 00         mov   $0x0,%eax  400560:       e8 ab fe ff ff         callq 400410 <printf@plt>        return 0;  400565:       b8 00 00 00 00         mov   $0x0,%eax }

3、 GDB调试程序

gdb的调试步骤一般分为6步,实际调试过程当然并不是每一步都必须用到,需要在使用中灵活进行组合。

(1)GDB的调试步骤

  1. 启动gdb,加载要调试的可执行文件,这有两种常用的方式
    1. 用break为程序设置断点,设置的方式有很多,这里就介绍两种
      1. 用run启动并运行已经加载的程序
        1. 查看程序运行的当前状态
            • 程序的当前断点位置信息
              • 反映程序已经执行了哪些指令,下一步需要执行哪一条指令。其中,寄存器rip(return instruction pointer)保存了下一条将要执行的指令地址:
            • 当前的通用寄存器内容
              • 当前存储器中存储的数据信息
                • 当前的栈帧信息等
                  • x86-64用栈来支持过程的嵌套调用,过程的入口参数、返回地址、被保存寄存器的值、被调用过程中的非静态局部变量都会被保存到栈中。所谓栈帧,就是系统为每一个执行的过程分配的栈空间称为栈帧。

                  • 这里的rbp是栈底指针,rsp是栈顶指针
                  • 由栈底向栈顶生长,栈底是高地址,栈顶是低地址
            1. 继续执行下一条指令
              1. quit退出调试

              (1)简单实例

              这里以下面这个简单程序为例说明:
              #include <stdio.h> int main() {   int x, y, z;   x = 3;   y = 4;   z = x + y;   print("x + y = %d\n", z);   return 0; }
              我们编译的时候,加上-g的调试选项
              gcc -g -o test test.c
              然后进入到调试模式:
              gdb ./test
              这样我们可以设置断点在main 函数入口处:
              (gdb) break main Breakpoint 1 at 0x400538: file test.c, line 5.
              然后输入run,让程序停止在断点处:
              (gdb) run Starting program: /home/bio/bi296/chap6/code/test Breakpoint 1, main () at test.c:5 5               x = 3;
              查看寄存器的信息:
              (gdb) info registers rax           0x400530 4195632 // accumulator寄存器64位版本 rbx           0x0     0 // base寄存器的64位版本 rcx           0x400570 4195696 // counter寄存器的64位版本 rdx           0x7fffffffe418   140737488348184 // data寄存器的64位版本 rsi           0x7fffffffe408   140737488348168 // source index rdi           0x1     1 // destination index rbp           0x7fffffffe320   0x7fffffffe320   // bp寄存器的64位版本 rsp           0x7fffffffe310   0x7fffffffe310 // 堆栈指针sp寄存器的64位版本 r8             0x7ffff7dd5e80   140737351868032 r9             0x0     0 r10           0x7fffffffde20   140737488346656 r11           0x7ffff7a2f460   140737348039776 r12           0x400440 4195392 r13           0x7fffffffe400   140737488348160 r14           0x0     0 r15           0x0     0 rip           0x400538 0x400538 <main+8> // IP寄存器的64位版本,下一条指令地址 eflags         0x202   [ IF ] // 32位的标识寄存器 cs             0x33     51 // 代码段寄存器code ss             0x2b     43 // 堆栈段寄存器stack ds             0x0     0 // 数据段寄存器data es             0x0     0 // 附加段寄存器extra fs             0x0     0 // 标识段寄存器flag gs             0x0     0 // 全局段寄存器global
              可以看到的是一系列寄存器中的信息:
              # 常用的gdb命令及其意义 break/b 行号 在行号后设置断点 break/b 函数名 在函数名处设置断点 breaktrace/bt 查看各级函数调用及参数 continue/c 继续往下执行 info/i 查看当前栈局部变量的值 info break 查看断点信息 finish 执行到当前函数返回,然后停下来等待命令 list/l + 行号/函数名 输出C代码 next/n 执行下一句代码,然后停住 print/p 输出一个变量或表达式的值 quit/q 退出gdb run/r 从头开始执行程序 set var 修改变量的值 step/s 进入函数调用,到下一条语句或指令 x 查看内存地址中的内容

              查看内存地址内容的语法

              x/nfu addr x addr x
              • n - 内存单元的数量
              • f - 显示形式
                • x - 十六进制hexademical
                • d - 十进制decimal integer
                • u - 无符号十进制整数unsigned decimal integer
                • o八进制octal
                • t - 二进制
                • a - 无符号十六进制
                • c - 字符character
                • f - 浮点数float
                • s - 字符串string
              • u单元的单位
                • b - 字节bytes
                • h - 两个字节halfwords
                • w四个字节words
                • g - 八个字节giantwords

              GDB警告

              1. Missing separate debuginfos, use: debuginfo-install glibc-2.17-307.el7.1.x86_64
                1. 现在的centos7发行版内核中是不带debuginfo的,需要自行安装。你可以从http://debuginfo.centos.org/7/x86_64/中寻找到跟内核版本匹配的两个包:
                  • kernel-debuginfo-$(uname ).rpm
                  • kernel-debug-debuginfo-$(uname).rpm
                  安装以后就可以用:
                  debuginfo-install glibc-2.17-307.el7.1.x86_64

                7.4 实例代码调试过程

                #include <stdio.h> int main() {        float x, y, z;        x = 3.25;        y = 1.12;        z = x - y;        printf("x - y = %f\n", z);        return 0; }
                这是对应的objdump反汇编出来的代码:
                0000000000400530 <main>: #include <stdio.h> int main() {  400530:       55                     push   %rbp  400531:       48 89 e5               mov   %rsp,%rbp  400534:       48 83 ec 10             sub   $0x10,%rsp        float x, y, z;        x = 3.25;  400538:       8b 05 de 00 00 00       mov   0xde(%rip),%eax        # 40061c <__dso_handle+0x14>  40053e:       89 45 fc               mov   %eax,-0x4(%rbp)        y = 1.12;  400541:       8b 05 d9 00 00 00       mov   0xd9(%rip),%eax        # 400620 <__dso_handle+0x18>  400547:       89 45 f8               mov   %eax,-0x8(%rbp)        z = x - y;  40054a:       f3 0f 10 45 fc         movss -0x4(%rbp),%xmm0  40054f:       f3 0f 5c 45 f8         subss -0x8(%rbp),%xmm0  400554:       f3 0f 11 45 f4         movss %xmm0,-0xc(%rbp)       printf("x - y = %f\n", z);  400559:       f3 0f 10 45 f4         movss -0xc(%rbp),%xmm0  40055e:       0f 5a c0               cvtps2pd %xmm0,%xmm0  400561:       bf 10 06 40 00         mov   $0x400610,%edi  400566:       b8 01 00 00 00         mov   $0x1,%eax  40056b:       e8 a0 fe ff ff         callq 400410 <printf@plt>        return 0;  400570:       b8 00 00 00 00         mov   $0x0,%eax }
                然后执行gdb进行调试,可以看到
                (gdb) file test2 // 加载test2这个可执行文件 Reading symbols from /home/bio/bi296/chap6/code/test2...done. (gdb) break main // 在main函数处设置断点 Breakpoint 1 at 0x400538: file test2.c, line 5. (gdb) run // 执行到断点处 Starting program: /home/bio/bi296/chap6/code/test2 Breakpoint 1, main () at test2.c:5 5               x = 3.25; (gdb) x/1fw $rbp - 4 // 查看单精度浮点数的数值,一个word,用float形式 0x7fffffffe3bc: 3.25 (gdb) x/1xw $rbp - 4 // 查看单精度浮点数的IEEE-754表示,一个word,用十六 0x7fffffffe3bc: 0x40500000 (gdb) x/1tw $rbp - 4 // 查看二进制形式 0x7fffffffe3bc: 01000000010100000000000000000000 (gdb) s // 停在下一条指令后 7               z = x - y; (gdb) x/1fw $rbp - 8 // 查看y的浮点数值,$rbp - 8为其内存中的地址 0x7fffffffe3b8: 1.12

                第2讲 Makefile

                随着你的程序越来越复杂,项目可能被拆分为许多个源代码文件,甚至是多个模块。
                make就是一种明确源文件之间的依赖关系,并能将它们编译为可执行文件的一个程序工具。
                本讲我们将介绍如何用Makefile实现这一功能。

                初级Makefile

                下面我们将从C语言的入门例子出发,说明Makefile是如何工作的。
                // hello.c #include <stdio.h> int main() {   printf("Hello, World!\n");   return 0; }
                这是一个非常简单的Makefile:
                hello: hello.c gcc -o hello hello.c
                然后我们只要运行
                make
                启动编译过程:
                [bio@bioinfo lecture]$ make gcc -o hello hello.c [bio@bioinfo lecture]$ ll total 20 -rwxrwxr-x 1 bio bio 8511 5月  16 01:52 hello -rw-rw-r-- 1 bio bio   86 5月  16 01:51 hello.c -rw-rw-r-- 1 bio bio   37 5月  16 01:52 Makefile

                用内置变量简化Makefile

                我们可以利用make的一些内置变量来简化编译:
                hello: hello.c gcc -o $@ $<
                其中:

                头文件依赖关系的自动获取

                在Makefile中,可能有包含一系列的头文件,例如我们的main.c文件中就包含了defs.h这个头文件,这样,在Makefile中就会出现这种依赖关系:
                main.o: main.c defs.h
                这对于一个大型的工程来说要手动加上这些依赖关系是非常困难的。不过幸好大部分的C/C++编译器都支持自动寻找源文件中包含的头文件的功能,比如gcc的-M选项就可以实现这一过程。
                因此,大部分情况下我们不必考虑头文件的依赖问题。

                Makefile自定义变量

                在这个例子中:
                OBJS = file1.o file2.o CC = gcc CFLAGS = -Wall -O -g myprog: $(OBJS) $(CC) $(OBJS) -o myprog file1.o: file1.c file1.h file2.h $(CC) $(CFLAGS) -c file1.c -o file1.o file2.o: file2.c file2.h $(CC) $(CFLAGS) -c file2.c -o file2.o
                定义了三个变量:
                • OBJS = file1.o file2.o
                • CC = gcc
                • CFLAGS = -Wall -g -O2
                后面在引用变量的时候我们用的是$(varname),也可以用类似shell的语法${varname}这种方式。

                自定义变量的扩展赋值方法

                所谓扩展,也就是在定义的时候,引用了其他变量。自定义变量在进行赋值的时候,有几种常用的方式
                让我们以两个例子看看=与:=的区别:
                x = 5 y = $(x) x = 10 all: echo $(y)
                运行make后得到的结果为10。但是如果我们将第二行的=替换为:=,则输出结果为5。这完全符合我们前表中的介绍。

                常用函数用法

                让我们来看看下面这个Makefile的例子:
                SRC = $(wildcard *.c) OBJS = $(patsubst %.c, %.o, $(SRC)) all: echo $(OBJS)
                这里分别用了wildcardpatsubst两个函数:
                • 前者是用通配符在本文件夹内确定所有以.c作为扩展名的文件;
                • 后者是将SRC文件集中的.c文件替换为.o文件

                自动生成Makefile

                想要自动生成Makefile,需要安装autotools:
                yum install -y autoconf automake
                我们这里还是以最简单的hello world程序出发,尝试建立起Makefile。

                步骤1:运行autoscan生成configure.scan,修改为configure.ac

                • 执行autoscan,生成configure.scan文件:
                  • #                                               -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ([2.69])                         # autoconf的版本 AC_INIT([FULL-PACKAGE-NAME], [VERSION], [BUG-REPORT-ADDRESS]) # 包名,版本需要修改 AC_CONFIG_SRCDIR([hello.c])                                   # 源代码目录 AC_CONFIG_HEADERS([config.h]) # 检查头文件的宏 # Checks for programs. AC_PROG_CC # Checks for libraries. # Checks for header files. # Checks for typedefs, structures, and compiler characteristics. # Checks for library functions. AC_OUTPUT
                • 将configure.scan编辑修改为configure.ac文件
                #                                               -*- Autoconf -*- # Process this file with autoconf to produce a configure script. AC_PREREQ([2.69])                         # autoconf的版本 AC_INIT([amhello], [0.1], [ricket@sjtu.edu.cn]) # 包名,版本需要修改 AM_INIT_AUTOMAKE([-Wall -Werror foreign]) AC_CONFIG_SRCDIR([hello.c])                                   # 源代码目录 AC_CONFIG_HEADERS([config.h])  # 检查头文件的宏 AC_CONFIG_FILES([Makefile]) # Checks for programs. AC_PROG_CC # Checks for libraries. # Checks for header files. # Checks for typedefs, structures, and compiler characteristics. # Checks for library functions. AC_OUTPUT

                步骤2:运行aclocal和autoconf生成configure文件

                • 运行aclocal生成aclocal.m4文件
                • 运行autoconf生成configure文件

                步骤3:运行autoheader和automake生成Makefile.in文件

                • 编辑Makefile.am文件准备好
                  • bin_PROGRAMS = hello hello_SOURCES = hello.c
                • 运行autoheader生成config.h.in文件
                • 运行automake --add-missing生成Makefile.in文件

                步骤4:运行./configure生成Makefile文件

                步骤5: 编译和安装

                • 运行make完成编译
                • 运行sudo make install完成安装
                • 运行make distchek将项目完成打包

                第3讲 git

                介绍

                1. Git管理的文件分为:工作区,版本库。
                  1. 版本库又分为暂存区stage和暂存区分支master(仓库)。
                    一般来说,文件修改的流动方向是:工作区>>>>暂存区>>>>仓库。
                    • git add把文件从工作区>>>>暂存区;
                    • git commit把文件从暂存区>>>>仓库;
                    • git diff查看工作区和暂存区差异;
                    • git diff --cached查看暂存区和仓库差异;
                    • git diff HEAD 查看工作区和仓库的差异;
                    • git add的反向命令git checkout,撤销工作区修改,即把暂存区最新版本转移到工作区;
                    • git commit的反向命令git reset HEAD,就是把仓库最新版本转移到暂存区。
                1. 如果修改了工作区的文件并用git add提交到了暂存区
                    • 想要反悔:git reset HEAD <file>; git checkout -- <file>
                    • 想要提交到仓库:git commit -m "add a new file"
                1. 如果用rm删除了工作区的文件
                    • 又不想删除了:git checkout -- <file>
                    • 想要彻底删除:git rm <file>; git commit -m "remove <file>"
                    • 删除后又后悔了怎么办:git reset --hard <版本号>
                    • 如果只是想要回到前一个版本,那么git reset --hard HEAD^
                1. 怎么看版本号:
                    • git log可以看
                    • git log --pretty=oneline可以看得更加明晰
                1. 怎么建立远程库:
                    • 在github等远程主机上建立项目
                    • 在本机上git remote add <bi296> https://github.com/ricket1978/bi296.git
                    • 将本地的master推送到远程主机的master分支:git push -u bi296 master
                    • 将本地的所有分支推送到远程主机的对应分支:git push -u bi296 :
                1. 在本机创建分支:
                    • 创建分支:git branch <分支名>
                    • 切换分支:git switch <分支名>
                    • 创建分支并切换到分支:git switch -c <分支名>
                    • 将分支合并到master:git merge <分支名>
                    • 删除分支:git branch -d <分支名>
                    • 列举有的分支:git branch
                1. 实际开发中使用的分支策略:
                    • master分支是稳定的,一般仅用来发布新版本,平时不在上面进行开发
                    • 平时的开发工作都在dev分支上,这也意味着dev分支是不稳定的。当要发布新版本的是偶,将dev分支合并到master分支,在master上发布新版本;
                    • 平时多人协同都在dev分支上进行工作,每个人再建立自己的分支,定期往dev分支合并就可以了
                    • 合并分支时,加上-no-ff选项可以采用普通模式合并分支。这样合并后的历史有分支,能看出来进行过合并,而fast forward的合并方式就无法看出曾经进行过合并。
                1. 针对远程库的的信息
                    • 查看远程库信息,使用git remote -v
                    • 本地新建的分支如果不推送到远程,对其他人就是不可见的;
                    • 从本地推送分支,使用git push origin branch-name,如果推送失败,先用git pull抓取远程的新提交;
                    • 在本地创建和远程分支对应的分支,使用git checkout -b branch-name origin/branch-name,本地和远程分支的名称最好一致;
                    • 建立本地分支和远程分支的关联,使用git branch --set-upstream branch-name origin/branch-name
                    • 从远程抓取分支,使用git pull,如果有冲突,要先处理冲突。
                1. 分支合并思想

                实验过程

                1. 新建一个目录
                  1. mkdir testgit
                1. 初始化testgit
                  1. cd testgit git init git config --global user.name "your name" git config --global email.name "your email"
                1. 新建一个文件README.md
                  1. cat > README.md <<EOF # Test Git This is Git demo for teaching git. EOF
                1. 查看状态
                  1. git status
                1. 添加到暂存区
                  1. git add README.md git status
                1. 提交到版本仓库
                  1. git commit -m "add short message to README.md" git status
                1. 查看日志:
                  1. git log git log --pretty=oneline git reflog
                1. 查看当前的分支信息:
                  1. git branch
                1. 新建分支dev并切换到该分支:
                  1. git checkout -b dev
                1. 查看新的分支信息:
                  1. git branch
                1. 查看文件内容
                  1. cat README.md
                1. 往文件中添加内容
                  1. echo "bullshit stuff" >> README.md git add README.md git status
                1. 后悔了,不想被老板看到
                  1. git reset HEAD README.md git checkout -- README.md cat README.md
                1. 继续往里头添加内容
                  1. echo "Wonderful stuff" >> README.md git add README.md git commit -m "add wonderful stuff to README" git status
                1. 切换到master分支,将dev修改合并到master分支
                  1. git checkout master cat README.md git branch git merge dev cat README.md
                1. 切换回dev分支
                  1. git checkout dev git branch
                1. 新建两个分支tom、mary
                  1. git branch tom git branch mary
                1. 切换到tom分支,进行修改
                  1. git checkout tom git branch echo "I don't want to use git" >> README.md
                1. 退出到mary分支
                  1. git stash git checkout mary git branch echo "Now I like git, it's really helpful in saving my project." >> README.md git add README.md git commit -m "Mary commit"
                1. 退回到dev分支并合并
                  1. git checkout dev git branch git merge --no-ff mary git branch -d mary
                1. 重新回到tom分支,并用stash pop
                  1. git checkout tom git branch git stash list git stash pop
                1. 重新提交修改
                  1. git add README.md git commit -m "Tom commit"
                1. 退回到dev并试图合并
                  1. git checkout dev git merge tom
                    报告有冲突。返回到tom去解决冲突
                1. 返回到tom解决冲突
                  1. git checkout tom # 编辑README.md文件
                1. 进入dev,重新合并?
                  1. git merge tom git branch -d tom # git branch -D tom # 强制删除分支
                1. 其实远程合并也是相同的道理,只不过需要将dev分支推送到远程主机上
                1. 每个人在推送的时候可能都会出现冲突,这就需要将远程主机上的dev分支pull到本地,然后在本地解决了冲突之后再push到远程主机上。也就是说,我们看起来在远程主机上实现了合并,实际上还是在本地的合并,只是通过了远程主机作为媒介而已。