有的系统调用会返回复杂的C类型的数据结构,下面就介绍如何在汇编里使用这种类型的系统调用...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第377页到第391页,目前翻译完前12章 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为391 / 577)。

Advanced System Call Return Values 系统调用返回复杂的数据结构:

    有的系统调用会返回复杂的C类型的数据结构,下面就介绍如何在汇编里使用这种类型的系统调用。

The sysinfo system call - sysinfo系统调用:

    sysinfo系统调用可以用来返回系统相关的配置信息和系统里可用的资源信息,在命令行输入man 2 sysinfo可以看到sysinfo的帮助信息如下:

NAME
       sysinfo - returns information on overall system statistics

SYNOPSIS
       #include <sys/sysinfo.h>

       int sysinfo(struct sysinfo *info);
..........................................
..........................................


    从上面输出可以看到,sysinfo系统调用会接受一个struct sysinfo *info的参数,该参数是一个结构体的指针,sysinfo会根据该指针,将数据写入到info指向的内存区域,struct sysinfo结构体的定义如下(在上面man命令输出的帮助信息里有该结构体的定义):

struct sysinfo {
	long uptime;             /* Seconds since boot */
	unsigned long loads[3];  /* 1, 5, and 15 minute load averages */
	unsigned long totalram;  /* Total usable main memory size */
	unsigned long freeram;   /* Available memory size */
	unsigned long sharedram; /* Amount of shared memory */
	unsigned long bufferram; /* Memory used by buffers */
	unsigned long totalswap; /* Total swap space size */
	unsigned long freeswap;  /* swap space still available */
	unsigned short procs;    /* Number of current processes */
	unsigned long totalhigh; /* Total high memory size */
	unsigned long freehigh;  /* Available high memory size */
	unsigned int mem_unit;   /* Memory unit size in bytes */
	char _f[20-2*sizeof(long)-sizeof(int)]; /* Padding for libc5 */
	};


    因此,在汇编里就需要定义一个类似上面结构的内存区域,用于存放结果:

.section .data
result:
uptime:
    .int 0
load1:
    .int 0
load5:
    .int 0
load15:
    .int 0
totalram:
    .int 0
freeram:
    .int 0
sharedram:
    .int 0
bufferram:
    .int 0
totalswap:
    .int 0
freeswap:
    .int 0
procs:
    .byte 0x00, 0x00
totalhigh:
    .int 0
freehigh:
    .int 0
memunit:
    .int 0

    在汇编代码的数据段部分定义如上所示的内存区域,由result指向这段内存,然后将result作为参数传递给sysinfo系统调用,这样在sysinfo里就会根据result指向的内存位置,将相关字段依次进行填充。

Using the return structure - 使用sysinfo的完整汇编例子:

    下面的sysinfo.s程式就完整的演示了如何在汇编里使用sysinfo系统调用来获取系统相关信息的方法:

# sysinfo.s - Retrieving system information via kernel system calls
.section .data
result:
uptime:
    .int 0
load1:
    .int 0
load5:
    .int 0
load15:
    .int 0
totalram:
    .int 0
freeram:
    .int 0
sharedram:
    .int 0
bufferram:
    .int 0
totalswap:
    .int 0
freeswap:
    .int 0
procs:
    .byte 0x00, 0x00
totalhigh:
    .int 0
freehigh:
    .int 0
memunit:
    .int 0
.section .text
.globl _start
_start:
    nop
    movl $result, %ebx
    movl $116, %eax
    int $0x80
    movl $0, %ebx
    movl $1, %eax
    int $0x80
 

    上面的代码先通过movl $result, %ebx指令将result引用的内存位置赋值给EBX寄存器,作为系统调用的第一个参数,然后通过movl $116, %eax设置sysinfo的系统调用号(sysinfo的系统调用号为116,该调用号可以在上一篇提到的unistd.h头文件里找到),最后通过$0x80的中断进入系统调用执行,执行的结果可以在gdb调试器里查看到:

