linux内核调试指南

 

linux内核调试指南 

  http://blog.csdn.net/adcxf/archive/2008/09/13/2924735.aspx

本文档由大家一起自由编写,修改和扩充,sniper负责维护。引用外来的文章要注明作者和来处。本文档所有命令都是在ubuntu/debian下的操作。选取的内核源码是文档开始编写时最新的内核版本–2.6.26。

[方括号内的文字是旁白,说明需要扩展哪些内容,有何建议,等。也就说,文档完成后可以删除掉的内容,才可以放在方括号内。除此外的其他用法都是不对的]

一些前言

你能做什么

1. 如果你认为哪部分解释不充分,有疑问,可以在那部分旁边加个方括号,给出你的疑问。比如 [??太简短,希望扩展]

2. 如果认为哪里有错误,照样如上..

3. 如果认为需要加上某种内容,就加上大标题。有人可能就会扩展该内容

4. 发现了很好参考文章,可以奉献出它的网址

5. 当然,更欢迎你亲自动手,修改或扩展完善本文档。

6. 总之,没任何限制。我能操作的,你也可以。

知识从哪里来

1. 永远不要忘记的三大帮助命令

  • XXX -h(xxx –help)
  • man -a XXX
  • info XXX

2. 如何安装帮助文档

  • $ sudo synaptic 界面出来后,在“组别”->“文档”选取你要的文档进行安装
  • 或$ apt-cache search Documentation | grep XXX 搜索需要的文档进行安装

3. 从软件/工具的官方网站阅读/下载文档

4. 从irc获取帮助 irc.freenode.net

5. 从邮件列表获取帮助 mailist http://lkml.org/ http://marc.info/

6. 发行版社区文档或社区 https://help.ubuntu.com/community/ http://wiki.ubuntu.org.cn/

7. 利用google搜索文档或阅读他人文章

8. 获取内核文档

  • 源码本身
  • 源码中的注释
  • 内核源码附带的文档 Documentation
  • 相关的教科书
  • 论文 免费论文引擎 http://citeseerx.ist.psu.edu/
  • 内核子系统的官方网站
  • 获取内核源码目录Documentation/DocBook/ 下已经编译好的书籍
找到最新版本的文档
$ apt-cache search linux-doc
安装最新的文档
$ sudo apt-get install linux-doc-2.6.24
阅读Documentation/DocBook/ 下已经编译好的书籍(html格式)
$ firefox /usr/share/doc/linux-doc-2.6.24/html/index.html

9. 买书

10. 书籍最后面的参考书目

11. 文章末尾的参考文章

本文档内容的界定

1. 主要地,本文档聚焦于描述如何利用gdb对内核进行源码级别和汇编级别的观察和调试。

而这种调试的目的有两个:

  • 确定bug产生的引入点。这部分内容放于本文档第一部分。
  • 配合源码阅读工具(source insight,kscope等),观察内核实时运行的状况,观察内核数据的产生和变化,从而以一种精确的动态的和验证性的方式来理解内核运作的原理。这部分内容放于本文档第二部分

前者是调试器应用的主要价值,而后者却是本文档的兴趣所在。

2. 因为需要观察用户层和内核层的交互,演示调试工具的全面功能等原因,本文档内容不完全局限于内核层。

3. 另外,为了提供内核调试知识的全面叙述,我们对其他调试工具,其他调试的问题比如检测内存泄露等内容,也会进行说明。此部分内容放于本文档的第三部分。

为什么需要汇编级调试

  • 逆向工程的需要

例子1:NT 内核的进程调度分析笔记 http://www.whitecell.org/list.php?id=11

例子2: NT 下动态切换进程分析笔记 http://www.whitecell.org/list.php?id=13

在windows的世界里,内核源码和具体原理是不公开的。但很多牛人就凭一个破烂调试器阅读反汇编代码就能得到内部真相,可见调试器汇编级调试威力之大。但是在linux是源码公开的情况下,就没必要干那样的辛苦活了。但是因为以下原因,汇编级调试还是必要的。

  • 汇编比C语言更低层

有时(比如代码优化)情况下,因为C代码经过了编译器的处理,调试器在c源码调试这个级别下给出的信息是无法理解的,甚至看起来是错误的。但是如果直接对调试器给出的反汇编代码进行分析,就不会受到那类问题的束缚。也就是说,进行汇编级别的调试能最大程度的利用调试器的功能。

  • 汇编是C语义的解释

当你对某句C语言不是很理解时,看看编译器是怎么想的,是个很不错的办法。

  • 能锻炼汇编源码的阅读能力

另一方面,内核中本来存在很多汇编源代码,进行汇编级调试也是锻炼阅读汇编源码能力的最有效方法。

当然,汇编级调试虽然强大,但代价也是很昂贵。和源码级调试相比,分析汇编代码花的时间要多上几十倍。所以,在源码公开的情况下,应该以源码级调试为主,特殊情况下才需要汇编级调试。

调试器的局限性

[待扩展] 无非有两大类

1.无法提供信息

2.提供了错误信息

(gdb) bt
#0 sys_mount (dev_name=0xc01fae00 "sysfs", dir_name=0xc01fadf8 "/sys", type=0xc01fae00 "sysfs", flags=0, data=0x0) at fs/namespace.c:1548
#1 0xc00204f0 in name_to_dev_t (name=0xc001df70 "/dev/ram0") at init/do_mounts.c:146
#2 0xc000916c in prepare_namespace () at init/do_mounts.c:430
#3 0xc002019c in init (unused=<value optimized out>) at init/main.c:756
#4 0xc003aa88 in sys_waitid (which=0, pid=-1071665672, infop=0xc01fae00, options=-1007657176, ru=Cannot access memory at address 0x4
) at kernel/exit.c:1634
Backtrace stopped: previous frame inner to this frame (corrupt stack?)

(gdb) bt
#0 sys_read (fd=0, buf=0xbee47b4b "l", count=1) at fs/read_write.c:357
#1 0xc0020d40 in __switch_to ()
Backtrace stopped: frame did not save the PC
(gdb)


(gdb) s
Cannot find bounds of current function
(gdb) n
Cannot find bounds of current function
(gdb) f
No function contains program counter for selected frame.
(gdb) bt
#0 0x000744c8 in ?? ()
(gdb) s
Cannot find bounds of current function
(gdb) bt
#0 0x000744c8 in ?? ()
(gdb)

本文档的特征

1.涉及操作的章节部分,步骤清晰,解释充分又简洁,这里没人会付稿费给你;

2.不重复做功夫,网上有了的内容,如果不需要大的修改或大的提炼,把网址粘上就可以了,记得加上标题,以免网址失效。

3.参考网址极其多,我一直认为一本书最重要部分是它列出的参考书目

总之,用最短的时间,最短的篇幅,最少的付出,向您提供最有用最详细的信息, 这就是我们的目标。所以,如果有的章节只有资料的网址链接,这意味着该节还没展开,或者甚至是意味着,你必须打开并浏览该网址,否则下面的内容可能看不懂。

***第一部分:基础知识***

建立调试环境

选择合适的战场

为什么选debian

[如题] http://www.debian.org/ http://www.emdebian.org/

为什么本人选择debian?因为:引用内容来之www.debian.org

“Debian 计划 是一个致力于创建一个自由操作系统的合作组织。...屁话省略...屁话..N多屁话之后: 当然,人们真正需要的是应用软件,也就是帮助他们完成工作的程序: 从文档编辑,到电子商务,到游戏娱乐,到软件开发。Debian 带来了超过 18733 个 软件包 (为了能在您的机器上轻松的安装,这些软件包都已经被编译包装为一种方便的格式) — 这些全部都是 自由 软件。”

原因终于看到了,选择debian是因为本人比较懒,比较笨。而debian正好迎合了我这种人的需求。

1. 它”带来了超过 18733 个 软件包”。18733这个数目非常不直观,而且或许是N年前的数据了。我们可以到debian的ftp看看,现在它可供安装的软件和工具达到了5个DVD的容量。难以想象,在这5个DVD容量的工具库中,还会找不到我所想要的东西。

2. debian有一个非常出名的安装包管理机制。你需要做的就是,打开“立新得”软件,然后在一个小方框里写上你需要东西的相关信息,然后再点点一个叫做“搜索”的小方块。接着,debian就会在它5个DVD大的工具库中寻找你想要的工具。在结果返回后,选择好你的工具,再点点一个叫做“应用”的小方块,过一会,就可以使用你的工具了。

再也没有了“缺少什么什么包”的烦人提示了,一切都这么简单,又这么强大。这,正是我想要的。

debian与ubuntu

[两者区别,版本外号,支持社区,source list等] 1. ubuntu的易用性比debian要好。尤其是中文支持,还有ubuntu国内有活跃的社区。 2. 虽然ubuntu是基于debian的,apt 软件库也能获取到debian的软件,但它毕竟是不同的系统环境,理念不同,对于一些偏门或太旧或太新的软件时,ubuntu往往不支持,安装不了。比如,gcc-3.4-arm-linux-gnu这个包,发行时间已久,ubuntu下安装不了,但在debian下则可以。 http://www.ubuntu.com/community/ubuntustory/debian

如不特别说明,本文档所有命令都是在ubuntu Hardy Heron8.04版本 和debian testing版本下的操作。

从0安装debian

[如果想领教古典linux相对于windows的特色,请安装一次debian吧。尽管和以前比,已经很智能了。但安装了debian,选了中文环境,发现汉字都是歪歪倒倒的。而且没有汉字输入法,装了汉字输入法后,却用不了。不知道是我笨还是程序有bug.所以不得不用英文写下本烂文,怕把安装过程给忘了。需要翻译回中文]

How to install and configure a debian system from zero

1.install the system with one CD

Download CD iso file from debian official website, and burn it into a CD. Note that, we can just download the first CD iso but not DVDs or the whole serials of CDs, because the first CD has already contained all the basis components of dedian system and many other most common applications. We can use the first CD to install debian system, and then to install some other needed programs from it if needed. In this way, you can save much time spent on touching many inrelatived things.

2.install application & tool from CD

ou can install some common apllications from the CD with the following commnad: apt-get install expected-application. Why can we do that without any more configuration? Why is it not need to has a ability to access internet? Well, Let’s look at the file named sourse.list which idenifying where to get software’s pakage?? deb cdrom:[Debian GNU/Linux testing _Lenny_ - Official Snapshot i386 CD Binary-1 20080605-15:01]/ lenny main It means that system try to get somethig from your CD, so obviously that you can get some the most common but not all the tools available in debian official apllication repository.

3.try to access the internet

Thank to the first CD, we can do that easily. Fist, install the tool ppp contained in CD and its’ configuration tool pppoeconfig. All these steps are described in file ADSL(PPPOE)接入指南.txt

4.search any useful information through the internet

now, we have built a base debian system, but it is too simple. I want to do some some thing, for example, to chat with some other people with pidgin, but it is not contained in the first CD, which just downloaded by you. And you may want to search some helps with google,etc. Just to do it, google is a most useful tool.

5.search the internet updating source

I think you have get much thing through the google. But the most important thing is to get a available update source for your system, and change the source.list–that is /etc/apt/source.list. Now, I have got a good one, and it seems good. Don’t forget to turn on the security entry in the orgion file source.list. That file looks like following after my updataion:

#deb cdrom:[Debian GNU/Linux testing _Lenny_ - Official Snapshot i386 CD Binary-1 20080605-15:01]/ lenny main
deb http://ftp.debian.org/debian/ lenny main contrib non-free
deb http://security.debian.org/ lenny/updates main
deb-src http://security.debian.org/ lenny/updates main

You should note that the internet address is debian office’s, but It takes some while to get it. And my searching tool is google. :) Oh, we shoul run a command to update the new configuration to system before using it, don’t ferget: apt-get update

6.get help from IRC

Well, we have already been able to get some applications or tools from internet with command apte-get or wget,etc.. But I think the first thing to do is to get and install a very valuable tool named pidgin which can bring you into IRC world. Because Many experiance and kind person live in channel #debian of irc.freenode.net. You can get help from it very quickly. How to configure pidgin? Sorry, I don’t like to answer such a problem , please just to google it or try it by yourselft. I am not so kind as some guys living in IRC : )

7.get and install synaptic

If you ever used ubuntu, you should agree that synaptic is good tool to update you system. It can save you much time of searching tools, typing commnad, or managing the downloaded tools. But Unfortunately, such a important tool is not installed in the default system, and it is not contained in the first CD. So, We can just to get it with command “apt-get install synaptic”. After doing that successfully, I don’t want to type that command anymore. It’s so tedious to me.

8.get more tools with the help of synaptic

synaptic is my GOD in the linux world. Without it, I will become crazy. But now, I have owned it, so I can fly very freely in the internet sky. Just to search any tools and to update your system. And now, the CD used to install debian can be discarded, if you will never reinstall or rescure the system with it in future.

Now, the sun has raise up, and you have found the road to reback to civilization. Why? Just to ask your google and synaptic. :)

debian重要命令

[来源]《APT and Dpkg 快速参考表》 http://i18n.linux.net.cn/others/APT_and_Dpkg.php

建立编译环境

$ sudo apt-get install build-essential autoconf automake1.9 cvs subversion

其余的根据出错的提示,利用“立新得”搜索,然后$ sudo apt-get install XXX 进行安装。没有“立新得”界面程序的可以利用$ apt-cache search XXX 来搜索,然后安装。

安装交叉编译工具

交叉编译工具下载网址

下面是几个交叉编译工具下载网址,需要手动安装时,对比一下编译器的名称可以找到合适的下载地址。debian维护有自己的已经打包成.deb形式安装包,在debian软件库中。

http://www.codesourcery.com/gnu_toolchains/arm/download.html
(据说是arm公司推荐的)

Download Sourcery G++ Lite Edition for ARM

Target OS Download
EABI Sourcery G++ Lite 2008q1-126
All versions...
uClinux Sourcery G++ Lite 2008q1-152
All versions...
GNU/Linux Sourcery G++ Lite 2008q1-126
All versions...
SymbianOS Sourcery G++ Lite 2008q1-126
All versions...

到底是选EABI还是GNU/LINUX呢?应该是后者....

点GNU/LINUX的连接进去,可看到

Download MD5 Checksum
IA32 GNU/Linux Installer 93eee13a08dd739811cd9b9b3e2b3212
IA32 Windows Installer fac5b0cee1d9639c9f15e018e6d272ad

Documentation

Title Format
Assembler (PDF) PDF
Binary Utilities (PDF) PDF
C Library (GLIBC) (PDF) PDF
Compiler (PDF) PDF
Debugger (PDF) PDF
Getting Started Guide (PDF) PDF
Linker (PDF) PDF
Preprocessor (PDF) PDF
Profiler (PDF) PDF

Advanced Packages

Expert users may prefer packages in these formats.

Download MD5 Checksum
IA32 GNU/Linux TAR 4f11b0fa881864f220ab1bd84666108b
IA32 Windows TAR ed6d25fd68301e728a1fba4cd5cb913f
Source TAR 2db28fb2aa80134e7d34d42b7039d866

名字标识不是很明显,进去看才知道。比如,IA32 GNU/Linux Installer对应的安装包
名字叫arm-2008q1-126-arm-none-linux-gnueabi.bin
为什么有个none?迷茫中..
---------------------------------
http://ftp.snapgear.org:9981/pub/snapgear/tools/arm-linux/
[DIR] Parent Directory 30-Sep-2003 15:44 -
[ ] arm-linux-tools-20031127.tar.gz 26-Nov-2007 16:56 141M
[ ] arm-linux-tools-20051123.tar.gz 24-Nov-2005 00:50 228M
[ ] arm-linux-tools-20061213.tar.gz 13-Dec-2006 13:31 230M
[ ] arm-linux-tools-20070808.tar.gz 30-Nov-2007 03:21 271M
[ ] binutils-2.16.tar.gz 16-Nov-2005 15:44 15.6M
[ ] binutils-2.17.tar.gz 06-Dec-2007 10:24 17.4M
[ ] build-arm-linux-3.4.4 02-Aug-2006 14:32 6k
[ ] build-arm-linux-4.2.1 30-Jul-2008 10:13 7k
[ ] elf2flt-20060707.tar.gz 17-Jan-2008 22:23 101k
[ ] elf2flt-20060708.tar.gz 30-Jul-2008 10:14 110k
[ ] gcc-3.4.4.tar.bz2 16-Nov-2005 15:39 26.3M
[ ] gcc-4.2.1.tar.bz2 06-Dec-2007 10:11 42.0M
[ ] genext2fs-1.3.tar.gz 03-Sep-2003 10:23 19k
[ ] glibc-2.3.3.tar.gz 16-Nov-2005 15:49 16.7M
[ ] glibc-2.3.6.tar.gz 06-Dec-2007 10:39 17.9M
[ ] glibc-linuxthreads-2.3.3.tar.gz 16-Nov-2005 15:49 303k
[ ] glibc-linuxthreads-2.3.6.tar.gz 06-Dec-2007 10:39 320k
--------------------------
http://www.handhelds.org/download/projects/toolchain/
[DIR] Parent Directory -
[ ] README 28-Jul-2004 17:37 788
[DIR] archive/ 28-Jul-2004 17:34 -
[ ] arm-linux-gcc-3.3.2.tar.bz2 03-Nov-2003 10:23 71M
[ ] arm-linux-gcc-3.4.1.tar.bz2 29-Jul-2004 14:01 41M
[DIR] beta/ 28-Jul-2004 17:36 -
[ ] crosstool-0.27-gcc3.4.1.tar.gz 28-Jul-2004 17:21 2.0M
[ ] gcc-build-cross-3.3 31-Oct-2003 15:43 5.1K
[DIR] jacques/ 24-Jul-2001 18:45 -
[ ] kernel-headers-sa-2.4.19-rmk6-pxa1-hh5.tar.gz 12-Mar-2003 17:42 4.7M
[DIR] monmotha/ 13-Aug-2002 17:54 -
[DIR] osx/ 14-Dec-2003 11:45 -
[DIR] pb/ 22-Nov-2002 20:10 -
[DIR] source/ 18-Mar-2004 16:12 -
------------------------------------
http://ftp.arm.linux.org.uk/pub/armlinux/toolchain/
[DIR] Parent Directory -
[ ] Oerlikon-DevKit-XScalev2.tar.gz 07-Feb-2003 22:30 3.7K
[ ] cross-2.95.3.tar.bz2 20-Jul-2001 21:12 35M
[ ] cross-3.0.tar.bz2 20-Jul-2001 22:27 39M
[ ] cross-3.2.tar.bz2 23-Aug-2002 11:04 81M
[ ] cross-3.2.tar.gz 23-Aug-2002 10:01 93M
[DIR] src-2.95.3/ 14-Jan-2002 17:52 -
[DIR] src-3.2/ 23-Aug-2002 10:53 -
--------------------------------------------
http://linux.omap.com/pub/toolchain/
[DIR] Parent Directory -
[ ] obsolete-gcc-3.3.2.t..> 15-May-2004 12:18 76M
---------------------------
http://www.uclinux.org/pub/uClinux/arm-elf-tools/
To install the Linux binaries, login as root and run "sh ./XXX-elf-tools-20030314.sh".

m68k-elf-20030314/arm-elf-20030314
Get the m68k binaries or the ARM binaries. The source is here.

m68k-elf-20020410/arm-elf-20011219
Get the m68k binaries or the ARM binaries. The source is here.

m68k-elf-20020218/arm-elf-20011219
Get the m68k binaries or the ARM binaries. The source is here.

m68k/arm-elf-20011219
Get the m68k binaries or the ARM binaries. The source is here.

You can also get Bernhard Kuhn's RPMs here.

m68k-elf-20010716
Get the binaries here and the source from here.

m68k-elf-20010712
Get the binaries here and the source from here.

m68k-elf-20010610
Get the binaries here and the source from here.

m68k-elf-20010228
The binaries are in two files, the compilers and the g++ headers. The source is here.
安装arm-linux-gnueabi-XXX 工具集

[参考]http://www.emdebian.org/tools/crosstools.html

步骤:

1. 往/etc/apt/sources.list文件加入下面软件源

deb http://buildd.emdebian.org/debian/ unstable main
deb-src http://buildd.emdebian.org/debian/ unstable main
deb http://buildd.emdebian.org/debian/ testing main
deb-src http://buildd.emdebian.org/debian/ testing main

别忘了更新一下: $sudo apt-get update

2. 安装交叉编译器

$ sudo apt-get install libc6-armel-cross libc6-dev-armel-cross binutils-arm-linux-gnueabi gcc-4.3-arm-linux-gnueabi g++-4.3-arm-linux-gnueabi

[注,在ubuntu下,只能安装4.2版。]

3. 安装交叉调试器

$sudo apt-get install arm-linux-gnueabi-gdb

