上一篇对Linux内核做了一个简单的介绍,下面我们就具体的看下内核给用户程序提供了哪些可用的系统调用,以及如何在汇编里使用它们...

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

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

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

System Calls 系统调用:

    上一篇对Linux内核做了一个简单的介绍,下面我们就具体的看下内核给用户程序提供了哪些可用的系统调用,以及如何在汇编里使用它们。

Finding system calls 查找系统调用:

    通常,每发布一个新的内核,都会新增一些系统调用,要查找当前系统里可用的系统调用,可以从一些开发用的C语言头文件里看到,如果你的系统已经配置为编程开发环境(系统里已经装好了gcc,gdb之类的开发工具),则可以在 /usr/include/asm/unistd.h 的头文件里查看到系统调用的宏定义,不过在译者的Ubuntu系统里,如果是32位系统,unistd.h会转去加载unistd_32.h里的定义,如果是64位系统,则加载unistd_64.h里的定义:

$ cat /usr/include/asm/unistd.h
# ifdef __i386__
#  include <asm/unistd_32.h>
# else
#  include <asm/unistd_64.h>
# endif

    下面是unistd_32.h里的部分内容:

$ cat /usr/include/asm/unistd_32.h 
#ifndef _ASM_X86_UNISTD_32_H
#define _ASM_X86_UNISTD_32_H

/*
 * This file contains the system call numbers.
 */

#define __NR_restart_syscall      0
#define __NR_exit		  1
#define __NR_fork		  2
#define __NR_read		  3
#define __NR_write		  4
#define __NR_open		  5
#define __NR_close		  6
#define __NR_waitpid		  7
#define __NR_creat		  8
#define __NR_link		  9
#define __NR_unlink		 10
#define __NR_execve		 11
#define __NR_chdir		 12
#define __NR_time		 13
#define __NR_mknod		 14
#define __NR_chmod		 15
#define __NR_lchown		 16
...................................
...................................


    上面的输出内容显示,每个系统调用都被定义为以__NR_开头的宏定义,对应的宏值为具体的系统调用号,系统调用号对于汇编开发来说是很重要的,因为汇编程序就是靠系统调用号来使用对应的系统调用里的功能的。

Finding system call definitions 查找系统调用的帮助说明:

    在上面的头文件里列举了很多的系统调用,可以使用man命令来查看这些系统调用的帮助信息(前提是系统里已经安装了相关的man pages(帮助文档),如果已经搭建了开发环境的话,一般已经装好了,如果没安装,则需要针对具体的发行版进行相关的安装)。

    系统调用的帮助信息位于man pages(帮助文档)的第二节,例如,要查看exit系统调用的信息,则需要输入如下命令:

$ man 2 exit

    如果只在命令行输入man exit的话,就会输出如下结果:

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

NAME
       exit - cause normal process termination

SYNOPSIS
       #include <stdlib.h>

       void exit(int status);
.......................................
.......................................


    可以看到默认显示的是第三节里的信息,SYNOPSIS(用法简介)里,显示的帮助信息只是标准C库里的exit函数的信息,并非我们所需的系统调用的信息。

    如果是输入man 2 exit的话,则输出结果如下:

_EXIT(2)                   Linux Programmer's Manual                  _EXIT(2)

NAME
       _exit, _Exit - terminate the calling process

SYNOPSIS
       #include <unistd.h>

       void _exit(int status);

       #include <stdlib.h>

       void _Exit(int status);
.......................................
.......................................
DESCRIPTION
       The function _exit() terminates the calling process "immediately".  Any
       open file descriptors belonging to the process are closed; any children
       of the process are inherited by process 1, init, and the process's par‐
       ent is sent a SIGCHLD signal.
.......................................
.......................................
RETURN VALUE
       These functions do not return.
.......................................
.......................................


    上面输出的_exit及_Exit才是所需的系统调用函数。

    另外,从上面的输出里也可以看到,系统调用帮助文档里的信息主要由以下4个部分组成:
  • NAME:显示系统调用的名称
  • SYNOPSIS:显示系统调用的C语言用法简介
  • DESCRIPTION:显示系统调用的描述信息
  • RETURN VALUE:系统调用结束时的返回值
    其中,SYNOPSIS用法简介部分,使用的是C语言格式,不过汇编开发时也可以借鉴这种格式来编写对应的汇编代码。

Common system calls 常见的系统调用:

    尽管有很多的系统调用可供选择,不过这些系统调用也可以进行归类,下面就对一些常见的系统调用按照内核功能进行简单的分类。

    常见的和内存访问相关的系统调用如下表所示:

