0%

在Git中,用HEAD表示当前版本,上一个版本就是HEAD^,上上一个版本就是HEAD^^
当然往上100个版本写100个^比较容易数不过来,所以写成HEAD~100


撤销一个已提交修改

撤销某次提交,此次操作之前和之后的commit和history都会保留,并且把这次撤销作为一次最新的提交

git revert HEAD   撤销前一次 commit
git revert HEAD^  撤销前前一次 commit
git revert commit (比如:fa042ce57ebbe5bb9c8db709f719cec2c58ee7ff)撤销指定的版本,撤销也会作为一次提交进行保存。

git revert是提交一个新的版本,将需要revert的版本的内容再反向修改回去,版本会递增,不影响之前提交的内容

修改最近一次的提交信息

提交时,可能没写好或者误操作导致提交的信息不合适,但你还没有 push 到远程分支时,修改上一次的提交信息。

git commit –amend或git commit --amend -m "Fixes bug #42"

如果需要修改已push的文件,需要如下操作:

git rebase -i HEAD~3 
git commit -amend 
git rebase --continue

撤销本地修改

git checkout -- file

git checkout -- file可以丢弃工作区的修改,把file文件在工作区的修改全部撤销,这里有两种情况:

  1. 一种是自修改后还没有被放到暂存区,现在,撤销修改就回到和版本库一模一样的状态;
  2. 一种是已经添加到暂存区后,又作了修改,现在,撤销修改就回到添加到暂存区后的状态。

总之,就是让这个文件回到最近一次git commit或git add时的状态。

重置修改

已经使用git add,但是没有使用git commit,需要修改暂存区内容:

git reset HEAD file

仅用HEAD指向的目录树充值暂存区,工作区不受影响。相当于把git add命令更新到暂存区的内容撤出暂存区

git reset --hard HEAD^

把版本库、暂存区和工作区都回退到HEAD的前一个版本。head版本的提交全部丢失。慎用!

git reset --hard HEAD

等价与:

git reset HEAD file
git checkout -- file

版本回退

git reset --hard HEAD^
git reset --hard commit_id

撤销本地后重做

已经提交了一些内容,并使用git reset –hard撤销了这些更改(见上面),突然意识到:你想还原这些修改!

git reflog

git reflog是一个用来恢复项目历史记录的好办法。你可以通过git reflog恢复几乎任何已提交的内容。
git reflog与git log类似,只不过git reflog显示的是HEAD变更次数的列表。

具体一个例子,假设有三个commit,

git st:
commit3: add test3.c
commit2: add test2.c
commit1: add test1.c

如果执行git reset –hard HEAD~1则 删除了commit3,如果发现删除错误了,需要恢复commit3,这个时候就要使用

git reflog
HEAD@{0}: HEAD~1: updating HEAD
63ee781 HEAD@{1}: commit: test3:q

运行git log则没有这一行记录,可以使用git reset –hard 63ee781将红色记录删除,则恢复了cmmit3,运行git log后可以看到:

git reset --hard 63ee781
git log
commit3: add test3.c
commit2: add test2.c
commit1: add test1.c

停止跟踪一个已被跟踪的文件

场景:你意外将application.log添加到仓库中,现在你每次运行程序,Git都提示application.log中有unstaged的提交。
你在.gitignore中写上”*.log”,但仍旧没用——怎样告诉Git“撤销”跟踪这个文件的变化呢?

git rm --cached application.log

发生了什么:尽管.gitignore阻止Git跟踪文件的变化,甚至是之前没被跟踪的文件是否存在,
但是,一旦文件被add或者commit,Git会开始持续跟踪这 个文件的变化。
类似的,如果你用git add –f来“强制”add,或者覆盖.gitignore,Git还是会继续监视变化。所以以后最好不要使用 –f来add .gitignore文件。

如果你希望移除那些应当被忽略的文件,git rm –cached可以帮助你,并将这些文件保留在磁盘上。
因为这个文件现在被忽略了,你将不会在git status中看到它,也不会再把这个文件commit了。

git reset 与 git revert

git revert 是生成一个新的提交来撤销某次提交,此次提交之前的commit都会被保留

git reset 是回到某次提交,提交及之前的commit都会被保留,但是此次之后的修改都会被退回到暂存区

假设有三个commit,

git st
commit3: add test3.c
commit2: add test2.c
commit1: add test1.c

当执行git revert HEAD~1时, commit2被撤销了,git log可以看到:

revert “commit2”:this reverts commit 5fe21s2…
commit3: add test3.c
commit2: add test2.c
commit1: add test1.c

git status 没有任何变化,如果换做执行git reset –soft(默认) HEAD~1后,运行git log

commit2: add test2.c
commit1: add test1.c

运行git status, 则test3.c处于暂存区,准备提交。

如果换做执行git reset –hard HEAD~1后,

显示:HEAD is now at commit2,运行git log

commit2: add test2.c
commit1: add test1.c

运行git st, 没有任何变化,工作目录文件也不存在

  1. 如何在Git中撤销一切
  2. 撤销修改
  3. 10 个迅速提升你 Git 水平的提示
  4. 团队开发里频繁使用 git rebase 来保持树的整洁好吗?

minicom选项

支持颜色显示

minicom -c on

支持中文显示

env LANG=en_US minicom

支持数据十六进制显示

minicom -H

支持脚本

minicom -S

支持回显

minicom
Ctrl+A->Z
E local Echo on/off..E

minicom自动发送数据

首先需要设置:

-> minicom 
-> Ctrl+A->O 
-> 回车 
-> 选D- Script Program 
-> 设置为 /bin/bash
-> 保存为默认设置 dfl
-> Ctrl+A , Q 退出minicom

通过启动时指定脚本来实现

minicom -S autosend.sh

脚本如下:

#!/bin/bash

while [ 1 -ne 0  ]
do
    echo 12345678
    sleep 1
done

启动之后,指定脚本路径来执行

minicom -H
Ctrl+A->Z
回车
G run script (Go)
C Name of script 绝对路径
回车运行脚本

使用外部脚本

不通过minicom执行相关脚本,直接在其他窗口执行脚本如下:

#!/bin/bash

while [ 1 -ne 0  ]
do
    echo 12345678 > /dev/ttyUSB0
    sleep 1
done

串口与Shell编程

在这之前先介绍一下stty,stty(set tty)命令用于检查和修改当前注册的终端的通信参数。

stty -a 查看当前注册端口的设置情况:
stty -ixon将流控制设置为OFF,而命令
stty ixon则将流控其置为ON。

在stty命令中可以同时设置多个选项:stty ixon 1200
设置波特率为1200 并且设置流控为on
具体更多的参数设置可以使用man指令查看。
下面说下如何通过shell来实现数据的读取,这里主要是使用的cat指令:

cat /dev/ttyUSB0

但在如果执行上面命令,程序会一直停止在读取的界面,如果我们的要求是想读取一段一段的数据,
即分批次读取一定之间内的数据,保存并处理,那应该怎么办呢?
我们就需要先用stty将模式设置成raw 并且设置最大连接时间:

stty -F /dev/ttyUSB0 raw speed 9600 min 0 time 10

然后我们可以把每次读取到的数据存储到一个临时文件中,然后对其进行处理:

cat /dev/ttyUSB0 >> “tmpFile”

完成的代码如下:

stty -F /dev/ttyUSB0 raw speed 9600 min 0 time 10
while [ 1 -eq 1  ]
do
    cat /dev/ttyUSB0 >> "tmpFile"
    echo `date`
done

需求

根据src下目录名称来生成对应的库,不需要修改makefile,只需要添加文件夹从而生成新的库。

相关知识点:Makefile中嵌入一段shell脚本及函数列表

实现

SRC = $(shell find . -iname "*.c")
DIRS = $(shell ls -I include ./src)

.PHONY : clean install

all : objects libs

objects:$(SRC)
    @echo "making objects dir"
    @mkdir -p $(objects_dir)
    @echo "Generating new objects file...";
    @for f in $(SRC); do \
        OBJ=$(objects_dir)/`basename $$f|sed -e 's/\.c/\.o/'`; \
        echo "compiling \033[032m[$(CC)]\033[0m": $$f; \
        $(CC) $(CFLAGS) -c $$f -o $$OBJ; \
    done

libs:
    @echo "\nmaking libs ..."
    @for subdir in $(DIRS); do \
        target=$(addprefix lib, $(addsuffix .a, $$subdir)); \
        objs=`ls ./src/$$subdir|sed -e 's/\.c/\.o/'|sed -e 's#^#./$(objects_dir)/#'`; \
        $(AR) -rcs $$target $$objs; \
        echo "In subdir\033[32m" [$$subdir] "\033[0mGenerating new lib\033[31m": $$target"\033[0m"; \
    done

clean :
    @echo [Clean all]
    @rm -rf $(objects_dir)
    @rm -rf $(install_dir)
    @find -name "*.o" | xargs rm -rf
    @rm -rf *.a