注意: ubuntu下,arm-linux-gnueabi-gdb和gdb有冲突。解决方法:需要使用arm-linux-gnueabi-gdb时先卸载gdb,记下卸载gdb时与gdb一起被卸载的软件名,然后安装arm-linux-gnueabi-gdb。 想换回gdb时,在反操作。apt-install remove arm-linux-gnueabi-gdb 然后 apt-get install gdb和gdb一起被卸载包。可以写个脚本自动完成这些操作

什么是EABI

答: 来自AAPCS

ABI: Application Binary Interface:

1). The specifications to which an executable must conform in order to execute in a specific execution environment. For example, the Linux ABI for the ARM Architecture.

2). A particular aspect of the specifications to which independently produced relocatable files must conform in order to be statically linkable and executable. For example, the C++ ABI for the ARM Architecture, the Run-time ABI for the ARM Architecture, the C Library ABI for the ARM Architecture.

ARM-based … based on the ARM architecture …

EABI: An ABI suited to the needs of embedded (sometimes called free standing) applications.

参考:

ABI/EABI/OABI http://blog.csdn.net/hongjiujing/archive/2008/07/21/2686556.aspx

Re: 关于kernel ARM_EABI http://zh-kernel.org/pipermail/linux-kernel/2008-January/002793.html

Why ARM’s EABI matters http://www.linuxdevices.com/articles/AT5920399313.html

Why switch to EABI? http://www.applieddata.net/forums/topic.asp?TOPIC_ID=2305

ArmEabiPort http://wiki.debian.org/ArmEabiPort

安装arm-elf-XXX 工具集

1. 依据要求搜索下载相应的arm-elf-tools安装包。比如arm-elf-tools-20030315.sh

2. 安装: $ ./arm-elf-tools-20030315.sh

3. 如果,该安装包年代过老,比如arm-elf-tools-20030315.sh,会出现下面的错误提示 “tail: 无法打开“ 43” 读取数据: 没有那个文件或目录。”。 这时需要修改安装包源码。方法:vi arm-elf-tools-20030315.sh, 搜索tail,在它后面加 -n .比如 把tail ${SKIP} ${SCRIPT} | gunzip | tar xvf -改成如下:tail -n ${SKIP} ${SCRIPT} | gunzip | tar xvf -

4.如何卸载已安装的arm-elf-tools? 答,重新安装一次,注意看终端提示。或直接vi arm-elf-tools-20030315.sh,看脚本的内容,

bin工具集的使用

[该怎么称呼这类工具?待详述]

arm-elf-addr2line   arm-elf-elf2flt     arm-elf-gdb         arm-elf-objdump     arm-elf-size
arm-elf-ar arm-elf-flthdr arm-elf-ld arm-elf-protoize arm-elf-strings
arm-elf-as arm-elf-g++ arm-elf-ld.real arm-elf-ranlib arm-elf-strip
arm-elf-c++ arm-elf-gasp arm-elf-nm arm-elf-readelf arm-elf-unprotoize
arm-elf-c++filt arm-elf-gcc arm-elf-objcopy arm-elf-run

arm-linux-gnueabi-addr2line arm-linux-gnueabi-g++ arm-linux-gnueabi-gprof arm-linux-gnueabi-readelf
arm-linux-gnueabi-ar arm-linux-gnueabi-g++-4.2 arm-linux-gnueabi-ld arm-linux-gnueabi-size
arm-linux-gnueabi-as arm-linux-gnueabi-gcc arm-linux-gnueabi-nm arm-linux-gnueabi-strings
arm-linux-gnueabi-c++filt arm-linux-gnueabi-gcc-4.2 arm-linux-gnueabi-objcopy arm-linux-gnueabi-strip
arm-linux-gnueabi-cpp arm-linux-gnueabi-gdb arm-linux-gnueabi-objdump
arm-linux-gnueabi-cpp-4.2 arm-linux-gnueabi-gdbtui arm-linux-gnueabi-ranlib

如何获取这些工具的命令选项? 看章节“知识从哪里来” 一般是用命 xxxxxx –help就能得到简单的命令选项列表

下载arm-linux-gnueabi- 手册地址 http://www.codesourcery.com/gnu_toolchains/arm/portal/release324

然后搜索”arm”,便能找到处理器相关的特殊命令选项

arm-linux-gnueabi-gcc

查看arm处理器相关的编译选项

$ vi arch/arm/Makefile

阅读Makefile文件,并联系源码根目录下的.config文件,便能知道arm-linux-gnueabi-gcc用了哪些编译选项。再到手册中查找,便能知道这些选项是干什么用的,但手册中说的不是很详细。另外查找有用解释的方法的是,利用make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig,找到与命令选项有关联的CONFIG_XXX的菜单项,看它的帮助说明.比如

$ vi arch/arm/Makefile
....
ifeq ($(CONFIG_AEABI),y)
CFLAGS_ABI :=-mabi=aapcs-linux -mno-thumb-interwork
else
CFLAGS_ABI :=$(call cc-option,-mapcs-32,-mabi=apcs-gnu) $(call cc-option,-mno-thumb-interwork,)
endif
..

再查看CONFIG_AEABI的帮助文档 $ make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig 找到CONFIG_AEABI相关的菜单,看它的帮助文档,便能知道选项-mabi=aapcs-linux -mno-thumb-interwork的整体效果怎样的。

  ┌───────────────────── Use the ARM EABI to compile the kernel ──────────────────────┐
│ CONFIG_AEABI: │
│ │
│ This option allows for the kernel to be compiled using the latest │
│ ARM ABI (aka EABI). This is only useful if you are using a user │
│ space environment that is also compiled with EABI. │
│ │
│ Since there are major incompatibilities between the legacy ABI and │
│ EABI, especially with regard to structure member alignment, this │
│ option also changes the kernel syscall calling convention to │
│ disambiguate both ABIs and allow for backward compatibility support │
│ (selected with CONFIG_OABI_COMPAT). │
│ │
│ To use this you need GCC version 4.0.0 or later. │
│ │
│ Symbol: AEABI [=n] │
│ Prompt: Use the ARM EABI to compile the kernel │
│ Defined at arch/arm/Kconfig:554 │
│ Location: │
│ -> Kernel Features

arm-linux-gnueabi-gcc的主要编译选项有如下几个。但是在编译内核时,这些选项是不需要手工去写的,而是通过make menuconfig生成包含了编译选项配置信息的.config文件。在make编译内核时,再利用Makefile文件中的规则结合.config文件提取出那些选项。

太多了,手册吧
arm-linux-gnueabi-gdb

注意它的默认选项设置

$ arm-linux-gnueabi-gdb
(gdb) show arm
abi: The current ARM ABI is "auto" (currently "APCS").
apcs32: Usage of ARM 32-bit mode is on.
disassembler: The disassembly style is "std".
fpu: The current ARM floating point model is "auto" (currently "fpa").
(gdb)

但是,如果如果在命令后有参数vmlinux的话,它会自动识别出内核的abi,从而自动设置了gdb的abi。比如,在编译内核时,如果选了CONFIG_AEABI,则gdb的提示如下

$ arm-linux-gnueabi-gdb vmlinux 
...
(gdb) show arm
abi: The current ARM ABI is "auto" (currently "AAPCS"). <--注意
apcs32: Usage of ARM 32-bit mode is on.
disassembler: The disassembly style is "std".
fpu: The current ARM floating point model is "auto" (currently "softvfp").

同时,反汇编出来的机器和编译时没选CONFIG_AEABI的内核也不一样。下面是选了CONFIG_AEABI的内核

   ┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc00084c0 <smp_setup_processor_id> mov r12, sp │
│0xc00084c4 <smp_setup_processor_id+4> push {r11, r12, lr, pc} │
│0xc00084c8 <smp_setup_processor_id+8> sub r11, r12, #4 ; 0x4 │
b+>│0xc00084cc <smp_setup_processor_id+12> ldm sp, {r11, sp, pc} │
│0xc00084d0 <initcall_debug_setup> mov r12, sp │
│0xc00084d4 <initcall_debug_setup+4> push {r11, r12, lr, pc} │
│0xc00084d8 <initcall_debug_setup+8> sub r11, r12, #4 ; 0x4 │
│0xc00084dc <initcall_debug_setup+12> ldr r3, [pc, #12] ; 0xc00084f0 <initcall_debug_setup+32│
│0xc00084e0 <initcall_debug_setup+16> mov r2, #1 ; 0x1 │
│0xc00084e4 <initcall_debug_setup+20> mov r0, r2 │
│0xc00084e8 <initcall_debug_setup+24> str r2, [r3] │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: smp_setup_processor_id Line: 481 PC: 0xc00084cc
Breakpoint 1 at 0xc00084cc: file init/main.c, line 481.
(gdb) c
Continuing.
Can't send signals to this remote system. SIGHUP not sent.

Program received signal SIGHUP, Hangup.
smp_setup_processor_id () at init/main.c:481
(gdb) show arm
abi: The current ARM ABI is "auto" (currently "AAPCS").
apcs32: Usage of ARM 32-bit mode is on.
disassembler: The disassembly style is "std".
fpu: The current ARM floating point model is "auto" (currently "softvfp").
(gdb)

下面是编译时没选CONFIG_AEABI的内核

$ arm-linux-gnueabi-gdb vmlinux 
.....
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc00084c0 <smp_setup_processor_id> mov r12, sp │
│0xc00084c4 <smp_setup_processor_id+4> push {r11, r12, lr, pc} │
│0xc00084c8 <smp_setup_processor_id+8> sub r11, r12, #4 ; 0x4 │
b+>│0xc00084cc <smp_setup_processor_id+12> ldm sp, {r11, sp, pc} │
│0xc00084d0 <initcall_debug_setup> mov r12, sp │
│0xc00084d4 <initcall_debug_setup+4> push {r11, r12, lr, pc} │
│0xc00084d8 <initcall_debug_setup+8> sub r11, r12, #4 ; 0x4 │
│0xc00084dc <initcall_debug_setup+12> ldr r3, [pc, #12] ; 0xc00084f0 <initcall_debug_setup+32│
│0xc00084e0 <initcall_debug_setup+16> mov r2, #1 ; 0x1 │
│0xc00084e4 <initcall_debug_setup+20> mov r0, r2 │
│0xc00084e8 <initcall_debug_setup+24> str r2, [r3] │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: smp_setup_processor_id Line: 481 PC: 0xc00084cc
Breakpoint 1 at 0xc00084cc: file init/main.c, line 481.
(gdb) c
Continuing.
Can't send signals to this remote system. SIGHUP not sent.

Program received signal SIGHUP, Hangup.
smp_setup_processor_id () at init/main.c:481
(gdb) show arm
abi: The current ARM ABI is "auto" (currently "APCS").
apcs32: Usage of ARM 32-bit mode is on.
disassembler: The disassembly style is "std".
fpu: The current ARM floating point model is "auto" (currently "softvfp").
(gdb)

我们看到,同样是smp_setup_processor_id函数,经过反汇编看到。因为内核编译选项的不同,编译器在编译时用了不同的指令。(晕,穿帮了,自打嘴巴,机器码一样。得找个体现EABI的效果的代码段出来)

qemu的使用

参考手册

http://bellard.org/qemu/user-doc.html

http://wiki.debian.org.tw/index.php/QEMU

http://www.h7.dion.ne.jp/~qemu-win/

邮件列表

http://lists.gnu.org/archive/html/qemu-devel/

参考文章

“QEMU安装使用全攻略” http://forum.ubuntu.org.cn/viewtopic.php?p=248267&sid=f4e95025bdaf6a24a218315d03ad9933

[补充命令]引用自http://bbs.chinaunix.net/viewthread.php?tid=779540

安装过程中,要求换盘:
在qemu中按ctrl+alt+2切换到qemu monitor模式 输入?或help可以查看可用命令及使用说明。
(在其他版本的qemu中,运行qemu加载OS后,这个shell就会自动变成qemu monitor模式)
change device filename -- change a removable media
看来它就是用来换盘的了 : change cdrom /rhel4/EL_disc2.iso

切换回安装界面ctrl+alt+1

monitor下还有几个常用的命令:
savevm filename 将整个虚拟机当前状态保存起来
loadvm filename 恢复 (最初我没用change换盘时,就是先savevm->重新运行qemu->loadvm )
sendkey keys 向VM中发送按键,例如你想在虚拟机里切换到另一个终端,按下了ctrl-alt-F2
不幸的是,切换的却是你的主系统,所以就需要用 sendkey了 sendkey ctrl-alt-f2
还有其他几个命令,自己看看啦。

经过N久终于装好了,现在可以启动试试:
[root@LFS distro]#qemu redhat.img -enable-audio -user-net -m 64
-user-net 相当于VMware的nat,主系统可以上,虚拟机就可以
-m 64 使用64M内存,缺省下使用128M

ctrl-alt-f 全屏
ctrl-alt 主机/虚拟机鼠标切换
qemu还有一些其他参数,输入qemu可以查看其相关说明

initrd.img的原理与制作

[扩展,原理,相关命令。下面的skyeye可能需要这部分知识]

“Linux2.6 内核的 Initrd 机制解析” http://www.ibm.com/developerworks/cn/linux/l-k26initrd/

“Introducing initramfs, a new model for initial RAM disks” http://www.linuxdevices.com/articles/AT4017834659.html

””深入理解 Linux 2.6 的 initramfs 機制 (上)“ http://blog.linux.org.tw/~jserv/archives/001954.html

MKINITRAMFS http://www.manpage.org/cgi-bin/man/man2html?8+mkinitramfs

x86虚拟调试环境的建立

参考

“debugging-linux-kernel-without-kgdb” http://memyselfandtaco.blogspot.com/2008/06/debugging-linux-kernel-without-kgdb.html

“使用 KGDB 调试 Linux 内核” http://blog.chinaunix.net/u/8057/showart_1087126.html

“透過虛擬化技術體驗 kgdb (1)” http://blog.linux.org.tw/~jserv/archives/002045.html

基于qemu和内置kgdb

缺点:相对于下节的“基于qemu和模拟硬件调试”,利用内核内置的调试的缺点是配置麻烦。

优点:真机远程调试时只能使用内置kgdb这个方法。

[等待扩展,,,,]

终极参考

“Using kgdb and the kgdb Internals” http://www.kernel.org/pub/linux/kernel/people/jwessel/kgdb/index.html

参考文章

“使用 KGDB 调试 Linux 内核” http://blog.chinaunix.net/u/8057/showart_1087126.html

基于qemu和模拟硬件调试
  • 参考文章

“Debugging Linux Kernel Without KGDB Patch (Qemu + GDB)” http://memyselfandtaco.blogspot.com/2008/06/debugging-linux-kernel-without-kgdb.html

  • 优缺点

优点:相对上节,优点的操作简单,模拟硬件调试几乎不需要什么配置

缺点:真机的远程调试,就只能利用内核的内置kgdb了

[说明 x86系统和arm等嵌入式系统很不一样,根本没必要利用 -kernel 在虚拟机外面挂个内核, 还利用-append 传递起奇形怪样的参数等等。简单问题复杂化。或许更高调试要求时,那些复杂的用法就有用武之地。]

[太概过了,待扩展...]

  • 过程:

1. 利用qemu安装一个系统.

2. 在真机中配置并编译一个用于安装到虚拟系统中的新内核,注意配置时的选择

* 配置和启动
1. 内核选项
同时,为了能在系统运行时中断系统并出发远程 gdb,必须打开内核 Magic Sys-Rq 键选项 :[后记,没实验去掉会怎样,估计没影响]
CONFIG_MAGIC_SYSRQ=y
打开内核符号调试:
CONFIG_DEBUG_INFO=y

3. 在真机下编译好虚拟机新内核的源码

4. 结束qemu,用以下命令在真机上挂载虚拟硬盘。然后把编译好的整个源码目录都拷贝到挂载好的虚拟硬盘上(真机上保留一份源码)。

$ sudo mount -o loop,offset=32256 debian.img /mnt

拷贝完后,在真机上卸载虚拟硬盘

$ sudo umount /mnt

5.启动虚拟机,进入旧系统,在新内核源码根目录下用以下命令给qemu的虚拟系统安装一个新的内核

拷贝模块
$ make modules_install
安装内核
$ make install
制作initrd.img
$ cd /boot
$ mkinitramfs /lib/modules/2.6.26/ -o initrd.img-2.6.26
检查/boot/grub/menu.lst 文件内容是否妥当

6.用以下命令重启虚拟系统,并选择进入新系统,确认新系统是否安装成功。

$ shutdown -r now

7. 在真机新内核源码目录下建立一个文件 .gdbinit 内容是

target remote localhost:1234
b start_kernel
#c

注意我把c注释掉是因为ddd和gdb有切换的需要。见”gdb技巧”

8. 再关闭虚拟系统,并用以下新命令启动虚拟机

qemu -hda debian.img -cdrom ../debian-testing-i386-CD-1.iso -m 500 -S -s

9. 在真机新内核源码目录下运行 gdb ./vmlinux

[实验记录] 实验过了,.config中不选择kgdb,利用qemu照样能调试。也不能调试start_kernel以前的代码。比如head_32.S中的代码。

CONFIG_HAVE_ARCH_KGDB=y
# CONFIG_KGDB is not set

但是不知CONFIG_HAVE_ARCH_KGDB是在menuconfig菜单的哪里。想试试把这项去了qemu还能不能调试。

经测试,取消CONFIG_HAVE_ARCH_KGDB后,qemu也能进行调试。情况不变。看来qemu能完全脱离内核中的kgdb就能调试内核。

arm虚拟调试环境的建立

利用qemu
利用qemu安装debian linux

目标:本节在qemu虚拟机上安装一个基于arm的“桌面“系统,可以有X桌面,该虚拟系统能利用apt-get从debian的软件库下载数不完的用交叉编译已经编译好的arm下的程序和工具。除了虚拟处理器是arm外,简直就是PC机。非常适合调试arm下的应用程序。后面章节的arm应用程序的反汇编分析将要在该系统下进行。但是,本人装的时候,如果选了安装桌面环境,内核就启动失败,好像是提示文件系统出错。[成功的麻烦把过程贴出来]

过程是: Debian on an emulated ARM machine http://www.aurel32.net/info/debian_arm_qemu.php

下面是过程的提炼步骤,方便查看。

1.创建虚拟硬盘

$ qemu-img create -f qcow hda.img 40G

2.下载必要文件

$ wget http://people.debian.org/~aurel32/arm-versatile/vmlinuz-2.6.18-6-versatile
$ wget http://people.debian.org/~aurel32/arm-versatile/initrd.img-2.6.18-6-versatile
$ wget http://ftp.de.debian.org/debian/dists/etch/main/installer-arm/current/images/rpc/netboot/initrd.gz

2.安装系统

qemu-system-arm -M versatilepb -kernel vmlinuz-2.6.18-6-versatile -initrd initrd.gz -hda hda.img -append "root=/dev/ram" 
在安装过程中,为了节省时间,在这步choose a mirror of the debian archive
选http 回车 ;
debian archive mirror country 选taiwan 回车;
debian archive mirror 选ftp.tw.debian.org

安装好基本系统后,不要选择安装Desktop environment

安装完成后,它提示你把光盘拿掉并重启系统时,终止掉qemu。并用下一步的命令启动qemu.不要回车,否则又重新安装。

3. 第一次启动系统

$ qemu-system-arm -M versatilepb -kernel vmlinuz-2.6.18-6-versatile -initrd initrd.img-2.6.18-6-versatile -hda hda.img -append "root=/dev/sda1"

4. 把旧的内核,intrd.img制作工具安装到虚拟机的系统内(操作在虚拟机内)

$ apt-get install initramfs-tools
$ wget http://people.debian.org/~aurel32/arm-versatile/linux-image-2.6.18-6-versatile_2.6.18.dfsg.1-18etch1+versatile_arm.deb
$ su -c "dpkg -i linux-image-2.6.18-6-versatile_2.6.18.dfsg.1-18etch1+versatile_arm.deb"

5.其他更多的玩法请看原文http://www.aurel32.net/info/debian_arm_qemu.php

利用qemu安装能进行内核调试的系统

[暂时没法子,期待扩展。下面这个例子可以,但没尝试]

使用qemu-jk2410做為學習環境 http://wiki.jk2410.org/wiki/%E4%BD%BF%E7%94%A8qemu-jk2410%E5%81%9A%E7%82%BA%E5%AD%B8%E7%BF%92%E7%92%B0%E5%A2%83

[参考]

Debian ARM Linux on Qemu

http://909ers.apl.washington.edu/~dushaw/ARM/#SYSTEM

Running Linux for ARM processors under QEMU

http://iomem.com/index.php?archives/2-Running-Linux-for-ARM-processors-under-QEMU.html&serendipity [entrypage]=2

Debian on an emulated ARM machine

http://www.aurel32.net/info/debian_arm_qemu.php

利用skyeye
skyeye虚拟机的用途

[更多扩展,最好从官方翻译点东西过来]

本人认为,skyeye适合调试内核,不适合调试虚拟系统下的应用程序。因为1.skyeye运行时太占资源,虚拟系统启动后即使什么操作都不进行也一样 2. 如何搞一个能像章节“利用qemu”提到的那样功能强大的虚拟系统出来呢?

skyeye的安装与使用

该文非常好,好像没啥要扩充的]

SkyEye硬件模拟平台,第二部分: 安装与使用

http://www.ibm.com/developerworks/cn/linux/l-skyeye/part2/

SkyEye User Manual http://www.skyeye.org/wiki/UserManual

http://skyeye.wiki.sourceforge.net/

出乎想象的快感] [参考文档] Linux-2.6.20 on XXX platform

