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?
- 如果采用大尾方式存储,则
a
存储12
,a+1
存储34
; - 如果采用小尾策略存储,则
a
存储34
,a+1
存储12
。
LSB stands for least-significant byte first, or we can call it little-endian.
假设存在第一个无符号十六进制数值0x1234,内存中至少需要两个字节来表示。内存中是以字节为单位进行编址的,假设也就是存放在地址
a
和地址a+1
中。这时候就有两种存放的策略- 夹在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
- 预处理(Preprocessing)
gcc -E test.c -o test.i
- 编译(Compiling)
gcc -S test.i -o test.s
- 汇编(Assembling)
- ELF文件
- 64-bit
- LSB:字节序
- relocatable:可重定位目标文件,与可执行目标文件不同
gcc -c test.s -o test.o
怎么理解下面这个输出结果?
$ file test.o test.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
- 链接(Linking)
- 一个函数
- 一个全局变量
- 一个静态变量
gcc test.o -o test
链接需要完成的两个任务为:
符号解析:将每个符号的引用 与每个符号的定义关联起来。重定位:编译和汇编生成从地址0开始的代码和数据节。链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向相应的内存位置。
这里所说的符号对应汇编文件中的一个个标签(label),包含
(2)GCC常用编译选项
GCC常用编译选项
选项
含义
链接静态库,禁止使用动态共享库
进行动态库编译,链接动态库
在动态库的搜索路径中增加dir目录
链接静态库(libname.a)或动态库(libname.so)的库文件
生成使用相对地址无关的目标代码
注解
- PIC是位置无关代码(Position-independent code)的缩写;
- 用
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
的使用
objdump
是binutils
包的重要组成部分,其目的是可以进行反汇编,将二进制可执行目标文件或者可重定位目标(.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的调试步骤
- 启动gdb,加载要调试的可执行文件,这有两种常用的方式
- 用break为程序设置断点,设置的方式有很多,这里就介绍两种
- 用run启动并运行已经加载的程序
- 查看程序运行的当前状态
- 继续执行下一条指令
- 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
- 十六进制hexademicald
- 十进制decimal integeru
- 无符号十进制整数unsigned decimal integero
八进制octalt
- 二进制a
- 无符号十六进制c
- 字符characterf
- 浮点数floats
- 字符串string
u
单元的单位b
- 字节bytesh
- 两个字节halfwordsw
四个字节wordsg
- 八个字节giantwords
GDB警告
- Missing separate debuginfos, use: debuginfo-install glibc-2.17-307.el7.1.x86_64
- kernel-debuginfo-$(uname ).rpm
- kernel-debug-debuginfo-$(uname).rpm
现在的centos7发行版内核中是不带debuginfo的,需要自行安装。你可以从http://debuginfo.centos.org/7/x86_64/中寻找到跟内核版本匹配的两个包:
安装以后就可以用:
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)
这里分别用了
wildcard
和patsubst
两个函数:- 前者是用通配符在本文件夹内确定所有以
.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
介绍
- Git管理的文件分为:工作区,版本库。
- git add把文件从工作区>>>>暂存区;
- git commit把文件从暂存区>>>>仓库;
- git diff查看工作区和暂存区差异;
- git diff --cached查看暂存区和仓库差异;
- git diff HEAD 查看工作区和仓库的差异;
- git add的反向命令git checkout,撤销工作区修改,即把暂存区最新版本转移到工作区;
- git commit的反向命令git reset HEAD,就是把仓库最新版本转移到暂存区。
版本库又分为暂存区stage和暂存区分支master(仓库)。
一般来说,文件修改的流动方向是:工作区>>>>暂存区>>>>仓库。
- 如果修改了工作区的文件并用git add提交到了暂存区
- 想要反悔:
git reset HEAD <file>; git checkout -- <file>
- 想要提交到仓库:
git commit -m "add a new file"
- 如果用rm删除了工作区的文件
- 又不想删除了:
git checkout -- <file>
- 想要彻底删除:
git rm <file>; git commit -m "remove <file>"
- 删除后又后悔了怎么办:
git reset --hard <版本号>
- 如果只是想要回到前一个版本,那么
git reset --hard HEAD^
- 怎么看版本号:
git log
可以看git log --pretty=oneline
可以看得更加明晰
- 怎么建立远程库:
- 在github等远程主机上建立项目
- 在本机上
git remote add <bi296> https://github.com/ricket1978/bi296.git
- 将本地的master推送到远程主机的master分支:
git push -u bi296 master
- 将本地的所有分支推送到远程主机的对应分支:
git push -u bi296 :
- 在本机创建分支:
- 创建分支:
git branch <分支名>
- 切换分支:
git switch <分支名>
- 创建分支并切换到分支:
git switch -c <分支名>
- 将分支合并到master:
git merge <分支名>
- 删除分支:
git branch -d <分支名>
- 列举有的分支:
git branch
- 实际开发中使用的分支策略:
- master分支是稳定的,一般仅用来发布新版本,平时不在上面进行开发
- 平时的开发工作都在dev分支上,这也意味着dev分支是不稳定的。当要发布新版本的是偶,将dev分支合并到master分支,在master上发布新版本;
- 平时多人协同都在dev分支上进行工作,每个人再建立自己的分支,定期往dev分支合并就可以了
- 合并分支时,加上
-no-ff
选项可以采用普通模式合并分支。这样合并后的历史有分支,能看出来进行过合并,而fast forward
的合并方式就无法看出曾经进行过合并。
- 针对远程库的的信息
- 查看远程库信息,使用
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
,如果有冲突,要先处理冲突。
- 分支合并思想
实验过程
- 新建一个目录
mkdir testgit
- 初始化testgit
cd testgit git init git config --global user.name "your name" git config --global email.name "your email"
- 新建一个文件README.md
cat > README.md <<EOF # Test Git This is Git demo for teaching git. EOF
- 查看状态
git status
- 添加到暂存区
git add README.md git status
- 提交到版本仓库
git commit -m "add short message to README.md" git status
- 查看日志:
git log git log --pretty=oneline git reflog
- 查看当前的分支信息:
git branch
- 新建分支dev并切换到该分支:
git checkout -b dev
- 查看新的分支信息:
git branch
- 查看文件内容
cat README.md
- 往文件中添加内容
echo "bullshit stuff" >> README.md git add README.md git status
- 后悔了,不想被老板看到
git reset HEAD README.md git checkout -- README.md cat README.md
- 继续往里头添加内容
echo "Wonderful stuff" >> README.md git add README.md git commit -m "add wonderful stuff to README" git status
- 切换到master分支,将dev修改合并到master分支
git checkout master cat README.md git branch git merge dev cat README.md
- 切换回dev分支
git checkout dev git branch
- 新建两个分支tom、mary
git branch tom git branch mary
- 切换到tom分支,进行修改
git checkout tom git branch echo "I don't want to use git" >> README.md
- 退出到mary分支
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"
- 退回到dev分支并合并
git checkout dev git branch git merge --no-ff mary git branch -d mary
- 重新回到tom分支,并用stash pop
git checkout tom git branch git stash list git stash pop
- 重新提交修改
git add README.md git commit -m "Tom commit"
- 退回到dev并试图合并
git checkout dev git merge tom
报告有冲突。返回到tom去解决冲突
- 返回到tom解决冲突
git checkout tom # 编辑README.md文件
- 进入dev,重新合并?
git merge tom git branch -d tom # git branch -D tom # 强制删除分支
- 其实远程合并也是相同的道理,只不过需要将dev分支推送到远程主机上
- 每个人在推送的时候可能都会出现冲突,这就需要将远程主机上的dev分支pull到本地,然后在本地解决了冲突之后再push到远程主机上。也就是说,我们看起来在远程主机上实现了合并,实际上还是在本地的合并,只是通过了远程主机作为媒介而已。