install : *.a
    @echo "making install dir"
    @mkdir -p $(install_dir)
    @mv *.a $(install_dir)
    @cp include/*.h $(install_dir)
    @cp porting/*.h $(install_dir)

注意

addprefix

target=$(addprefix lib, $(addsuffix .a, $$subdir))

addprefix在shell脚本片段中,处理的文本为多个时会失败,具体原因未知…

shell变量与makefile变量

$(objects_dir)为shell变量,$$target为makefile变量

shell与``[反引号]

在for循环中,使用类似:

objs=$(shell ls ./src/$$subdir|sed -e 's/\.c/\.o/'|sed -e 's#^#./$(objects_dir)/#'); \ 

执行不成功:

/bin/sh: 3: ./output/objects/cmm: not found

修改为:

objs=`ls ./src/$$subdir|sed -e 's/\.c/\.o/'|sed -e 's#^#./$(objects_dir)/#'`; \

  • toc
    {:toc}

作者: 阮一峰
日期: 2015年8月 5日
原文

团队开发中,遵循一个合理、清晰的Git使用流程,是非常重要的。
否则,每个人都提交一堆杂乱无章的commit,项目很快就会变得难以协调和维护。
下面是ThoughtBot 的Git使用规范流程。
我从中学到了很多,推荐你也这样使用Git。

Git Process

第一步:新建分支

首先,每次开发新功能,都应该新建一个单独的分支(这方面可以参考《Git分支管理策略》)。

# 获取主干最新代码
$ git checkout master
$ git pull

# 新建一个开发分支myfeature
$ git checkout -b myfeature

第二步:提交分支commit

分支修改后,就可以提交commit了。

$ git add --all
$ git status
$ git commit --verbose

git add 命令的all参数,表示保存所有变化(包括新建、修改和删除)。
从Git 2.0开始,all是 git add 的默认参数,所以也可以用 git add . 代替。
git status 命令,用来查看发生变动的文件。
git commit 命令的verbose参数,会列出 diff 的结果。

第三步:撰写提交信息

提交commit时,必须给出完整扼要的提交信息,下面是一个范本。

Present-tense summary under 50 characters

* More information about commit (under 72 characters).
* More information about commit (under 72 characters).

http://project.management-system.com/ticket/123

第一行是不超过50个字的提要,然后空一行,罗列出改动原因、主要变动、以及需要注意的问题。
最后,提供对应的网址(比如Bug ticket)。

第四步:与主干同步

分支的开发过程中,要经常与主干保持同步。

$ git fetch origin
$ git rebase origin/master

第五步:合并commit

分支开发完成后,很可能有一堆commit,但是合并到主干的时候,往往希望只有一个(或最多两三个)commit,这样不仅清晰,也容易管理。
那么,怎样才能将多个commit合并呢?这就要用到 git rebase 命令。

$ git rebase -i origin/master

git rebase命令的i参数表示互动(interactive),这时git会打开一个互动界面,进行下一步操作。
下面采用Tute Costa 的例子,来解释怎么合并commit。

pick 07c5abd Introduce OpenPGP and teach basic usage
pick de9b1eb Fix PostChecker::Post#urls
pick 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

# Rebase 8db7e8b..fa20af3 onto 8db7e8b
#
# Commands:
#  p, pick = use commit
#  r, reword = use commit, but edit the commit message
#  e, edit = use commit, but stop for amending
#  s, squash = use commit, but meld into previous commit
#  f, fixup = like "squash", but discard this commit's log message
#  x, exec = run command (the rest of the line) using shell
#
# These lines can be re-ordered; they are executed from top to bottom.
#
# If you remove a line here THAT COMMIT WILL BE LOST.
#
# However, if you remove everything, the rebase will be aborted.
#
# Note that empty commits are commented out

上面的互动界面,先列出当前分支最新的4个commit(越下面越新)。
每个commit前面有一个操作命令,默认是pick,表示该行commit被选中,要进行rebase操作。
4个commit的下面是一大堆注释,列出可以使用的命令。

  • pick:正常选中
  • reword:选中,并且修改提交信息;
  • edit:选中,rebase时会暂停,允许你修改这个commit(参考这里)
  • squash:选中,会将当前commit与上一个commit合并
  • fixup:与squash相同,但不会保存当前commit的提交信息
  • exec:执行其他shell命令

上面这6个命令当中,squash和fixup可以用来合并commit。先把需要合并的commit前面的动词,改成squash(或者s)。

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
s 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

这样一改,执行后,当前分支只会剩下两个commit。第二行和第三行的commit,都会合并到第一行的commit。
提交信息会同时包含,这三个commit的提交信息。

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

# This is the 2nd commit message:
Fix PostChecker::Post#urls

# This is the 3rd commit message:
Hey kids, stop all the highlighting

如果将第三行的squash命令改成fixup命令。

pick 07c5abd Introduce OpenPGP and teach basic usage
s de9b1eb Fix PostChecker::Post#urls
f 3e7ee36 Hey kids, stop all the highlighting
pick fa20af3 git interactive rebase, squash, amend

运行结果相同,还是会生成两个commit,第二行和第三行的commit,都合并到第一行的commit。
但是,新的提交信息里面,第三行commit的提交信息,会被注释掉。

# This is a combination of 3 commits.
# The first commit's message is:
Introduce OpenPGP and teach basic usage

# This is the 2nd commit message:
Fix PostChecker::Post#urls

# This is the 3rd commit message:
# Hey kids, stop all the highlighting

Pony Foo提出另外一种合并commit的简便方法,就是先撤销过去5个commit,然后再建一个新的。

$ git reset HEAD~5
$ git add .
$ git commit -am "Here's the bug fix that closes #28"
$ git push --force

squash和fixup命令,还可以当作命令行参数使用,自动合并commit。

$ git commit --fixup  
$ git rebase -i --autosquash 

这个用法请参考这篇文章,这里就不解释了。

第六步:推送到远程仓库

合并commit后,就可以推送当前分支到远程仓库了。

$ git push --force origin myfeature

git push命令要加上force参数,因为rebase以后,分支历史改变了,跟远程分支不一定兼容,有可能要强行推送(参见这里)。

第七步:发出Pull Request

提交到远程仓库以后,就可以发出 Pull Request 到master分支,然后请求别人进行代码review,确认可以合并到master。

作者: 阮一峰
日期: 2015年12月24日
原文

Git 作为一个源码管理系统,不可避免涉及到多人协作。
协作必须有一个规范的工作流程,让大家有效地合作,使得项目井井有条地发展下去。
“工作流程”在英语里,叫做”workflow”或者”flow”,原意是水流,比喻项目像水流那样,顺畅、自然地向前流动,不会发生冲击、对撞、甚至漩涡。

Git Workflow

本文介绍三种广泛使用的工作流程:

  • Git flow
  • Github flow
  • Gitlab flow

如果你对Git还不是很熟悉,可以先阅读下面的文章。

功能驱动

本文的三种工作流程,有一个共同点:都采用“功能驱动式开发”(Feature-driven development,简称FDD)。

它指的是,需求是开发的起点,先有需求再有功能分支(feature branch)或者补丁分支(hotfix branch)。
完成开发后,该分支就合并到主分支,然后被删除。

Git flow

最早诞生、并得到广泛采用的一种工作流程,就是Git flow 。

特点

它最主要的特点有两个。

gitflow

首先,项目存在两个长期分支。

  • 主分支master
  • 开发分支develop

前者用于存放对外发布的版本,任何时候在这个分支拿到的,都是稳定的分布版;后者用于日常开发,存放最新的开发版。

其次,项目存在三种短期分支。

  • 功能分支(feature branch)
  • 补丁分支(hotfix branch)
  • 预发分支(release branch)

一旦完成开发,它们就会被合并进develop或master,然后被删除。
Git flow 的详细介绍,请阅读我翻译的中文版《Git 分支管理策略》

评价

Git flow的优点是清晰可控,缺点是相对复杂,需要同时维护两个长期分支。
大多数工具都将master当作默认分支,可是开发是在develop分支进行的,这导致经常要切换分支,非常烦人。
更大问题在于,这个模式是基于”版本发布”的,目标是一段时间以后产出一个新版本。
但是,很多网站项目是”持续发布”,代码一有变动,就部署一次。
这时,master分支和develop分支的差别不大,没必要维护两个长期分支。

Github flow

Github flow 是Git flow的简化版,专门配合”持续发布”。它是 Github.com 使用的工作流程。

流程

它只有一个长期分支,就是master,因此用起来非常简单。
官方推荐的流程如下。

github flow

  1. 根据需求,从master拉出新分支,不区分功能分支或补丁分支。
  2. 新分支开发完成后,或者需要讨论的时候,就向master发起一个pull request(简称PR)。
  3. Pull Request既是一个通知,让别人注意到你的请求,又是一种对话机制,大家一起评审和讨论你的代码。对话过程中,你还可以不断提交代码。
  4. 你的Pull Request被接受,合并进master,重新部署后,原来你拉出来的那个分支就被删除。(先部署再合并也可。)

评价

Github flow 的最大优点就是简单,对于”持续发布”的产品,可以说是最合适的流程。
问题在于它的假设:master分支的更新与产品的发布是一致的。也就是说,master分支的最新代码,默认就是当前的线上代码。
可是,有些时候并非如此,代码合并进入master分支,并不代表它就能立刻发布。
比如,苹果商店的APP提交审核以后,等一段时间才能上架。
这时,如果还有新的代码提交,master分支就会与刚发布的版本不一致。
另一个例子是,有些公司有发布窗口,只有指定时间才能发布,这也会导致线上版本落后于master分支。
上面这种情况,只有master一个主分支就不够用了。
通常,你不得不在master分支以外,另外新建一个production分支跟踪线上版本。

Gitlab flow

Gitlab flow 是 Git flow 与 Github flow 的综合。
它吸取了两者的优点,既有适应不同开发环境的弹性,又有单一主分支的简单和便利。
它是 Gitlab.com 推荐的做法。

上游优先

Gitlab flow 的最大原则叫做”上游优先”(upsteam first),即只存在一个主分支master,它是所有其他分支的”上游”。
只有上游分支采纳的代码变化,才能应用到其他分支。

Chromium项目就是一个例子,
它明确规定,上游分支依次为:

  • Linus Torvalds的分支
  • 子系统(比如netdev)的分支
  • 设备厂商(比如三星)的分支

持续发布

Gitlab flow 分成两种情况,适应不同的开发流程。

gitlab flow

对于”持续发布”的项目,它建议在master分支以外,再建立不同的环境分支。
比如,”开发环境”的分支是master,”预发环境”的分支是pre-production,”生产环境”的分支是production。

开发分支是预发分支的”上游”,预发分支又是生产分支的”上游”。
代码的变化,必须由”上游”向”下游”发展。
比如,生产环境出现了bug,这时就要新建一个功能分支,先把它合并到master,确认没有问题,
再cherry-pick到pre-production,这一步也没有问题,才进入production。
只有紧急情况,才允许跳过上游,直接合并到下游分支。

版本发布

gitlab release

对于”版本发布”的项目,建议的做法是每一个稳定版本,都要从master分支拉出一个分支,比如2-3-stable、2-4-stable等等。
以后,只有修补bug,才允许将代码合并到这些分支,并且此时要更新小版本号。

一些小技巧

Pull Request

merge

功能分支合并进master分支,必须通过Pull Request(Gitlab里面叫做 Merge Request)。

request

前面说过,Pull Request本质是一种对话机制,你可以在提交的时候,
@相关人员团队,引起他们的注意。

Protected branch

master分支应该受到保护,不是每个人都可以修改这个分支,以及拥有审批 Pull Request 的权力。

Github
Gitlab 都提供”保护分支”(Protected branch)这个功能。

Issue

Issue 用于 Bug追踪和需求管理。建议先新建 Issue,再新建对应的功能分支。功能分支总是为了解决一个或多个 Issue。
功能分支的名称,可以与issue的名字保持一致,并且以issue的编号起首,比如”15-require-a-password-to-change-it”。

开发完成后,在提交说明里面,可以写上”fixes #14”或者”closes #67”。
Github规定,只要commit message里面有下面这些动词 + 编号,
就会关闭对应的issue。

  • close
  • closes
  • closed
  • fix
  • fixes
  • fixed
  • resolve
  • resolves
  • resolved

这种方式还可以一次关闭多个issue,或者关闭其他代码库的issue,格式是username/repository#issue_number。
Pull Request被接受以后,issue关闭,原始分支就应该删除。如果以后该issue重新打开,新分支可以复用原来的名字。

Merge节点

Git有两种合并:一种是”直进式合并”(fast forward),不生成单独的合并节点;
另一种是”非直进式合并”(none fast-forword),会生成单独节点。

前者不利于保持commit信息的清晰,也不利于以后的回滚,建议总是采用后者(即使用–no-ff参数)。
只要发生合并,就要有一个单独的合并节点。

Squash 多个commit

为了便于他人阅读你的提交,也便于cherry-pick或撤销代码变化,在发起Pull Request之前,应该把多个commit合并成一个。
(前提是,该分支只有你一个人开发,且没有跟master合并过。)

这可以采用rebase命令附带的squash操作,具体方法请参考我写的《Git 使用规范流程》

作者: 阮一峰
日期: 2014年6月12日
原文

Git是目前最流行的版本管理系统,学会Git几乎成了开发者的必备技能。
Git有很多优势,其中之一就是远程操作非常简便。本文详细介绍5个Git命令,它们的概念和用法,理解了这些内容,你就会完全掌握Git远程操作。

  • git clone
  • git remote
  • git fetch
  • git pull
  • git push

本文针对初级用户,从最简单的讲起,但是需要读者对Git的基本用法有所了解。
同时,本文覆盖了上面5个命令的几乎所有的常用用法,所以对于熟练用户也有参考价值。

Git Remote

git clone

远程操作的第一步,通常是从远程主机克隆一个版本库,这时就要用到git clone命令。

$ git clone <版本库的网址>

比如,克隆jQuery的版本库。

$ git clone https://github.com/jquery/jquery.git

该命令会在本地主机生成一个目录,与远程主机的版本库同名。如果要指定不同的目录名,可以将目录名作为git clone命令的第二个参数。

$ git clone <版本库的网址> <本地目录名>

git clone支持多种协议,除了HTTP(s)以外,还支持SSH、Git、本地文件协议等,下面是一些例子。

$ git clone http[s]://example.com/path/to/repo.git/
$ git clone ssh://example.com/path/to/repo.git/
$ git clone git://example.com/path/to/repo.git/
$ git clone /opt/git/project.git 
$ git clone file:///opt/git/project.git
$ git clone ftp[s]://example.com/path/to/repo.git/
$ git clone rsync://example.com/path/to/repo.git/

SSH协议还有另一种写法。

$ git clone [user@]example.com:path/to/repo.git/

通常来说,Git协议下载速度最快,SSH协议用于需要用户认证的场合。各种协议优劣的详细讨论请参考官方文档。

git remote

为了便于管理,Git要求每个远程主机都必须指定一个主机名。git remote命令就用于管理主机名。
不带选项的时候,git remote命令列出所有远程主机。

$ git remote
origin

使用-v选项,可以参看远程主机的网址。

$ git remote -v
origin  git@github.com:jquery/jquery.git (fetch)
origin  git@github.com:jquery/jquery.git (push)

上面命令表示,当前只有一台远程主机,叫做origin,以及它的网址。
克隆版本库的时候,所使用的远程主机自动被Git命名为origin。如果想用其他的主机名,需要用git clone命令的-o选项指定。

$ git clone -o jQuery https://github.com/jquery/jquery.git
$ git remote
jQuery

上面命令表示,克隆的时候,指定远程主机叫做jQuery。

git remote show命令加上主机名,可以查看该主机的详细信息。

$ git remote show <主机名>

git remote add命令用于添加远程主机。

$ git remote add <主机名> <网址>

git remote rm命令用于删除远程主机。

$ git remote rm <主机名>

git remote rename命令用于远程主机的改名。

$ git remote rename <原主机名> <新主机名>

git fetch

一旦远程主机的版本库有了更新(Git术语叫做commit),需要将这些更新取回本地,这时就要用到git fetch命令。

$ git fetch <远程主机名>

上面命令将某个远程主机的更新,全部取回本地。
git fetch命令通常用来查看其他人的进程,因为它取回的代码对你本地的开发代码没有影响。
默认情况下,git fetch取回所有分支(branch)的更新。如果只想取回特定分支的更新,可以指定分支名。

$ git fetch <远程主机名> <分支名>

比如,取回origin主机的master分支。

$ git fetch origin master

所取回的更新,在本地主机上要用”远程主机名/分支名”的形式读取。比如origin主机的master,就要用origin/master读取。
git branch命令的-r选项,可以用来查看远程分支,-a选项查看所有分支。

$ git branch -r
origin/master

$ git branch -a
* master
  remotes/origin/master

上面命令表示,本地主机的当前分支是master,远程分支是origin/master。
取回远程主机的更新以后,可以在它的基础上,使用git checkout命令创建一个新的分支。

$ git checkout -b newBrach origin/master

上面命令表示,在origin/master的基础上,创建一个新分支。
此外,也可以使用git merge命令或者git rebase命令,在本地分支上合并远程分支。

$ git merge origin/master
$ git rebase origin/master

上面命令表示在当前分支上,合并origin/master。

git pull

git pull命令的作用是,取回远程主机某个分支的更新,再与本地的指定分支合并。它的完整格式稍稍有点复杂。

$ git pull <远程主机名> <远程分支名>:<本地分支名>

比如,取回origin主机的next分支,与本地的master分支合并,需要写成下面这样。

$ git pull origin next:master

如果远程分支是与当前分支合并,则冒号后面的部分可以省略。

$ git pull origin next

上面命令表示,取回origin/next分支,再与当前分支合并。实质上,这等同于先做git fetch,再做git merge。

$ git fetch origin
$ git merge origin/next

在某些场合,Git会自动在本地分支与远程分支之间,建立一种追踪关系(tracking)。
比如,在git clone的时候,所有本地分支默认与远程主机的同名分支,建立追踪关系,
也就是说,本地的master分支自动”追踪”origin/master分支。
Git也允许手动建立追踪关系。

git branch --set-upstream master origin/next

上面命令指定master分支追踪origin/next分支。
如果当前分支与远程分支存在追踪关系,git pull就可以省略远程分支名。

$ git pull origin

上面命令表示,本地的当前分支自动与对应的origin主机”追踪分支”(remote-tracking branch)进行合并。
如果当前分支只有一个追踪分支,连远程主机名都可以省略。

$ git pull

上面命令表示,当前分支自动与唯一一个追踪分支进行合并。
如果合并需要采用rebase模式,可以使用–rebase选项。

$ git pull --rebase <远程主机名> <远程分支名>:<本地分支名>

如果远程主机删除了某个分支,默认情况下,git pull 不会在拉取远程分支的时候,删除对应的本地分支。
这是为了防止,由于其他人操作了远程主机,导致git pull不知不觉删除了本地分支。
但是,你可以改变这个行为,加上参数 -p 就会在本地删除远程已经删除的分支。

$ git pull -p
# 等同于下面的命令
$ git fetch --prune origin 
$ git fetch -p

git push

git push命令用于将本地分支的更新,推送到远程主机。它的格式与git pull命令相仿。

$ git push <远程主机名> <本地分支名>:<远程分支名>

注意,分支推送顺序的写法是<来源地>:<目的地>,所以git pull是<远程分支>:<本地分支>,而git push是<本地分支>:<远程分支>。
如果省略远程分支名,则表示将本地分支推送与之存在”追踪关系”的远程分支(通常两者同名),如果该远程分支不存在,则会被新建。

$ git push origin master

上面命令表示,将本地的master分支推送到origin主机的master分支。如果后者不存在,则会被新建。
如果省略本地分支名,则表示删除指定的远程分支,因为这等同于推送一个空的本地分支到远程分支。

$ git push origin :master
# 等同于
$ git push origin --delete master

上面命令表示删除origin主机的master分支。
如果当前分支与远程分支之间存在追踪关系,则本地分支和远程分支都可以省略。

$ git push origin

上面命令表示,将当前分支推送到origin主机的对应分支。
如果当前分支只有一个追踪分支,那么主机名都可以省略。

$ git push

如果当前分支与多个主机存在追踪关系,则可以使用-u选项指定一个默认主机,这样后面就可以不加任何参数使用git push。

$ git push -u origin master

上面命令将本地的master分支推送到origin主机,同时指定origin为默认主机,后面就可以不加任何参数使用git push了。
不带任何参数的git push,默认只推送当前分支,这叫做simple方式。
此外,还有一种matching方式,会推送所有有对应的远程分支的本地分支。
Git 2.0版本之前,默认采用matching方法,现在改为默认采用simple方式。如果要修改这个设置,可以采用git config命令。

$ git config --global push.default matching
# 或者
$ git config --global push.default simple

还有一种情况,就是不管是否存在对应的远程分支,将本地的所有分支都推送到远程主机,这时需要使用–all选项。

$ git push --all origin

上面命令表示,将所有本地分支都推送到origin主机。
如果远程主机的版本比本地版本更新,推送时Git会报错,要求先在本地做git pull合并差异,然后再推送到远程主机。
这时,如果你一定要推送,可以使用–force选项。

$ git push --force origin 

上面命令使用–force选项,结果导致远程主机上更新的版本被覆盖。
除非你很确定要这样做,否则应该尽量避免使用–force选项。
最后,git push不会推送标签(tag),除非使用–tags选项。

$ git push origin --tags

作者: 阮一峰
日期: 2014年6月12日
原文

一般来说,日常使用只要记住下图6个命令,就可以了。但是熟练使用,恐怕要记住60~100个命令。

Git Remote

下面是我整理的常用 Git 命令清单。几个专用名词的译名如下。

Workspace:工作区
Index / Stage:暂存区
Repository:仓库区(或本地仓库)
Remote:远程仓库

新建代码库

# 在当前目录新建一个Git代码库
$ git init

# 新建一个目录,将其初始化为Git代码库
$ git init [project-name]

# 下载一个项目和它的整个代码历史
$ git clone [url]

配置

Git的设置文件为.gitconfig,它可以在用户主目录下(全局配置),也可以在项目目录下(项目配置)。

# 显示当前的Git配置
$ git config --list

# 编辑Git配置文件
$ git config -e [--global]

# 设置提交代码时的用户信息
$ git config [--global] user.name "[name]"
$ git config [--global] user.email "[email address]"

增加/删除文件

# 添加指定文件到暂存区
$ git add [file1] [file2] ...

# 添加指定目录到暂存区,包括子目录
$ git add [dir]

# 添加当前目录的所有文件到暂存区
$ git add .

# 删除工作区文件,并且将这次删除放入暂存区
$ git rm [file1] [file2] ...

# 停止追踪指定文件,但该文件会保留在工作区
$ git rm --cached [file]

# 改名文件,并且将这个改名放入暂存区
$ git mv [file-original] [file-renamed]

代码提交

# 提交暂存区到仓库区
$ git commit -m [message]

# 提交暂存区的指定文件到仓库区
$ git commit [file1] [file2] ... -m [message]

# 提交工作区自上次commit之后的变化,直接到仓库区
$ git commit -a

# 提交时显示所有diff信息
$ git commit -v

# 使用一次新的commit,替代上一次提交
# 如果代码没有任何新变化,则用来改写上一次commit的提交信息
$ git commit --amend -m [message]

# 重做上一次commit,并包括指定文件的新变化
$ git commit --amend [file1] [file2] ...

分支

# 列出所有本地分支
$ git branch

# 列出所有远程分支
$ git branch -r

# 列出所有本地分支和远程分支
$ git branch -a

# 新建一个分支,但依然停留在当前分支
$ git branch [branch-name]

# 新建一个分支,并切换到该分支
$ git checkout -b [branch]

# 新建一个分支,指向指定commit
$ git branch [branch] [commit]

# 新建一个分支,与指定的远程分支建立追踪关系
$ git branch --track [branch] [remote-branch]

# 切换到指定分支,并更新工作区
$ git checkout [branch-name]

# 建立追踪关系,在现有分支与指定的远程分支之间
$ git branch --set-upstream [branch] [remote-branch]

# 合并指定分支到当前分支
$ git merge [branch]

# 选择一个commit,合并进当前分支
$ git cherry-pick [commit]

# 删除分支
$ git branch -d [branch-name]

# 删除远程分支
$ git push origin --delete [branch-name]
$ git branch -dr [remote/branch]

标签

# 列出所有tag
$ git tag

# 新建一个tag在当前commit
$ git tag [tag]

# 新建一个tag在指定commit
$ git tag [tag] [commit]

# 删除本地tag
$ git tag -d [tag]

# 删除远程tag
$ git push origin :refs/tags/[tagName]

# 查看tag信息
$ git show [tag]

# 提交指定tag
$ git push [remote] [tag]

# 提交所有tag
$ git push [remote] --tags

# 新建一个分支,指向某个tag
$ git checkout -b [branch] [tag]

查看信息

# 显示有变更的文件
$ git status

# 显示当前分支的版本历史
$ git log

# 显示commit历史,以及每次commit发生变更的文件
$ git log --stat

# 显示某个commit之后的所有变动,每个commit占据一行
$ git log [tag] HEAD --pretty=format:%s

# 显示某个commit之后的所有变动,其"提交说明"必须符合搜索条件
$ git log [tag] HEAD --grep feature

# 显示某个文件的版本历史,包括文件改名
$ git log --follow [file]
$ git whatchanged [file]

# 显示指定文件相关的每一次diff
$ git log -p [file]

# 显示指定文件是什么人在什么时间修改过
$ git blame [file]

# 显示暂存区和工作区的差异
$ git diff

# 显示暂存区和上一个commit的差异
$ git diff --cached [file]

# 显示工作区与当前分支最新commit之间的差异
$ git diff HEAD

# 显示两次提交之间的差异
$ git diff [first-branch]...[second-branch]

# 显示某次提交的元数据和内容变化
$ git show [commit]

# 显示某次提交发生变化的文件
$ git show --name-only [commit]

# 显示某次提交时,某个文件的内容
$ git show [commit]:[filename]

# 显示当前分支的最近几次提交
$ git reflog

远程同步

# 下载远程仓库的所有变动
$ git fetch [remote]

# 显示所有远程仓库
$ git remote -v

# 显示某个远程仓库的信息
$ git remote show [remote]

# 增加一个新的远程仓库,并命名
$ git remote add [shortname] [url]

# 取回远程仓库的变化,并与本地分支合并
$ git pull [remote] [branch]

# 上传本地指定分支到远程仓库
$ git push [remote] [branch]

# 强行推送当前分支到远程仓库,即使有冲突
$ git push [remote] --force

# 推送所有分支到远程仓库
$ git push [remote] --all

撤销

# 恢复暂存区的指定文件到工作区
$ git checkout [file]

# 恢复某个commit的指定文件到暂存区和工作区
$ git checkout [commit] [file]

# 恢复暂存区的所有文件到工作区
$ git checkout .

# 重置暂存区的指定文件,与上一次commit保持一致,但工作区不变
$ git reset [file]

# 重置暂存区与工作区,与上一次commit保持一致
$ git reset --hard

# 重置当前分支的指针为指定commit,同时重置暂存区,但工作区不变
$ git reset [commit]

# 重置当前分支的HEAD为指定commit,同时重置暂存区和工作区,与指定commit一致
$ git reset --hard [commit]

# 重置当前HEAD为指定commit,但保持暂存区和工作区不变
$ git reset --keep [commit]

# 新建一个commit,用来撤销指定commit
# 后者的所有变化都将被前者抵消,并且应用到当前分支
$ git revert [commit]

其他

# 生成一个可供发布的压缩包
$ git archive

示例

源码:

#include <linux/init.h>
#include <linux/module.h>

static int __init hello_init(void)
{
    printk("Hello, world!\n");
    return 0;
}

static void __exit hello_exit(void)
{
    printk("Goodbye, world!\n");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("printk");
MODULE_DESCRIPTION("\"Hello, world!\"");
MODULE_VERSION("printk");     

使用的交叉编译,Makefile:

ARCH=xxx
CROSS_COMPILE=$(ARCH)-linux-
obj-m := printk.o

KDIR := /home/workspace/kernel/2.6.27.55
PWD   := $(shell pwd)

default:
    $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) modules

clean:
    $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KDIR) M=$(PWD) clean    

obj-m := printk.o 是kbuild Makefile中的目标定义部分,同时目标定义部分也是kbuild Makefile的重要组成部分。
该例子告诉Kbuild在这目录里,有一个名为printk.o的目标文件。
将会编译成一个可加载的模块而不是直接编译进内核

makefiles.txt

根据Linux内核文档makefiles.txt的说明:

  1. obj-y Built-in object goals 对应内核配置[Y]
  2. obj-m Loadable module goals 对应内核配置[M]

obj-m由单个文件构成,其中$(CONFIG_ISDN_PPP_BSDCOMP)为配置系统配置为‘m’,示例:

obj-$(CONFIG_ISDN_PPP_BSDCOMP) += isdn_bsdcomp.o

obj-m由多个文件构成,其中$(CONFIG_ISDN)为配置系统配置为‘m’,示例:

obj-$(CONFIG_ISDN) += isdn.o
isdn-objs := isdn_net_lib.o isdn_v110.o isdn_common.o

如果.config中存在以下配置CONFIG_SCSI=y,那么drivers/Makefile中存在以下配置obj-$(CONFIG_SCSI) += scsi/,示例:

obj-y               += base/ block/ misc/ mfd/ net/ media/
obj-$(CONFIG_NUBUS)     += nubus/
obj-$(CONFIG_ATM)       += atm/
obj-y               += macintosh/
obj-$(CONFIG_IDE)       += ide/
obj-$(CONFIG_SCSI)      += scsi/                                                                                                    
obj-$(CONFIG_ATA)       += ata/
obj-$(CONFIG_FUSION)        += message/

当将模块配置为build-in时,即表示要将这些代码的相关目录放入编译目录中!

Linux Makefile

Kernel/Documentation/kbuild/makefiles.txt 中描述Linux Makefile包含五部分:

  1. Makefile the top Makefile.
  2. .config the kernel configuration file.
  3. arch/$(ARCH)/Makefile the arch Makefile.
  4. scripts/Makefile.* common rules etc. for all kbuild Makefiles.
  5. kbuild Makefiles there are about 500 of these.

各个部分的作用:

  1. 顶层Makefile读取.config内核配置文件,顶层Makefile负责编译两个主要的镜像文件:vmlinux(驻留内核镜像)和 内核模块,
    它通过递归便利内核源码树的子目录来编译这些目标文件,访问的子目录列表取决于内核的配置。
  2. .config内核配置文件
  3. 顶层Makefile还会包含arch/$(ARCH)/Makefile,这些平台Makefile向顶层Makefile提供架构特性信息
  4. 每一个子目录下有一个kbulid Makefile会展开从上面传下来的命令,
    kbulid Makefile利用来自.config的配置信息通过kbulid构建各种不同的文件编译内置或是模块可加载的目标
  5. scripts/Makefile.* 包含了所有的定义、规则等等,他们在kbulid makefiles的基础上构建内核

Makefile.modpost

在script目录下有许多Makefile文件,由于各种情况;其中,Makefile.modpost由于module的生成。

第一步:

  1. 编译驱动的每个.o文件。
  2. 将每个.o文件链接到.o。
  3. 在$(MODVERDIR)/生成一个.mod文件,列出.ko及每个.o文件。

第二步:

  1. 找出所有在$(MODVERDIR)/的modules。
  2. 接着使用modpost
  3. 为每个module创建.mod.c
  4. 创建一个Module.symvers文件,保存了所有引出符号及其CRC校验。
  5. 编译全部 .mod.c文件。
  6. 链接所有的module成为一个文件。

第三步:替换module里一些ELF段,包括:

Version magic (see include/vermagic.h for full details)

  • Kernel release
  • SMP is CONFIG_SMP
  • PREEMPT is CONFIG_PREEMPT
  • GCC Version

Module info

  • Module version (MODULE_VERSION)
  • Module alias’es (MODULE_ALIAS)
  • Module license (MODULE_LICENSE)
  • See include/linux/module.h for more details

第四步:

Step 4 is solely used to allow module versioning in external modules,
where the CRC of each module is retrieved from the Module.symers file.
KBUILD_MODPOST_WARN can be set to avoid error out in case of undefined
symbols in the final module linking stage
KBUILD_MODPOST_NOFINAL can be set to skip the final link of modules.
This is solely usefull to speed up test compiles

编译及结果

源码结构:

.
├── hello_printk.c
└── Makefile

编译过程:

make ARCH=xxx CROSS_COMPILE=xxx-linux- -C /home/workspace/kernel/2.6.27.55 M=/data/linux_drivers/hello_printk modules
make[1]: Entering directory '/home/workspace/kernel/2.6.27.55'
CC [M]  /data/linux_drivers/hello_printk/hello_printk.o
Building modules, stage 2.
MODPOST 1 modules
CC      /data/linux_drivers/hello_printk/hello_printk.mod.o
LD [M]  /data/linux_drivers/hello_printk/hello_printk.ko
make[1]: Leaving directory '/home/workspace/kernel/2.6.27.55'

结构:

.
├── hello_printk.c
├── hello_printk.ko
├── .hello_printk.ko.cmd
├── hello_printk.mod.c
├── hello_printk.mod.o
├── .hello_printk.mod.o.cmd
├── hello_printk.o
├── .hello_printk.o.cmd
├── Makefile
├── modules.order
├── Module.symvers
└── .tmp_versions
    └── hello_printk.mod

其中生成<module>.mod.c文件, 内容如下:

#include <linux/module.h>
#include <linux/vermagic.h>
#include <linux/compiler.h>

MODULE_INFO(vermagic, VERMAGIC_STRING);

struct module __this_module
__attribute__((section(".gnu.linkonce.this_module"))) = {
    .name = KBUILD_MODNAME,
    .init = init_module,
#ifdef CONFIG_MODULE_UNLOAD
    .exit = cleanup_module,
#endif                                                                                                                              
    .arch = MODULE_ARCH_INIT,
};



static const char __module_depends[]
__used
__attribute__((section(".modinfo"))) =
"depends=";


MODULE_INFO(srcversion, "D9CAF6BFCBD9B121FB4765D");

其实就是在生成的文件里加入.gnu.linkonce.this_module这样一个段,
驱动加裁时会找到这个段并调用.init函数,卸载时调用.exit函数。

定义一个module结构类型的变量struct module __this_module
这个变量是放在ELF文件的段名为.gnu.linkonce.this_module的段中,通过readelf工具也可以看到相关的段:

$ readelf -S hello_printk.mod.o
共有 11 个节头,从偏移量 0x218 开始:

节头:
    [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
    [ 0]                   NULL            00000000 000000 000000 00      0   0  0
    [ 1] .text             PROGBITS        00000000 000034 000000 00  AX  0   0  1
    [ 2] .data             PROGBITS        00000000 000034 000000 00  WA  0   0  1
    [ 3] .bss              NOBITS          00000000 000034 000000 00  WA  0   0  1
    [ 4] .gnu.linkonce.thi PROGBITS        00000000 000034 0000f4 00  WA  0   0  4
    [ 5] .rela.gnu.linkonc RELA            00000000 000524 000018 0c      9   4  4
    [ 6] .modinfo          PROGBITS        00000000 000128 00004f 00   A  0   0  4
    [ 7] .comment          PROGBITS        00000000 000177 000043 01  MS  0   0  1
    [ 8] .shstrtab         STRTAB          00000000 0001ba 00005d 00      0   0  1
    [ 9] .symtab           SYMTAB          00000000 0003d0 0000e0 10     10  11  4
    [10] .strtab           STRTAB          00000000 0004b0 000071 00      0   0  1
Key to Flags:
    W (write), A (alloc), X (execute), M (merge), S (strings)
    I (info), L (link order), G (group), T (TLS), E (exclude), x (unknown)
    O (extra OS processing required) o (OS specific), p (processor specific)

最后将hello_printk.mod.ohello_printk.o链接为ko,ko符号表如下:

nm hello_printk.ko
00000000 T cleanup_module
00000000 t hello_exit
00000000 t hello_init
00000000 T init_module
0000003c r __mod_author59
00000010 r __mod_description60
00000060 r __mod_license58
0000006c r __mod_srcversion23
00000090 r __module_depends
0000009c r __mod_vermagic5
00000000 r __mod_version61
U printk
00000000 D __this_module

module中可以使用的符号表为cleanup_moduleinit_module,其他为局部符号,对外不可见。

Linux设备驱动程序示例

源代码:

#include <linux/kernel.h>
#include <linux/module.h>
#include <linux/proc_fs.h>
#include <linux/init.h>
#include <linux/vmalloc.h>

static int num = 1;                                                                                                                 
static char *mode = "show";

static int __init hello_module_init(void)
{
    printk("Hello,world\n");
}

static void __exit hello_module_exit(void)
{
    printk("Good Bye\n");
}

module_init(hello_module_init);
module_exit(hello_module_exit);
module_param(mode, charp, S_IRUGO);
module_param(num, int, S_IRUGO);

MODULE_DESCRIPTION("driver for the Hello World.");
MODULE_AUTHOR("Hello");
MODULE_LICENSE("GPL");

Makefile:

obj-m += hello.o    #设置模块名
hello-objs := bsp_hello.o hello_mod_linux.o

all:
    $(MAKE) ARCH=$(ARCH) CROSS_COMPILE=$(CROSS_COMPILE) -C $(KERNEL_PATH) SUBDIRS=`pwd`

clean:
    rm -rf *.ko *.o *.mod.c .tmp_versions Module.symvers modules.order .tmp_versions
    find ../ -name "*.cmd" -delete

install:
    cp $(HELLO_TARGET) $(OUTDIR) -f

Tips:obj-m += (module name).o

相关宏定义

Linux设备驱动编译时,如果MODULE未定义,表明设备驱动是build-in模式。
相应的module_init/module_exit宏定义展开不同。

linux对只需要初始化运行一次的函数都加上init属性,
__init 宏告诉编译器如果这个模块被编译到内核则把这个函数放到(.init.text)段,
module_exit的参数卸载时同
init类似,如果驱动被编译进内核,
exit宏会忽略清理函数,因为编译进内核的模块不需要做清理工作,
**显然
init和__exit对动态加载的模块是无效的,只支持完全编译进内核**。

在kernel初始化后期,释放所有这些函数代码所占的内存空间。
连接器把带__init属性的函数放在同一个section里,在用完以后,把整个section释放掉。
当函数初始化完成后这个区域可以被清除掉以节约系统内存。
Kenrel启动时看到的消息“Freeing unused kernel memory: xxxk freed”同它有关。

start_kernel->rest_init->kernel_init->init_post->free_initmem()来释放初始化代码和数据。

特殊宏定义

  • MODULE_LICENSE(license) 代码使用的许可证
  • MODULE_AUTHOR(author) 描述模块作者
  • MODULE_DESCRIPTION(description) 说明模块用途的简短描述
  • MODULE_VERSION(version_string) 代码修订号
  • MODULE_DEVICE_TABLE(table_info) 告诉用户空间模块所支持的设备
  • MODULE_ALIAS(alternate_name) 模块的别名

加载函数宏定义module_init()

module_init定义如下:

include/linux/init.h
#ifndef MODULE
/**
 * module_init() - driver initialization entry point
 * @x: function to be run at kernel boot time or module insertion
 * 
 * module_init() will either be called during do_initcalls() (if
 * builtin) or at module insertion time (if a module).  There can only
 * be one per module.
 */