http://skyeye.wiki.sourceforge.net/Linux

uClinux-dist-20070130 on XXX platform

http://skyeye.wiki.sourceforge.net/uClinux

http://www.linuxfans.org/bbs/thread-182101-1-1.html

在ubuntu系统下的安装,但版本是v1.2,不是最新的

$sudo apt-get install skyeye

快速试玩

目的: 尽可能快的成功运行一个arm linux虚拟机。如果您化了很长时间也无法编译出一个能运行的内核,或写不出一个恰当的skyeye.conf时,在你的热情受到打击之前,我想这节是你急需的。

1.安装skyeye $sudo apt-get install skyeye

2.下载测试套件 地址:http://sourceforge.net/project/showfiles.php?group_id=85554 3.解压,进入 目录 skyeye-testsuite-1.2.5/linux/s3c2410/s3c2410x-2.6.14

可以看到有三个文件initrd.img skyeye.conf vmlinux

4.运行虚拟机 $skyeye -e vmlinux

注意下面的提示 NOTICE: you should be root at first !!! NOTICE: you should inmod linux kernel net driver tun.o!!! NOTICE: if you don’t make device node, you should do commands: NOTICE: mkdir /dev/net; mknod /dev/net/tun c 10 200 NOTICE: now the net simulation function can not support!!! NOTICE: Please read SkyEye.README and try again!!!

5.可以看到,一个2.6.14 版本的linux跑起来了,还带有一个lcd.

快速配置能调试的环境

快速配置能调试的环境 [参考]http://skyeye.wiki.sourceforge.net/linux_2_6_17_lubbock

环境条件:

1. ubuntu hardy 8.04

2. 安装了debian提供的交叉编译工具套件 arm-linux-gnueabi- (4.2版本)

目标:这小节能得到基于pxa平台(类似s3c2410,也基于arm核心)的linux2.6.20内核的虚拟系统,具备调试功能。相比“基于qemu和内置kgdb”该节,利用skyeye的调试有那节所没有的优点:调试时可以从内核运行的第一条指令开始[这就是模拟硬件调试?]。

参考手册:

XScale PXA250开发手册 http://soft.laogu.com/download/intelpxa250.pdf

ARMv5 体系结构参考手册 http://www.arm.com/community/university/eulaarmarm.html

过程:

1. 下载linux-2.6.20 (由于交叉编译器太新,如果利用linux-2.6.17则编译不过)

2. 修改文件include/asm-arm/arch-pxa/memory.h 第18行

#define PHYS_OFFSET UL(0xa0000000)

#define PHYS_OFFSET UL(0xc0000000)

3. 下载内核配置选项,放置于linux-2.6.20源码的根目录下 http://skyeye.wiki.sourceforge.net/space/showimage/skyeye_2.6.17_lubbock.config

这个下载好的配置文件已经帮我们做了的两件事

首先,在block device菜单下配置了ramdisk和initrd的支持

其次,把内核原来的启动参数改为 root=/dev/ram0 console=ttyS0 initrd=0xc0800000,0×00800000 rw mem=64M

4. 把下载到的skyeye_2.6.17_lubbock.config更名为.config

5. 编译内核 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-

6. 创建文件 skyeye.conf,内容如下:

cpu: pxa25x
mach: pxa_lubbock
mem_bank: map=I, type=RW, addr=0x40000000, size=0x0c000000
mem_bank: map=M, type=RW, addr=0xc0000000, size=0x00800000
mem_bank: map=M, type=RW, addr=0xc0800000, size=0x00800000, file=./initrd.img
mem_bank: map=M, type=RW, addr=0xc1000000, size=0x00800000
mem_bank: map=M, type=RW, addr=0xc1800000, size=0x02800000

7. 从skyeye的测试套件中拷贝initrd.img到linux-2.6.20源码根目录下。该initrd.img的路径是: skyeye-testsuite-1.2.5/linux/pxa/2.6.x/

8. 运行内核看看,在linux-2.6.20源码根目录下运行命令: skyeye -e vmlinux 可以看到,内核成功运行

9. 进行调试 在linux-2.6.20源码根目录下运行命令: skyeye -d -e vmlinux ; 启动另一个终端,在linux-2.6.20源码根目录下运行命令: arm-linux-gnueabi-gdb ./vmlinux; 再在(gdb) target remote:12345

可以看到,下断点,查看汇编等一切调试功能和x86下都一样。可以利用该虚拟机熟悉arm汇编,系统启动流程等。

10. ddd下如何调用arm-linux-gnueabi-gdb ? 答 $ ddd –debugger arm-linux-gnueabi-gdb ./vmlinux

移植配置最新的s3c24xx下的linux内核

[失败,难道是编译器有关????]

目标:得到一个基于s3c2410cpu2.6.26最新稳定内核的虚拟系统,带有内核调试功能。希望能不断改进,模拟更多的硬件。

[参考]http://skyeye.wiki.sourceforge.net/Linux http://www.linuxfans.org/bbs/thread-182101-1-1.html 我们以Linux-2.6.26 版本来研究

1.依据“安装交叉编译工具”这节,安装好交叉编译工具

2.修改源码

将include/asm-arm/arch-s3c2410/map.h里的
#define S3C2410_CS6 (0x30000000)
改为
#define S3C2410_CS6 (0xc0000000)

将include/asm-arm/arch-s3c2410/memory.h里的
#define PHYS_OFFSET UL(0x30000000)
改为
#define PHYS_OFFSET UL(0xc0000000)

3.把默认.config替换为s3c2410版本

make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- s3c2410_defconfig

3.make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi- menuconfig

进入[Device Driver] →[ Character Driver] → [Serial Driver] 等菜单下 ,取消8250/16550 and compatible serial support的选择

4. 在Boot option –> Default kernel command string 里输入

mem=32M console=ttySAC0 root=/dev/ram initrd=0×0800000,0×0800000 ramdisk_size=2048 rw

5.编译 make ARCH=arm CROSS_COMPILE=arm-linux-gnueabi-

6.从skyeye的测试套件中拷贝相应的文件initrd.img和skyeye.conf到linux-2.6.26源码根目录下。这两个文件的位于skyeye-testsuite-1.25/linux/s3c2410/s3c2410x-2.6.14/中

7.启动虚拟机fqh@ubuntu:~/dt/linux-2.6.26$ sudo skyeye -e vmlinux

真机调试环境的建立

[虚拟机调试内核侧重于理解内核原理,遇到某些实际问题,还是要真枪实弹地干]

基于串口
基于网口

gdb基础

基本命令

推荐这篇,内容很全: gdb 使用手册 http://blog.chinaunix.net/u/11240/showart.php?id=340632

终极参考: Debugging with GDB http://sourceware.org/gdb/current/onlinedocs/gdb.html#SEC_Top

gdb之gui

网址:

cgdb:http://cgdb.sourceforge.net/

kgdb:http://www.kdbg.org/screenshot.php

ddd:http://www.gnu.org/software/ddd/

insight:http://sourceware.org/insight/

这些工具在ubuntu下都有编译好的.deb安装包,利用“立新得”就直接搜索然后在线安装。

这篇短文是我的浅陋之见,我接触这些gui的时间也不久。错误难免。 虚拟机:qemu

内核内置kgdb

developer machine: 运行gdb

除了只用命令行gdb外,还可以用gdb的gui,有

1.cgdb 缺点:界面简陋,自动化程度低,只是把terminal分为两部分,上面部分显示源码,下面打命令。由于没有显示反汇编的窗体,不适合要求使用到 stepi命令的场合。优点:运行快,锻炼手指头. 最大的优点是,它有完美的代码着色功能。其他几款调试器中都没有。

2.ddd: 缺点:与kdbg相比,界面凌乱。优点:代码显示效果比kdbg好,c和反汇编代码分开在两个窗口。 可以随时暂停程序的运行。data windows 这个功能非常强大灵活。提示 ddd –tty 2>/dev/null ./vmlinux ; remote target localhost:1234

3. kdbg: 缺点:功能比ddd弱。字体太小,c和反汇编代码交错显示,反汇编代码折叠隐藏在C代码之间,要显示反汇编代码要手动展开,不可忍受。太过界面化,居然找不到是在哪里手动打gdb命令。致命缺点是,内核跑起来后,如果没有断点拦截,就没法把内核的运行暂停下来,kdbg成了没事姥,源码窗口的显示不更新。另一个致命缺点是,如果没有源码只有二进制文件,虽然可以下断点,但无法显示反汇编代码,没意义。据说kdbg是用来调试kde程序的,实际上也能调试内核。优点:窗口可以整合到一块,稳定。有变化的寄存器会显示红色。提示 kdbg -r localhost:1234 ./vmlinux

4. insight: 和ddd都是基于TCL/TK,比较相似。优点:源码显示功能最强,可以选择C和反汇编代码分开和交叉显示。可以选择反汇编代码使用intel还是at&t格式。可以列出当前有哪些源文件,当前文件有哪些函数。变化的寄存器有改变颜色的功能,ddd则没有。缺点:和ddd一样,小窗口无法整合到到窗口中,但比ddd差的是,主窗口最大化后小窗口无法保持置顶。相对ddd的大劣势是没有一个强大的data windows。感觉界面比ddd强大,但灵活性比ddd差点。对于调试内核来说,还有一个和kdbg相同的大缺点,内核只能通过断点暂停运行,而ddd 下还可以用ctrl+c暂停内核。另外它有个SB错误,显示backtrace的窗口,标题居然是stack. 提示: insight ./vmlinux

5. xxgdb: 古董级别。没事干的时候可以玩玩

6. 其实,gdb自带了一个基于curses的gui。启动方式是gdbtui xxx; 或者在gdb启动之后用命令layout启动gui。很好用,可以至多同时显示三个分窗口。要是代码有着色功能就好了。

针对内核调试的总结:

1. kdbg不适合调试内核

3. 如果想复习gdb强大的命令,选cgdb或纯gdb。

4. 如果想学习汇编,insight是不二选择。

5 如果倾向于把调试器当作浏览器使用,作为source insight等工具的辅助工具,在内核运行中拦截函数,分析函数的调用关系,不需要反汇编的话,则cgdb是不错的选择 .(source insight等源码分析工具有个共同的缺点,因为体系和内核配置不同,一个函数有很多的定义,借助调试器可以在内核运行的时候找出实际调用的那个)

6.insight和ddd很接近,各有千秋。但如果侧重于追溯数据结构体间的联系,ddd更好一点,因为它有data window,它的强项是数据和数据结构关系分析并用图像方式显示出来(What is DDD? Data Display Debugger)。如果侧重于分析汇编指令是怎么在cpu中跑的,推荐用insight,因为它汇编代码显示功能更细致。

7.可惜目前在ubuntu8.04下,ddd+qemu组合用来调试驱动时有bug:驱动函数被拦截时如果正在qemu的系统下操作,鼠标就会冻结在qemu的屏幕中。其实调试单个驱动,用gdb就足够了。ddd等gui一般用来调试理解内核原理。

gdb技巧

[有啥妙招心得,欢迎分享]

大家好,我现在在用vmware提供的gdb调试linux内核,想问问如何从某个变量开始顺藤摸瓜找到系统中所有进程的task_struct信息,不知道有没有方法,谢谢了

以上是zh-kernel里一个朋友的问题。我用gdb试试,得到了答案。贴下来备忘

过程: 利用init_task变量取得task_struct结构体中tasks域在task_struct中的偏移值。3步

(gdb) p &init_task->tasks
$32 = (struct list_head *) 0xc03513bc

(gdb) p &init_task
$33 = (struct task_struct *) 0xc03512f4

(gdb) p/x 0xc03513bc - 0xc03512f4
$35 = 0xc8

查看init_task结构

