愚人的linux内核2440移植札记(超曲折版)
http://blog.csdn.net/dreambegin/article/details/6904822
原来文章叫——编译内核之初体验。后来想了想,这篇文章让我体验了好多遍。不该叫这么大气的名字,还是改了吧。中间记录了很多在内核移植中可能遇到的问题。
还是把名字改为:愚人的内核移植札记(超曲折版)
千呼万唤始出来,让我们开始移植内核吧!
环境:Ubuntu + EABI-4.3.3(就是前边配置好的环境)
内核版本:linux-2.6.34.10 /*这个版本有什么特殊意义?木有!刚好电脑上有它就拿来用了,还不知道里边有没有bug。*/
何为内核移植?
Linux内核移植就是对linux内核源码进行修改,使其可以移植到对应的平台上,并可以正常运行。实际上就是做一些必要的修改,并编译出一个能在开发板上跑起来的.bin的二进制文件。
开始:
1、 将源码放置到虚拟机中,你可以选择FTP上传、共享文件目录、通过VMwaretools的直接复制粘贴、甚至其他任何你想使用的方式(这些方法都在前边文章中有所介绍)
2、 解压源码,得到源码树(注意,这里的源码树千万不要和前边将linux内核编译模块时的源码树搞混了,你可以认为这个源码树就是一个开源裸板的应用程序的源代码目录)
/*我的工作目录依然是 “/home/jun/arm” */
$ tar –xjvf linux-2.6.34.10.tar.bz2 //因为在用户目录下,所以不用sudo
3、 在得到源码目录后,就可以对源码进行相应的修改,使之满足运行条件。
修改ing :
1、 第一个要修改的是Makefile。
源码根目录下的Makefile是整个内核工程的总Makefile文件,向下联系着每个子目录中的Makefile。子目录中的Makefile又控制着所在目录的编译,并联系着它的下级目录,就这样循环往复。
Makefile文件控制着linux下工程开发中,整个项目的编译链接顺序和选项以及规则。也是一个自动编译工具,使得部分文件改动时只用对改动文件的进行编译,而不用再次编译连接整个工程。
默认情况下,源码根目录下的Makefile中定义着编译成的目标平台为x86,编译器选项为Gcc;而我们的需求是ARM平台和arm-linux-gcc编译器。知道了我们想要什么,就知道该改些什么了。
/*进入源码目录:*/
$ cd linux-2.6.34.10/
$ vi Makefile
在vi的底行名利模式中输入:set number 可以显示出行号来,然后,我们找到第189、190行:
ARCH ?= $(SUBARCH)
CROSS_COMPILE ?=
我们把它改为:
ARCH ?=arm
CROSS_COMPILE ?=arm-linux-
1、 第二个要修改的是.config文件
源码根目录下的.config是整个内核工程的总配置单,向下联系着每个子目录的Kconfig。子目录中的Kconfig又联系着它的下级目录,就这样循环往复。
刚解压出来的源码目录并没有.config文件。我们通过下面命令从内核已经包含的一些平台的配置文件中copy来一份。
$ cparch/arm/configs/s3c2410_defconfig .config
$ makemenuconfig /*我们来预览一下拷贝过来的配置单*/
Oops!报错了,哈哈,我喜欢,报错预示着可以学到新的知识。报错内容:
make: ***/home/jun/arm/linux-2.6.34.10/arch/arm: Is a directory. Stop.
哦,疏忽,这个错误比较常见,就是在上边配置Makefile时:
ARCH ?= $(SUBARCH)
这一行后边不小心加了有了空格了,改过来,try again。(在vi的底行模式:num 189,这样可以直接跳至189行)
Ok,预览成功。在这样的menu菜单里,就可以比较方便的配置内核了。(默认情况下Ubuntu下是不可以直接menuconfig的,不过我们在前面构建内核源码树的文章中已经安装了相应的支持包,这里主要依赖的是ncurses包,参照Ubuntu+下构建内核源码树_图文教程刚开始安装的几个工具。)
因为我们用的是EABI编译的,所以要在内核中加上EABI支持:
在上述的配置菜单中配置:
KernelFeatures --->
Memory split (3G/1G user/kernel split) ---> | |
|| Preemption Model (No ForcedPreemption (Server)) ---> | |
|| [*] Use the ARM EABI to compile thekernel | |
|| [*] Allow old ABI binaries to run with this kernel (EXPERIMENTAL) (| |
|| [ ] High Memory Support(EXPERIMENTAL) | |
|| Memory model (Flat Memory) ---> | |
|| [ ] Enable KSM for page merging | |
(4096) Low address space to protect from userallocation | |
[ ] Use kernelmem{cpy,set}() for {copy_to,clear}_user() (EXPERIMENTA|
1、 第三要改的是晶振。
默认的晶振是16934400,而我的板子TQ2440的晶振是12000000,据我所知当前主流的2440板子的外部时钟晶振都是12000000吧,TQ2440、mini2440、精智2440的板子都是。
要改的在文件arch/arm/mach-s3c2440/mach-smdk2440.c 中的第163行。
OK,不出意外的话,现在的内核已经可以跑起来了,我们make一下,下载到板子里边试试:
$make zImage -j4
一番等待之后,我的编译通过了,在arch/arm/boot/ 下会生成适用于arm平台的压缩格式内核zImage。
我通过cp命令把它拷贝到虚拟机和我本机的共享目录中,这样,它已经在我的windows下了,大小1.92M,多么惊人的尺寸,这就见证了linux强悍的可移植性。
此时已经可以在开发板里跑起来了,但是能跑并不代表会跑,这样的结果是会摔跟头。那么我们就边摔边跑吧,不经历风雨,怎么见彩虹?!
4、第四要改的是机器ID(板子的ID)。
将上边编译出来的zImage用bootloader(TQ2440自带的出场Uboot),通过dnw烧到nand flash里,然后boot the system:(让我们看看它到底摔了什么跟头?)
出现:
这种问题通常是系统不认识uboot传过来的机器ID,要修改内核中的机器ID与uboot的一致,因为开启的过程是,先加载bootloader-----》bootloader准备好硬件信息(以及OS的运行环境)-----》OS检测传递过来的硬件信息(cpu信息、开发板信息等)自己是否支持……支持就继续下一步工作,不支持就直接停掉,不浪费时间了。而TQ2440出场的uboot非常蛋疼的设置为了168,所以我们进到内核源码,把对应文件修改一下,在内核源码的“arch/arm/tools/mach-types”文件中,大约379行,原来是362,我们把它改的和uboot中默认的一致就OK了:
然后重新编译,并下载到开发板,结果:
这证明系统已经可以跑了,只是还没有做好分区与文件系统。
5、第五要改的是Nand分区。(原因是不同的镜像,如内核镜像、文件系统镜像…要烧写到不同的分区中,因为虽然我们不知道分区内部到底是什么,但是我们知道分区的起始地址和大小,这样只要按照约定把相应的镜像放到相应的分区(对应的地址),这样我们就可以正常启动系统)。
既然是分区的事,那下一步就来修改分区。这个分区一般称作MTD分区(MemoryTechnology Device),是linux抽象出来的存储设备层。它提供了底层存储设备的抽象,封装了底层硬件的读、写、擦除等操作。直入主题,我们要改的就是内核源码“arch/arm/plat-s3c24xx/commonsmdk.c”文件的smdk_default_nand_part[]结构体,这里,因为本人用的是TQ2440出场Uboot(当然这是linux移植开篇,没有用自己移植的Uboot,不管用哪个,Uboot和linux都是要配合好才可以启动的),而TQ2440出厂Uboot的默认启动参数是mtdblock1,也就是第二个分区,它的nand
flash分区设计是,第一个分区存放Nand下的Uboot镜像,第二个是kernel镜像,第三个是文件系统。而分区的约定又是和Uboot的下载命令中设置的默认下载地址是对应的,所以,这里我们还是得按着这个约定来。
首先先改smdk_nand_info结构体(大约在commonsmdk.c 的141行):
修改后如图:(原来)(后边我做测试的时候把名字都改为了“jun_uboot”,"jun_kernel"和"jun_yaffs2")
下边是我们要定义的smdk_default_nand_part[]结构体:
static structmtd_partition smdk_default_nand_part[] = {
[0] = {
.name = "nand_uboot",
.offset = 0x00000000,
.size = 0x00040000,
},
[1] = {
.name = "nand_kernel",
.offset = 0x00200000,
.size = 0x00200000,
},
[2] = {
.name = "nand_yaffs2",
.offset = 0x00400000,
.size = 0x0FB80000,
}
};
因为这里,nandflash是256M的,给Uboot分了0x00040000(256k),kernel分了0x00200000(2M),剩余的0x0FB80000(251M)给了yaffs分区。
编辑好以后,保存,在编译运行一下,看看效果。。。。
在源码目录下再make一下得到zImage,再次测试,结果为:
看吧,虽然分区调整成三个了,他还是不认识我们的分区依然是mtdblock0( driver? ),
虽然识别了分区,但是识别不出来分区的名字。为什么呢?(因为我们还没有添加文件系统的支持) 不过不可否认的是分区已经被我们调整成3个,且kernel已经知道了现在是三个,以及他们的大小。所以全宇宙我无法阻止我们继续下去。
主要要看内核此时崩溃的原因(kernel是通过panic反馈给我们的):
No filesystem could mount root, tried: ext3 ext2 cramfs vfat msdos iso9660 romfs
Kernel panic - not syncing: VFS: Unable tomount root fs on unknown-block(31,2)。。。
这个panic的原因一目了然,因为我们还没有添加文件系统的支持。兵来将挡,水来土掩。我们这就添加内核对yaffs2文件系统的支持。
这里,发现好多资料文档里给的下载源码地址http://www.aleph1.co.uk/cgi-bin/viewcvs.cgi/这个网址已经失效了,下载yaffs源码,找到了http://www.yaffs.net官网。看了一番说明……额时代变了,yaffs和yaffs2早已分开维护了,小看了一下,应该是这样下载的:http://www.aleph1.co.uk/gitweb?p=yaffs2.git;a=tree
,他这里是用git作的版本控制,打开网页直接点击“snapshot”会下载到一个源码快照,当然,这里下到的是yaffs2。你手头有原来的老版本的包的话,也可以用老的。这里得到的是“yaffs2-HEAD-d43e901.tar.gz”,和老版本(还是用的cvs版本控制时的)源码包比较,少了yaffs目录。我们把它解压到工程目录中(你做开发的目录,不用放到kernel目录里)。然后执行下边一系列命令,将yaffs2补丁打到我们的内核中:
$ tar -xzvf yaffs2-HEAD-d43e901.tar.gz # 解压到当前目录。
$ cd yaffs2-HEAD-d43e901/ # 进入该目录
$ ./patch-ker.sh c s ../linux-2.6.34.10/ # 这里是打补丁的命令,要加上c和s选项
打补丁和打后效果如图,证明Makefile和内核配置单都已经修改过了。
然后我们makemenuconfig 在配置中加入yaffs选项,给内核添加yaffs文件系统支持。
在配置单的这里添加:
Filesystems --->
[*]Miscellaneous filesystems --->
<*> yaffs2 file system support(new)
多人性化,他会有个(new),告诉你这就是你才添加的。
另外:
在256MB 以及更大容量的Nand Flash 需要选择上硬件ECC 校验的选项,否则会出现yaffs2 文件系统不能挂载而导致系统起不来的情况。我们照办就是了:
Device Drivers --->
<*> MemoryTechnology Device (MTD) support --->
<*> NAND Device Support --->
<*> NAND Flash support for S3C2410/S3C2440 SoC
[*] S3C2410 NAND Hardware ECC
好的,我们再次make 一下内核进行测试。。。。。$ make zImage -j5 ……..
编译报错了:
fs/yaffs2/yaffs_vfs.c: In function'yaffs_setattr':
fs/yaffs2/yaffs_vfs.c:522: error: implicitdeclaration of function 'setattr_copy'
fs/yaffs2/yaffs_vfs.c:525: error: implicitdeclaration of function 'truncate_setsize'
fs/yaffs2/yaffs_vfs.c: At top level:
fs/yaffs2/yaffs_vfs.c:871: warning:initialization from incompatible pointer type
fs/yaffs2/yaffs_vfs.c:902: warning:initialization from incompatible pointer type
fs/yaffs2/yaffs_vfs.c: In function'yaffs_evict_inode':
fs/yaffs2/yaffs_vfs.c:1062: error: implicitdeclaration of function 'end_writeback'
fs/yaffs2/yaffs_vfs.c: At top level:
fs/yaffs2/yaffs_vfs.c:1952: error: unknownfield 'evict_inode' specified in initializer
fs/yaffs2/yaffs_vfs.c:1952: warning: initializationfrom incompatible pointer type
make[2]: *** [fs/yaffs2/yaffs_vfs.o] Error1
应该是版本问题,那就不用新版本的,我们退而求其次,还用老的吧,这里用的是嵌入式Linux开发完全手册里附带的版本,【下载地址】。
先把已经打的补丁去除了,再按照同样的方法打该版本的补丁。这里的补丁不是通过patch打的,而是用的shell,那就笨着来吧,手动去除,手动去除的话你得明白它到底对你的内核做了哪些手脚:执行patch-ker.sh,主要做了一下工作:
<1>修改内核文件/fs/Kconfig,增加下面两行(在177行附近):
if MISC_FILESYSTEMS
source "fs/adfs/Kconfig"
source "fs/affs/Kconfig"
source "fs/ecryptfs/Kconfig"
source "fs/hfs/Kconfig"
source "fs/hfsplus/Kconfig"
source "fs/befs/Kconfig"
source "fs/bfs/Kconfig"
source "fs/efs/Kconfig"
source"fs/yaffs2/Kconfig"
source "fs/jffs2/Kconfig"
# UBIFS File system configuration
<2>修改内核文件/fs/Makefile,增加下面两行(在129行附近):
obj-$(CONFIG_GFS2_FS) += gfs2/
obj-$(CONFIG_EXOFS_FS) += exofs/
obj-$(CONFIG_YAFFS_FS) += yaffs2/
<3>在内核文件的fs目录下创建yaffs2子目录,然后复制如下文件:
将yaffs2源码目录下的Makefile.kernel文件复制为内核fs/yaffs2/Makefile文件。
将yaffs2源码目录下的Kconfig文件复制为内核fs/yaffs2/目录下。
将yaffs2源码目录下的*.c、*.h文件(不包括子目录下的文件)复制为内核fs/yaffs2/目录下。
笨人有笨法,我们手动的把它修改回去,然后记着编译一下,确保没有其他错误,然后再继续。。。。。。。。。。。。。。测试中。。。。。。。。。。。。。。。。。。。
OK,编译正常,证明去除没有干扰到其他部分,可以继续了。
同样,以相同的方式将上边给出版本的yaffs安装到内核,不再嗷述。(不过这里的安装后确实和上边的有些出入,初步估计是上边的源码下载有误,非完全版。)
就绪后,我们重新编译一下。。。。。。。。。。。。。。。。编译中。。。。。。。。。。。。。。。。。。。
我勒个去,报了如此多的错误。。。。。都是yaffs的。。。。。。
fs/yaffs2/yaffs_fs.c:212: error: unknownfield 'prepare_write' specified in initializer
fs/yaffs2/yaffs_fs.c:212: warning:initialization from incompatible pointer type
fs/yaffs2/yaffs_fs.c:213: error: unknownfield 'commit_write' specified in initializer
fs/yaffs2/yaffs_fs.c:213: warning: initializationfrom incompatible pointer type
fs/yaffs2/yaffs_fs.c:287: error: unknownfield 'read_inode' specified in initializer
fs/yaffs2/yaffs_fs.c:287: warning:initialization from incompatible pointer type
fs/yaffs2/yaffs_fs.c:288: error: unknownfield 'put_inode' specified in initializer
fs/yaffs2/yaffs_fs.c: In function'yaffs_get_inode':
fs/yaffs2/yaffs_fs.c:847: error: implicitdeclaration of function 'iget'
fs/yaffs2/yaffs_fs.c:847: warning:assignment makes pointer from integer without a cast
fs/yaffs2/yaffs_fs.c: In function'yaffs_mknod':
fs/yaffs2/yaffs_fs.c:1021: error: 'structtask_struct' has no member named 'fsuid'
fs/yaffs2/yaffs_fs.c:1022: error: 'structtask_struct' has no member named 'fsgid'
fs/yaffs2/yaffs_fs.c: In function 'yaffs_symlink':
fs/yaffs2/yaffs_fs.c:1201: error: 'structtask_struct' has no member named 'fsuid'
fs/yaffs2/yaffs_fs.c:1202: error: 'structtask_struct' has no member named 'fsgid'
fs/yaffs2/yaffs_fs.c: In function'yaffs_internal_read_super':
fs/yaffs2/yaffs_fs.c:1676: warning: format'%d' expects type 'int', but argument 2 has type 'uint64_t'
fs/yaffs2/yaffs_fs.c: In function'init_yaffs_fs':
从报错的内容来看,应该是版本问题,这个内核是比较新的,而yaffs的版本比较老。
我们重新把内核还原回去(当然还是手动的完成那三步了)。真蛋疼。。。。
(现在官网上不用cvs做版本控制了,用的git,ubuntu下安装git工具,可以直接clone到最新的yaffs2,用$ sudo apt-get install git-core安装,然后用 $ git clone git://www.aleph1.co.uk/yaffs2 命令会直接在当前命令下clone一份,并在当前目录下创建yaffs2目录,是当前目录下,所以最好不要在内核目录下执行该命令)
实际测试是用git在官网上clone下来的最新版本,也报错了。。。。。。。
fs/yaffs2/yaffs_vfs.c: In function'yaffs_setattr':
fs/yaffs2/yaffs_vfs.c:522: error: implicitdeclaration of function 'setattr_copy'
fs/yaffs2/yaffs_vfs.c:525: error: implicitdeclaration of function 'truncate_setsize'
fs/yaffs2/yaffs_vfs.c: At top level:
fs/yaffs2/yaffs_vfs.c:871: warning:initialization from incompatible pointer type
fs/yaffs2/yaffs_vfs.c:902: warning:initialization from incompatible pointer type
fs/yaffs2/yaffs_vfs.c: In function'yaffs_evict_inode':
fs/yaffs2/yaffs_vfs.c:1062: error: implicitdeclaration of function 'end_writeback'
fs/yaffs2/yaffs_vfs.c: At top level:
fs/yaffs2/yaffs_vfs.c:1952: error: unknownfield 'evict_inode' specified in initializer
fs/yaffs2/yaffs_vfs.c:1952: warning:initialization from incompatible pointer type
make[2]: *** [fs/yaffs2/yaffs_vfs.o] Error1
make[1]: *** [fs/yaffs2] Error 2
make[1]: *** Waiting for unfinishedjobs....
想了想,内核版本是比较新的,yaffs2版本也是比较新的……应该没有太大落差,是不是尝试自己用新内核往板子上做移植的上辈子真的是折翼的天使。记得上次用的是2.6.30的内核用的老版本yaffs2一下就过了。。。。。。。。
待我用2.6.30的内核再重新做一遍上面全部的工作,来测试一下这个新版本的yaffs。。。(真的很悲催…………)
………………………工作中,等待………………………
对于2.6.30的内核添加yaffs2最新版也会报出和2.6.34同样的错误,于是我猜测'setattr_copy' 、'truncate_setsize'……等这些应该是新内核(3.0)的新特性,不过该如何选择适合自己内核的yaffs2版本呢……确实令人纠结。
然后对2.6.30添加yaffs2老版本进行测试(记着去除当前版本先)……
至此,嵌入式linux开发完全手册里的版本也会报错,我不淡定了。我决定,必须把这个问题解决,所以我去追溯一个和linux2.6.30 时间比较温和的版本,在这里,我决定选择2009年圣诞节的版本 http://www.aleph1.co.uk/gitweb?p=yaffs2.git;a=shortlog;pg=4
,2009-12-25,但愿圣诞老人给我面子,让它过了吧……(我不淡定了,编译时我都开始-j6,开六个线程了,其实时间都是在等待中给浪费掉了……)
………………………编译中,等待………………………
OK,圣诞节还是神圣的,它编译通过了……现在测试通过的版本是:2.6.30.4的kernel + 2009-12-25的yaffs2 。你是不是也感觉很纠结。我们烧乳板子测试下,刚刚编译出来的zImage。
运行后效果:
好的,到这里,内核的移植已经暂时告一段落,我该把文章帖出来了……事实证明,虽然笨,但是花些时间还是可以做到的……
(有时候版本问题还是很令人头疼的!!!!)
下一步是进行跟文件系统的构建…… (待续……)
愚人的linux内核2440移植札记(超曲折版)的更多相关文章
- 初探内核之《Linux内核设计与实现》笔记下
定时器和时间管理 系统中有很多与时间相关的程序(比如定期执行的任务,某一时间执行的任务,推迟一段时间执行的任务),因此,时间的管理对于linux来说非常重要. 主要内容: 系统时间 定时器 定时器相关 ...
- 《Linux内核设计与实现》读书笔记(十九)- 可移植性
linux内核的移植性非常好, 目前的内核也支持非常多的体系结构(有20多个). 但是刚开始时, linux也只支持 intel i386 架构, 从 v1.2版开始支持 Digital Alpha, ...
- Linux内核中Makefile、Kconfig和.config的关系(转)
我们在编译Linux内核时,往往在Linux内核的顶层目录会执行一些命令,这里我以RK3288举例,比如:make firefly-rk3288-linux_defconfig.make menuco ...
- 第32课 Linux内核链表剖析
1. Linux内核链表的位置及依赖 (1)位置:{linux-2.6.39}\\include\linux\list.h (2)依赖 ①#include<linux\types.h> ② ...
- linux内核链表使用
原文链接:http://blog.csdn.net/xnwyd/article/details/7359373 Linux内核链表的核心思想是:在用户自定义的结构A中声明list_head类型的成员p ...
- 数据结构开发(10):Linux内核链表
0.目录 1.老生常谈的两个宏(Linux) 1.1 offsetof 1.2 container_of 2.Linux内核链表剖析 3.小结 1.老生常谈的两个宏(Linux) Linux 内核中常 ...
- 2017-2018-1 20179215《Linux内核原理与分析》第十周作业
第17章 设备与模块 一.设备类型 除了以上3种典型的设备之外,其实Linux中还有一些其他的设备类型,其中见的较多的应该算是"伪设备".所谓"伪设备",其实 ...
- linux内核书籍
1, 关于操作系统理论的最初级的知识.不需要通读并理解<操作系统概念><现代操作系统>等巨著,但总要知道分时(time-shared)和实时(real-time)的区别是什么, ...
- 关于Linux内核学习的误区以及相关书籍介绍
http://www.hzlitai.com.cn/article/ARM9-article/system/1605.html 写给Linux内核新手-关于Linux内核学习的误区 先说句正经的:其实 ...
随机推荐
- 什么是GIL锁以及作用
全局解释锁,每次只能一个线程获得cpu的使用权:为了线程安全,也就是为了解决多线程之间的数据完整性和状态同步而加的锁,因为我们知道线程之间的数据是共享的.
- oracle decode的用法
需求:分别统计emp表中1980,1981,1982,1987年入职的同事的数量. 这里用decode很容易就解决了: select sum(t.num_1980) as "1980&quo ...
- UVALive - 7427 the math 【二分匹配】
题目链接 https://icpcarchive.ecs.baylor.edu/index.php?option=com_onlinejudge&Itemid=8&page=show_ ...
- iOS block 闭包的学习
iOS 闭包 学习 理解: 1 . 闭包外界无法访问内部变量 ,它是一个独立的代码块. 2 . 闭包可以作为 一个方法 ,甚至局部变量 全局 变量 3 . 闭包 是一种引用类型 注 ...
- 《程序员代码面试指南》第三章 二叉树问题 判断t1 树中是否有与t2 树拓扑结构完全相同的子树
题目 判断t1 树中是否有与t2 树拓扑结构完全相同的子树 java代码 package com.lizhouwei.chapter3; /** * @Description: 判断t1 树中是否有与 ...
- es5严格模式简谈
一.用法: 在全局或局部开头加上“use strict”即可 就是一行字符串,不会对不兼容严格模式的浏览器产生影响.二.不再兼容es3的一些不规则语法.使用全新的es5规范.三.两种用法: 全局严格模 ...
- location记录<18.7.21>
// var index = location.href; // console.log(index) // // indexOf() 方法可返回某个指定的字符串值在字符串中首次出现的位置. // v ...
- Python 变量(赋值,数据类型,数据类型转换)
一.python 变量赋值方式有三种: 1.直接赋值:age = 28 2.多个变量赋值 age, sex = 28, 1 #每个变量都必须要有个对应的值 3.特殊形式的赋值(链式赋值) a = ...
- 【反思】一个价值两天的BUG,无论工作还是学习C语言的朋友都看看吧!
博文原创,转载请联系博主! 使用C语言也有两个年头了,BUG写出来过不少,也改过不少BUG.但是偏偏就是有这么一个BUG让我手头的项目停工了两天,原因从百度找到谷歌,资料从MAN手册找到RFC也没有找 ...
- git忽略文件和目录
******************************************************** http://jingxuan.io/progit/2-Git%E5%9F%BA%E7 ...