System Call 
系统调用
Description 
描述
brk Change the data segment size. 
通过修改进程数据段的结束位置,从而修改数据段的尺寸
mlock lock parts of memory. 
将调用进程的一部分虚拟内存锁定在RAM物理内存里,防止这部分内存被交换到swap(磁盘交换分区)
mlockall lock all memory. 
将调用进程的所有虚拟内存都锁定在物理内存里
mmap Map files or devices into memory. 
将文件或设备映射到调用进程的虚拟内存空间
mprotect set protection on a region of memory. 
对某段内存区域设置保护措施,例如可以设置该段内存不准被访问,或者只允许读之类的
mremap Remap a virtual memory address. 
将某段虚拟内存进行重新映射,从而调整虚拟内存空间的尺寸,该系统调用可以用来实现非常高效的realloc操作
msync synchronize a file with a memory map. 
将mmap映射到内存里的文件数据同步到磁盘里
munlock unlock parts of memory. 
执行和mlock相反的操作,将一部分虚拟内存解锁,让其可以在需要时被交换到swap分区
munlockall unlock all memory. 
执行和mlockall相反的操作,将所有的虚拟内存解锁
munmap Unmap files or devices from memory. 
删除指定虚拟内存区域里的文件或设备的映射

    常见的和设备访问相关的系统调用如下表所示:

System Call 
系统调用
Description 
描述
access check real user's permissions for a file. 
判断用户是否具有对某文件的读、写、执行等的操作权限
chmod change permissions of a file. 
修改某文件的读、写、执行之类的操作权限
chown change ownership of a file. 
修改文件的owner(拥有者)和group(组)之类的信息
close close a file descriptor. 
根据文件描述符来关闭某文件
dup duplicate a file descriptor. 
创建一个文件描述符的拷贝
fcntl manipulate file descriptor. 
对文件描述符进行一些拷贝、读取设置文件描述符的flags(标志)之类的操作
fstat get file status. 
获取文件的状态信息,例如文件或设备的ID、uid(用户ID)、gid(组ID)等信息
ioctl control device. 
通过一些控制指令对设备进行相关的管理
link Assign a new name to a file descriptor. 
给文件创建一个hard link(硬链接),从而为文件赋予一个新的文件名
lseek reposition read/write file offset. 
将打开的文件里的指针游标重定位到一个指定的位置
mknod creates a file system node. 
为文件、设备或命名管道创建一个文件系统节点
open Open/create a file descriptor for a device or file. 
打开某个设备或文件,并返回可用于操作的文件描述符
read read from a file descriptor. 
根据文件描述符,将某文件里的数据读取到缓冲区域
write write to a file descriptor. 
根据文件描述符,向某文件写入数据

    由于Linux里的设备被组织成文件的形式,所以上面的系统调用里,对文件的操作,如access等,同样适用于设备。

    常见的和文件系统相关的系统调用如下表所示:

System Call 
系统调用
Description 
描述
chdir change working directory. 
修改调用进程的当前工作目录
chroot change root directory. 
修改调用进程的根目录
flock apply or remove an advisory lock on an open file. 
在已打开的文件上设置或删除建议性锁,可以设置共享锁,也可以设置独占锁(使用man 2 flock命令查看详情)
statfs get file system statistics. 
获取某个已挂载的文件系统的信息,例如文件系统的类型、文件系统的数据块统计信息等
getcwd Get the current working directory. 
获取调用进程的当前工作目录信息
mkdir Create a directory. 
根据指定的路径信息创建目录
rmdir Remove a directory. 
删除某个目录
symlink Make a new name for a file. 
为某个文件创建符号链接
umask Set the file creation mask. 
设置文件创建时的权限掩码,在创建文件时需要用到umask(权限掩码),表示文件在创建时需要去掉哪些存取权限
mount Mount and unmount file systems. 
挂载或卸载某个文件系统
swapon start swapping to file/device. 
将swap(交换区域)设置到指定的文件或块设备,这样当物理内存不足时,内存页面就可以交换到指定的文件或块设备里
swapoff stop swapping to file/device. 
将指定的文件或块设备从swap(交换区域)移除

    最后,常见的和进程管理相关的系统调用如下表所示:

System Call 
系统调用
Description 
描述
acct Switch process accounting on or off. 
禁止或启用系统记录进程信息
capget Get process capabilities. 
获取进程的权能
capset Set process capabilities. 
设置进程的权能
clone Create a child process. 
创建一个子进程,和下面的fork类似
execve Execute program. 
执行某个程序
exit Terminate the current process. 
终止当前的进程
fork Create a child process. 
创建一个子进程
getgid Get the group identity. 
获取调用进程的组ID
getpgrp Get the process group. 
获取进程的组
setpgrp Set the process group. 
设置进程的组
getpid Get process ID of the calling process. 
获取调用进程的pid(进程标识符)
getppid Get process ID of the parent of the calling process. 
获取调用进程的父进程的pid(进程标识符)
getpriority Get program scheduling priority 
获取进程的调度优先级
setpriority Set program scheduling priority
设置进程的调度优先级
getuid Get the user identity. 
获取进程的用户ID
kill Send signal to any process group or process 
用于向任何进程组或进程发送信号
nice Change the process priority. 
修改进程的优先级
vfork Create a child process and block the parent. 
创建并运行一个子进程,同时父进程会被挂起直到子进程结束

Using System Calls 使用系统调用:

    在汇编里使用系统调用可能会有点复杂,下面就通过例子来说明如何在汇编程序中使用系统调用。

The system call format 系统调用的格式:

    典型的使用exit系统调用的汇编代码如下:

movl $1, %eax
int $0x80

    上面的代码里使用int $0x80软件中断来进入内核的系统调用部分,在linux系统的0x80的内核中断处理程式里,会根据EAX里的值来判断具体是哪个系统调用,在之前的unistd.h头文件输出的宏定义里,将__NR_exit宏定义为1,说明exit的系统调用号为1,所以上面代码就将立即数1赋值给EAX寄存器,这样int $0x80执行时就会转去执行exit系统调用。

System call input values 系统调用的输入参数:

    在C语言风格的函数里,输入参数是放置在内存栈里的,但是系统调用的输入参数需要放置在寄存器里,上面已经提到,EAX寄存器用于存放系统调用号,输入参数则可以放置在如下5个寄存器里:
  • EBX (first parameter)
    EBX用于存放第一个参数
  • ECX (second parameter)
    ECX用于存放第二个参数
  • EDX (third parameter)
    EDX用于存放第三个参数
  • ESI (fourth parameter)
    ESI用于存放第四个参数
  • EDI (fifth parameter)
    EDI用于存放第五个参数
    如果将参数放置在错误的寄存器里就会产生错误的结果,另外,不要使用EIP,EBP以及ESP寄存器,因为使用这些寄存器会对程序的执行产生不良的影响。

    如果系统调用所需的输入参数超过5个时,可以将输入参数按顺序存储到某个内存位置,然后将该内存位置的指针值存储到EBX寄存器,这样系统调用就会从EBX指向的内存位置里读取到所需的输入参数了。

    要了解某个系统调用有哪些参数,就需要通过上面提到的man命令来查看帮助文档,例如,write系统调用是将数据写入到某个文件或设备,使用man 2 write命令输出的帮助信息如下:

NAME
       write - write to a file descriptor

SYNOPSIS
       #include <unistd.h>

       ssize_t write(int fd, const void *buf, size_t count);

    从SYNOPSIS用法里可以看到,write需要三个输入参数,第一个参数fd表示需要写入的文件或设备的文件描述符,第二个buf参数用于指向需要写入的数据的指针位置,第三个参数count表示需要写入数据的字节数,所以write的作用就是:将buf指向的内存位置里的count个字节的数据写入到fd对应的文件里。

    因为有三个参数,所以这些参数与寄存器之间的关系如下:
  • EBX: The integer file descriptor
    EBX存储整数类型的文件描述符
  • ECX: The pointer (memory address) of the string to display
    ECX存储需要写入的数据的内存位置
  • EDX: The size of the string to display
    EDX存储写入数据的字节数
    下面的syscalltest1.s汇编程式就演示了write系统调用的用法:

# syscalltest1.s - An example of passing input values to a system call
.section .data
output:
    .ascii "This is a test message.\n"
output_end:
    .equ len, output_end - output