(gdb) p init_task
$36 = {state = 0, stack = 0xc0378000, usage = {counter = 2}, flags = 8448, ptrace = 0, lock_depth = -1, prio = 140, static_prio = 120, normal_p…

以下下两步获取init_task→tasks→next所在的task_struct结构的地址

(gdb) p init_task->tasks->next
$47 = (struct list_head *) 0xdc43f968

(gdb) p/x 0xdc43f968 - 0xc8
$39 = 0xdc43f8a0

查看该task_struct结构的价值,其余类推,便可查看所有task_struct结构

(gdb) p *(struct task_struct *)0xdc43f8a0
$45 = {state = 1, stack = 0xdc440000, usage = {counter = 2}, flags = 4194560, ptrace = 0, lock_depth = -1, prio = 120, static_prio = 120, …….

另外有用的命令 ptype, whatis

----

更多相关技巧:

1. 获取struct page结构的大小

(gdb) p mem_map
$80 = (struct page *) 0xc1000000
(gdb) p mem_map+1
$81 = (struct page *) 0xc1000020
(gdb) p/x 0xc1000020 - 0xc1000000
$82 = 0×20

2.

打印前从指针mem_map所指起的5个page结构体

(gdb) p *mem_map@5
$83 = {{flags = 1024, _count = {counter = 1}, {_mapcount = {counter = -1}, {inuse = 65535, objects = 65535}}, {{private = 0, mapping = 0×0}, ptl =…

用ddd的图形显示命令是 (gdb) graph display *mem_map@5

参考 p *array@len

@的左边是数组的首地址的值,也就是变量array所指向的内容,右边则是数据的长度,其保存在变量len中

3.

每运行一次stepi/next等命令后显示下一步要将要运行的反汇编指令

(gdb) display/i $pc
6: x/i $pc
0xc0144fb6 <init_cgroup_root+22>: mov %esp,%ebp
(gdb) stepi
6: x/i $pc
0xc0144fb8 <init_cgroup_root+24>: mov %edx,0×44(%eax)

提示:display的管理:

undisplay delete display disable display enable display info display

4.使结构体的显示更漂亮

(gdb) show print pretty
Prettyprinting of structures is on.
(gdb) set print pretty off
(gdb) p *init_task->group_info
$12 = {ngroups = 0, usage = {counter = 14}, small_block = {0 <repeats 32 times>}, nblocks = 0, blocks = 0xc0355530}
(gdb) set print pretty on
(gdb) p *init_task->group_info
$13 = {
ngroups = 0,
usage = {
counter = 14
},
small_block = {0 <repeats 32 times>},
nblocks = 0,
blocks = 0xc0355530
}

5. 由汇编指令地址确定该指令所对应源码的所在行(注:一行c语言一般对应几行汇编指令)

info line *xxxxxxx (xxx是汇编指令地址)

(注:6.7.条来自http://techcenter.dicder.com/2006/0906/content_173.html )

6. 使用自定义命令。

(gdb) define nid

Type commands for definition of “nid”.

End with a line saying just “end”.

>ni

>disassemble $pc $pc+16

>end

7. 纯gdb的多窗口显示 GUI调试器可以同时打开多个小窗口,分别显示寄存器、汇编和源代码等。在gdb里也可以做到,但同时最多只能显示两个窗口,试了一下也很方便的。基本命令如下:

a) `layout src’ 仅显示源代码窗口。

b) `layout asm’ 仅显示汇编代码窗口。

c) `layout split’ 显示源代码和汇编代码窗口。

d) `layout regs’ 显示寄存器和源代码窗口,或者寄存器和汇编代码窗口。

e) `layout next` 和 `layout prev’ 切换窗口。

f) ctrl + L 刷新屏幕。

g) `C-x 1′ 单窗口模式。

h) `C-x 2′ 双窗口模式。

i) `C-x a’ 回到传统模式。

8. 字符gdb中,如何在每执行一次next命令后都自动显示backtrace的内容 这个问题实际是如何一次执行多条命令。用自定义命令解决

(gdb) define nbt
Type commands for definition of “nbt”.
End with a line saying just “end”.
>next
>bt
>end
(gdb) nbt
#0 early_cpu_init () at arch/x86/kernel/cpu/common.c:626
#1 0xc0384ca9 in setup_arch (cmdline_p=0xc0379fe8)
at arch/x86/kernel/setup_32.c:765
#2 0xc037f62e in start_kernel () at init/main.c:564
#3 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#4 0×00000000 in ?? ()
(gdb)

9. gdb在TUI模式下如何把光标焦点炸转移到command窗口,以便能用上下箭头键能快速翻出历史指令?

实际是转换“active”窗口。
C-x o: ctrl+x,接着放开这两个键,然后在按o(不需要+ctrl)
关于TUI更多信息:
http://sourceware.org/gdb/current/onlinedocs/gdb_23.html#SEC236
还有组合键
C-x C-a
C-x a
C-x A 退出TUI模式

C-x 1 只用一个窗口
C-x 2 用两个窗口,按多次会有不同两个窗口的组合形式
C-x o active 窗口转移
C-x s 进入和退出TUI SingleKey 模式
注:C-x o多次使用相当于依次执行以下命令
focus src 转移焦点到源码窗口。
focus asm
focus regs
focus cmd

TUI模式还有以下专用命令
info win
layout next
layout prev
layout src
layout asm
layout split
layout regs
focus next
refresh
tui reg float
tui reg general
tui reg next
tui reg system





update
winheight name +count
winheight name -count
tabset nchars

10. 如何在子函数调用和退出时都暂停运行 watch $ebp

11. 如何获取结构体中特定域的相对偏移量,比如struct stak_struct 中lock_depth的相对偏移量?

(gdb) p/x &(*(struct task_struct *)0).lock_depth
$7 = 0x14

12. 如何能够交换使用ddd与gdb,也就是说使用ddd调试时,想换回使用纯gdb,同时保证启用gdb后保证“调试上下文”没任何变化?

只要.gdbinit 文件没包含 c, next..等等能驱动gdb继续调试的命令就可以。

13. 如何通过函数名确定所在的源文件

(gdb) info line vfs_mkdir
Line 2131 of "fs/namei.c" starts at address 0xc017c048 <vfs_mkdir> and ends at 0xc017c052 <vfs_mkdir+10>.

14. 如何快速定位函数中某句C语句对应汇编指令的开始地址。比如以下 [内容太大,准备移到其他位置]

2130	int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
2131 {
.......
2145 DQUOT_INIT(dir);
2146 error = dir->i_op->mkdir(dir, dentry, mode);//<-我们想确定这句语句的汇编指令开始地址,注意它在源文件中的行数
2147 if (!error)
2148 fsnotify_mkdir(dir, dentry);
2149 return error;
2150 }

首先,通过函数名查询对应的源文件

(gdb) info line vfs_mkdir
Line 2131 of "fs/namei.c" starts at address 0xc017c048 <vfs_mkdir> and ends at 0xc017c052 <vfs_mkdir+10>.

然后,利用info line 源文件:目标语句的行数 就能查询到

(gdb) info line fs/namei.c:2146
Line 2146 of "fs/namei.c" starts at address 0xc017c0ee <vfs_mkdir+166> and ends at 0xc017c0fe <vfs_mkdir+182>.

验证一下

(gdb) disass 0xc017c0ee
Dump of assembler code for function vfs_mkdir:
0xc017c048 <vfs_mkdir+0>: push %ebp
.....
0xc017c0e4 <vfs_mkdir+156>: mov 0x24(%eax),%ecx
0xc017c0e7 <vfs_mkdir+159>: or $0xffffffff,%edx
0xc017c0ea <vfs_mkdir+162>: mov %esi,%eax
0xc017c0ec <vfs_mkdir+164>: call *(%ecx)
0xc017c0ee <vfs_mkdir+166>: mov 0x98(%esi),%ebx //
0xc017c0f4 <vfs_mkdir+172>: mov %edi,%edx //参数 dentry -> %edx
0xc017c0f6 <vfs_mkdir+174>: mov %esi,%eax //参数dir -> %eax
0xc017c0f8 <vfs_mkdir+176>: mov -0x10(%ebp),%ecx //参数mode -> %ecx
0xc017c0fb <vfs_mkdir+179>: call *0x14(%ebx) //dir->i_op->mkdir(dir, dentry, mode)
0xc017c0fe <vfs_mkdir+182>: test %eax,%eax //判断返回值(error = dir->i_op->mkdir(dir, dentry, mode);)
0xc017c100 <vfs_mkdir+184>: mov %eax,%ebx //保存返回值
0xc017c102 <vfs_mkdir+186>: jne 0xc017c15d <vfs_mkdir+277> //如果返回值 != 0,也就是mkdir失败,跳到最后返回。成功则继续
0xc017c104 <vfs_mkdir+188>: testb $0x4,0x11c(%esi) //内联函数fsnotify_mkdir 及子函数->inode_dir_notify在这里展开
//static inline void inode_dir_notify(struct inode *inode, unsigned long event)
//{
// if (inode->i_dnotify_mask & (event)) <-注意这里判断位,刚好对应testb $0x4,0x11c(%esi)

0xc017c10b <vfs_mkdir+195>: je 0xc017c119 <vfs_mkdir+209>
.....
0xc017c15d <vfs_mkdir+277>: lea -0xc(%ebp),%esp
0xc017c160 <vfs_mkdir+280>: mov %ebx,%eax

我们通过mkdir参数个数,及testb 指令基本判定我们的猜测没错。也就是说vfs_mkdir函数中dir→i_op→mkdir的实际调用是在0xc017c0fb <vfs_mkdir+179>: call *0×14(%ebx)

15. 下断点的形式

1. b 函数名
2. b *指令地址
3. b 源码:行数
(gdb) b fs/namei.c:2146
Breakpoint 9 at 0xc017c0ee: file fs/namei.c, line 2146.

gdb宏

参考资料

kgdb官方的gdb宏 http://kgdb.linsyssoft.com/downloads.htm

“Fun with strace and the GDB Debugger” http://www.ibm.com/developerworks/aix/library/au-unix-strace.html

“GNU Project Debugger: More fun with GDB” http://www.ibm.com/developerworks/aix/library/au-gdb.html

“14.3.4. Useful Kernel gdb Macros” from “Embedded Linux Primer” http://book.opensourceproject.org.cn/embedded/embeddedprime/

gdb宏的使用

假设要使用下节的lsmod,该gdb宏能列举内核中的模块。 在内核源码目录下建立一个新文件lsmod,内容见下节。

装载宏
(gdb) source lsmod
查看说明
(gdb) help lsmod
List the loaded kernel modules and their start addresses
使用
(gdb) lsmod
Address Module
0xE069D560 ipv6
0xE013D740 ppdev
....
0xE0012DE0 processor
0xE0008EA0 fan
0xE00223E0 thermal_sys
(gdb)

我们查看一下processor模块结构体的内容
(gdb) p *(struct module *)0xE0012DE0
$10 = {
state = MODULE_STATE_LIVE,
list = {
next = 0xe0008ea4,
prev = 0xe0018984
},
name = "processor", '/0' <repeats 50 times>,
mkobj = {
kobj = {
name = 0xd5910ba0 "processor",
kref = {
refcount = {
counter = 3
}
},
entry = {
next = 0xe00189d0,
...
...

为了方便查看该结构中指针域所指向的结构体,可在ddd下用以下命令打开数据图形然后展开查看
(gdb) graph display *(struct module *)0xE0012DE0
实例

给出的例子都在2.6.26内核上上测试通过。

宏名: lsmod

作用: 列举内核模块的名称及对应模块结构体的地址

define lsmod
printf "Address/t/tModule/n"
set $m=(struct list_head *)&modules
set $done=0
#获取结构体内特定域的相对偏移,见"gdb技巧"
set $offset=&(*(struct module *)0).list
while ( !$done )
set $mp=(struct module *)((char *)$m->next - (char *)$offset)
printf "0x%08X/t%s/n", $mp, $mp->name
if ( $mp->list->next == &modules)
set $done=1
end
set $m=$m->next
end
printf "----end----/n"
end

document lsmod
List the loaded kernel modules and their start addresses
end

宏名: ps

作用: 列举所有task的结构地址,状态,PID,PPID,comm

define show_state
if ($arg0->state == 0)
printf "running/t/t"
else
if ($arg0->state == 1)
printf "sleeping/t"
else
if ($arg0->state == 2)
printf "disksleep/t"
else
if ($arg0->state == 4)
printf "zombie/t"
else
if ($arg0->state == 8)
printf "stopped/t"
else
if ($arg0->state == 16)
printf "wpaging/t"
else
printf "%d/t/t", $arg0->state
end
end
end
end
end
end
end



define ps
printf "address/t/tstate/t/tuid/tpid/tppid/tcomm/n"
set $it = &init_task
printf "0x%08X/t", $it
show_state $it
printf "%d/t%d/t%d/t%s/n", /
$it->uid, $it->pid, $it->parent->pid, $it->comm


set $offset = &((struct task_struct *)0)->tasks
set $t = (struct task_struct *)((char *)$it->tasks.next - (char *)$offset)
while $t != $it
printf "0x%08X/t", $t
show_state $t
printf "%d/t%d/t%d/t%s/n", /
$t->uid, $t->pid, $t->parent->pid, $t->comm
set $t = (struct task_struct *)((char *)$t->tasks.next - (char *)$offset)
end
printf "----end----/n"

end

document ps
print information for all task
end

效果如下

(gdb) source ps
(gdb) ps
address state uid pid ppid comm
0xC03512F4 running 0 0 0 swapper
0xDC43F8A0 sleeping 0 1 0 init
0xDC43F490 sleeping 0 2 0 kthreadd
0xDC43F080 sleeping 0 3 2 migration/0
0xDC43EC70 sleeping 0 4 2 ksoftirqd/0
0xDC43E860 sleeping 0 5 2 watchdog/0
.....
0xDC44E060 sleeping 0 1707 1 acpid
0xD8AE6100 sleeping 104 1716 1 dbus-daemon
0xDC46ECD0 sleeping 0 1739 1 cupsd
0xDC45E080 sleeping 101 2009 1 exim4
0xD5A6C0E0 sleeping 0 2026 1 inetd
0xD5A6CD10 sleeping 0 2034 1 dhcdbd
0xDBD45160 sleeping 105 2044 1 hald
0xDBD45570 sleeping 0 2045 2044 hald-runner
....

----end----

宏名: lssp

作用: 列举超级块地址及其s_id域

define lssp
printf "address/t/ts_id/n"
set $sb_lh=(struct list_head *)&super_blocks
#获取结构体内特定域的相对偏移,见"gdb技巧"
set $offset=&(*(struct super_block *)0).s_list
set $sbp=(struct super_block *)((char *)$sb_lh->next - (char *)$offset)
while ( &$sbp->s_list != $sb_lh )
printf "0x%08X/t%s/n", $sbp, $sbp->s_id
set $sbp=(struct super_block *)((char *)$sbp->s_list.next - (char *)$offset)
end
printf "----end----/n"

end

document lssp
List the super_block and their start addresses
end

效果

(gdb) lssp
address s_id
0xDC40DC00 sysfs
0xDC40DA00 rootfs
0xDC40D800 bdev
0xDC40D400 proc
0xDC41B200 sockfs
0xDC431C00 debugfs
0xDC486600 pipefs
0xDC486000 anon_inodefs
0xD58C5A00 tmpfs
0xD58C5200 inotifyfs
0xD8C09800 devpts
0xD8C09600 hugetlbfs
0xD8C09400 mqueue
0xD590E000 tmpfs
0xD59E4C00 hda1
0xD5908A00 tmpfs
0xD7753200 tmpfs
0xDBD66400 hdc
----end----

汇编基础--X86篇

注意:某些内容不具备普遍性。比如给出的反汇编代码,在不同的优化等级下是不同的。但是在熟悉了典型的函数调用链反汇编代码,对于有变化的其他形式也就不难理解了。

用户手册

Intel® 64 and IA-32 Architectures Software Developer’s Manuals

http://www.intel.com/products/processor/manuals/index.htm

AT&T汇编格式

参考

“AT&T汇编语言与GCC内嵌汇编简介” http://blog.chinaunix.net/u2/73528/showart_1110874.html

[杂类文章]

“Linux Assembly and Disassembly an Introduction” http://www.milw0rm.com/papers/47

内联汇编

汇编与C函数的相互调用

调用链形成和参数传递

参考文章 [多如牛毛]

“Guide: Function Calling Conventions” http://www.delorie.com/djgpp/doc/ug/asm/calling.html

“Intel x86 Function-call Conventions - Assembly View” http://www.unixwiz.net/techtips/win32-callconv-asm.html

“C Function Call Conventions and the Stack” http://www.cs.umbc.edu/~chang/cs313.s02/stack.shtml

“The C Calling Convention and the 8086: Using the Stack Frame” http://www.et.byu.edu/groups/ece425web/stable/labs/StackFrame.html

“C Function Calling Convention” http://adamw-dev.blogspot.com/2007/05/c-function-calling-convention.html

“C函数调用在GNU汇编中的实现” http://www.unixresources.net/linux/clf/cpu/archive/00/00/59/75/597564.html

“函数调用的几个概念:_stdcall,_cdecl....” http://blog.chinaunix.net/u2/67530/showart_601750.html

“Calling conventions(调用规则)” http://www.bobd.cn/itschool/Program/delphi/200612/itschool_12084.html

[扩展,简要说明原理。并用实例解析]

x86终极参考

CHAPTER 6 PROCEDURE CALLS, INTERRUPTS, AND EXCEPTIONS of

IA-32 Intel_ Architecture Software Developer’s Manual Volume 1_ Basic Architecture.pdf http://download.intel.com/design/processor/manuals/253665.pdf

寄存器的角色与保护
  • 寄存器的角色

1. %esp: 栈指针

指向栈的顶端,也就是指向栈的最后一个正在使用的元素。%esp的值隐式地受到几个机器指令的影响,比如push,pop,call,ret等。

2. %ebp: 基址指针

指向当前栈的基地址,有时也称为“帧指针”。与%esp不同的是,它必须显式地进行操作才能改变值。

3. %eip: 指令指针

保存着下一个被执行机器指令的地址。当CPU执行call指令时,%eip的值自动被保存到栈中。还有,任何一个“jump”跳转指令都会直接地改变%eip

  • 两条规则

1. gcc要求在函数调用的前后,寄存器%ebx,%esi,%edi,%ebp,%esp,%ds, %es,%ss的值保持不变。所以被调用函数如果需要修改这些寄存器的值,被调用函数必须负责对它们进行保护。[后三个??]

2. gcc规定在函数调用的前后,寄存器%eax,%edx,%ecx的值可以改变。所以调用函数如果需要防止子函数破坏这三个寄存器的值,调用者必须在函数调用前自己负责保护它们。

我们注意到,是保护,不一定是保存。如果确认没用到某寄存器,那么该寄存器就不需要一定要有一个先保存到栈而后再恢复原值的过程。

这两条规则实际是定义了对系统资源使用的权限和义务。

第一条规则,是银行和借贷者的关系。有人向银行借了几千万,结果赌博全输光了。还钱的期限到了,银行的行长对借贷者说“没事,你回家吧。几千万而已,我拿我工资给你垫上”。我想这样的事决不会发生,行长一个电话110过去,借贷者一天后就把钱还清了。所以,这里,调用函数是银行行长,子函数是借贷者。

第二条规则,则是老爸和儿子的关系了。儿子对老爸说“老爸,解我100去买球鞋,我明天还你”。结果,第二天,老爸没钱吃饭了,问儿子“还钱”。儿子说“昨晚逛街碰到一个美女,请了一顿,把钱化光了”。老爸没法子,总不能把儿子绳以正法吧。怪只能怪自己事前没防这招咯。所以,这里,调用函数是老爸,子函数是儿子你。

  • 返回值
  1. Integers (of any size up to 32 bits) and pointers are returned in the %eax register.
2. Floating point values are returned in the 387 top-of-stack register, st(0).
3. Return values of type long long int are returned in %edx:%eax (the most significant word in %edx and the least significant in %eax).
4. Returning a structure is complicated and rarely useful; try to avoid it. (Note that this is different from returning a pointer to a structure.)
5. If your function returns void (e.g. no value), the contents of these registers are not used.
调用链的形成
  • 应用层实例解析

我们回头看看“寄存器的角色”这一小节,很快就能明白调用链的形成的本质。

调用链包含两方面的内容

1.返回地址的保存与恢复

2.旧栈帧的保存与恢复

因为在普通的调用形式中(call调用),返回地址的保存与恢复是由处理器机制本身保证的,不需人工维护。调用指令call的执行自动将call指令之下的指令地址压入栈中,被调用函数返回时,ret指令的执行会重新将返回地址从栈弹出传送到pc中。要求下面分析旧栈帧的保存与恢复。

旧栈帧的保存与恢复,无非就是要解决两大问题:

1. 建立新栈帧 这一步很简单,栈帧无非有两个头,底端和顶端。%esp指向栈的顶端,而%esp是不需要手工维护的,随着push,pop等指令,它自己就在改变自己。那么又怎么建立栈帧的底端呢?我们知道,栈底(也就是基址)是由%ebp指定的,在一个栈帧的整个生命周期里,%ebp的值都不变,也就是说,赋个合适的值给它就完事。怎么赋值就是问题所在了。我们知道,%esp指向栈中最后一个被使用的元素。所以,当我们正在使用(我们认为的)第一个元素时,把%esp的值赋给%ebp,%ebp不就是指向栈的基址了吗?

2. 保护旧栈帧的信息 同样的问题,保护旧栈帧的信息,就是保存旧栈帧指向底端和顶端的指针值,也就是旧%ebp,%esbp的值。当函数调用指令刚执行完,马上就要保护作案现场了。首先,push %ebp,这句就把旧栈帧的基地址保存在栈的顶端。此时,%esp指向的内存地址中,就放着旧栈帧的基地址的值。但是还不够啊,%esp是个不可靠的东西,它经常在变化,必须把这个地址放到一个不会隐式变化的寄存器中。于是选择了%ebp。mov %esp %ebp.这样,%ebp指向的内存地址中,就放着旧栈帧的基地址的值。这就解放了%esp,可以用%esp来动态指向新栈帧的顶端了。按照定义,%ebp所指向的地址是新栈帧的底端,也就是新栈帧的第一个元素,也就是说新栈帧第一个元素的值是旧栈帧基址。

但是注意,%ebp指向的地址再加4bytes的地址上,存放的是被调用函数的返回地址。在执行call指令时,call指令后面的那个指令的地址(也就是被调用函数的返回地址)被自动隐式地放到了栈中。

当子函数返回时,再按照上面文字进行逆操作,就能恢复旧栈帧的信息。

#include <stdio.h>

void func()
{}

void funb()
{
func();
}


void funa()
{
funb();
}

int main()
{
funa();
}
-------
08048344 <func>:
#include <stdio.h>

void func()
{}
8048344: 55 push %ebp
8048345: 89 e5 mov %esp,%ebp
8048347: 5d pop %ebp
8048348: c3 ret

08048349 <funb>:

void funb()
{
8048349: 55 push %ebp
804834a: 89 e5 mov %esp,%ebp
func();
804834c: e8 f3 ff ff ff call 8048344 <func>
}


8048351: 5d pop %ebp
8048352: c3 ret

08048353 <funa>:


void funa()
{
8048353: 55 push %ebp
8048354: 89 e5 mov %esp,%ebp
funb();
8048356: e8 ee ff ff ff call 8048349 <funb>
}
804835b: 5d pop %ebp
804835c: c3 ret

0804835d <main>:
int main()
{
804835d: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048361: 83 e4 f0 and $0xfffffff0,%esp
8048364: ff 71 fc pushl -0x4(%ecx)
8048367: 55 push %ebp
8048368: 89 e5 mov %esp,%ebp
804836a: 51 push %ecx
funa();
804836b: e8 e3 ff ff ff call 8048353 <funa>
}
8048370: 59 pop %ecx
8048371: 5d pop %ebp
8048372: 8d 61 fc lea -0x4(%ecx),%esp
8048375: c3 ret
8048376: 90 nop
8048377: 90 nop
8048378: 90 nop
8048379: 90 nop
804837a: 90 nop
804837b: 90 nop
804837c: 90 nop
804837d: 90 nop
804837e: 90 nop
804837f: 90 nop


func被调用后内存如下

| |
| | | | hight
| | | |
| +--------------/ |
+---+ main's %ebp |/ |
+-> +--------------+ --funa's frame |
| | ret to funa | / |
| +--------------+X |
+---+ funa's %ebp | / |
+-->+--------------+ ---funb's frame |
| | ret to funb | / |
| +--------------+ |
+---+ funb's %ebp |<---func's frame | low
%esp--> +--------------+<---- %ebp v
| |
| |
| |
| |
| |



  • 内核层实例解析
栈帧结构与参数传递
  • 栈元素引用的就近原则

为了说明就近原则,我们先看看典型和全面的栈帧是怎样的。函数caller调用子函数callee所形成的栈帧。

1. 从被调用的子函数callee来看,获取caller的传递的实参,以及建立自身本地变量时,因为内存地址都靠近栈帧的基址,所以这两种引用都是利用%ebp加上偏移量的形式。

2. 相反,主函数在调用子函数前,在为子函数准备实参时,因为实参位于栈帧末端,所以对实参的引用都是利用%esp加上偏移量的形式(没画出来)

 caller's frame pointer                           
|
| | |
| | |
| | |
| +-------------------+
| | caller saved |
| | registers |
| | %eax,%ecx,%edx |
| | (as needed) |
| +-------------------+
| | argument #3 | [%ebp+16]
| +-------------------+
| | argument #2 | [%ebp+12]
| +-------------------+
| | argument #1 | [%ebp+8]
| +-------------------+
| | return address |
| +-------------------+ -----
+-----+ caller's %ebp |<---%ebp /
+-------------------+ /
| local var #1 | [%ebp-4] /
+-------------------+ |
| local var #2 | [%ebp-8] |
+-------------------+ |
| temporary | |
| storage | |
+-------------------+
| callee saved | callee stack frame
| registers | |
| %ebx,%esi,%edi | |
| (as needed) | |
+-------------------+ |
| | |
| | |
| | /
| |<----%esp /
|

caller:调用者 callee:被调用者
完整的调用过程

函数caller调用子函数callee,这是应用层的普通函数调用过程。如果是远调用,跨态调用要考虑的东西更多。但这个例子已经充分展示了调用过程的繁复部分。

  • 函数调用前调用者的动作

1.%eax,%edx,%ecx入栈(可选)

2.子函数的参数入栈

  • 函数调用 call callee

call机器指令,原子性自动地完成了两种任务.

1.%eip入栈, 保存了callee函数的返回地址

2.callee的函数地址传递到%eip.

所以下一指令就从callee函数的第一指令开始运行。控制权转移给callee

  • 函数调用后被调用者的动作

1.保存caller栈帧基址 push %ebp

2.建立callee栈帧基址 mov %esp,%ebp

3.分配本地变量和临时存储的空间 sub $XXX, %esp

4.本地变量赋值

5.%ebx,%esi,%edi入栈(可选)

  • 调用返回前被调用者的动作

1.%ebx,%esi,%edi还原(出栈,可选)

2.释放本地变量和临时存储的栈空间mov %ebp,%esp

3.还原caller栈帧的基址 pop %ebp

或者2.3.步用一条元语指令完成 leave

4.调用返回 ret

该指令把存放于栈的返回地址取出(出栈),存放到%eip中。下一指令就从call callee指令的下一指令开始运行。控制权返回给caller

  • 调用返回后调用者的动作

1.释放存放callee参数的栈空间 add $XXX, %esp

2.转移%eax的值(子函数的返回值,可选)

3.还原%eax,%edx,%ecx(出栈,可选)

  • 应用层实例解析

应用层参数的传入: 用户层参数的传递是利用栈来完成的。函数右边的参数先入栈,位于栈的高地址。反之, 函数左边的参数后入栈,位于栈的低地址。

例子请看 “C难点的汇编解释”
  • 内核层实例解析

内核层参数的传入: 混合使用寄存器和栈来传递参数。当参数个数不多于3个时,参数从左到右依次传递到%eax, %edx, %ecx.当参数个数多于3时,从第4个起的其余参数通过栈传递。同样,函数右边的参数先入栈,位于栈的高地址。反之, 函数左边的参数后入栈,位于栈的低地址。

C难点的汇编解释

例1

if ... else if

这个例子有人看来也许是非常非常地简单,但就这个例子,有的人还真给我考”倒”了。他的回话是“还真没见过这样子的代码”。但是,这样的代码在内核中比比皆是,比如后面附上的函数代码 do_path_lookup。如果对if ... else if 理解有偏差,对内核代码的逻辑理解根本就是差以千里。

#include <stdio.h>

int main()
{
int i = 1;
int j = 2;

if (i == 1)
printf("i,ok/n");
else if (j == 2)
printf("j,ok/n");

return 0;
}

这个例子,有人会疑问为什么”j,ok”没打印出来。现在我们分析下它的汇编代码

08048374 <main>:
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp //以上汇编码保存旧栈帧信息,建立新栈帧
8048381: 51 push %ecx //%ecx入栈保护
8048382: 83 ec 14 sub $0x14,%esp //建立本地变量栈空间,以及子函数实参栈空间
8048385: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) //变量i赋值,记得本地变量的地址靠近栈帧的基地址,所以用%ebp引用
804838c: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp) //变量j赋值
8048393: 83 7d f8 01 cmpl $0x1,-0x8(%ebp) //i和1比较
8048397: 75 0e jne 80483a7 <main+0x33> //如果i-1不等0,跳到地址80483a7执行。否则继续执行下面指令
8048399: c7 04 24 90 84 04 08 movl $0x8048490,(%esp) //printf函数第一个参数入栈,它的栈空间之前已经建好。
//记得子函数的实参空间靠近栈顶,所以引用实参用%esp
80483a0: e8 2f ff ff ff call 80482d4 <puts@plt> //调用printf
80483a5: eb 12 jmp 80483b9 <main+0x45> //printf返回后,接着执行这个指令,将跳到地址80483b9继续运行
80483a7: 83 7d f4 02 cmpl $0x2,-0xc(%ebp)
80483ab: 75 0c jne 80483b9 <main+0x45>
80483ad: c7 04 24 95 84 04 08 movl $0x8048495,(%esp)
80483b4: e8 1b ff ff ff call 80482d4 <puts@plt>
80483b9: b8 00 00 00 00 mov $0x0,%eax //%eax赋值0,%eax放的也就是main函数返回结果
80483be: 83 c4 14 add $0x14,%esp //撤销新栈帧的本地变量栈空间,以及子函数实参栈空间
80483c1: 59 pop %ecx //恢复保存的旧%ecx的值
80483c2: 5d pop %ebp //以下汇编码都是恢复旧栈帧的信息,main函数返回等
80483c3: 8d 61 fc lea -0x4(%ecx),%esp
80483c6: c3 ret

经过上面的汇编代码分析,可见c代码块

        else if (j == 2)
printf("j,ok/n");

对应的汇编代码是:

 80483a7:       83 7d f4 02             cmpl   $0x2,-0xc(%ebp)
80483ab: 75 0c jne 80483b9 <main+0x45>
80483ad: c7 04 24 95 84 04 08 movl $0x8048495,(%esp)
80483b4: e8 1b ff ff ff call 80482d4 <puts@plt>

上面的代码指令根本就没有机会运行。

结论,一个if ... else if ..else..

if (判断语句1)
代码块1
else if (判断语句2)
代码块2;
else if ....

..
else 代码块N;

语句块1,2..N的运行机会是一种互斥的关系。当然它们的“机会优先级”是不一样的。语句块1,2..N只有一个有被运行的机会,如果没有else甚至可能没有一个语句块能被运行。

内核代码实例

static int do_path_lookup(int dfd, const char *name,
unsigned int flags, struct nameidata *nd)
{
int retval = 0;
int fput_needed;
struct file *file;
struct fs_struct *fs = current->fs;

nd->last_type = LAST_ROOT; /* if there are only slashes... */
nd->flags = flags;
nd->depth = 0;

if (*name=='/') {
read_lock(&fs->lock);
if (fs->altroot.dentry && !(nd->flags & LOOKUP_NOALT)) {
nd->path = fs->altroot;
path_get(&fs->altroot);
read_unlock(&fs->lock);
if (__emul_lookup_dentry(name,nd))
goto out; /* found in altroot */
read_lock(&fs->lock);
}
nd->path = fs->root;
path_get(&fs->root);
read_unlock(&fs->lock);
} else if (dfd == AT_FDCWD) {
read_lock(&fs->lock);
nd->path = fs->pwd;
path_get(&fs->pwd);
read_unlock(&fs->lock);
} else {
struct dentry *dentry;

file = fget_light(dfd, &fput_needed);
retval = -EBADF;
if (!file)
goto out_fail;

dentry = file->f_path.dentry;

retval = -ENOTDIR;
if (!S_ISDIR(dentry->d_inode->i_mode))





goto fput_fail;

retval = file_permission(file, MAY_EXEC);
if (retval)
goto fput_fail;

nd->path = file->f_path;
path_get(&file->f_path);

fput_light(file, fput_needed);
}

retval = path_walk(name, nd);
out:
if (unlikely(!retval && !audit_dummy_context() && nd->path.dentry &&
nd->path.dentry->d_inode))
audit_inode(name, nd->path.dentry);
out_fail:
return retval;

fput_fail:
fput_light(file, fput_needed);
goto out_fail;
}
例2

短路逻辑算法。

这样的例子在内核代码中也是非常地多,一般用在短的函数或宏中。

#include <stdio.h>

int main()
{
int a = 1;
int b = 2;

if (a || ++b)
printf("%d/n", b);

return 0;
}

这个例子,有人会疑问为什么b的值没有变化,还是为2。现在我们分析下它的汇编代码

08048374 <main>:
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp //以上汇编码保存旧栈帧信息,建立新栈帧
8048381: 51 push %ecx //%ecx入栈保护
8048382: 83 ec 24 sub $0x24,%esp //创建本地变量和子函数实参的栈空间(实际上没全部使用到)
8048385: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%ebp) //变量a赋值,记得本地变量的地址靠近栈帧的基地址,所以用%ebp引用
804838c: c7 45 f4 02 00 00 00 movl $0x2,-0xc(%ebp) //变量b赋值
8048393: 83 7d f8 00 cmpl $0x0,-0x8(%ebp) //变量a和0比较,其实就是判断“表达式 a”是不是为假
8048397: 75 0a jne 80483a3 <main+0x2f> //a-0如果不等0,也就是a为真时就跳到地址80483a3执行。
//已经知道a==1,表达式a为真,所以将跳到地址80483a3执行
8048399: 83 45 f4 01 addl $0x1,-0xc(%ebp)
804839d: 83 7d f4 00 cmpl $0x0,-0xc(%ebp)
80483a1: 74 13 je 80483b6 <main+0x42>
80483a3: 8b 45 f4 mov -0xc(%ebp),%eax //把变量b的值放到临时寄存器%eax
80483a6: 89 44 24 04 mov %eax,0x4(%esp) //接着把它作为printf函数第二个实参入栈,
//记得子函数的实参空间靠近栈顶,所以引用实参用%esp
80483aa: c7 04 24 90 84 04 08 movl $0x8048490,(%esp) //printf函数第一个实参入栈。记得X86下用户层的子函数参数
//是保存到栈的,而且是从右到左依次入栈
80483b1: e8 22 ff ff ff call 80482d8 <printf@plt> //调用printf函数
80483b6: b8 00 00 00 00 mov $0x0,%eax //%eax赋值0,%eax放的也就是main函数返回结果
80483bb: 83 c4 24 add $0x24,%esp //撤销新栈帧的本地变量栈空间,以及子函数实参栈空间
80483be: 59 pop %ecx //恢复保存的旧%ecx的值
80483bf: 5d pop %ebp //以下汇编码都是恢复旧栈帧的信息,main函数返回等
80483c0: 8d 61 fc lea -0x4(%ecx),%esp
80483c3: c3 ret

分析可见C语句 if (a || ++b)中的++b对应的汇编码是

 8048399:       83 45 f4 01             addl   $0x1,-0xc(%ebp)
804839d: 83 7d f4 00 cmpl $0x0,-0xc(%ebp)
80483a1: 74 13 je 80483b6 <main+0x42>

可是因为a==1,表达式a已经为真,++b这个语句,也就是上面的汇编码,根本就没运行。所以变量b的值没有自增,还是保持为2。

结论

表达式 a, b
a || b: 如果a为真,b就不管;如果运行到b,a必已是假
a && b: 如果a为假,b就不管;如果运行到b,a必已是真

内核代码实例

static struct char_device_struct *
__register_chrdev_region(unsigned int major, unsigned int baseminor,
int minorct, const char *name)
{
......

i = major_to_index(major);

for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
if ((*cp)->major > major ||
((*cp)->major == major &&
(((*cp)->baseminor >= baseminor) ||
((*cp)->baseminor + (*cp)->minorct > baseminor))))
break;

.....
}
例3

自增自减

自增自减,以及增减的前后问题。这类代码在内核数不胜数。理解稍有偏差,就会产生“边界问题”,或者在条件判断时理解出错。

#include <stdio.h>

int main()
{
int i = -1;
if (!i++) {
printf("inner: %d/n", i);
}
printf("outer: %d/n", i);

return 0;
}

汇编代码

08048374 <main>:
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
8048378: 83 e4 f0 and $0xfffffff0,%esp
804837b: ff 71 fc pushl -0x4(%ecx)
804837e: 55 push %ebp
804837f: 89 e5 mov %esp,%ebp
8048381: 51 push %ecx
8048382: 83 ec 24 sub $0x24,%esp
8048385: c7 45 f8 ff ff ff ff movl $0xffffffff,-0x8(%ebp)
804838c: 83 45 f8 01 addl $0x1,-0x8(%ebp)
8048390: 83 7d f8 01 cmpl $0x1,-0x8(%ebp)
8048394: 75 13 jne 80483a9 <main+0x35>
8048396: 8b 45 f8 mov -0x8(%ebp),%eax
8048399: 89 44 24 04 mov %eax,0x4(%esp)
804839d: c7 04 24 90 84 04 08 movl $0x8048490,(%esp)
80483a4: e8 2f ff ff ff call 80482d8 <printf@plt>
80483a9: 8b 45 f8 mov -0x8(%ebp),%eax
80483ac: 89 44 24 04 mov %eax,0x4(%esp)
80483b0: c7 04 24 9b 84 04 08 movl $0x804849b,(%esp)
80483b7: e8 1c ff ff ff call 80482d8 <printf@plt>
80483bc: b8 00 00 00 00 mov $0x0,%eax
80483c1: 83 c4 24 add $0x24,%esp
80483c4: 59 pop %ecx
80483c5: 5d pop %ebp
80483c6: 8d 61 fc lea -0x4(%ecx),%esp
80483c9: c3 ret
80483ca: 90 nop

内核代码实例

int platform_add_devices(struct platform_device **devs, int num)

{
int i, ret = 0;

for (i = 0; i < num; i++) {
ret = platform_device_register(devs[i]);
if (ret) {
while (--i >= 0) /*没错,devs[i]没注册成功的话,从devs[i-1]起反注册*/
platform_device_unregister(devs[i]);
break;
}
}

return ret;
}
例14

函数指针

解释在“穿越交叉索引工具的盲区”→函数指针

#include <stdio.h>

int main()
{
int myfunc(int a, int b)
{
int c = a + b;
printf("%d/n", c);
return 0;
}
int (*funa)(int, int) = myfunc;
int (*funb)(int, int) = &myfunc;
int (*func)(int, int) = (int (*)(int, int))myfunc;
int (*fund)(int, int) = (int (*)(int, int))(&myfunc);

myfunc(1, 2);
funa(3, 4);
funb(5, 6);
func(7, 8);
fund(9, 10);

return 0;
}

编译:
$ gcc -g -Wall fuk.c //注意,没任何警告

int main()
{
8048374: 8d 4c 24 04 lea 0x4(%esp),%ecx
....省略
int (*funa)(int, int) = myfunc;
8048385: c7 45 f8 13 84 04 08 movl $0x8048413,-0x8(%ebp)
int (*funb)(int, int) = &myfunc;
804838c: c7 45 f4 13 84 04 08 movl $0x8048413,-0xc(%ebp)
int (*func)(int, int) = (int (*)(int, int))myfunc;
8048393: c7 45 f0 13 84 04 08 movl $0x8048413,-0x10(%ebp)
int (*fund)(int, int) = (int (*)(int, int))(&myfunc);
804839a: c7 45 ec 13 84 04 08 movl $0x8048413,-0x14(%ebp)

myfunc(1, 2);
...省略
funa(3, 4);
80483b5: c7 44 24 04 04 00 00 movl $0x4,0x4(%esp)
80483bc: 00
80483bd: c7 04 24 03 00 00 00 movl $0x3,(%esp)
80483c4: 8b 45 f8 mov -0x8(%ebp),%eax
80483c7: ff d0 call *%eax
funb(5, 6);
....省略,funb, func,fund汇编码和funa完全相同

return 0;
8048405: b8 00 00 00 00 mov $0x0,%eax
}
804840a: 83 c4 24 add $0x24,%esp
...省略

08048413 <myfunc.1933>:
#include <stdio.h>
int main()
{
int myfunc(int a, int b)
{
8048413: 55 push %ebp
.....省略
}


xxx@ubuntu:~/dt/test$ gdb a.out
GNU gdb 6.8-debian
...
(gdb) list
1 #include <stdio.h>

......
17 funa(3, 4);
....
20
(gdb) b 17
(gdb) r
Starting program: /home/xxx/桌面/test/a.out
Breakpoint 1, main () at fuck.c:17
17 funa(3, 4);
(gdb) display/i $pc
1: x/i $pc
0x80483b5 <main+65>: movl $0x4,0x4(%esp)
(gdb) stepi
0x080483bd 17 funa(3, 4);
1: x/i $pc
0x80483bd <main+73>: movl $0x3,(%esp)
(gdb)
0x080483c4 17 funa(3, 4);
1: x/i $pc
0x80483c4 <main+80>: mov -0x8(%ebp),%eax
(gdb)
0x080483c7 17 funa(3, 4);
1: x/i $pc
0x80483c7 <main+83>: call *%eax
(gdb) p/x $eax
$4 = 0x8048413
(gdb) info line *0x8048413
Line 6 of "fuck.c" starts at address 0x8048413 <myfunc> and ends at 0x8048419 <myfunc+6>.
(gdb)


优化级别的影响

这部分内容有点偏题,没必要这么钻牛角尖。但是为了说明“调试用的代码和实际运行的代码是不一样”的这个事实以及因为代码优化导致的“非理想状态”的调用链问题(见“内核初窥”),有必要用观察一个实例,以便有个直观的印象。

首先应该知道,有没有指定调试选项-g(–debug),在相同优先级下生成的代码都是一样的。差别只是,指定-g后,多生成了一个调试表。

优化选项

下面文字来自“ARM 系列应用技术完全手册”

使用-Onum选择编译器的优化级别。优化级别分别有

  • -O0: 除一些简单的代码编号外,关闭所有优化,该选项可提供最直接的优化信息。
  • -O1: 关闭严重影响调试效果的优化功能。使用该编译选项,编译器会移除程序中未使用到的内联函数和静态函数。如果于–debug(也就是-g)一起使用,该选项可以在较好的代码密度下,给出最佳调试视图。
  • -O2: 生成充分优化代码。如果与–debug一起使用,调试效果可能不令人满意,因为对目标代码到源代码的映射可能因为代码优化而发生变化。如果不生成调试表,这是默认优化级别。
  • -O3: 最高优化级别。使用该优化级别,使生成的代码在时间和空间上寻求平衡。
例子
#include <stdio.h>


int add(int a, int b)
{
return (a + b);
}


void funa()
{
int a = 3 + 4;
int b;
printf("%d/n", a);
b = add(5,6);
printf("%d/n", b);
}

int main()
{
int m = 1 + 2;
printf("%d/n", m);
funa();
}

$ gcc -g -O0 src.c (或者不指定优化选项: gcc -g src.c,编译出的机器码一样)
$ objdump -d a.out

得到一个结论:如果指定了-g而没指定优化等级,那么默认优化等级是最低的-O0

08048374 <add>:
8048374: 55 push %ebp
8048375: 89 e5 mov %esp,%ebp
8048377: 8b 45 0c mov 0xc(%ebp),%eax
804837a: 03 45 08 add 0x8(%ebp),%eax
804837d: 5d pop %ebp
804837e: c3 ret

0804837f <funa>:
804837f: 55 push %ebp
8048380: 89 e5 mov %esp,%ebp //保存旧栈帧,建立新栈帧
8048382: 83 ec 18 sub $0x18,%esp //分配栈帧空间,注意分配了$0x18
8048385: c7 45 fc 07 00 00 00 movl $0x7,-0x4(%ebp) //-0x4(%ebp)是本地变量a的地址,int a = 3 + 4;
//注意编译器已经完成了计算
804838c: 8b 45 fc mov -0x4(%ebp),%eax //a放到临时寄存器%eax
804838f: 89 44 24 04 mov %eax,0x4(%esp) //接着作为printf第二个参数入栈
8048393: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp) //printf第一个参数入栈
804839a: e8 39 ff ff ff call 80482d8 <printf@plt> //printf("%d/n", a);
804839f: c7 44 24 04 06 00 00 movl $0x6,0x4(%esp) //add(5,6);第二个参数入栈
80483a6: 00
80483a7: c7 04 24 05 00 00 00 movl $0x5,(%esp) //add(5,6);第一个参数入栈
80483ae: e8 c1 ff ff ff call 8048374 <add> //调用add
80483b3: 89 45 f8 mov %eax,-0x8(%ebp) //-0x8(%ebp)是本地变量b的地址,b = add(5,6);
80483b6: 8b 45 f8 mov -0x8(%ebp),%eax //b放到临时寄存器%eax
80483b9: 89 44 24 04 mov %eax,0x4(%esp) //接着作为printf第二个参数入栈
80483bd: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp) //printf第一个参数入栈
80483c4: e8 0f ff ff ff call 80482d8 <printf@plt> //printf("%d/n", b);
80483c9: c9 leave //撤销新栈帧空间
80483ca: c3 ret //funa返回

080483cb <main>:
80483cb: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483cf: 83 e4 f0 and $0xfffffff0,%esp
80483d2: ff 71 fc pushl -0x4(%ecx)
80483d5: 55 push %ebp
80483d6: 89 e5 mov %esp,%ebp
80483d8: 51 push %ecx
80483d9: 83 ec 24 sub $0x24,%esp
80483dc: c7 45 f8 03 00 00 00 movl $0x3,-0x8(%ebp)
80483e3: 8b 45 f8 mov -0x8(%ebp),%eax
80483e6: 89 44 24 04 mov %eax,0x4(%esp)
80483ea: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483f1: e8 e2 fe ff ff call 80482d8 <printf@plt>
80483f6: e8 84 ff ff ff call 804837f <funa>
80483fb: 83 c4 24 add $0x24,%esp
80483fe: 59 pop %ecx
80483ff: 5d pop %ebp
8048400: 8d 61 fc lea -0x4(%ecx),%esp
8048403: c3 ret
$ gcc -g -O1 src.c
$ objdump -d a.out


08048374 <add>:
8048374: 55 push %ebp
8048375: 89 e5 mov %esp,%ebp
8048377: 8b 45 0c mov 0xc(%ebp),%eax
804837a: 03 45 08 add 0x8(%ebp),%eax
804837d: 5d pop %ebp
804837e: c3 ret

0804837f <funa>: //funa与-O0相比,没有了向本地变量a,b赋值的过程。
//代码量少了,分配的栈帧空间也小了。
804837f: 55 push %ebp
8048380: 89 e5 mov %esp,%ebp
8048382: 83 ec 08 sub $0x8,%esp //分配栈帧空间,注意分配了$0x8,比-O0下小了
8048385: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp) //printf("%d/n", a);的第二个参数入栈。
//注意,与-O0相比,没有向本地变量a赋值的过程。
804838c: 00
804838d: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp)
8048394: e8 3f ff ff ff call 80482d8 <printf@plt> //printf("%d/n", a);
8048399: c7 44 24 04 06 00 00 movl $0x6,0x4(%esp)
80483a0: 00
80483a1: c7 04 24 05 00 00 00 movl $0x5,(%esp)
80483a8: e8 c7 ff ff ff call 8048374 <add> //add(5,6);
80483ad: 89 44 24 04 mov %eax,0x4(%esp) //add的返回结果作为printf("%d/n", b);的第二个参数入栈。
//注意,与-O0相比,没有向本地变量b赋值的过程。
80483b1: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp)
80483b8: e8 1b ff ff ff call 80482d8 <printf@plt> //printf("%d/n", b);
80483bd: c9 leave
80483be: c3 ret