$ as -gstabs -o sysinfo.o sysinfo.s 
$ ld -o sysinfo sysinfo.o 
$ gdb -q sysinfo
Reading symbols from /home/zengl/Downloads/asm_example/syscall/sysinfo...done.
(gdb) b *_start 
Breakpoint 1 at 0x8048074: file sysinfo.s, line 35.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/syscall/sysinfo 

Breakpoint 1, _start () at sysinfo.s:35
35	    nop
(gdb) s
36	    movl $result, %ebx
(gdb) s
37	    movl $116, %eax
(gdb) s
38	    int $0x80
(gdb) s
39	    movl $0, %ebx
(gdb) x/d &uptime 
0x8049090 <uptime>:	2453
(gdb) x/d &load1
0x8049094 <load1>:	6432
(gdb) x/d &load5
0x8049098 <load5>:	4544
(gdb) x/d &load15 
0x804909c <load15>:	3904
(gdb) x/d &totalram 
0x80490a0 <totalram>:	519663616
(gdb) x/d &freeram 
0x80490a4 <freeram>:	8466432
(gdb) x/d &sharedram 
0x80490a8 <sharedram>:	0
(gdb) x/d &bufferram 
0x80490ac <bufferram>:	40206336
(gdb) x/d &totalswap 
0x80490b0 <totalswap>:	534769664
(gdb) x/d &freeswap 
0x80490b4 <freeswap>:	531910656
(gdb) x/d &procs 
0x80490b8 <procs>:	304
(gdb) x/d &totalhigh 
0x80490ba <totalhigh>:	0
(gdb) x/d &freehigh 
0x80490be <freehigh>:	0
(gdb) x/d &memunit 
0x80490c2 <memunit>:	65536
(gdb) 


    从上面输出可以看到,当int $0x80执行完后,系统信息就被依次填充到各个内存字段里了,例如第一个字段uptime为2453,说明系统已启动了2453秒,totalram值为519663616,说明当前系统可用的物理内存的总字节数为519663616,等等。

Tracing System Calls 追踪系统调用:

    在Linux系统里提供了strace工具,该工具可以用来查看某个应用程序在执行过程中调用了哪些系统调用,以及这些系统调用的执行次数,返回值等,有了该工具,就可以帮助分析调试某个应用程序,下面就对strace工具的用法做个介绍。

The strace program strace的用法:

    只要你所在的用户拥有合适的权限,就可以使用strace来追踪程序的系统调用信息,该程序可以是strace启动的程序,还可以是当前系统里正在运行中的进程,下面就用strace来追踪上一篇文章里创建的syscalltest2程序的系统调用信息:

$ strace ./syscalltest2 
execve("./syscalltest2", ["./syscalltest2"], [/* 52 vars */]) = 0
getpid()                                = 3134
getuid()                                = 1000
getgid()                                = 1000
_exit(0)                                = ?
$


    通过向strace传递./syscalltest2的参数,strace就会根据该参数通过execve系统调用来运行该程序,syscalltest2在运行时会依次调用getpid,getuid及getgid这三个系统调用,上面输出列表里,左侧是系统调用的名称和传递给系统调用的参数,右侧等号后面的值是系统调用的返回值,例如getpid() = 3134,等号后面是3134,说明getpid返回的值是3134,同理getuid及getgid的返回值为1000,这些结果和上一篇文章里运行的情况一致,另外从上面输出还可以看到,syscalltest2执行结束时,会通过_exit系统调用来终止进程。

    另外,可以给strace传递-c参数,让其统计出系统调用的执行时间,调用次数等信息:

$ strace -c ./syscalltest2 
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
  -nan    0.000000           0         1           execve
  -nan    0.000000           0         1           getpid
  -nan    0.000000           0         1           getuid
  -nan    0.000000           0         1           getgid
------ ----------- ----------- --------- --------- ----------------
100.00    0.000000                     4           total
$


    上面统计出了execve,getpid之类的调用次数等信息。

Advanced strace parameters - strace的其他参数:

    strace命令的常用参数如下表所示:

Parameter 
参数
Description 
描述
-c Count the time, calls, and errors for 
each system call. 
统计每个系统调用所执行的时间,执行次数和出错的次数等
-d Show some debugging output of strace. 
输出strace的一些调试信息
-e Specify a filter expression for the output. 
指定一些表达式来对输出结果进行过滤筛选
-f Trace child processes as they are created. 
追踪由fork调用所产生的子进程
-ff If writing to an output file, write each child process 
in a separate file. 
如果提供了-o filename参数(表示将结果输出到filename文件),
则所有子进程的追踪结果会输出到相应的filename.pid中,
pid是各进程的进程标识符
-i Print the instruction pointer at the time 
of the system call. 
输出系统调用的入口指针
-o filename Write the output to the file specified. 
将输出结果写入到指定的filename文件里
(filename表示文件名称)
-p PID Attach to the existing process by the PID specified. 
附加到PID对应的进程(PID是-p参数指定的进程标识符)
-q Suppress messages about attaching and detaching. 
禁止输出附加进程及脱离进程时产生的消息
-r Print a relative timestamp on each system call. 
打印出每个系统调用的相对时间戳信息
-t Add the time of day to each line. 
在输出的每一行显示"时:分:秒"格式的时间
-tt Add the time of day, including microseconds to each line. 
在输出的每一行显示"时:分:秒.微秒"格式的时间(精确到了微秒)
-ttt Add the time of day in epoch (seconds since Jan. 1, 1970), 
including microseconds. 
在每行显示包括微秒在内的Unix时间戳(自1970年1月1日以来的秒数)
-T Show the time spent in each system call. 
显示每个系统调用所消耗的时间
-v Print unabbreviated versions of the system call 
information (verbose). 
显示出系统调用的完整详细的参数信息,例如,execve系统调用的最后一个参数是环境变量,由于环境变量比较多,默认情况下会被省略输出,加上-v参数后,就可以将所有的环境变量信息都完整的显示出来
-x Print all non-ASCII characters in hexadecimal format. 
所有不在标准的ASCII码范围内的字符都以十六进制的形式进行显示
-xx Print all strings in hexadecimal format. 
将所有的字符串都以十六进制格式进行输出显示

    这些参数都不难理解,其中比较常用的一个参数是-e,该参数可以对输出结果进行筛选过滤,从而只显示出需要的信息,-e参数的格式如下:

-e trace=call_list

    trace后面的call_list是以逗号分隔的系统调用列表,这样就可以只输出call_list列表里的系统调用信息,例如,假设你只想查看syscalltest2程序里的getpid和getgid的系统调用信息,则可以使用如下命令:

$ strace -e trace=getpid,getgid ./syscalltest2
getpid()                                = 2680
getgid()                                = 1000
$

    可以看到输出结果里只显示了getpid和getgid的信息,其他的系统调用信息则被过滤掉了。

Watching program system calls 查看程序的系统调用信息:

    strace的最大特点在于它可以用在系统里的任何一个程序或进程上,而且这些程序不需要在代码里添加任何额外的功能就可以使用strace。

    在上面strace的参数列表里可以看到一个-o参数,该参数可以将结果输出到指定的文件里,例如下面的命令:

$ strace -o mytrace id
uid=1000(zengl) gid=1000(zengl) 组=1000(zengl),4(adm),24(cdrom),27(sudo),29(audio),30(dip),46(plugdev),109(lpadmin),123(sambashare)
$ cat mytrace 
execve("/usr/bin/id", ["id"], [/* 52 vars */]) = 0
brk(0)                                  = 0x9dce000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb76e9000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=87327, ...}) = 0
mmap2(NULL, 87327, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb76d3000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0@A\0\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0644, st_size=120748, ...}) = 0
mmap2(NULL, 125852, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb76b4000
mmap2(0xb76d1000, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c) = 0xb76d1000
close(3)                                = 0
..........................................
..........................................
$


    上面的 strace -o mytrace id 命令将id程序执行时的系统调用信息都输出到了mytrace文件里,通过 cat mytrace 命令可以查看到该文件里将所有的系统调用的执行情况都保存了下来,从这些信息里可以清楚的看到id程序打开了哪些文件,以及这些文件是如何映射到内存里的。在我的系统里,mytrace文件里的内容一共有237行,如果你只想查看一共执行了多少系统调用,以及这些系统调用的执行时间、执行次数的统计情况,可以使用-c参数:

$ strace -c id
uid=1000(zengl) gid=1000(zengl) 组=1000(zengl),4(adm),24(cdrom),27(sudo),29(audio),30(dip),46(plugdev),109(lpadmin),123(sambashare)
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 38.71    0.000036           2        17           read
 37.63    0.000035           9         4         4 connect
 23.66    0.000022           1        32         2 open
  0.00    0.000000           0         1           write
  0.00    0.000000           0        36           close
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         9         9 access
  0.00    0.000000           0         3           brk
  0.00    0.000000           0        19           munmap
  0.00    0.000000           0         9           mprotect
  0.00    0.000000           0        22           _llseek
  0.00    0.000000           0        42           mmap2
  0.00    0.000000           0        29           fstat64
  0.00    0.000000           0         1           getuid32
  0.00    0.000000           0         1           getgid32
  0.00    0.000000           0         1           geteuid32
  0.00    0.000000           0         1           getegid32
  0.00    0.000000           0         2           getgroups32
  0.00    0.000000           0         1           set_thread_area
  0.00    0.000000           0         1           statfs64
  0.00    0.000000           0         4           socket
------ ----------- ----------- --------- --------- ----------------
100.00    0.000093                   236        15 total
$


    上面的输出就很详细的统计了每个系统调用所消耗的时间,调用的次数,每次执行的微秒数,以及执行出错的次数等,这里需要注意errors栏目,该栏所在的列显示的是每个系统调用执行出错的次数,例如,上面显示open系统调用有2次出错,以及connect系统调用的4次执行都出错了,如果你想了解出错的具体原因,可以使用-e参数来显示指定的系统调用的执行过程:

$ strace -e trace=open,connect id
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libselinux.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libdl.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/proc/filesystems", O_RDONLY|O_LARGEFILE) = 3
open("/proc/filesystems", O_RDONLY|O_LARGEFILE) = 3
open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE|O_CLOEXEC) = 3
open("/usr/share/locale/locale.alias", O_RDONLY|O_CLOEXEC) = 3
open("/usr/share/locale/zh_CN/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/zh/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/zh_CN/LC_MESSAGES/coreutils.mo", O_RDONLY) = 3
open("/usr/lib/i386-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
open("/etc/nsswitch.conf", O_RDONLY|O_CLOEXEC) = 3
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libnss_compat.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libnsl.so.1", O_RDONLY|O_CLOEXEC) = 3
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libnss_nis.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/lib/i386-linux-gnu/libnss_files.so.2", O_RDONLY|O_CLOEXEC) = 3
open("/etc/passwd", O_RDONLY|O_CLOEXEC) = 3
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
connect(3, {sa_family=AF_FILE, path="/var/run/nscd/socket"}, 110) = -1 ENOENT (No such file or directory)
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/proc/sys/kernel/ngroups_max", O_RDONLY) = 3
open("/proc/sys/kernel/ngroups_max", O_RDONLY) = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
open("/etc/group", O_RDONLY|O_CLOEXEC)  = 3
uid=1000(zengl) gid=1000(zengl) 组=1000(zengl),4(adm),24(cdrom),27(sudo),29(audio),30(dip),46(plugdev),109(lpadmin),123(sambashare)
$


    可以看到调用出错的原因基本上都是因为 ENOENT (No such file or directory) 即找不到指定的文件引起的。

Attaching to a running program 附加到指定的运行中的进程:

    strace工具还可以通过-p参数来附加到某个正在运行中的进程,然后捕获并显示该进程的系统调用的实时情况。

    下面的nanotest.s程式在循环时会进入睡眠,这样就有充足的时间让strace来附加到该进程,然后捕获该进程的系统调用情况:

# nanotest.s - Another example of using system calls
.section .data
timespec:
    .int 5, 0
output:
    .ascii "This is a test\n"
output_end:
    .equ len, output_end - output
.section .bss
    .lcomm rem, 8
.section .text
.globl _start
_start:
    nop
    movl $10, %ecx
loop1:
    pushl %ecx
    movl $4, %eax
    movl $1, %ebx
    movl $output, %ecx
    movl $len, %edx
    int $0x80
    movl $162, %eax
    movl $timespec, %ebx
    movl $rem, %ecx
    int $0x80
    popl %ecx
    loop loop1
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码里有个loop1循环体,该循环体里用到了两个系统调用:一个是write(系统调用号为4),另一个是nanosleep(系统调用号为162),write系统调用之前讲解过,就是向某文件描述符写入数据,这里write的作用是将output字符串输出到显示屏幕上(显示屏标准输出设备的文件描述符为1),而nanosleep系统调用则是让进程按照指定的时间间隔进行睡眠,使用man 2 nanosleep命令可以查看到该系统调用的用法:

NAME
       nanosleep - high-resolution sleep

SYNOPSIS
       #include 

       int nanosleep(const struct timespec *req, struct timespec *rem);
..........................................
..........................................


    该系统调用有两个输入参数,两个参数都是struct timespec类型,该结构体类型的定义如下:

struct timespec {
    time_t tv_sec;        /* seconds */
    long   tv_nsec;       /* nanoseconds */
    };

    timespec结构体里的第一个成员tv_sec表示秒数,而tv_nsec成员则表示纳秒数(纳秒值的范围为0到999999999),nanosleep的第一个输入参数req用于指定需要睡眠的时间(可以精确到纳秒),nanosleep系统调用在让进程进入睡眠的过程中,有三种情况会让进程醒过来,第一种情况是到了第一个参数req所指定的时间正常醒过来(此时nanosleep的返回值为0),第二种情况是进程被某个信号中断而醒过来(此时nanosleep的返回值为-1,同时errno错误号为EINTR),第三种情况是发生了其他的错误(返回值为-1,errno会被设置为对应的错误号)。

    当nanosleep因为第二种情况被某信号中断而返回时,由于只执行了一部分睡眠时间,所以剩余的睡眠时间就会写入第二个参数rem指向的timespec结构体,这样,线程可以根据需要再次调用nanosleep,而nanosleep就会继续将rem里的剩余时间给睡眠完,在C程序里可以做些if之类的条件判断,当睡眠完所需时间时才执行某项操作,如果被中断而只睡眠了一部分时间,则继续将剩余时间给睡眠完,再执行对应的操作,从而确保每项操作都是在指定的间隔时间里完成的,如下面的C代码:

#include <errno.h>
#include <time.h>

int better_sleep (double sleep_time) 
{
  struct timespec tv; 
  /* Construct the timespec from the number of whole seconds...  */ 
  tv.tv_sec = (time_t) sleep_time; 
  /* ... and the remainder in nanoseconds.  */ 
  tv.tv_nsec = (long) ((sleep_time - tv.tv_sec) * 1e+9); 
 
  while (1) 
  {
    /* Sleep for the time specified in tv. If interrupted by a 
       signal, place the remaining time left to sleep back into tv.  */ 
    int rval = nanosleep (&tv, &tv); 
    if (rval == 0) 
      /* Completed the entire sleep time; all done.  */ 
      return 0; 
    else if (errno == EINTR) 
      /* Interrupted by a signal. Try again.  */ 
      continue; 
    else 
      /* Some other error; bail out.  */ 
      return rval; 
  } 
  return 0; 
} 


    上面代码来源自 http://www.makelinux.net/alp/066 ,不过在我们上面的nanotest.s例子里并没有这么严谨的使用nanosleep,而是简单的为nanosleep提供两个输入参数,并没有对返回值做判断,这里只要了解nanosleep两个参数的含义即可。

    由于struct timespec结构体里的time_t和long类型都是32位的尺寸大小,所以在汇编里就使用.int伪指令来声明这两个字段:

timespec:
    .int 5, 0
....................
....................
.section .bss
    .lcomm rem, 8

    上面的timespec标签处第一个整数5表示需要睡眠的秒数为5秒,第二个整数0表示纳秒数为0,所以总的睡眠时间就是5秒,在.bss里声明rem为8个字节(表示struct timespec结构体)。nanotest.s里的rem用法并不严谨,没有起到像之前C代码里应有的作用,不过为了简单明了就没做过多的条件判断。

    要对nanotest.s程式进行strace追踪的话,需要先汇编链接该程式:

$ as -gstabs -o nanotest.o nanotest.s
$ ld -o nanotest nanotest.o
$
./nanotest
This is a test
This is a test

    上面的nanotest运行起来后,需要切换到另一个终端,然后输入如下命令:

$ su -
密码: 
root# ps aux | grep nanotest
zengl     2885  1.4  5.9 223420 30276 ?        Sl   14:48   0:04 gedit /home/zengl/Downloads/asm_example/syscall/nanotest.s
zengl     3080  0.0  0.0    148     4 pts/0    S+   14:53   0:00 ./nanotest
root      3082  0.0  0.1   6084   832 pts/1    S+   14:53   0:00 grep --color=auto nanotest
root# strace -p 3080
Process 3080 attached - interrupt to quit
restart_syscall(<... resuming interrupted call ...>) = 0
write(1, "This is a test\n", 15)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
_exit(0)                                = ?
Process 3080 detached
root#


    普通用户可能没有权限strace附加到nanotest进程,所以先用su -命令切换到root用户,然后通过ps aux | grep nanotest命令查找到nanotest的进程标识符为3080,接着就可以用strace -p 3080来附加到该进程了,从输出里可以看到该进程的系统调用的实时执行情况。

    如果不想切换到另一个终端,可以使用如下命令,将nanotest切换到后台执行:

root# ./nanotest &
[1] 3217
This is a test
root# strace -p 321This is a test
7
Process 3217 attached - interrupt to quit
restart_syscall(<... resuming interrupted call ...>) = 0
write(1, "This is a test\n", 15This is a test
)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15This is a test
)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15This is a test
)        = 15
nanosleep({5, 0}, 0x80490d0)            = 0
write(1, "This is a test\n", 15This is a test
...................................
...................................


    通过 ./nanotest & 命令将nanotest切换到后台执行,在该程序切换到后台时会自动显示出该进程的进程标识符为3217,这样接着就可以输入strace -p 3217来进行附加进程的操作了。不过由于nanotest的输出也会在该终端进行显示,所以strace和nanotest两个程序的输出会混淆在一块。(同样需要在root权限下执行strace附加进程的操作,至少在我的ubuntu系统里是这样的)

[zengl pagebreak]

System Calls versus C Libraries 系统调用与C语言库:

    在之前的章节,很多汇编例子里都用到了C语言库里的函数(即libc里的函数),例如:printf等。

    下面就对C库函数的使用以及这些库函数与系统调用之间的关系做个介绍。

The C libraries C语言库:

    和系统调用类似,C库里的函数也可以使用man命令来查看相关的帮助文档,例如在命令行下输入man exit命令就可以查看到exit函数的帮助信息:

EXIT(3)                    Linux Programmer's Manual                   EXIT(3)

NAME
       exit - cause normal process termination

SYNOPSIS
       #include <stdlib.h>

       void exit(int status);

DESCRIPTION
       The  exit() function causes normal process termination and the value of
       status & 0377 is returned to the parent (see wait(2)).

       All functions registered with atexit(3) and on_exit(3) are  called,  in
       the  reverse  order  of their registration.  (It is possible for one of
       these functions to use atexit(3) or on_exit(3)  to  register  an  addi‐
       tional  function  to be executed during exit processing; the new regis‐
       tration is added to the front of the list of functions that  remain  to
       be  called.)  If one of these functions does not return (e.g., it calls
       _exit(2), or kills itself with a signal), then none  of  the  remaining
       functions is called, and further exit processing (in particular, flush‐
       ing of stdio(3) streams) is abandoned.  If a function has  been  regis‐
       tered  multiple  times using atexit(3) or on_exit(3), then it is called
       as many times as it was registered.

       All open stdio(3) streams are flushed and  closed.   Files  created  by
       tmpfile(3) are removed.

       The  C standard specifies two constants, EXIT_SUCCESS and EXIT_FAILURE,
       that may be passed to exit() to  indicate  successful  or  unsuccessful
       termination, respectively.

RETURN VALUE
       The exit() function does not return.
.....................................................
.....................................................


    从SYNOPSIS用法简介里可以看到exit函数需要接受一个整数类型的参数,作为退出码。

    在之前"汇编函数的定义和使用"的相关文章里,我们介绍过C类型的函数都是通过栈来传递参数的,例如下面的C代码:

printf(“The answer is %d\n”, k);

    该C语句可以转换成类似下面的汇编代码:

pushl k
pushl $output
call printf
addl $8, %esp

    参数压栈的顺序是和C函数的参数列表里的顺序相反的(之前的章节也讲解过),先pushl k将第二个参数压入栈,再pushl $output将第一个参数压入栈(这里用$output表示输出字符串的内存位置),准备好输入参数后,通过call printf指令调用C库函数,执行完库函数后,最后通过addl $8, %esp指令将call指令之前压入栈的两个参数给丢弃掉。

Tracing C functions 追踪C库函数里的系统调用信息:

    C语言的库函数其实是对系统调用的一种更高层次的封装,用户调用这些C库函数后,在库函数内部它还是会使用系统调用来完成相关的功能,例如下面的cfunctest.s例子:

# cfunctest.s - An example of using C functions
.section .data
output:
    .asciz "This is a test\n"
.section .text
.globl _start
_start:
    movl $10, %ecx
loop1:
    pushl %ecx
    pushl $output
    call printf
    addl $4, %esp
    pushl $5
    call sleep
    addl $4, %esp
    popl %ecx
    loop loop1
    pushl $0
    call exit
 

    上面的代码可以完成和nanotest.s类似的功能,先用movl $10, %ecx设置ECX为10,从而将loop1循环体的循环次数设置为10,在loop1循环体里先通过printf库函数将output标签引用的字符串输出显示到屏幕上,然后通过sleep库函数让进程睡眠5秒钟,循环这种printf和sleep操作10次,最后通过exit库函数来终止进程。

    下面先对cfunctest程式进行汇编链接:

$ as -gstabs -o cfunctest.o cfunctest.s
$ ld -dynamic-linker /lib/ld-linux.so.2 -lc -o cfunctest cfunctest.o
$

    上面在用ld链接生成可执行程序时,为了能使用C标准库里的函数,就需要通过-dynamic-linker /lib/ld-linux.so.2参数来指定动态库的加载器,同时还要通过-lc参数来指定链接C标准库,这些链接方面的内容在之前的章节里都多次提到过。

    创建完可执行文件后,就可以使用strace来追踪cfunctest程式:

$ strace ./cfunctest
execve("./cfunctest", ["./cfunctest"], [/* 52 vars */]) = 0
brk(0)                                  = 0x95fe000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77d8000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=87327, ...}) = 0
mmap2(NULL, 87327, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77c2000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0000\226\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1734120, ...}) = 0
mmap2(NULL, 1743580, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7618000
mmap2(0xb77bc000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1a4) = 0xb77bc000
mmap2(0xb77bf000, 10972, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb77bf000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7617000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb7617900, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb77bc000, 8192, PROT_READ)   = 0
mprotect(0xb77fb000, 4096, PROT_READ)   = 0
munmap(0xb77c2000, 87327)               = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 0), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77d7000
write(1, "This is a test\n", 15This is a test
)        = 15
rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0
rt_sigaction(SIGCHLD, NULL, {SIG_DFL, [], 0}, 8) = 0
rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0
nanosleep({5, 0}, 0xbfd65780)           = 0
...........................................
...........................................


    从上面输出可以看到,在C库函数里执行了很多的系统调用,从开头的输出可以看到程序会先用open系统调用打开libc.so.6的库文件,并将其通过mmap2系统调用映射到内存里,这样程序就可以执行libc里的库函数了,另外,通过上面的write(1, "This is a test\n"....信息可以看到printf库函数在内部是通过write系统调用向1号文件描述符写入字符串从而将信息显示到屏幕上的,而sleep库函数则是通过nanosleep系统调用来让进程睡眠的。

    你也可以使用strace加-rc参数来统计cfunctest程式的系统调用信息:

$ strace -rc ./cfunctest
This is a test
..........................................
..........................................
This is a test
% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- ----------------
 63.16    0.000036           4        10           write
 36.84    0.000021          11         2           mprotect
  0.00    0.000000           0         1           read
  0.00    0.000000           0         2           open
  0.00    0.000000           0         2           close
  0.00    0.000000           0         1           execve
  0.00    0.000000           0         3         3 access
  0.00    0.000000           0         1           brk
  0.00    0.000000           0         1           munmap
  0.00    0.000000           0        10           nanosleep
  0.00    0.000000           0        10           rt_sigaction
  0.00    0.000000           0        20           rt_sigprocmask
  0.00    0.000000           0         7           mmap2
  0.00    0.000000           0         3           fstat64
  0.00    0.000000           0         1           set_thread_area
------ ----------- ----------- --------- --------- ----------------
100.00    0.000057                    74         3 total
$


Comparing system calls and C libraries 比较系统调用与C库函数:

    在汇编里使用系统调用和使用C库函数的一个明显的区别在于,C库函数会在执行具体的逻辑功能之前,先加载所需的库文件到内存里,所以比汇编里直接使用系统调用的开销要大,不过这些开销在大型的汇编程式开发时,就没那么明显了。

    当你的程序有如下几个需求时,可以考虑直接使用系统调用:
  • 需要尽可能的减少程序的size尺寸时(因为不需要加载第三方库,所以可以减少一些大小)
  • 需要尽可能的创建高效率的可执行代码时(由于没有第三方库的加载操作,所以可以减少一些开销)
  • 链接生成的可执行文件不想依赖任何额外的库文件时
    当你的程序有如下几个需求时,可以考虑使用C库函数:
  • C库文件里通常集成了很多现成的丰富的功能函数,当你不想从头写一遍这些函数时,例如,字符串与整数、浮点数之间的转换函数等,那么你可以直接使用C库函数
  • C标准库函数可以在多个操作系统之间很方便的进行移植,当你希望程序尽可能的跨平台时,可以使用C标准库函数
  • C的动态链接库文件在内存里只有一个副本,其他的程序可以直接访问到动态库里的函数,不需要在内存里创建多个动态库的副本,可以减少内存上的开销,当你希望减少这种内存上的开销时,可以使用C的动态库
    以上这几点只是一些建议,你需要根据自己的实际需求来决定,当然也可以在汇编里混合使用系统调用和C库函数。

    剩下是英文原著第12章的总结部分,限于篇幅,就不多说了。

    下一篇开始介绍如何在C代码里使用Inline Assembly(内联汇编)。

    OK,就到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 使用内联汇编 (一)

上一篇: 汇编里使用Linux系统调用 (二)

相关文章

调用汇编模块里的函数 (三) 静态库、共享库、本章结束篇

高级数学运算 (四) 高级运算结束篇

IA-32平台(三) 硬件介绍结束篇及各种处理器平台

汇编中使用文件 (三) 使用文件结束篇

Moving Data 汇编数据移动 (二)

汇编里使用Linux系统调用 (二)