本文由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 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系统调用的汇编代码如下:
上面的代码里使用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~~