080483bf <main>:
80483bf: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c3: 83 e4 f0 and $0xfffffff0,%esp
80483c6: ff 71 fc pushl -0x4(%ecx)
80483c9: 55 push %ebp
80483ca: 89 e5 mov %esp,%ebp
80483cc: 51 push %ecx
80483cd: 83 ec 14 sub $0x14,%esp
80483d0: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483d7: 00
80483d8: c7 04 24 c0 84 04 08 movl $0x80484c0,(%esp)
80483df: e8 f4 fe ff ff call 80482d8 <printf@plt>
80483e4: e8 96 ff ff ff call 804837f <funa>
80483e9: 83 c4 14 add $0x14,%esp
80483ec: 59 pop %ecx
80483ed: 5d pop %ebp
80483ee: 8d 61 fc lea -0x4(%ecx),%esp
80483f1: c3 ret
$ gcc -g -O2 src.c
$ objdump -d a.out

我们应该知道,如果没有指定-g和优化选项,那么默认的优化等级就是-O2


08048380 <add>:
8048380: 55 push %ebp
8048381: 89 e5 mov %esp,%ebp
8048383: 8b 45 0c mov 0xc(%ebp),%eax
8048386: 03 45 08 add 0x8(%ebp),%eax
8048389: 5d pop %ebp
804838a: c3 ret
804838b: 90 nop
804838c: 8d 74 26 00 lea 0x0(%esi),%esi

