linux5 syscall 流程_linux探险家

标签:

杂谈

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。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值