标签:
杂谈
query "/bin/mount" is installed by which deb package
$ dpkg-query -S /bin/mount
mount:
/bin/mount
query info of deb package 'mount'
$ dpkg-query -l mount
Desired=Unknown/Install/Remove/Purge/Hold
|
Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/
Name
Version
Description
+++-=====================-=====================-==========================================================
ii
mount
2.20.1-1ubuntu3
Tools for mounting and manipulating filesystems
query which files are installed from package 'mount'
$ dpkg-query -L mount
query which packages are installed
$ dpkg-query -l
标签:
杂谈
filesystem
kernel
linux
read/write
第四章中writeback
thread已经将要写的page或inode(meta data)变成了bio,bio就是已经对应到了block
device的哪一块了。本章我们就要搞清楚最后一步,即bio是怎样写到实际的存储media上的。下面这张图从submit_bio开始,揭示了bio最后被写到media上的过程。
从上图可以看到,写到media的过程以submit_bio()触发,通过BLOCK_SOFTIRQ完成接力。bio被合并成request,放在request
queue里。
1. request queue的init
前面讲writeback的时候已经讲过request
queue是在add block disk的时候创建的。下图以scsi disk为例,说明了request
queue的建立过程。request queue有6个重要的函数钩子,下图橙色方块中。这些钩子函数是由block
device的(这里是scsi disk)的
driver注册的,但q->make_request_fn()是用的通用的blk_queue_bio()。
2. submit_bio()的作用:
(1)将bio被合并到request
queue里的某一个request中,如不能合并则创建一个行的request。通过q->make_request_fn()完成。
(2)从request
queue里取得一个request,将该request写到media。通过q->request_fn()函数完成。
下图是submit_bio()的内部流程:
submit_bio()的核心函数是generic_make_request(bio)
在q->make_request_fn()中将bio
merge到某个request中,同时因为增加了一个bio,原本不相邻的两个request可能变得相邻,从而可以merge成一个request。
在q->request_fn(q)中执行真正的HW读写media。
3. q->request_fn()做了什么
q->request_fn()是由具体的block disk driver实现的,这里以scsi
disk为例简单看一下一个具体的block device driver里是怎样处理request的。
这里最重要的一点是最后在完成一个request后,将其link到per_cpu_list
blk_cpu_done上并将BLOCK_SOFTIRQ
raise起来。这样在下一次softirq或ksoftirqd的时候将接力处理下一个request。下面我们来看看block
softirq里是怎样接力的。
4. BLOCK_SOFTIRQ的接力
我们可以看到注册好的q->softirq_done_fn()函数会在softirq时调用,这个例子里q->softirq_done_fn()是scsi_softirq_done()。在这个函数里最终会再次调用q->request_fn(q),从queue里读出下一个request,开始下一轮接力,知道queue里不再有request为止。
至此,从bio写到media的过程就讲完了。全部5章的read()/write()的生命旅程也讲完了。其中许多细节还需要在阅读代码和调试过程中慢慢体会~~~
标签:
filesystem
linux
read/write
writeback
kernel
第三章write()中已经提到要写的page加到了writeback queue后将有writeback
thread将其真正写到block
device上,而不是每一次写都直接写到media上。如果要绕过writeback机制直接写到media上,在open()的时候指定O_DIRECT即可。Writeback机制的好处总结起来主要是两点:
加快write()的响应速度。因为media的读写相对于内存访问是较慢的。如果每个write()都访问media,势必很慢。将较慢的media访问交给writeback
thread,而write()本身的thread里只在内存里操作数据,将数据交到writeback
queue即返回。
便于合并和排序 (merge and sort)
多个write,merge是将多个少量数据的write合并成几个大量数据的write,减少访问media的次数;sort是将无序的write按照其访问media上的block的顺序排序,减少磁头在media上的移动距离。
下面将说明writeback的活动机制。
概念:writeback和bdi
从上面的阐述writeback的概念里可以看出,writeback是文件系统的概念,writeback是指文件(即inode)的部分数据由writeback
thread写入media。这里就产生一个问题,一个media上可能有多个文件系统,如sda1上是ext4,
sda2上是fat,但其实是在一个硬盘sda上,如果分别由不同的thread对sda1,sda2进行writeback的话,上面所说的排序(sort)功能将没有意义,因为可能thread1写了一会儿sda1,thread2又要把磁头移到别的地方去写sda2了。鉴于此,设计writeback机制时,writeback的主体应该是sda,而不是sda1,
sda2。这就是backing device的概念。sda是sda1上ext4, sda2上fat的backing
device,sda1和sda2的writeback都应该由backing device:sda的writeback
thread来完成。
bdi的全称是backing device info。它代表了一个backing
device。每个文件系统在mount的时候,它的backing
device就被记录到super->s_bdi中。如果backing device是个block
device,那么其inode所在的backing
device被记录到super->s_bdev->bd_inode->i_mapping->backing_dev_info中。Super和文件数据可以在不同的backing
device上。对于block device,在block device创建的同时就创建了一个backing device
info,基于该block device的文件系统在mount的时候将该backing device
info链到自己的super和inode的backing_dev_info。对于不在block
device上的文件系统,如FUSE,NFS等,是在mount的时候创建一个单独的backing device
info,这个backing
device并不是一个实际的device,只是一个虚拟的writeback的主体。此外,在init的时候,会创建一个default
backing device info,文件系统可以不必创建自己单独的backing device info,直接使用default
backing device info。
下面这张图全面展示了writeback的工作机制,接下来的章节我们将逐一讲述。
bdi的init, register
上节已经讲过default bdi在sys init时创建,而每个block
device在创建的时候或每个文件系统在mount的时候可以创建自己单独的bdi。创建一个bdi主要包含以下步骤:
(1). 在bdi_init中:
*
创建一个struct
backing_dev_info的变量来代表一个bdi。
*
创建一个work queue: bdi_wq用于存放writeback的事件。
*
创建一个delayed
work(即bdi->wb.dwork)的thread,这个thread负责完成writeback动作。
(2). 在bdi_register中:
*
将这个bdi链接到全局变量bdi_list所指的bdi list里
default
bdi和其他bdi的创建都是遵循了以上四个步骤,下面分别讲述。
default bdi的init,
register
下图是在kernel
init时default_bdi_init的流程,default
bdi由default_backing_dev_info这个全局变量来代表。除了default_backing_dev_info之外,还创建了一个noop_backing_dev_info,它用来代表没有writeback需要。
block device (scsi disk)
bdi的init, register
Block device的bdi创建过程和default bdi一样,只是backing dev
info是在创建sda的request queue的时候同时创建的。请注意backing dev
info是跟着sda的,不是sda1, sda2。
从writeback queue到bio
Writeback thread
在第三章write()里面我们已经提到在__mark_inode_diry()里dirty的inode被移到writeback
queue: bdi->wb.b_dirty里。这里我们将说明writeback queue的dirty inode的dirty
page是怎样变成bio的。
首先,我们从writeback thread启动开始。从前面的图中我们已经知道,writeback
thread是在bdi_init()时创建的一个delayed
work,即bdiX->wb.dwork。当bdiX->wb.dwork->timer
expire时,writeback
thread的函数bdi_writeback_workfn()就开始运行,它的流程如下:
注意writeback的优先级顺序,先是强制writeback的work,如用户执行了fsync(fd),然后是writeback
interval到了之后的work,最后是background的work。'work'是writeback的‘单位’,不管是sync()还是dirty_writeback_interval时间到了的writeback,都要包装成一个'work'交给writeback
thread。
一个'work'的Writeback过程
wb_writeback()函数负责完成一个work的writeback,下图是其流程。
一般一个文件系统mount的时候会有一个super
block,对于文件系统层面的sync产生的writeback work,它的super
block即work->sb是确定的。对于dirty_writeback_interval时间到了产生的work,所有在该bdi上的super
block的dirty
inode都要writeback,work->sb就不能指定了。典型的场景是sda1上有一个文件系统,sda2上有另一个文件系统,它们只有一个bdi。当只sync
sda1的时候,产生的work->sb指定为sda1的super
block,调用writeback_sb_inode()。当interval时间到了,sda bdi上所有的dirty
inode都要sync,此时产生的work->sb就不能指定了,调用__writeback_inodes_wb()。实际上在__writeback_inodes_wb()内部还是遍历每个inode,软后调用writeback_sb_inode()的。所以writeback_sb_inode()是核心函数,下图是它的流程,因为比较长,我画了两页图。简单的说,就是一个一个writeback
dirty
inode,由__writeback_single_inode() 完成,直到这次writeback的时间到了或者没有dirty
inode了。wbc是writeback control,用来控制writeback的。
writeback_sb_inode()的流程(图1):
writeback_sb_inode()的流程(图2, 接图1):
下面是__writeback_single_inode()的流程,其本质是通过实际文件系统实现时注册的address_space_operations的writebackpages()
或writbackpage()将dirty的Page写到media上,如果inode本身也是dirty的(比如文件的meta
data, size等也改变了),那么这些文件信息也要写回media。
下面是ext2里aops->writeback的流程,本质是在实际文件系统的格式里找到要写page对应的block位置,构建出相应的buffer
head(bh)以及bio,然后调用submit_bio()来产生一个对block device的write。
至此,我们已经讲完了dirty inode从writeback queue到bio的过程。bio还只是一个抽象的读写block
device的请求,bio要变成实际的block device access还要通过block device
driver,还要再排队,还要受到ioschduluer的控制。这些将在第五章讲述。
(2015-06-25 17:43)
标签:
it
linux
git
1. git log master -- file_path
file_path可以是以前存在过但当前working tree里已经删除的文件
克隆一个bare mirror的repo
git clone --mirror
https://github.com/xxx/xxx.git
reset至某个commit
git reset --soft
commit_id
与主repo同步
git fetch
将default branch设为my_branch
git symbolic-ref HEAD
refs/heads/my_branch
标签:
filesystem
linux
read/write
文件系统
it
本章将介绍write()系统调用的过程。和第二章read()一样我们将从file_operations.write()开始。
1. write():从file operation到page cache
file_operations.write()的核心函数是generic_perform_write()。
大部分文件系统的file_operations.write()最后都会调用generic_perform-write()这个通用的函数。generic_perform-write()的实现方法体现了Linux文件系统write()的典型流程。
这个函数的流程如下图:
从上图可以看到以下几个要点:
Write是一个一个Page Write的
大部分实质性的工作都是在aops.write_begin()和aops.write_end()里做的。
struct address_space_operations ext2_aops = {
........
.write_begin
= ext2_write_begin,
.write_end
= ext2_write_end,
}
write_begin()之后page就ready了,用户的数据就可以copy到page中了。
write_begin()的作用主要是:找到(创建)page->找到(创建)bh->map
buffer。流程如下图所示:
2. write():从page cache到writeback queue
用户的数据已经写到page cache中去了, 将page cache真正写到block
device的过程比read()复杂,因为当中有writeback的机制。整个writeback机制将在下一章做详细介绍。这里只介绍page
cache是怎样交给writeback
queue的,这个过程就是在aops.write_end()里完成的。要写的page加到了writeback
queue后将有writeback thread将其真正写到block device上。
下图是大部分aops.write_end()使用的典型函数generic_write_end()的流程,这个流程的调用层次比较深,大致可分为三个层次:generic_write_end()
-> __block_commit_write()
->__mark_inode_dirty()。下面也是分三张图来说明他们的流程和作用。
2.1 顶层write_end的典型函数:generic_write_end()
2.2 generic_write_end()主要调用了__block_commit_write()
2.3 将dirty inode放到writeback
queue的关键动作在__mark_inode_diry()里
标签:
filesystem
linux
文件系统
read/write
it
本章将介绍read()系统调用的过程。回忆一下第一章里讲到的read()系统调用发生后VFS将调用文件实际所在文件系统的file_operations.read(),我们就从这个地方开始。
1. read():从file
operation到page cache
这个过程是在file_operation.read()的核心函数:do_generic_file_read()中完成的。
虽然每种文件系统都会定义自己的file_operations,如ext2_file_operations,
ext3_file_operations,但大部分最后都会使用do_generic_file_read()这个通用的函数。所以说do_generic_file_read()的实现方法体现了Linux文件系统read()的典型流程。
这个函数的作用和流程如下:
2. read():从page
cache到bio
这个过程是在a_ops->readpage()的流程里完成的。每个文件系统都会定义自己的address space
operations, 即a_ops。以ext2为例,下图就是ext2_aops.readpage()的流程。
在上面的流程图中可以看到读数据的最终实现是要将buffer head对应(Map)到block
device上的某个block,然后向该block device发出bio。那么从page cache和buffer
head以及bio是怎样联系的呢?下一节将做详细解释。
3. 重要概念:page
cache, buffer head和bio
在第一章中已经讲过kernel将文件以page为单位缓存在内存中,叫做page
cache。这是从kernel管理文件的角度来看的。但是文件最终要被读写到存储介质的角度的,从读写存储介质的角度来看,文件读写最终转化为对存储介质上的某些区域的读和写。Linux里最常见的存储介质是block
device,读写block device必须以block
device规定的block大小为单位进行。那么page是怎样转化成block的呢?在Linux kernel里,用bio(block
io)这一数据结构来代表对block
device的读写,bio以block为单位组织要读写的数据。我们知道page通常大小为4096,而block的大小是由block
device自己决定的,Linux kernel使用logic block为单位进行读写,Logic block
size是由block device driver报告给Kernel的。Logic block
size是kernel认为的读写该block device的block size。最常见的logic block size是512
bytes,也可以是1024, 2048或4096。logic block size不能超过page size,而且必须能被page
size整除。这是因为Linux Kernel里将以下面的分割page的方法来把page转换成block。
以最常见的1 Page = 4096 bytes, 1 block = 512 bytes为例,1 Page分割为 8 blocks,如下图所示block 0 ~ block
7。在要发生从存储读数据到Page,如第2节所述的a_ops.readpage或将Page写到存储的时候,如下一章将要讲述的writeback的时候,Linux
kernel会为page里的每个block创建一个叫buffer
head(bh)的数据结构,bh的作用就是要将每个block对应到物理存储上的一个block。bh实际上是联系文件和物理存储介质的桥梁。
bh->b_page,bh->data指向该block在文件中的位置,实际是文件在内存中的缓存page中的位置。注意buffer
head只是指向page中的位置,并没有新的buffer,所谓的buffer还是page的一块,buffer和page是同一块内存。
bh->b_bdev表示block在哪个block
device上, bh->blocknr表示该block在存储介质上的位置。要从一个文件里的某一块block得到其位于存储介质上的位置,这是文件系统要做的翻译。做这个翻译工作就是非常重要的get_block()函数。这个翻译也叫Map。如上面的ext2_get_block(),它的工作就是根据ext2的格式算出page里的某一块在整个ext2所在partition上的对应的blocknr。更直接的说,如果/dev/sda1这个partition上是个ext2的文件系统,里面有个文件a.txt,现在要读a.txt的开头的block(512
bytes),那么ext2_get_block()就负责找到这个block在/dev/sda1上的位置。
bh->state代表了buffer的状态,比较重要的有BH_Mapped,
BH_Uptodate和BH_Dirty。他们的含义在下图中已说明。上面所说的get_block()后bh->state就变成了BH_Mapped。
标签:
it
linux
文件系统
filesystem
read/write
read()/write()是libc最常用的库函数,那么在application调用了read()/write()之后,发生了哪些事情,数据经过了怎样的流程才从media上读出到用户的buffer里,或是从用户buffer被写到media上的呢?本文将通过以下章节详细阐述整个过程。
第一章:文件系统基础
整个文件系统Overview
从libc到SYSCALL
VFS的分发
重要概念:file, inode, page cache, file mapping, address space
第二章:read()
read():从file operation到page cache
read():从page cache到bio
重要概念:page cache, buffer head和bio
第三章:write()
write():从file operation到page cache
write():从page cache到writeback queue
第四章:writeback:
writeback的init, register
从writeback queue到bio
第五章:从bio到media
block layer的核心:request queue
bio进入request queue
ioscheduler
request的接力
标签:
it
linux
文件系统
read/write
filesystem
第一章:文件系统基础
1. 整个文件系统Overview
下图显示了Linux中文件系统涉及的所有模块,我们会在之后的章节了一个一个描述他们的职责和关系。
2. 从libc到SYSCALL
这一部分比较简单,libc调用了Kernel的SYSCALL read()/write():
SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t,
count)
SYSCALL_DEFINE3(write, unsigned int, fd, const char __user *, buf,
size_t, count)。
本文之后的read()/write()均是指kernel的SYSCALL read()/write。
3. VFS的分发
VFS的全称是 Virtual File System,即虚拟文件系统。他是位于各种实际的文件系统如ext2, ext3,
nfs和用户层之间的接口,他的存在意义是:
对用户层而言:VFS屏蔽了各种实际文件系统的差异点,为用户层提供了一套统一的接口来进行文件操作,用户不必关心实际的文件系统是什么。这套统一接口就是文件操作相关的一系列SYSCALL,如open(),
read(), write(), seek()等。
对于各种实际文件系统而言:VFS提供了一套统一接口,只要实现了这套接口,就能为用户提供这种文件系统的支持。实际的文件系统不需要关心用户会进行怎样的操作。这套接口就是各种文件系统注册到VFS的file_operations结构。下面是ext2的file_operations:
const struct file_operations ext2_file_operations = {
.llseek
= generic_file_llseek,
.read
= do_sync_read,
.write
= do_sync_write,
.aio_read =
generic_file_aio_read,
.aio_write =
generic_file_aio_write,
unlocked_ioctl = ext2_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl =
ext2_compat_ioctl,
#endif
.mmap
= generic_file_mmap,
.open
= dquot_file_open,
.release =
ext2_release_file,
.fsync
= ext2_fsync,
.splice_read
= generic_file_splice_read,
.splice_write =
generic_file_splice_write,
};
在 发生SYSCALL
read()/write()后,VFS会根据fd来判断该文件是哪种文件系统的文件,并调用相应的file_operations.read()来处理。fd属于什么文件系统的对应关系是在read()/write()之前的open()时就已经建立的。下图表明了VFS的作用(假设APP要read/write的文件在ext2文件系统上)。
4.
重要概念:file, inode, page cache, address space
在讲解read()/write()的详细流程前,必须先理解文件系统的几个重要概念,否则后面的都是天书。
file:
是进程里一个打开的文件,它的有效范围就是打开它的进程。如通过open()获得一个fd,这个fd只在这个进程里代表这个文件,对于其他进程这个fd无意义。文件系统中的一个文件abc.txt,如果在process
1中fd1=open()一次,在proces
2中也fd2=open()一次,fd1和fd2是完全独立的,虽然他们打开的是media上的同一个文件。
inode:inode是kernel为每一个文件(目录也是文件)建立的数据结构。inode和media上实际的文件一一对应。前面提到的fd1和fd2打开的是同一个文件,所以他们指向一个inode。也就是说一个文件在许多进程中被打开,每个进程有自己的file,
但是这些file都指向同一个inode。这种指向是通过file->f_mapping->host来实现的。
page
cache:为了加快文件读写的速度,kernel会把最近访问的inode(也就是文件)的数据缓存在内存里,我们知道linux的内存管理的是以页为单位的,这些inode数据的缓存就叫做page
cache。 注意是inode的数据不是inode本身。inode->i_mapping->page_tree指向了一个inode的被kernel缓存的所有page
cache的tree。
file
mapping: 文件被load到page
cache里之后page还是物理地址,通过mmap2()或kmap()可以将page
map到虚拟地址上,这样用户或kernel就可以读写page cache里的内容了。这个过程叫做file
mapping。mmap2()是系统调用,将文件load到page cache并map到user
space。kmap()是kernel里的函数,将page映射到kernel space。
address
space: 这是一个比较难理解的概念。抽象的说struct
address_space是用来描述kernel里某一实体的物理缓存page,以及这些page对应的虚拟地址映射的关系的。在文件系统这个实体就是inode,所以address_space->host
!=
NULL,必须指向一个inode。在其他一个场合address_space->host可以为NULL。inode->i_mapping指向它的address
space。每个进程里的file也有一个file->f_mapping,其实file->f_mapping在open()的时候就已经file->f_mapping=inode->i_mapping了。所以,所有同一个inode的file会共享address
space。
一个inode的所有已经被缓存的page都在inode->i_mapping->page_tree里。这些page被map到所有进程的虚拟地址空间的描述即很多个vm_area_struct都在inode->mapping->i_mmap和inode->mapping->nonlinear里。注意:一个inode的page可以被不同的进程map,如Preocess
1和Process
2都map了file这个文件的page,因为每个进程的虚拟地址是独立的,所以在inode->mapping->i_mmap里有2个vm_area_struct分别描述在Process
1和Process 2里的map关系。
struct address_space里还有一个重要的成员struct address_space_operations
*a_ops。address_space_operations定义了page cache的一系列操作,如.readpage(),
.writepage()等。对于像linux这样支持page cache的文件系统,page
cache的操作至关重要。因为所有的文件读写都要通过page cache,对文件的读写最后都要转化为page
cache的读写。address_space_operations里的这些函数指针是每个特定的文件系统,如ext2,
ext3等必须要实现的。在后面我们将详细阐述。
下面这张图显示了file, inode, page cache, file mapping,address
space之间的关系。
(2014-03-16 18:20)
标签:
it
gcc
link
ld
linux
“链接”,就是一个可执行程序,调用了另一个已经编译好的程序里的函数。那个已经编译好的程序通常叫做“库”。比如,以下test.c中调用了函数abc(),abc()的实现在库mylibrary中。
mylibrary.c:
int abc(void){
return 0;
}
test.c:
#include
int main(int argc, char *argv[]) {
int err = -1;
err = abc(); // inside libmylibrary
if (err != 0) {
err = -EINVAL;
}
return err;
}
在生成可执行代码test时,需要在test里“记录”函数abc()的信息,以便运行时能找到abc()来调用。这里面包含了以下两个关键点:
1.
编译test时要知道到哪里去找abc()所在的那个lib,找到lib后要记录下abc()在lib里的位置
2.
运行test时也要知道到哪里去找lib,然后用test里记录的abc的位置找到abc,并运行它。
下面分别讲述这两个过程:
首先生成libmylibrary.so这个动态库
$ gcc mylibrary.c -shared -fPIC -o
libmylibrary.so
1.
编译时去哪儿找lib
如果编译test.c时不指定abc()所在的lib,会发生以下错误:
$ gcc test.c -o test
/tmp/ccLmMKci.o: In function `main':
test.c:(.text+0x19): undefined reference to `abc'
collect2: ld 返回 1
这个错误表示链接器找不到abc()
gcc负责搜索abc()所在的lib,找到后将其交给linker来链接。
gcc搜索lib的优先顺序从高到底依次是:
<1>. gcc command line 直接指定的lib
$ gcc test.c libmylibrary.so -o test
<2>. 到-L指定的目录下搜索-l指定的lib
-L: 后面跟要搜索目录,gcc会到这个目录下搜索lib.
-l:
后面跟要搜索的lib的名字,注意要去掉前缀lib,也没有后缀,ld会自动加上前缀lib。先找动态库 .so,若没有则再找静态库
.a。
$ gcc test.c -L./ -lmylibrary -o test
(到当前目录下搜索libmylibrary.so)
(3)
到环境变量$LIBARAY_PATH指定的目录下搜索-l指定的lib
$ LIBRARY_PATH=$LIBRARY_PATH:./ gcc test.c -lmylibrary -o
test
(4)
到gcc编译时内嵌的lib搜索路径下搜索-l指定的lib,通常包含/lib和/usr/lib.
$ sudo cp libmylibrary.so /usr/lib/
$ gcc test.c -lmylibrary -o test
gcc编译时内嵌的lib搜索路径用 gcc
--print-search-dirs可以看到。
$ gcc -print-search-dirs
| grep libraries
libraries:
=/usr/lib/gcc/i686-redhat-linux/4.4.7/:/usr/lib/gcc/i686-redhat-
linux/4.4.7/:/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../../i686-redhat-linux/lib/i686-redhat-linux/4.4.7/:/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../../i686-redhat-linux/lib/:/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../i686-redhat-linux/4.4.7/:/usr/lib/gcc/i686-redhat-linux/4.4.7/../../../:/lib/i686-redhat-linux/4.4.7/:/lib/:/usr/lib/i686-redhat-linux/4.4.7/:/usr/lib/
(5)
ld编译时内嵌的lib搜索路径下搜索-l指定的lib。
可用ld -verbose | grep
SEARCH_DIR 查看:
$ ld -verbose | grep
SEARCH
SEARCH_DIR("/usr/i686-redhat-linux/lib");
SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/lib");
SEARCH_DIR("/usr/lib");
2.
运行时去哪儿找lib:
运行上面(1)(2)(3)编译出来的test,会看到以下错误:
$
./test
./test: error while loading shared libraries: libmylibrary.so:
cannot open shared object file: No such file or directory
这个错误表示在运行test的时候找不到libmylibrary.so这个动态库。那么,运行的时候是怎样找动态库的呢?首先,要知道找动态库的工作是由一个ld的动态库完成的。这个ld是可以在编译链接生成elf时通过“--dynamic-linker=”来指定,如果不指定则使用链接器缺省的。在这个例子中我们没有指定,所以将使用缺省值。我们可以通过“readelf
-l”来查看实际使用的ld在哪儿,如下,Requesting program interpreter
就是运行时将要用来寻找动态库的ld。
$ readelf -l test
Elf file type is EXEC (Executable file)
Entry point 0x80483a0
There are 8 program headers, starting at offset 52
Program Headers:
Type
Offset
VirtAddr
PhysAddr FileSiz
MemSiz Flg Align
PHDR
0x000034 0x08048034 0x08048034 0x00100 0x00100 R E 0x4
INTERP
0x000134 0x08048134 0x08048134 0x00013 0x00013
R 0x1
[Requesting program interpreter: /lib/ld-linux.so.2]
LOAD
0x000000 0x08048000 0x08048000 0x005f4 0x005f4 R E 0x1000
LOAD
0x0005f4 0x080495f4 0x080495f4 0x00104 0x0010c RW
0x1000
DYNAMIC
0x000608 0x08049608 0x08049608 0x000d0 0x000d0 RW
0x4
NOTE
0x000148 0x08048148 0x08048148 0x00020 0x00020
R 0x4
GNU_EH_FRAME
0x000580 0x08048580 0x08048580 0x0001c 0x0001c
R 0x4
GNU_STACK
0x000000 0x00000000 0x00000000 0x00000 0x00000 RW
0x4
一般来说GNU的ld按照以下顺序来搜索动态库:
(1). 在链接时由‘-rpath’指定的路径下搜索.
$ gcc test.c -L./ -lmylibrary
-Wl,-rpath=./ -o test (-rpath指定当前目录下搜索)
$ ./test
(2).
链接时由环境变量LD_RUN_PATH指定的路径下搜索.
$ LD_RUN_PATH=$LD_RUN_PATH:./ gcc test.c -L./ -lmylibrary -o
test
$ ./test
以上(1),(2)是在生成可执行文件时,就已经指定了搜索路径,-rpath或LD_RUN_PATH指定的路径被写入了elf文件中。
(3).运行时由环境变量LD_LIBRARY_PATH指定的路径下搜索。
$ gcc test.c -L./ -lmylibrary -o
test
$LD_LIBRARY_PATH=$LD_LIBRARY_PATH:./
./test
(4). 运行时在文件
‘/etc/ld.so.conf’中被指定的路径下搜索。
可通过以下方式将mylibrary加到/etc/ld.so.conf中:
<1> 在/etc/ld.so.conf.d/下新建一个文件mylibrary.conf,
<2> 在mylibrary.conf中指定libmylibrary.so所在的路径,
<3> 运行 ldconfig 使改动生效
$ gcc test.c
-L./ -lmylibrary -o test
$
./test
(5). ld
默认的搜索路径,一般为 ‘/lib’ and ‘/usr/lib’.
以上(3),(4),(5)是在运行时,ld default的搜索路径,无需elf指定。
如何查看一个可执行程序到底会依赖了那些动态库呢?通过ldd就可以知道:
$ gcc test.c -L./ -lmylibrary -Wl,-rpath=./ -o test
$ ldd test
linux-gate.so.1 => (0x00c47000)
libmylibrary.so => ./libmylibrary.so (0x00554000)
依赖了当前目录下的libmylibrary.so **
libc.so.6 => /lib/libc.so.6 (0x00137000)
/lib/ld-linux.so.2 (0x00111000)
Note:如果是通过LD_RUN_PATH来搜索到的libmylibrary.so,ldd不会显示.so文件所在的目录。
标签:
gcc
linux
callstack
stackfram
栈帧
Kernel里的dump_stack(), oops打印出来的backstrace调用链表是怎样实现的呢?
大家都知道当发生函数调用的时候,函数的参数传递,返回值传递都要遵循一定的规则,在ARM体系架构下,这个规则叫做Procedure Call Standard for the ARM
Architecture。在这个规则里规定了函数调用的时候,返回地址在LR里面,第一到第四个参数在r0~r3里面,第五到第八个参数在Stack里面,返回值在r0里面。这是基本规则,C编译器产生汇编指令是必须遵循这些规则,这也是ABI(Application
Binary
Interface)的一部分。另外,为了实现连续的函数调用,如fun_a()->func_b()->func_c(),每个函数的入口处必须先把LR压到stack里,否则func_b()调了func_c()之后,LR变成了func_c()的返回地址,而func_b()返回地址就丢失了。
有了以上这些原则,可以追溯函数的调用历史。即只要找到堆栈里的LR就知道是那个从那个函数调用过来的。要知道stack里LR的位置就必须要知道当前函数stack开始的地方,否则stack里那个位置存放了LR。通用的定位每个函数stack开始的地方的方法是在编译的时候让编译器在目标代码中嵌入stack
frame(栈帧)。此外,gcc还能够在elf文件中生成unwind table,unwind table也能实现追踪call
stack。下面主要讲述stack frame。
什么是Stack frame:
stack frame(栈帧)是用来追踪代码的调用过程和调用时的参数,通过读取stack
frame信息可以知道到执行到当前位置的函数调用链表(call stack)。
Stack
frame的本质是每次函数调用都在stack里记录一个frame,每一次函数调用叫做一个frame。ARM里的fp寄存器就是用来指证当前所在函数的frame的,fp总是指向当前函数堆栈的frame。
Stack frame的产生方法:
stack frame是由编译器产生的,也就是gcc在生成可执行代码时,在每个函数入口的地方“放置”一个stack
frame。在调用gcc编译时可以指定参数来要求gcc产生或不产生stack frame,这个参数是:
-fomit-frame-point:让gcc不产生stack frame
-fno-omit-frame-pointer:让gcc产生stack frame
如果不指定这两个参数,是否产生stack frame取决于gcc default是omit-frame-point,还是
no-omit-frame-pointer。
Stack frame是什么样的
通过一个简单的C程序来看一下在binary里,stack frame究竟是怎样的
test.c:
static int func_b(int a, int b, int c, int d,
int e, int f)
{
return a + b + c + d + e + f;
}
static int func_c(int a, int b)
{
return a - b;
}
static int func_a(int x, int y)
{
int a = 3;
int b = 4;
int c = 5;
int d = 6;
int e = 7;
int f = 8;
int ret;
ret = func_b(a, b, c, d, e, f);
ret = func_c(a, ret);
return ret;
}
int main(int argc, char * argv[])
{
int a = 1;
int b = 2;
int ret;
ret = func_a(a, b);
return ret;
}
编译有stack frame的binary:
arm-none-linux-gnueabi-gcc -fno-omit-frame-pointer test.c -o
test.no-omit-frame-pointer编译无stack frame的binary:
arm-none-linux-gnueabi-gcc -fomit-frame-pointer test.c -o
test.omit-frame-pointer分别objdump以上编译所得的两个binary:
arm-none-linux-gnueabi-objdump -S test.no-omit-frame-pointer >
test.no-omit-frame-pointer.objdump
arm-none-linux-gnueabi-objdump -S test.omit-frame-pointer >
test.omit-frame-pointer.objdump
打开两个dump出来的assembly (只关注我们的main(), func_a(),
func_b()三个函数,忽略libc的部分):
test.no-omit-frame-pointer.objdump:
000084e4 <>:
84e4:
e52db004
push
{fp}
; (str fp, [sp, #-4]!)
84e8:
e28db000
add
fp, sp, #0
84ec:
e24dd014
sub
sp, sp, #20
84f0:
e50b0008
str
r0, [fp, #-8]
84f4:
e50b100c
str
r1, [fp, #-12]
84f8:
e50b2010
str
r2, [fp, #-16]
84fc:
e50b3014
str
r3, [fp,
#-20] ;
0xffffffec
8500:
e51b2008
ldr
r2, [fp, #-8]
8504:
e51b300c
ldr
r3, [fp, #-12]
8508:
e0822003
add
r2, r2, r3
850c:
e51b3010
ldr
r3, [fp, #-16]
8510:
e0822003
add
r2, r2, r3
8514:
e51b3014
ldr
r3, [fp,
#-20] ;
0xffffffec
8518:
e0822003
add
r2, r2, r3
851c:
e59b3004
ldr
r3, [fp, #4]
8520:
e0822003
add
r2, r2, r3
8524:
e59b3008
ldr
r3, [fp, #8]
8528:
e0823003
add
r3, r2, r3
852c:
e1a00003
mov
r0, r3
8530:
e28bd000
add
sp, fp, #0
8534:
e8bd0800
ldmfd
sp!, {fp}
8538:
e12fff1e
bx
lr
0000853c <>:
853c:
e52db004
push
{fp}
; (str fp, [sp, #-4]!)
8540:
e28db000
add
fp, sp, #0
8544:
e24dd00c
sub
sp, sp, #12
8548:
e50b0008
str
r0, [fp, #-8]
854c:
e50b100c
str
r1, [fp, #-12]
8550:
e51b2008
ldr
r2, [fp, #-8]
8554:
e51b300c
ldr
r3, [fp, #-12]
8558:
e0633002
rsb
r3, r3, r2
855c:
e1a00003
mov
r0, r3
8560:
e28bd000
add
sp, fp, #0
8564:
e8bd0800
ldmfd
sp!, {fp}
8568:
e12fff1e
bx
lr
0000856c <>:
856c:
e92d4800
push
{fp, lr}
8570:
e28db004
add
fp, sp, #4
8574:
e24dd030
sub
sp, sp, #48
; 0x30
8578:
e50b0028
str
r0, [fp,
#-40] ;
0xffffffd8
857c:
e50b102c
str
r1, [fp,
#-44] ;
0xffffffd4
8580:
e3a03003
mov
r3, #3
8584:
e50b3008
str
r3, [fp, #-8]
8588:
e3a03004
mov
r3, #4
858c:
e50b300c
str
r3, [fp, #-12]
8590:
e3a03005
mov
r3, #5
8594:
e50b3010
str
r3, [fp, #-16]
8598:
e3a03006
mov
r3, #6
859c:
e50b3014
str
r3, [fp,
#-20] ;
0xffffffec
85a0:
e3a03007
mov
r3, #7
85a4:
e50b3018
str
r3, [fp,
#-24] ;
0xffffffe8
85a8:
e3a03008
mov
r3, #8
85ac:
e50b301c
str
r3, [fp,
#-28] ;
0xffffffe4
85b0:
e51b3018
ldr
r3, [fp,
#-24] ;
0xffffffe8
85b4:
e58d3000
str
r3, [sp]
85b8:
e51b301c
ldr
r3, [fp,
#-28] ;
0xffffffe4
85bc:
e58d3004
str
r3, [sp, #4]
85c0:
e51b0008
ldr
r0, [fp, #-8]
85c4:
e51b100c
ldr
r1, [fp, #-12]
85c8:
e51b2010
ldr
r2, [fp, #-16]
85cc:
e51b3014
ldr
r3, [fp,
#-20] ;
0xffffffec
85d0:
ebffffc3
bl
84e4 <>
85d4:
e50b0020
str
r0, [fp,
#-32] ;
0xffffffe0
85d8:
e51b0008
ldr
r0, [fp, #-8]
85dc:
e51b1020
ldr
r1, [fp,
#-32] ;
0xffffffe0
85e0:
ebffffd5
bl
853c <>
85e4:
e50b0020
str
r0, [fp,
#-32] ;
0xffffffe0
85e8:
e51b3020
ldr
r3, [fp,
#-32] ;
0xffffffe0
85ec:
e1a00003
mov
r0, r3
85f0:
e24bd004
sub
sp, fp, #4
85f4:
e8bd8800
pop
{fp, pc}
000085f8 <>:
85f8:
e92d4800
push
{fp, lr}
85fc:
e28db004
add
fp, sp, #4
8600:
e24dd018
sub
sp, sp, #24
8604:
e50b0018
str
r0, [fp,
#-24] ;
0xffffffe8
8608:
e50b101c
str
r1, [fp,
#-28] ;
0xffffffe4
860c:
e3a03001
mov
r3, #1
8610:
e50b3008
str
r3, [fp, #-8]
8614:
e3a03002
mov
r3, #2
8618:
e50b300c
str
r3, [fp, #-12]
861c:
e51b0008
ldr
r0, [fp, #-8]
8620:
e51b100c
ldr
r1, [fp, #-12]
8624:
ebffffd0
bl
856c <>
8628:
e50b0010
str
r0, [fp, #-16]
862c:
e51b3010
ldr
r3, [fp, #-16]
8630:
e1a00003
mov
r0, r3
8634:
e24bd004
sub
sp, fp, #4
8638:
e8bd8800
pop
{fp, pc}
test.omit-frame-pointer.objdump:
000084e4 <>:
84e4:
e24dd010
sub
sp, sp, #16
84e8:
e58d000c
str
r0, [sp, #12]
84ec:
e58d1008
str
r1, [sp, #8]
84f0:
e58d2004
str
r2, [sp, #4]
84f4:
e58d3000
str
r3, [sp]
84f8:
e59d200c
ldr
r2, [sp, #12]
84fc:
e59d3008
ldr
r3, [sp, #8]
8500:
e0822003
add
r2, r2, r3
8504:
e59d3004
ldr
r3, [sp, #4]
8508:
e0822003
add
r2, r2, r3
850c:
e59d3000
ldr
r3, [sp]
8510:
e0822003
add
r2, r2, r3
8514:
e59d3010
ldr
r3, [sp, #16]
8518:
e0822003
add
r2, r2, r3
851c:
e59d3014
ldr
r3, [sp, #20]
8520:
e0823003
add
r3, r2, r3
8524:
e1a00003
mov
r0, r3
8528:
e28dd010
add
sp, sp, #16
852c:
e12fff1e
bx
lr
00008530 <>:
8530:
e24dd008
sub
sp, sp, #8
8534:
e58d0004
str
r0, [sp, #4]
8538:
e58d1000
str
r1, [sp]
853c:
e59d2004
ldr
r2, [sp, #4]
8540:
e59d3000
ldr
r3, [sp]
8544:
e0633002
rsb
r3, r3, r2
8548:
e1a00003
mov
r0, r3
854c:
e28dd008
add
sp, sp, #8
8550:
e12fff1e
bx
lr
00008554 <>:
8554:
e52de004
push
{lr}
; (str lr, [sp, #-4]!)
8558:
e24dd034
sub
sp, sp, #52
; 0x34
855c:
e58d000c
str
r0, [sp, #12]
8560:
e58d1008
str
r1, [sp, #8]
8564:
e3a03003
mov
r3, #3
8568:
e58d302c
str
r3, [sp, #44]
; 0x2c
856c:
e3a03004
mov
r3, #4
8570:
e58d3028
str
r3, [sp, #40]
; 0x28
8574:
e3a03005
mov
r3, #5
8578:
e58d3024
str
r3, [sp, #36]
; 0x24
857c:
e3a03006
mov
r3, #6
8580:
e58d3020
str
r3, [sp, #32]
8584:
e3a03007
mov
r3, #7
8588:
e58d301c
str
r3, [sp, #28]
858c:
e3a03008
mov
r3, #8
8590:
e58d3018
str
r3, [sp, #24]
8594:
e59d301c
ldr
r3, [sp, #28]
8598:
e58d3000
str
r3, [sp]
859c:
e59d3018
ldr
r3, [sp, #24]
85a0:
e58d3004
str
r3, [sp, #4]
85a4:
e59d002c
ldr
r0, [sp, #44]
; 0x2c
85a8:
e59d1028
ldr
r1, [sp, #40]
; 0x28
85ac:
e59d2024
ldr
r2, [sp, #36]
; 0x24
85b0:
e59d3020
ldr
r3, [sp, #32]
85b4:
ebffffca
bl
84e4 <>
85b8:
e58d0014
str
r0, [sp, #20]
85bc:
e59d002c
ldr
r0, [sp, #44]
; 0x2c
85c0:
e59d1014
ldr
r1, [sp, #20]
85c4:
ebffffd9
bl
8530 <>
85c8:
e58d0014
str
r0, [sp, #20]
85cc:
e59d3014
ldr
r3, [sp, #20]
85d0:
e1a00003
mov
r0, r3
85d4:
e28dd034
add
sp, sp, #52
; 0x34
85d8:
e8bd8000
ldmfd
sp!, {pc}
000085dc <>:
85dc:
e52de004
push
{lr}
; (str lr, [sp, #-4]!)
85e0:
e24dd01c
sub
sp, sp, #28
85e4:
e58d0004
str
r0, [sp, #4]
85e8:
e58d1000
str
r1, [sp]
85ec:
e3a03001
mov
r3, #1
85f0:
e58d3014
str
r3, [sp, #20]
85f4:
e3a03002
mov
r3, #2
85f8:
e58d3010
str
r3, [sp, #16]
85fc:
e59d0014
ldr
r0, [sp, #20]
8600:
e59d1010
ldr
r1, [sp, #16]
8604:
ebffffd2
bl
8554 <>
8608:
e58d000c
str
r0, [sp, #12]
860c:
e59d300c
ldr
r3, [sp, #12]
8610:
e1a00003
mov
r0, r3
8614:
e28dd01c
add
sp, sp, #28
8618:
e8bd8000
ldmfd
sp!, {pc}
可以看到有stack frame的binary
(test.no-omit-frame-pointer.objdump) 中每个main(), func_a(),
func_b()入口的地 方都有push fp, lr和add fp, sp, #4;而无stack frame的binary
(test.omit-frame-pointer.objdump) 中函数入口的地方并没有这两句指令。push
fp的作用是把调用函数(caller)的fp保存到被调用函数(callee)的stack开始处,随后add fp, sp,
#4将fp指向被调用函数(callee)
的stack开始的地方。fp总是指向当前函数的stack开始的地方,通过当前函数stack里保存的caller的fp可以追溯到caller的堆栈。通过fp的逐级连接就可以找到调用链上每个函数的stack,从而每个函数的LR(返回地址)都可以从stack里获得,只要在symbol
table里搜索一下LR的值,就可以知道caller是哪个函数了。有了stack
frame不但可以通过stack里的LR得到caller的地址,还可以知道函数调用时的参数,回想一下前面说的‘第一到第四个参数在r0~r3里面,第五到第八个参数在Stack里面’。具体分析一下上面的汇编代码,结合
Procedure Call Standard for the ARM
Architecture规定的调用原则,绘制出stack frame的结构如下:
从上面的例子可以看到一个函数(如func_a())的stack
frame里,开始的地方放的是fp,
lr(如果是最后一级的被调用函数,如func_b,func_c则lr无需保存到stack),然后是函数的内部变量,之后是该函数的参数1~参数4,最后是其调用的函数(本例中是func_b())的参数5~参数8。在本例中如果在func_b()中要追溯call
stack,从lr可以知道哪个函数调用了它,fp寄存器保存了func_b的stack
frame的起始地址,该地址上存放了上一级stack frame的起始地址(caller的stack
frame),该地址向stack缩小方向存放了caller调用func_b()的时候传递的参数5~8(如果有参数5~8的话),该地址向stack伸长方向到存放调用下一个函数的参数5~8之前的位置存放了caller调用func_b()的时候传递的参数1~4。