#define module_init(x)  __initcall(x);
#else /* MODULE */
/* Each module must use one module_init(). */
#define module_init(initfn)                     \
    static inline initcall_t __inittest(void)   \
    { return initfn;  }                         \                                                                                           
    int init_module(void) __attribute__((alias(#initfn)));
#endif

build-in分支

#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn)     __define_initcall("6",fn,6)
#define __define_initcall(level,fn,id) \                                                                                            
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" level ".init"))) = fn

因此使用module_init修饰的hello_module_init最终展开为:

static initcall_t  __initcall_hello_module_init6 __used __attribute__((__section__(".initcall6.init"))) = hello_module_init;

就是声明一类型为initcall_t(typedef int (*initcall_t)(void))函数指针类型的变量
__initcall_hello_module_init6并将hello_module_init赋值与它。

其中attribute((section(“.initcall6.init”)))表明变量__initcall_hello_module_init6放入section .initcall6.init中。

在文件vmlinux.lds中有以下定义:

.initcall.init : AT(ADDR(.initcall.init) - 0) {                                                                                    
 __initcall_start = .;
 *(.initcallearly.init) __early_initcall_end = .; *(.initcall0.init) *(.initcall0s.init) *(.initcall1.init) 
                                                *(.initcall1s.init) *(.initcall2.init) *(.initcall2s.init) 
                                                *(.initcall3.init) *(.initcall3s.init) *(.initcall4.init) 
                                                *(.initcall4s.init) *(.initcall5.init) *(.initcall5s.init)
                                                *(.initcallrootfs.init) *(.initcall6.init) *(.initcall6s.init) 
                                                *(.initcall7.init) *(.initcall7s.init)
 __initcall_end = .;
}

启动顺序

init/main.c
start_kernel()->rest_init()->kernel_init()->do_basic_setup()->do_initcalls()

static void __init do_initcalls(void)
{ 
    initcall_t *call;

    for (call = __early_initcall_end; call < __initcall_end; call++)
        do_one_initcall(*call);                                                                                                     

    /* Make sure there is no pending stuff from the initcall sequence */
    flush_scheduled_work();
} 

do_initcalls()将按顺序从由early_initcall_end开始,
initcall_end结束的section中以函数指针的形式取出这些编译到内核的驱动模块中初始化函数起始地址
来依次完成相应的初始化。

内核初始化函数do_basic_setup(): do_initcalls() 将从.initcall.init 中,
也就是这几个section中依次取出所有的函数指针,并调用这些函数指针所指向的函数,来完成内核的一些相关的初始化。

内核的加载的时候,会搜索”.initcall”中的所有条目,并按优先级加载它们,普通驱动程序的优先级是6。
其它模块优先级列出如下:值越小,越先加载。

Module分支

/* Each module must use one module_init(). */
#define module_init(initfn)                     \
    static inline initcall_t __inittest(void)   \
    { return initfn;  }                         \                                                                                           
    int init_module(void) __attribute__((alias(#initfn)));

typedef int (*initcall_t)(void)

其中

typedef int (*initcall_t)(void)
#define module_init(initfn)                     \
    static inline initcall_t __inittest(void)   \
    { return initfn;  }                         \                                                                                           

用于对传入的initfn进行类型检测,类型必须为 int (*initcall_t)(void) 的函数指针,然后通过alias将initfn变名为init_module:

/* type newname __attribute__((alias("oldname"))); */
int init_module(void) __attribute__((alias(#initfn)));

当调用insmod和rmmod时,只与init_module和cleanup_module有关,insmod和rmmod中调用这两个函数。

busybox/modutils/insmod.c:insmod_main()
rc = bb_init_module(filename, parse_cmdline_module_options(argv, /*quote_spaces:*/ 0));
.
busybox/modutils/modutils.c:bb_init_module()
init_module(image, image_size, options);
.
# define init_module(mod, len, opts) syscall(__NR_init_module, mod, len, opts)

insmod最终会调用到 syscall __NR_init_module

在Kernel中,各个arch下的文件unistd.h有如下定义:

#define __NR_init_module        128
#define __NR_delete_module      129
#define __NR_get_kernel_syms    130
#define __NR_quotactl           131
#define __NR_getpgid            132

然后,文件entry.S下有定义:

.data
ALIGN
sys_call_table:
    .
    .
    .long sys_mprotect      /* 125 */
    .long sys_sigprocmask
    .long sys_ni_syscall    /* old "create_module" */
    .long sys_init_module
    .long sys_delete_module
    .long sys_ni_syscall    /* 130 - old "get_kernel_syms" */
    .long sys_quotactl
    .
    .

当insmod调用到系统调用号128时,sys_call_table中取出128地址的函数指针进行执行,及sys_init_module。

现在来看系统调用sys_init_module

kernel/module.c
SYSCALL_DEFINE3(init_module, void __user *, umod, unsigned long, len, const char __user *, uargs)
.
#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)
#define SYSCALL_DEFINEx(x, name, ...)                   \                                                                           
    asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

宏定义__SC_DECL##x作用是将参数之间的‘,’去掉,定义如下:

#define __SC_DECL1(t1, a1)  t1 a1
#define __SC_DECL2(t2, a2, ...) t2 a2, __SC_DECL1(__VA_ARGS__)
#define __SC_DECL3(t3, a3, ...) t3 a3, __SC_DECL2(__VA_ARGS__)                                                                      
#define __SC_DECL4(t4, a4, ...) t4 a4, __SC_DECL3(__VA_ARGS__)
#define __SC_DECL5(t5, a5, ...) t5 a5, __SC_DECL4(__VA_ARGS__)
#define __SC_DECL6(t6, a6, ...) t6 a6, __SC_DECL5(__VA_ARGS__)

宏定义中的‘##’为(token-pasting)符号连接操作符,全部展开之后为:

asmlinkage long sys_init_module(void __user * umod, unsigned long len, const char __user * uargs)

在文件include/linux/syscalls.h中可以看到所有的展开的系统调用,例如:

asmlinkage long sys_init_module(void __user *umod, unsigned long len,                                                               
                const char __user *uargs);
asmlinkage long sys_delete_module(const char __user *name_user,
                unsigned int flags);

asmlinkage long sys_rt_sigprocmask(int how, sigset_t __user *set,
                sigset_t __user *oset, size_t sigsetsize);
asmlinkage long sys_rt_sigpending(sigset_t __user *set, size_t sigsetsize);
asmlinkage long sys_rt_sigtimedwait(const sigset_t __user *uthese,
                siginfo_t __user *uinfo,
                const struct timespec __user *uts,
                size_t sigsetsize);

在系统调用sys_init_module中:

.
mod = load_module(umod, len, uargs);  //主要过程都在这儿
.
.
/* Start the module */
if (mod->init != NULL)
    ret = do_one_initcall(mod->init);

load_module做了绝大部分的工作,将驱动拷贝到内核,重定位等等,do_one_initcall调用module注册的init函数。

卸载函数宏定义module_exit()

#ifndef MODULE
/**
 * module_exit() - driver exit entry point
 * @x: function to be run when driver is removed
 * 
 * module_exit() will wrap the driver clean-up code
 * with cleanup_module() when used with rmmod when
 * the driver is a module.  If the driver is statically
 * compiled into the kernel, module_exit() has no effect.
 * There can only be one per module.
 */
#define module_exit(x)  __exitcall(x);
#else /* MODULE */
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn)                 \
    static inline exitcall_t __exittest(void)       \
    { return exitfn;  }                  \
    void cleanup_module(void) __attribute__((alias(#exitfn)));
#endif

build-in

无效,没有意义

module

busybox:

# define delete_module(mod, flags) syscall(__NR_delete_module, mod, flags)

kernel:

.long sys_delete_module
#define __NR_delete_module      129

SYSCALL_DEFINE2(delete_module, const char __user *, name_user, unsigned int, flags)
{
    .
    if (mod->exit != NULL)
        mod->exit();
    .
}

asmlinkage long sys_delete_module(const char __user *name_user, unsigned int flags);

模块传参宏定义module_param()

include/linux/moduleparam.h
#define module_param(name, type, perm)              \                                                                               
    module_param_named(name, name, type, perm)

用于向模块传递参数,其允许驱动程序声明参数,并且用户在系统启动或模块装载时为参数指定相应值,
在驱动程序里,参数的用法如同全局变量。

  • name既是用户看到的参数名,又是模块内接受参数的变量;
  • type表示参数的数据类型,是下列之一:byte, short, ushort, int, uint, long, ulong, charp, bool, invbool;
  • perm指定了在sysfs中相应文件的访问权限。
    访问权限与linux文件爱你访问权限相同的方式管理,如0644,或使用stat.h中的宏如S_IRUGO表示。0表示完全关闭在sysfs中相对应的项。

这些宏不会声明变量,因此在使用宏之前,必须声明变量,典型地用法如下:

static unsigned int int_var = 0;
module_param(int_var, uint, S_IRUGO);

Tips

  • mod->exit()怎么调用到 alias 为 cleanup_module的函数
  • mod->init()怎么调用到 alias 为 init_module的函数

接下来分析module具体组成


重点命令display /i $pc

gdb 命令

  • file <文件名>加载被调试的可执行程序文件。
  • r Run的简写,运行被调试的程序。
  • c Continue的简写,继续执行被调试程序, 直至下一个断点或程序结束。
  • b <行号>
  • b <函数名称>
  • b *<函数名称>
  • b *<代码地址>
  • b: Breakpoint的简写,设置断点。两可以使用“行号”“函数名称”“执行地址”等方式指定断点位置。
    其中在函数名称前面加“*”符号表示将断点设置在“由编译器生成的prolog代码处”。
  • d [编号] Delete breakpoint的简写,删除指定编号的某个断点,或删除所有断点。断点编号从1开始递增。
  • s: 执行一行源程序代码,如果此行代码中有函数调用,则进入该函数;
    s 相当于其它调试器中的“Step Into (单步跟踪进入)”;
  • n: 执行一行源程序代码,此行代码中的函数调用也一并执行。
    n 相当于其它调试器中的“Step Over (单步跟踪)”。
  • si命令类似于s命令,ni命令类似于n命令。 所不同的是,这两个命令(si/ni)所针对的是汇编指令,而s/n针对的是源代码。
  • p <变量名称>Print的简写,显示指定变量(临时变量或全 局变量)的值。
  • display,设置程序中断后欲显示的数据及 其格式。
    例如,如果希望每次程序中断后可以看到即将被执行的下一条汇编指令,可以使用命令
    “display /i $pc”
    其中 $pc 代表当前汇编指令,/i 表示以十六进行显示。当需要关心汇编代码时,此命令相当有用。
  • undispaly,取消先前的display设 置,编号从1开始递增。
  • i Info的简写,用于显示各类信息,详情请查阅 “help i”。
  • q Quit的简写,退出GDB调试环境。
  • help [命令名称]GDB帮助命令,提供对GDB名种命令的解释说明。
    如果指定了“命令名称”参数,则显示该命令的详细说明;如果没有指定参数,则分类显示所有GDB命令,供用户进一步浏览和查询。

反汇编调试

 

显示汇编命令 display /i $pc

(gdb) display /i $pc
(gdb) si
20 n++;
1: x/i $pc 0x8048363 <main+23>: lea 0xfffffffc(%ebp),%eax
(gdb) si
0x08048366 20 n++;
1: x/i $pc 0x8048366 <main+26>: incl (%eax)
(gdb) si
21 n--;
1: x/i $pc 0x8048368 <main+28>: lea 0xfffffffc(%ebp),%eax
(gdb) si
0x0804836b 21 n--;
1: x/i $pc 0x804836b <main+31>: decl (%eax)
(gdb) si
23 nGlobalVar += 100;
1: x/i $pc 0x804836d <main+33>: addl $0x64,0x80494fc

 

汇编断点设置

使用命令“b *main”在 main 函数的 prolog 代码处设置断点
prolog、epilog,分别表示编译器在每个函数的开头和结尾自行插入的代码):

(gdb) b *main
Breakpoint 4 at 0x804834c: file gdb-sample.c, line 17.
(gdb) r
The program being debugged has been started already.
Start it from the beginning? (y or n) y
Starting program: /home/liigo/temp/test_jmp/test_jmp/gdb-sample

Breakpoint 4, main () at gdb-sample.c:17
17 {
1: x/i $pc 0x804834c <main>: push %ebp
(gdb) si
0x0804834d 17 {
1: x/i $pc 0x804834d <main+1>: mov %esp,%ebp
(gdb) si
0x0804834f in main () at gdb-sample.c:17
17 {
1: x/i $pc 0x804834f <main+3>: sub $0x8,%esp
(gdb) si
0x08048352 17 {
1: x/i $pc 0x8048352 <main+6>: and $0xfffffff0,%esp
(gdb) si
0x08048355 17 {
1: x/i $pc 0x8048355 <main+9>: mov $0x0,%eax
(gdb) si
0x0804835a 17 {
1: x/i $pc 0x804835a <main+14>: sub %eax,%esp
(gdb) si
19 n = 1;
1: x/i $pc 0x804835c <main+16>: movl $0x1,0xfffffffc(%ebp)

此时可以使用“i r”命令显示寄存器中的当前值———“i r”即“Infomation Register”:

(gdb) i r
eax 0xbffff6a4 -1073744220
ecx 0x42015554 1107383636
edx 0x40016bc8 1073834952
ebx 0x42130a14 1108544020
esp 0xbffff6a0 0xbffff6a0
ebp 0xbffff6a8 0xbffff6a8
esi 0x40015360 1073828704
edi 0x80483f0 134513648
eip 0x8048366 0x8048366
eflags 0x386 902
cs 0x23 35
ss 0x2b 43
ds 0x2b 43
es 0x2b 43
fs 0x0 0
gs 0x33 51
当然也可以显示任意一个指定的寄存器值:
(gdb) i r eax
eax 0xbffff6a4 -1073744220

 
1.gdb中汇编调试

文本内容

P:013.0:E:11054:H:27500:5DDC29D54BE4D7:F3381AD050DE3A
P:091.5:E:03960:H:29700:DAE69F8B6E0106:E0DE2F8903AE55
P:105.5:E:03960:H:27500:CEF7CE422FF89E:E42DB81767E02E
P:166.0:E:03760:V:27690:7199819B0CF04F:7EE0F00CFFBF66
P:138.0:E:03703:V:04444:AD5CDBA991505A:1E1376FD07FB13
P:166.0:E:03830:H:14000:956D98AD9E6E84:842F2801182286

或者:

B:042.0:E:11919:V:24444:00012:1000000000000000:TRT3
B:042.0:E:11954:A:08800:00001:1111110011111800:EINTERCOM
B:042.0:E:11953:A:02980:00001:1212123312121233:
B:042.0:E:11996:V:27500:00300:A33130046167824A:Sci-Tech TV
B:042.0:E:12015:H:27500:00009:1000001010000010:Teledunya 3D
B:042.0:E:12130:V:27500:00024:1000000000000000:Radio Migros
B:042.0:E:12130:V:27500:00025:1000000000000000:Radio Tansas

逐行读取文本strsep

char *tok = NULL;
for (tok = strsep(&p, "\n"); tok != NULL; tok = strsep(&p, "\n"))
{
    printf("\033[33m%s\n\033[0m", tok);
    //TODO
}

处理单行格式化文本sscanf

sscanf() - 从一个字符串中读进与指定格式相符的数据. 函数原型:

int sscanf( string str, string fmt, mixed var1, mixed var2 ...  );
int scanf( const char \*format [,argument]...  );

说明:

sscanf与scanf类似,都是用于输入的,只是后者以屏幕(stdin)为输入源,前者以固定字符串为输入源。
其中的format可以是一个或多个 %[*] [width] [{h | l | I64 | L}]type | ‘ ‘ | ‘\t’ | ‘\n’ | 非%符号:

  1. * 亦可用于格式中, (即 %*d 和 %*s) 加了星号 (*) 表示跳过此数据不读入. (也就是不把此数据读入参数中)
  2. {a|b|c}表示a,b,c中选一,[d],表示可以有d也可以没有d。
  3. width表示读取宽度。
  4. {h | l | I64 | L}:参数的size,通常h表示单字节size,I表示2字节size,L表示4字节size(double例外),l64表示8字节size。
  5. type :这就很多了,就是%s,%d,%c之类。
  6. 特别的:%*[width] [{h | l | I64 | L}]type 表示满足该条件的被过滤掉,不会向目标参数中写入值

支持集合操作:

  1. %[a-z] 表示匹配a到z中任意字符,贪婪性(尽可能多的匹配)
  2. %[A-Z] 表示匹配A到Z中任意字符,贪婪性(尽可能多的匹配)
  3. %[aB’] 匹配a、B、’中一员,贪婪性
  4. %[^a] 匹配非a的任意字符,贪婪性

注意:在读入的字符串是空字符串时,sscanf函数并不改变待读入到的字符串的值。

针对格式化文本的处理:

char longitude[6] = {0};
char direction = 0;
char frequency[6] = {0};
char polar = 0;
char symbol[6] = {0};
char sid[6] = {0};
char data0[EMU_KEY_STR_LEN] = {0};
char data1[EMU_KEY_STR_LEN] = {0};

if (type == 0)
{
    //B:004.0:W:11389:A:27500:00011:1A2B3C81C3B2A116:RTR                  
    sscanf(tok, "%*1c:%5c:%1c:%5c:%1c:%5c:%5c:%16c:%16c",
            longitude, &direction, frequency, &polar, symbol, sid, data0, data1);
}
else
{ 
    //P:105.5:E:03960:H:27500:CEF7CE422FF89E:E42DB81767E02E
    sscanf(tok, "%*1c:%5c:%1c:%5c:%1c:%5c:%14c:%14c",
            longitude, &direction, frequency, &polar, symbol, data0, data1);
}
pMinifs->symbol_rate = atoi(symbol);
.
.
.

注意:%s与%c的区别,注意buf的长度包含字符串结束符‘\0’,长度如果不对,后续处理可能出现问题。

强制内存转换处理格式化文本

针对格式化文本还有一种内存强制转换的处理方式:

struct DevData{
    char tag[2];
    char longitude[6];
    char direction[2];
    char frequency[6];
    char polar[2];
    char symbol_rate[6];
    char service_id[6];
    char key[KEY_STR_LEN+1];
    char prog_name[PROG_NAME_LENTH];
    char line_feed[2];//tail(0x0d0a or 0x0a)
} __attribute__((packed));

//获取文件数据
fd = fopen(path, "r");
p = (char *)mallocz(size);
fseek(fd, 0, SEEK_SET);
fread(fd, p, 1, size);

//强制转换
struct DevData *pRootData=NULL;
for(i=0;i<total_num;i++)
{
    pRootData = (struct DevData*)p1;
    .
    .
    //处理当前结构体pRootData,并获得偏移量用来取下一个结构体数据
    memset(atoi_buf,0,sizeof(atoi_buf));
    memcpy(atoi_buf,pRootData->symbol_rate,sizeof(pRootData->symbol_rate)-1);
    pMinifs->symbol_rate = atoi(atoi_buf);
    .
    p1 += offset;
}

正则表达式

特殊字符

\

转义字符,将下一字符标记为特殊字符、文本、反向引用或八进制转义符

'\n'匹配换行,
'\\'匹配'\'

^

匹配搜索字符串开始的位置。如果标志中包括 m(多行搜索)字符,^ 还将匹配 \n 或 \r 后面的位置。
如果将 ^ 用作括号表达式中的第一个字符,则会对字符集求反。

^\d{3} 与搜索字符串开始处的 3 个数字匹配。
[^abc] 与除 a、b 和 c 以外的任何字符匹配。

$

匹配搜索字符串结尾的位置。 如果标志中包括 m(多行搜索)字符,^ 还将匹配 \n 或 \r 前面的位置。

\d{3}$ 与搜索字符串结尾处的 3 个数字匹配。

*

零次或多次匹配前面的字符或子表达式。等效于 {0,}。

zo\* 与“z”和“zoo”匹配。

+

一次或多次匹配前面的字符或子表达式。 等效于 {1,}。

zo+ 与“zo”和“zoo”匹配,但与“z”不匹配。

?

零次或一次匹配前面的字符或子表达式。 等效于 {0,1}。
当 ? 紧随任何其他限定符(*、+、?、{n}、{n,} 或 {n,m})之后时,匹配模式是非贪婪的。
非贪婪模式匹配搜索到的、尽可能少的字符串, 而默认的贪婪模式匹配搜索到的、尽可能多的字符串。

zo? 与“z”和“zo”匹配,但与“zoo”不匹配。
o+? 只与“oooo”中的单个“o”匹配,而 o+ 与所有“o”匹配。
do(es)? 与“do”或“does”中的“do”匹配。

.

匹配除换行符 \n 之外的任何单个字符。 若要匹配包括 \n 在内的任意字符,请使用诸如 [\s\S] 之类的模式。

a.c 与“abc”、“a1c”和“a-c”匹配

[]

标记括号表达式的开始和结尾。

[1-4] 与“1”、“2”、“3”或“4”匹配。 [^aAeEiIoOuU] 与任何非元音字符匹配

{}

标记限定符表达式的开始和结尾。

a{2,3} 与“aa”和“aaa”匹配

()

标记子表达式的开始和结尾。 可以保存子表达式以备将来之用。

A(\d) 与“A0”至“A9”匹配。 保存该数字以备将来之用

|

指示在两个或多个项之间进行选择。

z|food 与“z”或“food”匹配。 (z|f)ood 与“zood”或“food”匹配

元字符

\b

与一个字边界匹配;即字与空格间的位置。

er\b 与“never”中的“er”匹配,但与“verb”中的“er”不匹配

\B

非边界字匹配。

er\B 与“verb”中的“er”匹配,但与“never”中的“er”不匹配

\d

数字字符匹配。 等效于 [0-9]。

在搜索字符串“12 345”中,\d{2} 与“12”和“34”匹配。 \d 与“1”、“2”、“3”、“4”和“5”匹配。

\D

非数字字符匹配。 等效于 [^0-9]。

\D+ 与“abc123 def”中的“abc”和“def”匹配。

\w

与以下任意字符匹配:A-Z、a-z、0-9 和下划线。 等效于 [A-Za-z0-9_]。

在搜索字符串“The quick brown fox…”中,\w+ 与“The”、“quick”、“brown”和“fox”匹配。

\W

与除 A-Z、a-z、0-9 和下划线以外的任意字符匹配。 等效于 [^A-Za-z0-9_]。

在搜索字符串“The quick brown fox…”中,\W+ 与“…”和所有空格匹配。

[xyz]

字符集。 与任何一个指定字符匹配。

[abc] 与“plain”中的“a”匹配。

[^xyz]

反向字符集。 与未指定的任何字符匹配。

[^abc] 与“plain”中的“p”、“l”、“i”和“n”匹配。

[a-z]

字符范围。 匹配指定范围内的任何字符。

[a-z] 与“a”到“z”范围内的任何小写字母字符匹配。

[^a-z]

反向字符范围。 与不在指定范围内的任何字符匹配。

[^a-z] 与不在范围“a”到“z”内的任何字符匹配。

{n}

正好匹配 n 次。 n 是非负整数。

o{2} 与“Bob”中的“o”不匹配,但与“food”中的两个“o”匹配。

{n,}

至少匹配 n 次。 n 是非负整数。

* 与 {0,} 相等。
+ 与 {1,} 相等。
o{2,} 与“Bob”中的“o”不匹配,但与“foooood”中的所有“o”匹配。

{n,m}

匹配至少 n 次,至多 m 次。 n 和 m 是非负整数,其中 n <= m。 逗号和数字之间不能有空格。

? 与 {0,1} 相等。
在搜索字符串“1234567”中,\d{1,3} 与“123”、“456”和“7”匹配。

(pattern)

与pattern 匹配并保存匹配项。 若要匹配括号字符 ( ),请使用“(”或者“)”。

(Chapter|Section) [1-9] 与“Chapter 5”匹配,保存“Chapter”以备将来之用。

(?:pattern)

与pattern 匹配,但不保存匹配项;即不会存储匹配项以备将来之用。 这对于用“or”字符 (|) 组合模式部件的情况很有用。

industr(?:y|ies) 与 industry|industries 相等。

(?=pattern)

正预测先行。 找到一个匹配项后,将在匹配文本之前开始搜索下一个匹配项。 不会保存匹配项以备将来之用。

^(?=.\*\d).{4,8}$ 对密码应用以下限制:其长度必须介于 4 到 8 个字符之间,并且必须至少包含一个数字。

在该模式中,.*\d 查找后跟有数字的任意多个字符。 对于搜索字符串“abc3qr”,这与“abc3”匹配。
从该匹配项之前(而不是之后)开始,.{4,8} 与包含 4-8 个字符的字符串匹配。 这与“abc3qr”匹配。
^ 和 $ 指定搜索字符串的开始和结束位置。 这将在搜索字符串包含匹配字符之外的任何字符时阻止匹配。

(?!pattern)

负预测先行。 匹配与模式 不匹配的搜索字符串。 找到一个匹配项后,将在匹配文本之前开始搜索下一个匹配项。 不会保存匹配项以备将来之用。

\b(?!th)\w+\b 与不以“th”开头的单词匹配。

在该模式中,\b 与一个字边界匹配。 对于搜索字符串“ quick ”,这与第一个空格匹配。 (?!th) 与非“th”字符串匹配。 这与“qu”匹配。
从该匹配项开始,\w+ 与一个字匹配。 这与“quick”匹配。

\cx

匹配 x 指示的控制字符。 x 的值必须在 A-Z 或 a-z 范围内。 如果不是这样,则假定 c 就是文本“c”字符本身。

\cM 与 Ctrl+M 或一个回车符匹配。

\xn

匹配 n,此处的 n 是一个十六进制转义码。
十六进制转义码必须正好是两位数长。 允许在正则表达式中使用 ASCII 代码。

\x41 与“A”匹配。 \x041 等效于后跟有“1”的“\x04”(因为 n 必须正好是两位数)。

\num

匹配 num,此处的 num 是一个正整数。 这是对已保存的匹配项的引用。

(.)\1 与两个连续的相同字符匹配。

\n

标识一个八进制转义码或反向引用。 如果 \n 前面至少有 n 个捕获子表达式,那么 n 是反向引用。
否则,如果 n 是八进制数 (0-7),那么 n 是八进制转义码。

(\d)\1 与两个连续的相同数字匹配。

\nm

标识一个八进制转义码或反向引用。 如果 \nm 前面至少有 nm 个捕获子表达式,那么 nm 是反向引用。
如果 \nm 前面至少有 n 个捕获子表达式,则 n 是反向引用,后面跟有文本 m。
如果上述情况都不存在,当 n 和 m 是八进制数字 (0-7) 时,\nm 匹配八进制转义码 nm。

\11 与制表符匹配。

\nml

当 n 是八进制数字 (0-3),m 和 l 是八进制数字 (0-7) 时,匹配八进制转义码 nml。

\011 与制表符匹配。

\un

匹配 n,其中 n 是以四位十六进制数表示的 Unicode 字符。

\u00A9 与版权符号 (©) 匹配。

非打印字符

  • \f 换页符。 \x0c 和 \cL
  • \n 换行符。 \x0a 和 \cJ
  • \r 回车符。 \x0d 和 \cM
  • \s 任何空白字符。 其中包括空格、制表符和换页符。 [ \f\n\r\t\v ]
  • \S 任何非空白字符。 [^ \f\n\r\t\v]
  • \t Tab 字符。 \x09 和 \cI
  • \v 垂直制表符。 \x0b 和 \cK
  1. sscanf的高级用法
  2. C语言函数sscanf()的用法
  3. scanf的正则表达式总结
  4. scanf、sscanf中的正则表达式

在调试过程中经常需要分析打印、数据等等,在大量文本中仅仅需要提取需要关心的部分,使用用例如下:

删除匹配行:

%!grep -v "cscope"
%!sed '/cscope/'d

只保留匹配行:

%!grep "cscope"

grep、sed可以使用正则表达式来完成复杂匹配。

gdb调试过程中从内存中dump出相关数据用于分析

help dump

在gdb中输入:

(gdb) help dump
Dump target code/data to a local file.

List of dump subcommands:

dump binary -- Write target code/data to a raw binary file
dump ihex -- Write target code/data to an intel hex file
dump memory -- Write contents of memory to a raw binary file
dump srec -- Write target code/data to an srec file
dump tekhex -- Write target code/data to a tekhex file
dump value -- Write the value of an expression to a raw binary file

Type "help dump" followed by dump subcommand name for full documentation.
Type "apropos word" to search for commands related to "word".
Command name abbreviations are allowed if unambiguous.

dump [格式] memory 文件名 起始地址 结构地址 #   把指定内存段写到文件
dump [格式] value 文件名 表达式             #   把指定值写到文件

格式包括:

  • binary 原始二进制格式
  • ihex intel 16进制格式
  • srec S-recored格式
  • tekhex tektronix 16进制格式

命令具体参数格式:

dump binary memory filename start_addr end_addr
    Dump contents of memory from start_addr to end_addr into raw binary format file filename.
dump binary value filename expression
    Dump value of expression into raw binary format file filename.
dump ihex memory filename start_addr end_addr
    Dump contents of memory from start_addr to end_addr into intel hex format file filename.
dump ihex value filename expression
    Dump value of expression into intel hex format file filename.
dump srec memory filename start_addr end_addr
    Dump contents of memory from start_addr to end_addr into srec format file filename.
dump srec value filename expression
    Dump value of expression into srec format file filename.
dump tekhex memory filename start_addr end_addr
    Dump contents of memory from start_addr to end_addr into tekhex format file filename.
dump tekhex value filename expression
    Dump value of expression into tekhex format file filename.

用法

(gdb) dump binary memory file $1 $2         //$1 $2为地址
(gdb) dump binary memory ./dump s1 s1+5     //s1为数组
(gdb) dump memory file 0x9000xxxx 0x9001xxxx     //s1为数组

通过以上command来完成内存对比。

  1. Copy between memory and a file
  2. gdb 内存复制到/从文件

安装

两种安装方式:

  1. 官网下载源码编译安装
  2. sudo apt-get install astyle

配置

两种配置方式:

  1. 配置文件~/.astylerc
  2. 执行命令携带参数

配置文件例子如下:

--style=linux                  # -A8
--indent=force-tab             # -T,  强制 TAB 缩进
--break-blocks                 # -f,  Pad empty lines around header blocks (e.g. 'if', 'for', 'while'...)
--attach-namespaces            # -xn, Attach brackets to a namespace statement
--attach-classes               # -xc, Attach brackets to a class statement
--delete-empty-lines           # -xe, 删除函数内多余的空行
--align-pointer=name           # -k3, *号靠近变量名
--remove-brackets              # -xj, if,while,for 等代码是单行行时,去掉 {}
--close-templates              # -xy, 模板中无空格, 如:Stack<int,List<int>> stack1;
--pad-oper                     # -p,  运算符两边加空格
--indent-preproc-define        # -w,  宏定义缩进
--indent-col1-comments         # -y,  注释缩进
--unpad-paren                  # -U,  Remove extra space padding around parenthesis on the inside and outside
--pad-header                   # -H,  关键字后加空格
--break-after-logical          # -xL
--lineend=linux                # -z2
--indent=tab                   # -t

执行参数例子如下:

-A8xnxcxek3xyfTpEwyUHxLz3t

命令

自动排版命令:

indent -npsl
astyle -A8xnxcxek3xyfTpEwyUHxLz3t
find -name "*.[ch]*" | xargs indent -npsl | xargs astyle -A8xnxcxek3xyfTpEwyUHxLz3t

与vim结合

最终成品如下:

function! CodeFormat()
    ks
    "let curfile = expand("%")
    "let curfile = expand("%:t")
    "echo curfile
    silent! %s/^M//g
    silent! exec '%!astyle -A3LYfpjk3NSEUHwyW3xC100 --style=break'
    silent! %g/^\s*$\n\s*$/d
    silent! %s/\s\+$//g
    's
endfunction

autocmd BufNewFile,BufRead *.c call CodeFormat()
autocmd BufNewFile,BufRead *.h call CodeFormat()
nmap <leader><leader>q :call CodeFormat()<CR>

解析如下:

  • ks ‘s 保存当前行位置
  • expand(“%”) 取file变量
  • expand(“%:t”) 取文件名
  • silent! 静默执行命令
  • %s/^M//g 将
    删除,相当于dos2unix
  • exec 执行命令
  • %!astyle -A3LYfpjk3NSEUHwyW3xC100 –style=break 携带参数执行外部命令
  • %g/^\s$\n\s$/d 删除多行空行为一行
  • %s/\s+$//g 删除行尾空格
  • autocmd BufNewFile,BufRead *.c 打开c文件时,自动调用函数CodeFormat()
  • nmap 映射快捷键

astyle参数解析:

  • -A3: Kernighan & Ritchie style uses linux brackets
  • -L: 标签缩进
  • -Y: 注释缩进
  • -f: 空行分隔没有关系的块,类,标签(不包括函数块)
  • -p: 操作符两端插入一个空格
  • -j: if,while,for 等代码是单行行时,加 {}
  • -k3: *号靠近变量名
  • -N: 缩进命名空间定义行
  • -S: switch 与case不同列,case缩进
  • -E: 块间空行的换行符前插入一个空格
  • -U: 移除括号两端多余空格
  • -H: 关键字后加空格
  • -w: 宏定义缩进
  • -y: else catch左边的大括号与else catch分隔
  • -W3: &号靠近变量名
  • -xC100: 代码最大长度100,超过之后进行换行

Ref

  1. 使用astyle美化代码
  2. Vim整合AStyle进行代码美化
  3. Astyle编程语言格式化工具的中文说明
  4. Astyle:代码格式化工具简明指南
  5. Artistic Style 3.1