08048390 <funa>:
8048390: 55 push %ebp
8048391: 89 e5 mov %esp,%ebp
8048393: 83 ec 08 sub $0x8,%esp
8048396: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp)
804839d: 00
804839e: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483a5: e8 2e ff ff ff call 80482d8 <printf@plt>
80483aa: c7 44 24 04 06 00 00 movl $0x6,0x4(%esp)
80483b1: 00
80483b2: c7 04 24 05 00 00 00 movl $0x5,(%esp)
80483b9: e8 c2 ff ff ff call 8048380 <add>
80483be: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp) //第二个参数入栈
80483c5: 89 44 24 04 mov %eax,0x4(%esp) //第一个参数入栈。注意和-O1相比,参数在栈帧空间的位置没变,
//但是入栈指令的执行顺序有变。
80483c9: e8 0a ff ff ff call 80482d8 <printf@plt> //printf("%d/n", b);
80483ce: c9 leave
80483cf: c3 ret

080483d0 <main>:
80483d0: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483d4: 83 e4 f0 and $0xfffffff0,%esp
80483d7: ff 71 fc pushl -0x4(%ecx)
80483da: 55 push %ebp
80483db: 89 e5 mov %esp,%ebp
80483dd: 51 push %ecx
80483de: 83 ec 14 sub $0x14,%esp
80483e1: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483e8: 00
80483e9: c7 04 24 d0 84 04 08 movl $0x80484d0,(%esp)
80483f0: e8 e3 fe ff ff call 80482d8 <printf@plt>
80483f5: e8 96 ff ff ff call 8048390 <funa>
80483fa: 83 c4 14 add $0x14,%esp
80483fd: 59 pop %ecx
80483fe: 5d pop %ebp
80483ff: 8d 61 fc lea -0x4(%ecx),%esp



8048402: c3 ret
$ gcc -g -O3 src.c
$ objdump -d a.out


048380 <add>:
8048380: 55 push %ebp
8048381: 89 e5 mov %esp,%ebp
8048383: 8b 45 0c mov 0xc(%ebp),%eax
8048386: 03 45 08 add 0x8(%ebp),%eax
8048389: 5d pop %ebp
804838a: c3 ret
804838b: 90 nop
804838c: 8d 74 26 00 lea 0x0(%esi),%esi

08048390 <funa>: //与-O2相比,对函数add()的调用被编译器优化消失
8048390: 55 push %ebp
8048391: 89 e5 mov %esp,%ebp
8048393: 83 ec 08 sub $0x8,%esp
8048396: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp)
804839d: 00
804839e: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483a5: e8 2e ff ff ff call 80482d8 <printf@plt>
80483aa: c7 44 24 04 0b 00 00 movl $0xb,0x4(%esp) //注意,与-O2相比,b = add(5,6);被优化掉了。
//之前应该有个优化为内联函数的过程,但因为add函数
//太简单,被直接计算了结果。(猜想)
//编译器直接计算出它的结果$0xb,也就是11
80483b1: 00
80483b2: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483b9: e8 1a ff ff ff call 80482d8 <printf@plt> //printf("%d/n", b);
80483be: c9 leave
80483bf: c3 ret

080483c0 <main>:
80483c0: 8d 4c 24 04 lea 0x4(%esp),%ecx
80483c4: 83 e4 f0 and $0xfffffff0,%esp
80483c7: ff 71 fc pushl -0x4(%ecx)
80483ca: 55 push %ebp
80483cb: 89 e5 mov %esp,%ebp
80483cd: 51 push %ecx
80483ce: 83 ec 14 sub $0x14,%esp
80483d1: c7 44 24 04 03 00 00 movl $0x3,0x4(%esp)
80483d8: 00
80483d9: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483e0: e8 f3 fe ff ff call 80482d8 <printf@plt>
80483e5: c7 44 24 04 07 00 00 movl $0x7,0x4(%esp)
80483ec: 00
80483ed: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
80483f4: e8 df fe ff ff call 80482d8 <printf@plt>
80483f9: c7 44 24 04 0b 00 00 movl $0xb,0x4(%esp)
8048400: 00
8048401: c7 04 24 e0 84 04 08 movl $0x80484e0,(%esp)
8048408: e8 cb fe ff ff call 80482d8 <printf@plt>
804840d: 83 c4 14 add $0x14,%esp
8048410: 59 pop %ecx
8048411: 5d pop %ebp
8048412: 8d 61 fc lea -0x4(%ecx),%esp
8048415: c3 ret

汇编基础--ARM篇

说明:

1. 部分内容和X86的重复,重复部分请参考X86的内容。

2. 某些内容不具备普遍性。比如给出的反汇编代码,在不同的优化等级下是不同的。但是在熟悉了典型的函数调用链反汇编代码,对于有变化的其他形式也就不难理解了。

用户手册

ARM7TDMI Technical Reference Manual

ARM920T Technical Reference Manual

http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.home/index.html

指令速查 http://www.arm.com/pdfs/QRC0001H_rvct_v2.1_arm.pdf

调用链形成和参数传递

注意:arm体系过程调用的文字说明部分,都是依据AAPCS标准。

壮观的标准

参考:

AAPCS

Procedure Call Standard for the ARM Architecture

http://infocenter.arm.com/help/topic/com.arm.doc.ihi0042b/IHI0042B_aapcs.pdf

终于在“ARM Procedure Call Standard”中找到了答案

PCS      Procedure Call Standard.
AAPCS Procedure Call Standard for the ARM Architecture (this standard).
APCS ARM Procedure Call Standard (obsolete).
TPCS Thumb Procedure Call Standard (obsolete).
ATPCS ARM-Thumb Procedure Call Standard (precursor to this standard).
PIC, PID Position-independent code, position-independent data.

下面的标准已过时

APCS

ARM Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/BGBGFIDA.html

Using the ARM Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0040d/Chdbceig.html

APCS 简介http://www.bsdmap.com/UNIX_html/ARM/apcsintro.html#01

TPCS

Thumb Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0041c/BCEEAHAF.html

Using the Thumb Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0040d/Cihdbchi.html

ATPCS

About the ARM-Thumb Procedure Call Standard http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0056d/Bcffcieh.html

别名的烦恼

arm体系的函数调用标准换了好几个版本,对寄存器的别名也是不一样。不同的调试器,或者它在不同的选项下,对同一个寄存器可能就有多种称呼。又或者你在调试器下看到的名称和书籍上的不一样。所以,又必要知道这些寄存器各自都有哪些别名。

我们运行下命令

$ arm-linux-gnueabi-objdump --help
....省略
The following ARM specific disassembler options are supported for use with
the -M switch:
reg-names-special-atpcs Select special register names used in the ATPCS
reg-names-atpcs Select register names used in the ATPCS
reg-names-apcs Select register names used in the APCS
reg-names-std Select register names used in ARM's ISA documentation
reg-names-gcc Select register names used by GCC
reg-names-raw Select raw register names
force-thumb Assume all insns are Thumb insns
no-force-thumb Examine preceeding label to determine an insn's type

我们下载它的源码打开看看

$ sudo apt-get source binutils-arm-linux-gnueabi

完成后,在下载目录下多了几个东东,其中有一个文件夹binutils-2.18.1~cvs20080103,这是debian对官方binutils进行过修改的源码。在里面搜索文件arm-dis.c,该文件中有以下这个数组。

就是不同标准下各个寄存器的不同别名。