.section .text
.globl _start
_start:
    movl $4, %eax
    movl $1, %ebx
    movl $output, %ecx
    movl $len, %edx
    int $0x80
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码里,将write的系统调用号“4”设置到EAX寄存器,然后将文件描述符“1”赋值给EBX,在Linux系统里包含如下三个特殊的文件描述符:
  • 0 (STDIN): The standard input for the terminal device (normally the keyboard)
    0 (标准输入):标准的终端输入设备(通常是键盘)
  • 1 (STDOUT): The standard output for the terminal device (normally the terminal screen)
    1 (标准输出):标准的终端输出设备(通常是显示屏幕)
  • 2 (STDERR): The standard error output for the terminal device (normally the terminal screen)
    2 (标准错误输出):出错信息的标准输出设备(通常是显示屏幕)
    接着代码将$output即output标签的内存位置设置到ECX,表示需要写入的数据为output所指向的字符串,最后将$len即字符串的长度设置到EDX,len常量是通过.equ伪指令定义的,这里没有使用硬编码的方式,而是通过output_end - output即字符串的结束位置减去起始位置来动态的获取到字符串的长度值。

    在设置好系统调用号和所需的输入参数后,通过int $0x80软件中断执行write操作,从而将output标签里的字符串数据给输出显示到1号文件描述符所表示的显示屏幕上,最后再通过1号系统调用(exit)来退出程序。

    在汇编链接后,程序的执行结果如下:

$ as -gstabs -o syscalltest1.o syscalltest1.s
$ ld -o syscalltest1 syscalltest1.o
$ ./syscalltest1

This is a test message.
$

System call return value 系统调用的返回值:

    系统调用执行完时,会将返回值设置到EAX寄存器,不过需要注意返回值的类型,例如上面的write系统调用的返回值是ssize_t类型,该类型是C语言里使用typedef定义的有符号的整数类型,write的返回值表示成功写入的字节数,当返回值为-1时表示write操作发生了错误,返回值的含义也可以在man pages帮助手册里找到。

    下面就通过syscalltest2.s程式来演示如何利用系统调用的返回值,来获取一些进程信息:

# syscalltest2.s - An example of getting a return value from a system call
.section .bss
    .lcomm pid, 4
    .lcomm uid, 4
    .lcomm gid, 4
.section .text
.globl _start
_start:
    nop
    movl $20, %eax
    int $0x80
    movl %eax, pid
    movl $24, %eax
    int $0x80
    movl %eax, uid
    movl $47, %eax
    int $0x80
    movl %eax, gid
end:
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面代码里将EAX系统调用号依次设置为20,24及47,这几个系统调用的含义如下:

System Call Value 
系统调用号
System Call 
系统调用
Description 
描述
20 getpid Retrieves the process ID of the 
running program 
获取当前进程的进程ID(进程标识符)
24 getuid Retrieves the user ID of the person 
running the program 
获取当前进程的用户ID
47 getgid Retrieves the group ID of the person 
running the program 
获取当前进程的组ID

    在执行完每个系统调用后,再将EAX里的返回值依次保存到pid,uid及gid对应的内存位置。

    在汇编链接程序后,可以在gdb调试器里,通过x命令来查看pid,uid及gid里保存的值:

$ as -gstabs -o syscalltest2.o syscalltest2.s
$ ld -o syscalltest2 syscalltest2.o
$ gdb -q syscalltest2

Reading symbols from /home/zengl/Downloads/asm_example/syscall/syscalltest2...done.
(gdb) break *end
Breakpoint 1 at 0x8048099: file syscalltest2.s, line 20.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/syscall/syscalltest2

Breakpoint 1, end () at syscalltest2.s:20
20        movl $1, %eax

(gdb) x/d &pid
0x80490a8 <pid>:    2871
(gdb) x/d &uid
0x80490ac <uid>:    1000
(gdb) x/d &gid
0x80490b0 <gid>:    1000
(gdb) c
Continuing.
[Inferior 1 (process 2871) exited normally]

(gdb) q

    从上面的输出可以看到,在程序执行到end标签时,pid里存储的2871是20号系统调用(getpid)返回的进程ID,uid里存储的1000是getuid返回的用户ID,gid里存储的1000是getgid返回的组ID 。

    在命令行下,可以通过id命令来验证上面输出的uid和gid:

$ id
uid=1000(zengl) gid=1000(zengl) groups=1000(zengl),4(adm),24(cdrom),27(sudo),29(audio),30(dip),46(plugdev),109(lpadmin),123(sambashare)
$

    可以看到id命令输出的结果中uid和gid的值与gdb调试器里显示的结果一样。

    以上就是汇编里使用系统调用的基本用法,下一篇介绍和系统调用相关的更多用法。

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

下一篇: 汇编里使用Linux系统调用 (三) 系统调用结束篇

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

相关文章

高级数学运算 (一) FPU寄存器介绍

使用内联汇编 (一)

汇编字符串操作 (二) 字符串操作结束篇

汇编开发示例 (二) 示例介绍结束篇

Moving Data 汇编数据移动 (四) 结束篇 栈操作

使用内联汇编 (三) 内联汇编结束篇