static const arm_regname regnames[] =
{
{ "raw" , "Select raw register names",
{ "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"}},
{ "gcc", "Select register names used by GCC",
{ "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "sl", "fp", "ip", "sp", "lr", "pc" }},
{ "std", "Select register names used in ARM's ISA documentation",
{ "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "sp", "lr", "pc" }},
{ "apcs", "Select register names used in the APCS",
{ "a1", "a2", "a3", "a4", "v1", "v2", "v3", "v4", "v5", "v6", "sl", "fp", "ip", "sp", "lr", "pc" }},
{ "atpcs", "Select register names used in the ATPCS",
{ "a1", "a2", "a3", "a4", "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "IP", "SP", "LR", "PC" }},
{ "special-atpcs", "Select special register names used in the ATPCS",
{ "a1", "a2", "a3", "a4", "v1", "v2", "v3", "WR", "v5", "SB", "SL", "FP", "IP", "SP", "LR", "PC" }},
};

但是可以看到,该列表并没有包含AAPCS标准,AAPCS标准对 r9 又引入了一个别名 TR,这样AAPCS下,r9使用了三个别名v6, SB, TR。选用哪个
别名,是依赖于不同平台的选择。

[扩展,简要说明原理。并用实例解析]

寄存器的角色与保护
  • 寄存器的角色(AAPCS标准)
寄存器 可选寄存器名 特殊寄存器名 在函数调用中的角色
r15 PC The Program Counter.
r14 LR The Link Register.
r13 SP The Stack Pointer.
r12 IP The Intra-Procedure-call scratch register.
r11 v8 Variable-register 8.
r10 v7 Variable-register 7.
r9 v6/SB/TR Platform register. The meaning of this register is defined by the platform standard
r8 v5 Variable-register 5.
r7 v4 Variable register 4.
r6 v3 Variable register 3.
r5 v2 Variable register 2.
r4 v1 Variable register 1.
r3 a4 Argument / scratch register 4.
r2 a3 Argument / scratch register 3.
r1 a2 Argument / result / scratch register 2.
r0 a1 Argument / result / scratch register 1.

前四个寄存器r0-r3 (a1-a4)用于传递参数给子函数或从函数中返回结果值。他们也可用于在一个函数中保存寄存器的值(但是,一般只用在子函数调用中)。

寄存器r12 (IP) 可在函数以及该函数调用的任何子函数中被链接器用作临时寄存器。它也可以在函数调用中用于保存寄存器的值。

寄存器r9的角色是平台相关的。虚拟系统可能赋予该寄存器任何角色,因此必须说明它的用法。比如,在位置无关数据模型中它可以指定为static base(SB),或者在带有本地线程存储的环境中指定它为thread register(TR)。该寄存器的使用可能要求在所有调用过程前后,它保存的值必须不变。在一个不需要这样特殊寄存器的虚拟平台上,r9可以指定为新增的callee-saved variable register,v6.

通常,寄存器r4-r8, r10 和 r11 (v1-v5, v7 和 v8)用于保存函数的本地变量。这些寄存器中,只有v1-v4能被整个thumb指令集一致地使用,但是AAPCS并没有规定Thumb代码只能使用这些寄存器。

子函数必须保护寄存器r4-r8, r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。

在所有的函数调用标准中,寄存器r12-r15都扮演特殊的角色。依据这些角色,它们被标注为IP, SP, LR 和 PC。

寄存器CPSR的属性(省)

  • 寄存器保护规则

子函数必须保护寄存器r4-r8, r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。 子函数调用

  • 子函数调用

ARM 和 Thumb 指令集都有一个函数调用指令元语,BL,它执行branch-with-link 操作。BL的执行效果是把紧跟程序计数器的下一个值--也就是返回地址--传送到链接寄存器(LR),然后把目标地址传送到程序寄存器(PC)中。如果BL指令是在Thumb状态下执行的,链接寄存器的Bit 0就设置为1;如果是在ARM状态下执行的,则设置为0。执行的结果是,把控制权转给目标地址,并把存放在LR中的返回地址作为附加的参数传递给了被调用的函数。

当返回地址装载到PC时,控制就返回给跟随BL后面的指令。

子函数调用可以由具有下面效果的任何指令序列完成:

       	LR[31:1] ← 返回地址
LR[0] ← 返回地址的代码类型 (0 ARM, 1 Thumb)
PC ← 子函数地址
...
返回地址:

例如,在ARM状态中,调用由r4指定了地址的子函数

do:
MOV LR, PC
BX r4
...

注意,相同的指令序列在Thumb状态中将不能工作,因为设置LR的指令并没有拷贝Thumb 状态标志位到LR[0]中。

在ARM V5架构中,ARM 和 Thumb指令集都提供了BLX指令,它将调用由一个寄存器指定了地址的子函数,并正确地设置返回地址为程序计数器的下一个值。

条件执行
操作码[31:28] 助记符扩展 解释 用于执行的标志位状态
0000 EQ 相等/等于0 Z置位
0001 NE 不等 Z清0
0010 CS/HS 进位/无符号数高于或等于 C置位
0011 CC/LO 无进位/无符号数小于 C清0
0100 MI 负数 N置位
0101 PL 正数或0 N清0
0110 VS 溢出 V置位
0111 VC 未溢出 V清0
1000 HI 无符号数高于 C置位,Z清0
1001 LS 无符号数小于或等于 C清0,Z置位
1010 GE 有符号数大于或等于 N等于V
1011 LT 有符号数小于 N不等于V
1100 GT 有符号数大于 Z清0且N等于V
1101 LE 有符号数小于或等于 Z置位且N不等于V
1110 AL 总是 任何状态
1111 NV 从不(未使用)
调用链的形成

注意对比ARM和X86在调用链形成的类似和区别之处。

区别,首先在寄存器的名称和角色的差异。

1. X86中寄存器%eip指向的是下一个将要执行的指令。在ARM中也有个类似别名的寄存器ip。但这个寄存器ip的作用并不是指向的是下一个将要执行的指令。在ARM中,寄存器pc才是起着X86中寄存器%eip的角色,也就是包含下一个将要执行指令的地址。而ARM中的ip寄存器,作用比较自由,类似干杂工的人,一般用于临时寄存器。[扩展,引用权威手册的话]

2. X86中,返回地址是直接保存在栈中的。但是ARM不一样了,它寄存器比X86多得多,财大气粗,所以,返回地址保存在了专用的寄存器lr(link register)中。但是,不要以为把返回地址放到专用的寄存器中会省事,其实反而多事了。因为,在调用函数刚执行完调用语句之时,lr保存的是子函数的返回地址,而指令控制权转移到了子函数后,子函数照样可能调用自己的子函数,依次需要使用lr。所以自然也就有了lr的值的保存与恢复的问题,解决方法还是要靠压栈解决。(参考下面的内容)

3. 我们知道,描述栈帧就是描述栈帧的基地址和顶端地址。在X86中,用专用的寄存器%ebp保存栈基址,也就是base pointer;%esp保存栈顶端地址,也就是stack pointer。在ARM中,也有专用的寄存器保存栈顶端地址,就是SP(stack pointer的简称)。但是,在保存栈基址这方面,依据最新的AAPCS标准,ARM就很吝啬了,没有一个保存栈基址的专用寄存器。又不过呢,在APCS和ATPCS标准中,有fp寄存器用于保存帧指针(frame pointer,也就是X86的base pointer)。在现在的编译器,可以看到,还是依照惯例把fp用于保存帧指针。既然如此,当然也有个入栈保存恢复的问题。

调用链包含两方面的内容,和X86类似

1.返回地址的保存与恢复

由调用函数在执行调用指令时把子函数的返回地址传送进连接寄存器lr中,指令控制权转交给子函数后,再由子函数负责把上层函数的lr(也就是子函数的返回地址)保存到栈中。然后子函数在返回前的最后时刻,再负责把lr的保存值从栈弹回到lr中,从而恢复了上层函数的lr。这时还没完事,子函数在执行返回指令时,由返回指令把lr的值传送到寄存器pc(Program Counter),从而导致接下来的指令是从子函数的返回地址开始运行。这样,指令控制权就返回给了调用函数。

我们应当注意到,ARM中调用指令也是多种多样的。有b,bl,bx,bxl。如果调用指令是不带连接的指令,比如b,bx,这时就要人工给lr赋值。不过为了简便,我不再区分这两类指令,而把实现跳转和连接以及可能的换态这些功能的整个指令序列为“调用指令”,相关区别参考指令手册。在ARM中,返回指令和调用指令都是同一套的。而X86,调用用call,返回用ret。

2.旧栈帧的保存与恢复

对比X86栈帧的保存与恢复的方式,ARM的更加简单直接。就是直接把上一栈帧的帧指针(frame pointer,也就是栈帧基地址)以及栈顶端指针sp(stack pointer)压入栈中。子函数返回时,在执行返回指令之前的最后关头才从栈弹出fp和sp的值,从而恢复旧栈帧。这个过程真的没有遗漏了吗?我们看下,上面的步骤保证了调用函数的栈帧不被破坏,但是子函数自己的栈帧却没有建立起来呢。首先是帧指针需要人赋值。这个情形和X86非常相似。子函数在使用栈帧之前,把上层函数的栈顶端指针sp赋给一个临时寄存器ip,然后在旧fp的值被压栈保存之后,把ip的值减去4,再赋给帧指针寄存器fp,此时,fp就指向了新栈帧的基址。这是因为,新栈帧基地址刚好位于旧栈帧栈顶之下,地址低了4字节。其次,子函数栈帧的栈顶指针sp也是要考虑的,根据压栈指令的不同,sp可能不需要人工维护,也可能需要人工维护[有疑问...????]。

我们还注意到,在X86中,子函数的栈帧的底端(也就是%ebp所指的内存位置)存放着上一层栈帧的基址指针(旧%ebp)的值,一层层下去,这样就形成回溯的链条。那么,在ARM之下,也是靠子函数的栈帧的底端提供回溯的能力的吗?当然不是。实际上子函数的栈帧的基址位置存放的是什么,这无所谓的。

[疑问???如果旧fp保存在新栈帧中的位置不是固定的,那么调试器是如何做到栈帧回溯的呢?]

根据AAPCS标准的规定,子函数必须保护寄存器r4-r8, r10, r11 和 SP(还有r9,如果在函数调用过程中r6被指定为v6的话)的值。注意,它用的字眼是“保护”,而不是“保存”。

  • 应用层实例解析
#include <stdio.h>

void func()
{}

void funb()
{
func();
}


void funa()
{
funb();
}


int main()
{
funa();
}
-----------
000083b0 <func>:
#include <stdio.h>

void func()
{}
83b0: e1a0c00d mov ip, sp
83b4: e92dd800 push {fp, ip, lr, pc}
83b8: e24cb004 sub fp, ip, #4 ; 0x4
83bc: e24bd00c sub sp, fp, #12 ; 0xc
83c0: e89d6800 ldm sp, {fp, sp, lr}

83c4: e12fff1e bx lr

000083c8 <funb>:

void funb()
{
83c8: e1a0c00d mov ip, sp
83cc: e92dd800 push {fp, ip, lr, pc}
83d0: e24cb004 sub fp, ip, #4 ; 0x4
func();
83d4: ebfffff5 bl 83b0 <func>
}
83d8: e24bd00c sub sp, fp, #12 ; 0xc
83dc: e89d6800 ldm sp, {fp, sp, lr}
83e0: e12fff1e bx lr

000083e4 <funa>:


void funa()
{

83e4: e1a0c00d mov ip, sp
83e8: e92dd800 push {fp, ip, lr, pc}
83ec: e24cb004 sub fp, ip, #4 ; 0x4
funb();
83f0: ebfffff4 bl 83c8 <funb>
}
83f4: e24bd00c sub sp, fp, #12 ; 0xc
83f8: e89d6800 ldm sp, {fp, sp, lr}
83fc: e12fff1e bx lr
00008400 <main>:

int main()
{
8400: e1a0c00d mov ip, sp
8404: e92dd800 push {fp, ip, lr, pc}
8408: e24cb004 sub fp, ip, #4 ; 0x4
funa();
840c: ebfffff4 bl 83e4 <funa>
}
8410: e24bd00c sub sp, fp, #12 ; 0xc
8414: e89d6800 ldm sp, {fp, sp, lr}
8418: e12fff1e bx lr

  • 内核层实例解析
栈帧结构与参数传递

[1.栈:栈对齐,栈限制。2.参数传递:variadic函数,nonvariadic函数。3.结果的返回 4.互交代码(ARM-Thumb interworking)]

栈帧示意图

                        +------------------------------ + ---------
| Register Save Area | |
+------------------------------ + |
| Locals and Temporaries | |
+------------------------------ +
| alloca() Locals | Caller's Frame
+------------------------------ +
| Incoming Args Past Four Words | |
+------------------------------ + ---------
| First Four Words Of Args | |
Frame Pointer--> +------------------------------ + |
| Register Save Area | |
+------------------------------ + Current Frame
| Locals and Temporaries |
+------------------------------ + |
| alloca() Locals | |
+------------------------------ + |
| Outgoing Args Past Four Words | |
Stack Pointer---> +------------------------------ + ---------
完整的调用过程

函数caller调用子函数callee,这是应用层的普通函数调用过程。如果是远调用,跨态调用要考虑的东西更多。但这个例子已经充分展示了调用过程的繁复部分。

  • 函数调用前调用者的动作
  • 函数调用 call callee
  • 函数调用后被调用者的动作
  • 调用返回前被调用者的动作
  • 调用返回后调用者的动作
  • 应用层实例解析
  • 内核层实例解析

内核初窥

目的: 结合内核代码,初步熟悉gdb命令和了解define语句,内嵌函数等“伪函数”的机器码形式等。

为了简化问题的描述,在实际分析前,先将知识点分解介绍一下。

理想状态的backtrace

下面我给出一个处于“理想状态”的经典backtrace(也就是本人说的调用链)。所谓“理想状态的”的backtrace是指,可以利用内核源码交叉索引工具,依据gdb给出的这个backtrace,从frame 0开始一级级往后最追溯,能够一直追溯到最前面的frame N,而且追溯的过程中,没有出现多出来的连接frameN和frame(N-1)的“过渡”frame.

注意其中的两个条件:1.能够 2.不多出。但是,在现实的世界里,往往没这么美好。源码浏览工具往往要么“不能”,要么“多出”。造成前者的原因在于源码浏览工具的局限性,造成后者的是代码优化。详细情况可看下节的分析。

追溯的方法对于source insight来说就是:打开”relation window”→选中要被追溯的函数→右键→选“view relation”→选“referenced by functions”,这样就能显示出调用了被选函数的函数来。

我们拿下面这个“理想状态”的backtrace分析一下

(gdb) bt
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
#1 0xc01de8be in kobject_init_internal (kobj=0xdc40abe0) at lib/kobject.c:149
#2 0xc01de928 in kobject_init (kobj=0xdc40abe0, ktype=0xc035b9dc) at lib/kobject.c:282
#3 0xc01de972 in kobject_create () at lib/kobject.c:619
#4 0xc01def53 in kobject_create_and_add (name=0xdc40abe4 "", parent=0xc035b9dc) at lib/kobject.c:641
#5 0xc0393b04 in mnt_init () at fs/namespace.c:2333
#6 0xc039382b in vfs_caches_init (mempages=108676) at fs/dcache.c:2212
#7 0xc037f868 in start_kernel () at init/main.c:666
#8 0xc037f008 in i386_start_kernel () at arch/x86/kernel/head32.c:13
#9 0x00000000 in ?? ()

理想状态下的backtrace各个域的含义是(注意,在非理想状态的backtrace中,这些含义往往对不上号)

#frameN的编号 frame(N-1)的返回地址(注:fram0没有这项) in frameN所处的函数(该函数的参数...) at 该函数所处的源文件 : frameN函数内对frame(N-1)函数的调用语句在源文件中所处的行数 

我们看下

#0  kref_init (kref=0xdc40abe4) at lib/kref.c:33

它说明frame0时,kref_init正要运行。传入的参数是0xdc40abe4。函数kref_init从源文件lib/kref.c第33行开始。 在gdb下调用shell来查看源文件

(gdb) shell vi lib/kref.c

vi 出来后打命令:set nu可看到

 31  */
32 void kref_init(struct kref *kref)
33 {
34 kref_set(kref, 1);
35 }
36

我们再看看frame0这一瞬间是不是“kref_init正要运行”。应该知道,“正要运行”和“正要被调用”是两个不同的概念。前者来说,到了下一个指令,代码的控制权就会交给了被调用的函数;而后者,到了下一个指令,代码的控制权还在调用者手里,

(gdb) f 0
#0 kref_init (kref=0xdc40abe4) at lib/kref.c:33
33 {
(gdb) info registers
....
edi 0x0 0
eip 0xc01df520 0xc01df520 <kref_init> //<-注意eip是下一个将要运行的指令地址
eflags 0x282 [ SF IF ]
....
(gdb) disass kref_init
Dump of assembler code for function kref_init:
0xc01df520 <kref_init+0>: push %ebp //对比上面,eip指向这里
0xc01df521 <kref_init+1>: mov %esp,%ebp
...
0xc01df52f <kref_init+15>: ret
End of assembler dump.
(gdb)

可见,kobject_init_internal的调用指令call已经执行完毕,到了frame0时,下一个指令“将要运行”函数kref_init。

再看看

#1  0xc01de8be in kobject_init_internal (kobj=0xdc40abe0) at lib/kobject.c:149

frameN与frame(N-1)之间是调用的关系,前者调用了后者。也就是说,frame1的kobject_init_internal调用frame0的kref_init,并且kref_init函数返回后,将返回到地址0xc01de8be继续执行。0xc01de8be就在kobject_init_internal的体内,函数kobject_init_internal中调用kref_init的C语句位于lib/kobject.c的149行。

查看一下kobject_init_internal的反汇编码

(gdb) disass kobject_init_internal
Dump of assembler code for function kobject_init_internal:
0xc01de8ac <kobject_init_internal+0>: push %ebp
0xc01de8ad <kobject_init_internal+1>: test %eax,%eax
0xc01de8af <kobject_init_internal+3>: mov %esp,%ebp
0xc01de8b1 <kobject_init_internal+5>: push %ebx
0xc01de8b2 <kobject_init_internal+6>: mov %eax,%ebx
0xc01de8b4 <kobject_init_internal+8>: je 0xc01de8d3 <kobject_init_internal+39>
0xc01de8b6 <kobject_init_internal+10>: lea 0x4(%eax),%eax
0xc01de8b9 <kobject_init_internal+13>: call 0xc01df520 <kref_init>
0xc01de8be <kobject_init_internal+18>: lea 0x8(%ebx),%eax //注意这个地址0xc01de8be是kref_init的返回地址
0xc01de8c1 <kobject_init_internal+21>: mov %eax,0x8(%ebx)

再看看lib/kobject.c,看看最后的那个行数的意义

145 static void kobject_init_internal(struct kobject *kobj)
146 {
147 if (!kobj)
148 return;
149 kref_init(&kobj->kref); //注意kobject_init_internal调用子函数kref_init的C语句位于行数149
150 INIT_LIST_HEAD(&kobj->entry);
151 kobj->state_in_sysfs = 0;
152 kobj->state_add_uevent_sent = 0;
153 kobj->state_remove_uevent_sent = 0;
154 kobj->state_initialized = 1;
155 }

在验证一下

#2  0xc01de928 in kobject_init (kobj=0xdc40abe0, ktype=0xc035b9dc) at lib/kobject.c:282

看看kobject_init的反汇编码

(gdb) disass kobject_init
Dump of assembler code for function kobject_init:
0xc01de8f3 <kobject_init+0>: push %ebp
........
0xc01de923 <kobject_init+48>: call 0xc01de8ac <kobject_init_internal>
0xc01de928 <kobject_init+53>: mov %esi,0x18(%ebx) //注意这个地址0xc01de928是kobject_init_internal的返回地址
......
0xc01de94b <kobject_init+88>: pop %ebp
0xc01de94c <kobject_init+89>: ret
End of assembler dump.

看看看看lib/kobject.c,看看最后的那个行数的意义

263 void kobject_init(struct kobject *kobj, struct kobj_type *ktype)
264 {
265 char *err_str;
.......
282 kobject_init_internal(kobj); 注意kobject_init调用子函数kobject_init_internal的C语句位于行数282
283 kobj->ktype = ktype;
......
287 printk(KERN_ERR "kobject (%p): %s/n", kobj, err_str);
288 dump_stack

通过这两个例子,可见最初的猜想是正确的。

调用链的层次

1. 人观念层次

2. 交叉解析器层次

2. c调用层次

3. 编译器(机器码静态)层次

4. 运行时(机器码动态)层次,也叫调试器层次

很明显,前面所讲的“理想状态”的backtrace就是指在交叉解析器层次下和在调试器层次下的表现相同的调用链。

追溯ramfs注册函数被调用的过程

任务:

从一个断点开始,从后向前推导,分析出ramfs注册函数的调用过程。同时,观察调试器的优点和局限性。

ramfs文件系统的注册函数是register_filesystem(&ramfs_fs_type)。为了更快定位,在上层函数init_ramfs_fs下断点。而后在gdb下得到的调用链是

(gdb) bt
#0 register_filesystem (fs=0xc03595cc) at fs/filesystems.c:68
#1 0xc0394594 in init_ramfs_fs () at fs/ramfs/inode.c:213
#2 0xc037f473 in kernel_init (unused=<value optimized out>) at init/main.c:708
#3 0xc010463f in kernel_thread_helper () at arch/x86/kernel/entry_32.S:1013

我们注意到:

1. 这个backtrace包含的函数只有4个,实际上并非如此。经过分析,它实际上(用C的观点看)调用链如下所示,这是为什么呢?

start_kernel→rest_init→kernel_thread→kernel_thread_helper→call %ebx (即call kernel_init)→do_basic_setup→do_initcalls→do_one_initcall→result = fn() (即call init_ramfs_fs)→register_filesystem

2. backtrace推溯到kernel_thread_helper后就再没下文了。又是什么使得调试器变成了瞎子,无法看得再远了呢?

欲见其详,且听下回分解

[下面准备材料]

kernel_init对do_basic_setup的调用被优化成内联函数

do_basic_setup对do_initcalls的调用被优化成内联函数

do_initcalls对do_one_initcall的调用被优化成内联函数

有三层的非内联函数都被被优化成内联函数,整个代码被优化的乱七八糟。

838 static int __init kernel_init(void * unused)
839 {
.....
864 cpuset_init_smp();
865
866 do_basic_setup();
867
.......
887 return 0;
888 }


static void __init do_basic_setup(void)
{
/* drivers will send hotplug events */
init_workqueues();
usermodehelper_init();
driver_init();
init_irq_proc();
do_initcalls();
}

741 static void __init do_initcalls(void)
742 {
743 initcall_t *call;
744
745 for (call = __initcall_start; call < __initcall_end; call++)
746 do_one_initcall(*call);
747
748 /* Make sure there is no pending stuff from the initcall sequence */
749 flush_scheduled_work();
750 }

static void __init do_one_initcall(initcall_t fn)
{
int count = preempt_count();
ktime_t t0, t1, delta;
char msgbuf[64];
int result;

if (initcall_debug) {
print_fn_descriptor_symbol("calling %s/n", fn);
t0 = ktime_get();
}

result = fn();

if (initcall_debug) {
....
}

static inline void print_fn_descriptor_symbol(const char *fmt, void *addr)
{
#if defined(CONFIG_IA64) || defined(CONFIG_PPC64)
addr = *(void **)addr;
#endif
print_symbol(fmt, (unsigned long)addr);
}


(gdb) disass kernel_init
Dump of assembler code for function kernel_init:
0xc037f349 <kernel_init+0>: push %ebp
0xc037f34a <kernel_init+1>: mov %esp,%ebp
0xc037f34c <kernel_init+3>: push %edi
0xc037f34d <kernel_init+4>: push %esi
......
0xc037f413 <kernel_init+202>: call 0xc0391454 <cpuset_init_smp>
0xc037f418 <kernel_init+207>: call 0xc0390081 <init_workqueues> //<-do_basic_setup被优化成内联函数,在这里开始展开
0xc037f41d <kernel_init+212>: call 0xc039004e <usermodehelper_init>
0xc037f422 <kernel_init+217>: call 0xc039b7d1 <driver_init>
0xc037f427 <kernel_init+222>: call 0xc0153e18 <init_irq_proc>
0xc037f42c <kernel_init+227>: movl $0xc03aa470,-0x5c(%ebp) //do_initcalls被优化成内联函数,在这里开始展开
0xc037f433 <kernel_init+234>: pop %eax
0xc037f434 <kernel_init+235>: pop %edx
0xc037f435 <kernel_init+236>: jmp 0xc037f559 <kernel_init+528>
0xc037f43a <kernel_init+241>: mov -0x5c(%ebp),%eax //do_one_initcall被优化成内联函数,在这里开始展开
0xc037f43d <kernel_init+244>: mov (%eax),%eax
0xc037f43f <kernel_init+246>: mov %eax,-0x58(%ebp)
0xc037f442 <kernel_init+249>: mov %esp,%eax
0xc037f444 <kernel_init+251>: and $0xffffe000,%eax
0xc037f449 <kernel_init+256>: mov 0x14(%eax),%eax
0xc037f44c <kernel_init+259>: cmpl $0x0,0xc03a1820
0xc037f453 <kernel_init+266>: mov %eax,-0x54(%ebp)
0xc037f456 <kernel_init+269>: je 0xc037f470 <kernel_init+295>
0xc037f458 <kernel_init+271>: mov -0x58(%ebp),%edx //内联函数print_fn_descriptor_symbol在这里开始展开
0xc037f45b <kernel_init+274>: mov $0xc030d1be,%eax
0xc037f460 <kernel_init+279>: call 0xc013f598 <__print_symbol>//内联函数print_fn_descriptor_symbo的展开结束
0xc037f465 <kernel_init+284>: call 0xc013352f <ktime_get>
0xc037f46a <kernel_init+289>: mov %eax,-0x64(%ebp)
0xc037f46d <kernel_init+292>: mov %edx,-0x60(%ebp)
0xc037f470 <kernel_init+295>: call *-0x58(%ebp) //do_one_initcall中的调用语句result = fn();

.....
0xc037f553 <kernel_init+522>: pop %edi
0xc037f554 <kernel_init+523>: pop %eax
0xc037f555 <kernel_init+524>: addl $0x4,-0x5c(%ebp)
0xc037f559 <kernel_init+528>: cmpl $0xc03aa804,-0x5c(%ebp) //
0xc037f560 <kernel_init+535>: jb 0xc037f43a <kernel_init+241> //

穿越盲区

[观察积累中,待扩展]

穿越gdb的盲区

进程切换
中断异常
系统调用

穿越交叉索引工具的盲区

函数指针

我们经常碰到这种情况:如果内核中函数A是通过函数指针调用函数B,那么源码交叉索引工具(如source insight, kscope等)就无法通过函数B的名称回溯到上层函数A。这是因为在函数A内部对函数B的调用并不是通过函数B的名称,而是利用指向函数B代码块的指针(函数指针)。

要想克服这种交叉索引工具无法克服的障碍,方法有两个:

1. 熟悉该函数涉及的子系统的上层源码和机制,从上到下的“反回溯”(frameN到frame(N-1)),以求找到实际调用目标函数的源码位置。然而,这个方法并不是十分地有效,因为当目标函数属于你不熟悉的子系统时,要阅读的代码量可能很大。

2. 利用调试工具。在目标函数处下断点。调试器器会实时拦截该函数的调用,然后用bt命令就能看到整个调用链。

然而,我们研究的目标并不满足于知道调用链。下面我们观察函数究竟是怎样利用函数指针调用子函数的。[待整理]

2130 int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
2131 {
2132 int error = may_create(dir, dentry, NULL);
2133
2134 if (error)
2135 return error;
2136
2137 if (!dir->i_op || !dir->i_op->mkdir)
2138 return -EPERM;
2139
2140 mode &= (S_IRWXUGO|S_ISVTX);
2141 error = security_inode_mkdir(dir, dentry, mode);
2142 if (error)
2143 return error;
2144
2145 DQUOT_INIT(dir);
2146 error = dir->i_op->mkdir(dir, dentry, mode);
2147 if (!error)
2148 fsnotify_mkdir(dir, dentry);
2149 return error;
2150 }

对源码文件下断点

(gdb) b fs/namei.c:2146
Breakpoint 9 at 0xc017c0ee: file fs/namei.c, line 2146.

问题一:

动态分析call *0x14(%ebx)是怎么回事,函数指针

-------------------------
┌──Register group: general───────────────────────────────────────────────────────────────────────────────────────────────────┐
│eax 0xdc20b0a8 -601837400 ecx 0x1ed 493 │
│edx 0xdb9526c0 -610982208 ebx 0xe01c87d4 -535001132 │
│esp 0xd8c5bf1c 0xd8c5bf1c ebp 0xd8c5bf34 0xd8c5bf34 │
│esi 0xdc20b0a8 -601837400 edi 0xdb9526c0 -610982208 │
│eip 0xc017c0fb 0xc017c0fb <vfs_mkdir+179> eflags 0x200246 [ PF ZF IF ID ] │
│cs 0x60 96 ss 0x68 104 │
│ds 0x7b 123 es 0x7b 123 │
│fs 0xd8 216 gs 0x33 51 │
│ │
│ │
│ │
│ │
│ │
│ │
┌─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
│0xc017c0ea <vfs_mkdir+162> mov %esi,%eax │
│0xc017c0ec <vfs_mkdir+164> call *(%ecx) │
B+ │0xc017c0ee <vfs_mkdir+166> mov 0x98(%esi),%ebx │
│0xc017c0f4 <vfs_mkdir+172> mov %edi,%edx │
│0xc017c0f6 <vfs_mkdir+174> mov %esi,%eax │
│0xc017c0f8 <vfs_mkdir+176> mov -0x10(%ebp),%ecx │
>│0xc017c0fb <vfs_mkdir+179> call *0x14(%ebx) │
│0xc017c0fe <vfs_mkdir+182> test %eax,%eax │
│0xc017c100 <vfs_mkdir+184> mov %eax,%ebx │
│0xc017c102 <vfs_mkdir+186> jne 0xc017c15d <vfs_mkdir+277> │
│0xc017c104 <vfs_mkdir+188> testb $0x4,0x11c(%esi) │
│0xc017c10b <vfs_mkdir+195> je 0xc017c119 <vfs_mkdir+209> │
│0xc017c10d <vfs_mkdir+197> mov $0x4,%edx │
│0xc017c112 <vfs_mkdir+202> mov %esi,%eax │
└─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
remote Thread 42000 In: vfs_mkdir Line: 2146 PC: 0xc017c0fb
i_state = 1,
dirtied_when = 0,
i_flags = 0,
i_writecount = {
counter = 0
},
i_security = 0x0,
i_private = 0x0
}
(gdb) p/x $ebx
$20 = 0xe01c87d4
(gdb) p/x $ebx+0x14
$21 = 0xe01c87e8


(gdb) p &sfs_dir_inode_ops
$13 = (struct inode_operations *) 0xe01c87d4

(gdb) p/x *(int * )0xe01c87d4@10
$18 = {0xe01c75b1, 0xe01c7677, 0xc018d3f0, 0xc018cc91, 0xe01c75dd, 0xe01c75c0, 0xc018d441, 0xe01c7510, 0xc018d474, 0x0}


(gdb) disass sfs_mkdir
Dump of assembler code for function sfs_mkdir:
0xe01c75c0 <sfs_mkdir+0>: push %ebp //<-
0xe01c75c1 <sfs_mkdir+1>: or $0x40,%ch
0xe01c75c4 <sfs_mkdir+4>: mov %esp,%ebp
0xe01c75c6 <sfs_mkdir+6>: push %ebx
0xe01c75c7 <sfs_mkdir+7>: mov %eax,%ebx
0xe01c75c9 <sfs_mkdir+9>: push $0x0
0xe01c75cb <sfs_mkdir+11>: call 0xe01c7510 <sfs_mknod>
0xe01c75d0 <sfs_mkdir+16>: pop %edx
0xe01c75d1 <sfs_mkdir+17>: test %eax,%eax
0xe01c75d3 <sfs_mkdir+19>: jne 0xe01c75d8 <sfs_mkdir+24>
0xe01c75d5 <sfs_mkdir+21>: incl 0x28(%ebx)
0xe01c75d8 <sfs_mkdir+24>: mov -0x4(%ebp),%ebx
0xe01c75db <sfs_mkdir+27>: leave
0xe01c75dc <sfs_mkdir+28>: ret
End of assembler dump.
(gdb) p/x *0xe01c87e8
$9 = 0xe01c75c0 // <-sfs_mkdir的地址
(gdb)

struct inode_operations {
int (*create) (struct inode *,struct dentry *,int, struct nameidata *);
struct dentry * (*lookup) (struct inode *,struct dentry *, struct nameidata *);
int (*link) (struct dentry *,struct inode *,struct dentry *);
int (*unlink) (struct inode *,struct dentry *);
int (*symlink) (struct inode *,struct dentry *,const char *);
int (*mkdir) (struct inode *,struct dentry *,int);
......
};

struct inode_operations sfs_dir_inode_ops = {
...
.mkdir = sfs_mkdir,
...
};

-----------------------------------------------------------------------
0xc017c0fb <vfs_mkdir+179> call *0x14(%ebx) 为什么要加 * ?

call *0x14(%ebx) ==
push %eip
mov 0x14(%ebx) %eip

注意call与mov指令语义的区别

mov 0x14(%ebx) %eax; 把存放在地址0x14(%ebx)中的32位数据拷贝到%eax
mov %eax 0x14(%ebx); 把%eax的值拷贝到地址0x14(%ebx)指向的内存中
call 0x14(%ebx) : 结果是跳到地址0x14(%ebx)继续执行(当然对于本例来说,该地址指向的并不是目标代码段)
call *0x14(%ebx) : 取出存放在地址0x14(%ebx)中的32位数据,把该数据作为目标地址,跳到该地址继续执行。

mov $0xe01c75c9 %eax ; 0xe01c75c9被认为是立即数,前面有$。没有mov 0xe01c75c9 %eax这种形式
call 0xe01c75c9 ;0xe01c75c9被认为是地址。没有call $0xe01c75c9这种形式。
注意,也没有call %eax等形式(假设%eax放着目标地址)。需用 call *%eax,同样,*%eax表示从%eax获取地址值



| - | | -- |
| - | | -- | 4. call sfs_mkdir == call 0xe01c75c0
| - | | -- |
+--------------------+ <-------> | -- | 3. 0xe01c75c0 == fetch from 0xe01c87e8
| --- | | -- | *0x14(%ebx)
+--------------------+ +---------------+
| init (*mkdir)(..) +--+ | 0xe01c75c0 | 2. 0xe01c87e8 == calculate 0x14(%ebx)
0x14(%ebx)---> +--------------------+ | +---------------+
| ... | | | 0xe01c75dd |
+--------------------+ | +---------------+
| ... | | | 0xc018cc91 |
+--------------------+ | +---------------+
| ... | | | 0xc018d3f0 |
+--------------------+ | +---------------+ 1. 0xe01c87d4 == fetch from %ebx
| ... | | | 0xe01c7677 |
+--------------------+ | +---------------+ +------------+
| int (*create)(..) | | | 0xe01c75b1 | 0xe01c87d4 | 0xe01c87d4 |
%ebx-------> +--------------------+ | +---------------+ +------------+
struct inode_operations | contents address register %ebx
sfs_dir_inode_ops |
| call *0x14(%ebx) 的过程
|
+-----------------------------+
|
static int sfs_mkdir(..) |
0xe01c75c0 <sfs_mkdir+0>: +-> push %ebp
0xe01c75c1 <sfs_mkdir+1>: or $0x40,%ch
0xe01c75c4 <sfs_mkdir+4>: mov %esp,%ebp
0xe01c75c6 <sfs_mkdir+6>: push %ebx
0xe01c75c7 <sfs_mkdir+7>: mov %eax,%ebx
0xe01c75c9 <sfs_mkdir+9>: push $0x0
0xe01c75cb <sfs_mkdir+11>: call 0xe01c7510 <sfs_mknod>
0xe01c75d0 <sfs_mkdir+16>: pop %edx
0xe01c75d1 <sfs_mkdir+17>: test %eax,%eax
0xe01c75d3 <sfs_mkdir+19>: jne 0xe01c75d8 <sfs_mkdir+24>
0xe01c75d5 <sfs_mkdir+21>: incl 0x28(%ebx)
0xe01c75d8 <sfs_mkdir+24>: mov -0x4(%ebp),%ebx
0xe01c75db <sfs_mkdir+27>: leave
0xe01c75dc <sfs_mkdir+28>: ret

address contents
---------------------------------------------------------------------------
问题二:

下面的dir->i_op->mkdir(),为什么不是dir.i_op.mkidr. . 和 -> 有什么区别

static int sfs_mkdir(struct inode * dir, struct dentry * dentry, int mode)
{
....
}

2130 int vfs_mkdir(struct inode *dir, struct dentry *dentry, int mode)
2131 {
....
2146 error = dir->i_op->mkdir(dir, dentry, mode);
...
2150 }

struct inode {
...
const struct inode_operations *i_op;
...
};

struct inode_operations {
...
int (*mkdir) (struct inode *,struct dentry *,int);
...
};

dir: 取得(struct inode *)dir
dir->i_op: 取得(const struct inode_operations *)i_op
dir->i_op->mkdir: 取得(int (*) (struct inode *,struct dentry *,int))mkdir

dir->i_op->mkdir(dir, dentry, mode)也就是 函数指针变量名(参数...)

函数指针是一个指针,它向目标函数的代码块的第一个指令。

函数名的值等于该函数第一条指令的地址。

(gdb) p sfs_mkdir
$20 = {int (struct inode *, struct dentry *, int)} 0xe01c75c0 <sfs_mkdir>
(gdb) p &sfs_mkdir
$21 = (int (*)(struct inode *, struct dentry *, int)) 0xe01c75c0 <sfs_mkdir>
(gdb) p dir->i_op->mkdir
$18 = (int (*)(struct inode *, struct dentry *, int)) 0xe01c75c0 <sfs_mkdir>
前者指明变量名/函数名的类型,后者是它的值

struct inode_operations sfs_dir_inode_ops = {
...
.mkdir = sfs_mkdir,
...
};


函数的两种调用形式: 函数指针变量名(参数...) 函数名(参数...)
严格地说,从C语言的形式看来,前者通过函数指针变量名调用函数,后者通过函数名调用,是不同的。
但从汇编级代码看来,都是转化为指令call 函数地址。是一样的。
引入了函数指针变量后,这个变量就可以动态地赋值,从而指向不同的函数体,实现某些特殊的功能。

我们再看下函数指针的赋值.mkdir = sfs_mkdir,
严格地说,mkdir和sfs_mkdir是类型不同的东西,但在编译时自动经过了类型转换。所以下面这些写法效果都一样
.mkdir = sfs_mkdir,
.mkdir = &sfs_mkdir,
.mkdir = (int (*)(struct inode *, struct dentry *, int))sfs_mkdir,
.mkdir = (int (*)(struct inode *, struct dentry *, int))(&sfs_mkdir),
---
例子
#include <stdio.h>

int main()
{
int myfunc(int a)
{
printf("%d/n", a);
return 0;
}
int (*funa)(int) = myfunc;
int (*funb)(int) = &myfunc;
int (*func)(int) = (int (*)(int))myfunc;
int (*fund)(int) = (int (*)(int))(&myfunc);

myfunc(1);
funa(2);
funb(3);
func(4);
fund(5);

return 0;
}


查看函数的参数

我们知道,一个函数的计算结果并不都是通过它的返回值返回的,有时会通过函数的参数返回真正感兴趣的数据。看内核源码的时候,如果调用链过长,涉及内容和数据结构过多的话,往往是看到最后都记不住函数的参数哪些是已经“初始化的”。

这也是交叉索引工具无法克服的先天弱点。它能动态索引源码,却无法动态查看数据。此时,可以利用gdb给目标函数下断点,而后可以用命令info args查看参数,另外命令info local可查看本地变量。当然在ddd下查看效果会更好。

内容简单,不展开了。

工程方法

二叉断点

实例 “什么/proc下无法创建目录?”

给调用指令下断点

如果对目标函数下断点后,受到很多骚扰,那么就转为在上层函数内对目标函数的调用指令处下断点。如果你已经进入了上层函数,对调用指令下断点,是更为精确的断点方法。

bug 与 OOPS

[主要研究定位bug的技巧,找出是哪条指令引发了panic似乎很容易。但要找出错误产生的源头似乎是门艺术了]

参考手册

“Using kgdb and the kgdb Internals” http://www.kernel.org/pub/linux/kernel/people/jwessel/kgdb/index.html

kgdb官网 http://kgdb.linsyssoft.com/

参考书籍(freeebsd)

“Debugging Kernel Problems” http://www.google.cn/search?q=Debugging+Kernel+Problems&ie=utf-8&oe=utf-8&aq=t&rls=com.ubuntu:zh-CN:unofficial&client=firefox-a

“Chapter 10 Kernel Debugging” http://www.freebsd.org/doc/en_US.ISO8859-1/books/developers-handbook/kerneldebug.html

参考书籍(linux)

Chapter 14. Kernel Debugging Techniques of “Embedded Linux Primer: A Practical, Real-World Approach”

http://book.opensourceproject.org.cn/embedded/embeddedprime/

参考文章

“掌握 Linux 调试技术” http://www.ibm.com/developerworks/cn/linux/sdk/l-debug/index.html

“定位Oops的具体代码行” http://blog.chinaunix.net/u/12592/showart_1092733.html

“跟踪内核 oops” http://wiki.zh-kernel.org/doc/oops-tracing.txt

“例解Linux Kernel Debug” http://blog.chinaunix.net/u/2108/showart_164703.html

“kernel debug的一些小手段” http://blog.chinaunix.net/u/12592/showart_499502.html

“Kernel Debugging Techniques” http://www.linuxjournal.com/article/9252

[参考文章]有的已过时,而且深度不够。

***第二部分:内核分析***

这部分的内容侧重于内核原理分析,其中涉及gdb调试器的内容不是很多,但它起的作用很关键,主要用于观察内核数据的生成及变化,在对源码理解有困惑时用于验证自己的猜想。

另外,调试内核时,利用gdb的“list 函数名”命令看到的C代码都是当前处理器当前配置下内核实际运行的函数版本:”disass 函数名”看到的都是处理器实际运行时的机器代码,也就是说define语句和inline函数已经被编译器处理了,而且编译器也完成了优化。所以,gdb本身就是一种不可替代的源码浏览工具,它能筛选掉出实际运行的函数版本,又能呈现出实际运行的机器码。

内核参考书籍

综合类:

“understand linux kernel”

”linux kernel development“

“linux源代码情景分析”

“Embedded.Linux.Primer.A.Practical.Real.World.Approach.”

“The_Linux_Kernel_Primer_A_Top_Down_Approach_For_x86_and_PowerPC_Architectures”

子系统类:

文件系统:

“UNIX Filesystems Evolution, Design, and Implementation”

“File System Forensic Analysis”

“Windows NT File System Internals”

内存管理:

“Understanding The Linux Virtual Memory Manager”

网络系统:

“Understanding.Linux.Network.Internals”

驱动开发:

“linux device drivers”

“Essential.Linux.Device.Drivers”

源码本身及附带文档

参考文章:

IBM-Linux 相关专题 http://www.ibm.com/developerworks/cn/linux/

驱动分析

[分析一个简单的驱动,观察函数调用流程。重点观察驱动与驱动模型,以及和系统内核的交互过程。比如,中断的整个生命周期。]

参考:

“Debugging kernel modules” http://lwn.net/Articles/90913/

“Linux 系统内核的调试” http://www.ibm.com/developerworks/cn/linux/l-kdb/

“Linux 可加载内核模块剖析” http://www.ibm.com/developerworks/cn/linux/l-lkm/

“使用 KGDB 调试 Linux 内核” http://blog.chinaunix.net/u/8057/showart_1087126.html

“使用 /proc 文件系统来访问 Linux 内核的内容” http://www.ibm.com/developerworks/cn/linux/l-proc.html

如何查找出当前系统所安装模块驱动对应的源码,从而对其做些修改等实验?

提示:

1. lsmod 列出模块名

2. modinfo 模块名, 查看模块信息

3. 模块名,模块信息中的别名,模块的参数说明文字都可结合source insight查找该模块的源码文件;模块信息中的模块路径也可用来定位对应源码的路径以及相关的kconfig文件,从而获取更多相关信息。一般源码文件的名称就是模块名或在模块名的基础上加上某些后缀,用模块名的方法查找不出时再利用其他信息查找。

4. 如果利用以上方法还找不到源文件,或者一个模块对应着几个源文件,可使用最后的必杀绝招。比如lsmod后得到一个sr_mod。我们用modinfo sr_mod的得到它的已编译文件的路径是 /lib/modules/2.6.24-19-generic/kernel/drivers/scsi/sr_mod.ko ;把它拷贝出来,并用命令objdump -d sr_mod.ko 查看它的机器码,就可以知道它使用了哪些函数,利用这些函数名就可以结合source insight搜索出源码了。

载入模块符号

首先,在虚拟系统上装入目标模块foo,然后到/sys/module/foo/sections/下查看目标模块的section偏移地址信息.

实例

debian:/sys/module/smplefs/sections# cat .text .data .bss
0xe01c7000
0xe01c864c
0xe01c8b20

然后,到真机的gdb下用add-symbol-file命令装载目标模块的符号信息 格式如下

    add-symbol-file /path/to/module 0xe01c7000 /  # .text
-s .data 0xe01c864c /
-s .bss 0xe01c8b20

实例

(gdb) add-symbol-file  test/day11/samplefs.ko 0xe01c7000 -s .data 0xe01c864c -s .bss 0xe01c8b20
add symbol table from file "test/day11/samplefs.ko" at
.text_addr = 0xe01c7000
.data_addr = 0xe01c864c
.bss_addr = 0xe01c8b20
(y or n) y
Reading symbols from /storage/myqemu/new/linux-2.6.26/test/day11/samplefs.ko...done.
(gdb)

然后,余下的对模块的调试就类似对内核的调试了。

seq_file.c的分析

module.c的分析

中断处理过程

s3c24xx内存初始化分析

[从这节开始,侧重于利用kgdb和source insight理解内核原理] [网上好像没这个内容。只看源码的话,因为source insight不能解析汇编源文件,在汇编源码中定位到初始化的源头好像很难,利用调试器很容易做到这点]

应用程序与虚拟地址空间

用户层的观察窗

[描述应用程序眼中的世界..] [还不是很明白,这个图太简单..待扩展]

    top, 4G --->+-------------------+
| |
| malloc()'ed memory|
| interrupt stack | kernel
| data |
| text |
kernel, 3G--->+-------------------+
| |
| argv,envp |
| user stack |
| | |
| | |
| v |
| | user process
| ^ |
| | |
| | |
| heap |
| data |
| text |
user, 0G---> +-------------------+

Layout of virtual address space

我们验证一下用户空间的内容(上图的下部分)[未完,待续] 引用于http://linux.chinaunix.net/bbs/viewthread.php?tid=978491

查看进程的虚拟地址空间是如何使用的。
该文件有6列,分别为:
地址:库在进程里地址范围
权限:虚拟内存的权限,r=读,w=写,x=,s=共享,p=私有;
偏移量:库在进程里地址范围
设备:映像文件的主设备号和次设备号;
节点:映像文件的节点号;
路径: 映像文件的路径
每项都与一个vm_area_struct结构成员对应,
----
struct vm_area_struct {
struct mm_struct * vm_mm; /* The address space we belong to. */
unsigned long vm_start; /* Our start address within vm_mm. */
unsigned long vm_end; /* The first byte after our end address
within vm_mm. */

/* linked list of VM areas per task, sorted by address */
struct vm_area_struct *vm_next;

pgprot_t vm_page_prot; /* Access permissions of this VMA. */
unsigned long vm_flags; /* Flags, listed below. */

struct rb_node vm_rb;

/*
* For areas with an address space and backing store,
* linkage into the address_space->i_mmap prio tree, or
* linkage to the list of like vmas hanging off its node, or
* linkage of vma in the address_space->i_mmap_nonlinear list.
*/
union {
struct {
struct list_head list;
void *parent; /* aligns with prio_tree_node parent */
struct vm_area_struct *head;
} vm_set;

struct raw_prio_tree_node prio_tree_node;
} shared;

/*
* A file's MAP_PRIVATE vma can be in both i_mmap tree and anon_vma
* list, after a COW of one of the file pages. A MAP_SHARED vma
* can only be in the i_mmap tree. An anonymous MAP_PRIVATE, stack
* or brk vma (with NULL file) can only be in an anon_vma list.
*/
struct list_head anon_vma_node; /* Serialized by anon_vma->lock */
struct anon_vma *anon_vma; /* Serialized by page_table_lock */

/* Function pointers to deal with this struct. */
struct vm_operations_struct * vm_ops;

/* Information about our backing store: */
unsigned long vm_pgoff; /* Offset (within vm_file) in PAGE_SIZE
units, *not* PAGE_CACHE_SIZE */
struct file * vm_file; /* File we map to (can be NULL). */
void * vm_private_data; /* was vm_pte (shared mem) */
unsigned long vm_truncate_count;/* truncate_count or restart_addr */

#ifndef CONFIG_MMU
atomic_t vm_usage; /* refcount (VMAs shared if !MMU) */
#endif
#ifdef CONFIG_NUMA
struct mempolicy *vm_policy; /* NUMA policy for the VMA */
#endif

[todo 换个简单的程序]
$ ps -aux | grep firefox
Warning: bad ps syntax, perhaps a bogus '-'? See http://procps.sf.net/faq.html
fqh 8230 4.7 2.5 205872 80024 ? Tl 14:54 0:19 /usr/lib/firefox-3.0.1/firefox
fqh 8313 0.0 0.0 3220 764 pts/1 R+ 15:01 0:00 grep firefox


(gdb) attach 8230
...
.....
Loaded symbols for /usr/lib/libflashsupport.so
Reading symbols from /usr/lib/libpulse.so.0...(no debugging symbols found)...done.
Loaded symbols for /usr/lib/libpulse.so.0
Reading symbols from /lib/libcap.so.1...(no debugging symbols found)...done.
Loaded symbols for /lib/libcap.so.1

(no debugging symbols found)
0xb7f24410 in __kernel_vsyscall ()
(gdb) bt
#0 0xb7f24410 in __kernel_vsyscall ()
#1 0xb7d46c07 in poll () from /lib/tls/i686/cmov/libc.so.6
#2 0xb6b4e1c6 in ?? () from /usr/lib/libglib-2.0.so.0
#3 0xb6b4e74e in g_main_context_iteration () from /usr/lib/libglib-2.0.so.0
#4 0xb77ba87c in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#5 0xb77cf624 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#6 0xb77cfa6f in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#7 0xb787ecd6 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#8 0xb784e31f in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#9 0xb77cf75e in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#10 0xb765f122 in ?? () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#11 0xb70b3a88 in XRE_main () from /usr/lib/xulrunner-1.9.0.1/libxul.so
#12 0x08049033 in ?? ()
#13 0xb7c90450 in __libc_start_main () from /lib/tls/i686/cmov/libc.so.6
#14 0x08048cc1 in ?? ()
(gdb)


$ cat /proc/8230/maps
08048000-0804f000 r-xp 00000000 08:01 7022914 /usr/lib/firefox-3.0.1/firefox <-text,注意标志 可读可执行不可写私有
0804f000-08050000 rw-p 00006000 08:01 7022914 /usr/lib/firefox-3.0.1/firefox <-data,注意标志 可读可写不可执行
08050000-0abd4000 rw-p 08050000 00:00 0 [heap] <-heap,一共45.5多MB[todo:验证向上增长]可读可写不可执行
ae060000-ae063000 r-xp 00000000 08:01 6941098 /usr/lib/libflashsupport.so <-libflashsupport.so 共享库的代码段, 可读可执行不可写
ae063000-ae064000 rw-p 00002000 08:01 6941098 /usr/lib/libflashsupport.so <-libflashsupport.so 共享库的数据段, 可读可写不可执行
.....
..
b7f20000-b7f21000 rw-p 00001000 08:01 6942869 /usr/lib/libplds4.so.0d
b7f21000-b7f22000 r--p 00000000 08:01 6966184 /usr/lib/locale/zh_CN.utf8/LC_IDENTIFICATION
b7f22000-b7f24000 rw-p b7f22000 00:00 0
b7f24000-b7f25000 r-xp b7f24000 00:00 0 [vdso]
b7f25000-b7f3f000 r-xp 00000000 08:01 2326545 /lib/ld-2.7.so
b7f3f000-b7f41000 rw-p 00019000 08:01 2326545 /lib/ld-2.7.so
bfbcd000-bfc0a000 rw-p bffc3000 00:00 0 [stack] <-stack,不到0.24MB,可读可执行不可写[todo:验证向下增长]
[todo:验证argv,envp]
$

交互,从内核层分析

[扩展]

理解设备模型

[结合source insight分析一个内核子系统的原理。源码分析工具虽好,但却是个死的东西,不能实时观察数据的生成和变化。如果在内核运行的时候,搭配调试器来分析,这个过程一定很形象和有趣]

python面向对象的实现

设备模型的分层

外围支持机制

sysfs
hotplug

文件系统

***第三部分:其他工具***

其他工具

strace
  • 作用: strace能拦截和记录应用程序发起的系统调用和它收到的信号。主要用于观察应用层和内核层的交互。
  • 命令选项: 查看,$strace –help 或$man strace 或 $info strace
  • 实例
ltrace
  • 作用: ltrace用于监控程序发起的库函数调用以及程序收到的信号。
SystemTap
MEMWATCH
  • 作用: 跟踪程序中的内存泄漏和错误
YAMD
  • 作用: 查找 C 和 C++ 中动态的、与内存分配有关的问题

杂文

“Debugging Kernel Modules with User Mode Linux” http://www.linuxjournal.com/article/5749

“Debugging Memory on Linux” http://www.linuxjournal.com/article/4681

“DDD—Data Display Debugger” http://www.linuxjournal.com/article/2315

“Linux 系统内核的调试” http://www.ibm.com/developerworks/cn/linux/l-kdb/

System Dump和Core Dump的区别 http://hi.baidu.com/iruler/blog/item/c203de3522ff398ea61e122c.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值