linux设备驱动程序(1) – helloworld

经过这几天痛苦debug,ztun终于是勉强能用了。(bug应该还不少,但不想改了呀wwww

然后,昨天看完了《linux系统编程》今天开始正式学习《linux设备驱动程序》,自然先从helloworld开始。

对于驱动开发,首先你得有内核源代码树。如果你是ubuntu之类的发行版,一般软件仓库里会有linux-kernel-header包,下载一个适合自己的版本就可以了,它会安装在/scr中,但一般使用/lib/module/uname -r/build这个路径(这是个连接,连接到/src中)

如果你是使用开发板做嵌入式,那么就麻烦一些,因为没有人给你做好内核树让你用,你必须自己从kernel.org或者你的零售商那里获取合适版本的内核源代码。然后cd到源代码目录执行以下操作:

1
2
3
make oldconfig
make prepare
make scripts #不一定需要, 我这里就不用

如果是交叉编译还应该在make前加上ARCH=XXX CROSS_COMPILE=XXX,比如:

1
ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- make

好的,这样准备工作就完成了,可以写helloworld了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/* file: hello.c */
#include "linux/init.h"
#include "linux/module.h"

MODULE_LICENSE("Dual BSD/GPL");

static int hello_init(void)
{
printk("Hello world\n");
return 0;
}

static void hello_exit(void)
{
printk("Goodbye\n");
}

module_init(hello_init);
module_exit(hello_exit);

而一向让人头疼的Makefile却很好写:

1
obj-m := hello.o

什么?你TM在逗我?只有一行?对,只有一行,但是命令行需要麻烦点:

1
make -C /lib/module/`uname -r`/build M=`pwd` modules

obj-m的意思是需要编译的模块,因为使用:=所以表示覆盖前面的值,所以只会编译hello.c而不会编译内核中的其他模块。命令行中/lib/module/uname -r/build是内核源代码树的路径,你可以改成你自己的。-C指令表示make执行时先chdir到内核源代码树,调用那里的Makefile,M=pwd代表模块代码在当前目录。

执行成功后会在当前目录下生成一个hello.ko文件,这就是刚编译好的内核模块了。执行

1
2
3
4
sudo insmod ./hello.ko
dmesg
sudo remod ./hello.ko
dmesg

可以在两次dmesg中看到Hello world和Goodbye。

但是每次都打那么长的命令行是不能接受的,特别是交叉编译时命令行更长。这个稍复杂点的Makefile可以解救你:

1
2
3
4
5
6
7
8
9
10
11
12
13
ifneq ($(KERNELRELEASE),)
obj-m := hello.o
else
KERNELDIR ?= /lib/modules/$(shell uname -r)/build
PWD := $(shell pwd)

all:
$(MAKE) -C $(KERNELDIR) M=$(PWD) modules

clean:
$(MAKE) -C $(KERNELDIR) M=$(PWD) clean

endif

只需要在模块目录打一个make就可以编译。

下面分析一下这个Makefile。这个其实有点像递归,执行make时,KERNELRELEASE变量还没有被设置,所以执行else里的。首先吧KERNELDIR设置成内核源代码树路径。PWD变量设置成当前目录。然后执行$(MAKE) -C $(KERNELDIR) M=$(PWD) modules。这句话被翻译成刚刚的那句命令行。于是又执行一次make(递归来了。。。)递归时会先读取内核源代码树里的Makefile,里面设置了KERNELRELEASE变量所以还是刚刚的那句代码obj-m := hello.o。于是,完成编译。

书上没写清楚的东西都说清楚了,希望对那些像我一样在看了书之后搞不出helloworld的人有帮助吧。

基本上找到问题了 Connection refused

对udp套接字执行read时一直报错: connection refused. 关键是udp还会报connection就太不正常了, udp可是无连接的.

udp套接字执行read时一直报错: connection refused. 关键是udp还会报connection就太不正常了, udp可是无连接的.

刚刚查到了这篇博文: UDP怎么会返回Connection refused 原来是因为对端传来了一个ICMP包(传来一个port unreachable), 结果某些内核会把这个包为已连接(执行过connect函数)的udp套接字保存起来, 并在下一次操作时(读取, 写入)返回一个connection refused错误.

但是到底为什么会有一个ICMP包, 刚刚做了些实验, 原因是 一台客户机和stun服务器建立udp连接之后, nat路由器会记录下这条路线[既保存下这个四元组], 如果有非stun服务器的主机向客户机端口发来udp包, 会返回一个ICMP错误. (想想你这路由器真够没事找事呢, 可能是为了安全性考虑?)

现在的想法是, 客户机另开一个端口和另一台客户进行p2p通讯, 不再用和stun通讯的端口, 应该可以解决问题.

具体做法是在向stun服务器登陆时, 报出另一条线路的端口.

网络管理中的ioctl

ioctl函数传统上用于哪些不适合归入其他精细定义类别的特性的系统接口. 虽然POSIX一直在致力于创造特殊函数来取代ioctl函数, 但目前来说大多数网络编程相关的特性还需要用ioctl来实现. 特别是用于网络管理方面的相当之多(如设置ip, 获取接口, 访问路由表, 访问arp).

原型:

1
2
3
#include <sys/ioctl.h>

int ioctl(int d, int request, ... /* void *arg */);

ioctl的具体用法就不说了, 熟悉linux编程的都或多或少用过. 现在具体说下在网络编程下request的可选值和对应arg指向的的数据类型(在网络编程中arg是一个指针, 下面给出的数据类型都是这个指针要指向的类型).

image_1bl0lpcf75ua1ea638dn4f1oo89.png-184kB

总结一下的话就是:

  • 除了文件相关的请求, 其他网络相关的都是SIOC为前缀(貌似是socket io control?).
  • 设置的请求在前缀后面跟一个S, 获取的请求在后面跟一个G.
  • 接口请求在SIOC[G/S]后面都跟一个IF(interface).
  • 接口请求除了SIOCGIFCONF(获取接口列表)的参数是struct ifconf以外, 其他所有的都是struct ifreq.
  • arp请求的参数都是struct arpreq.
  • 路由的请求都是struct rtentry (route entry).

下面重点学习一下接口请求:

接口请求除了上面图上的, 还有很多. 具体的可以去参考ioctl_list(2)中. 文章最下面也有一个表, 有所有的接口相关的request.

一般情况下, 进行网络配置时, 都会先获取所有网络接口列表, 从内核获取接口列表使用SIOCGIFCONF请求完成. 它会用到struct ifconf结构, 而ifconf结构又会用到ifreq结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* --<net/if.h>-- */
struct ifreq
{
# define IFHWADDRLEN 6
# define IFNAMSIZ IF_NAMESIZE
union
{
char ifrn_name[IFNAMSIZ]; /* Interface name, e.g. "en0". */
} ifr_ifrn;

union
{
struct sockaddr ifru_addr;
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short int ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
__caddr_t ifru_data;
} ifr_ifru;
};
# define ifr_name ifr_ifrn.ifrn_name /* interface name */
# define ifr_hwaddr ifr_ifru.ifru_hwaddr /* MAC address */
# define ifr_addr ifr_ifru.ifru_addr /* address */
# define ifr_dstaddr ifr_ifru.ifru_dstaddr /* other end of p-p lnk */
# define ifr_broadaddr ifr_ifru.ifru_broadaddr /* broadcast address */
# define ifr_netmask ifr_ifru.ifru_netmask /* interface net mask */
# define ifr_flags ifr_ifru.ifru_flags /* flags */
# define ifr_metric ifr_ifru.ifru_ivalue /* metric */
# define ifr_mtu ifr_ifru.ifru_mtu /* mtu */
# define ifr_map ifr_ifru.ifru_map /* device map */
# define ifr_slave ifr_ifru.ifru_slave /* slave device */
# define ifr_data ifr_ifru.ifru_data /* for use by interface */
# define ifr_ifindex ifr_ifru.ifru_ivalue /* interface index */
# define ifr_bandwidth ifr_ifru.ifru_ivalue /* link bandwidth */
# define ifr_qlen ifr_ifru.ifru_ivalue /* queue length */
# define ifr_newname ifr_ifru.ifru_newname /* New name */

struct ifconf
{
int ifc_len; /* Size of buffer. */
union
{
__caddr_t ifcu_buf;
struct ifreq *ifcu_req;
} ifc_ifcu;
};
# define ifc_buf ifc_ifcu.ifcu_buf /* Buffer address. */
# define ifc_req ifc_ifcu.ifcu_req /* Array of structures. */

另外给一个《unix网络编程》上的图:

image_1bl0ltenadtdr9f1ks21kiiiuqm.png-118.5kB

看起来很复杂, 实际上分开看就不很复杂.

先看ifconf结构, 其实只有两个成员, 一个是ifc_len, 一个是ifc_ifcu, 这是一个联合所以名字以u结尾, 但是我们不会直接用ifc_ifcu, 直接用的话会很长很麻烦, 而是用下面定义的两个宏, ifc_buf和ifc_req. 当想使用联合的ifcu_buf时, 只需要写ifc.ifc_buf即可, 否则需要写ifc.ifc_ifcu.ifcu_buf. 同理, ifreq也是这样设计的.

为什么要这样设计呢? 因为这个ifreq结构体需要供多个request使用, 当你请求获取ip地址时, 需要存在ifr_addr中, 当你获取子网掩码时需要存在ifr_netmask中, 但是因为需要保存在同一个结构中, 所以用联合比较方便(而且节省空间).

现在继续看ifconf结构, 可以看到, 两个成员成员一ifc_len为缓冲区长度, 成员二ifc_buf(既ifc_req)是一个指针, 当调用ioctl(sock, SIOCGIFCONF, &ifc)之前, 需要把成员二当成ifc_buf, 既把它当成一个缓冲区, 所以我们需要自己申请出缓冲区空间, 并且把缓冲区的大小保存在ifc_len中. 当调用之后, 内核会把所有的接口信息保存在我们刚刚申请的ifc_buf缓冲区中. 此时, 缓冲区中的数据是有意义的了, 所以我们应当把成员二当成ifc_req(既当成一个指向struct ifreq的指针), 它指向一个ifreq结构数组, 保存着所有的接口信息. 同时, ifc_len也被更新, 保存着更改之后ifc_req的大小.

上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define BUF_SIZE 1024
int main()
{
int sock;
struct ifconf ifc;
struct ifreq* pifr;
int n, i;
if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket");
exit(errno);
}

bzero(&ifc, sizeof(ifc));
if ((ifc.ifc_buf = malloc(BUF_SIZE)) == NULL)
{
perror("malloc");
exit(errno);
}
ifc.ifc_len = BUF_SIZE;

if (ioctl(sock, SIOCGIFCONF, &ifc) == -1)
{
perror("ioctl");
exit(errno);
}
n = ifc.ifc_len / sizeof(struct ifreq);
printf("%d interfaces on your computer\n", n);
for (i = 0, pifr = ifc.ifc_req; i < n; ++i, ++pifr)
printf("\tinterface %d : %s\n", i, pifr->ifr_name);
free(ifc.ifc_buf);
return 0;
}

不用解释了吧, 先申请ifc.ifc_buf的空间, 调用ioctl之后, ifc.ifc_buf被填充, 然后把它当做ifc_req来读取.

看图会更清晰:

ioctl前: image_1bl0lvsc41v0a13lv6un1pt810eo13.png-21.1kB

ioctl后: image_1bl0m0e19ragkr17lcv8dt9s1g.png-90.1kB

剩下的使用ifreq的用法就简单了, 直接上代码吧:

这个是获取接口ip地址的函数, 可以和上面那个配合使用(在循环中输出接口ip)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <arpa/inet.h>

char* getifaddr(int sock, const char* ifname, char* addr, size_t n)
{
struct ifreq ifr;
struct sockaddr_in *paddr;

bzero(&ifr, sizeof(ifr));
strncpy(ifr.ifr_name, ifname, IFNAMSIZ);
if (ioctl(sock, SIOCGIFADDR, &ifr) == -1)
{
perror("ioctl");
return NULL;
}
paddr = (struct sockaddr_in*)&ifr.ifr_addr;
if (inet_ntop(AF_INET, &paddr->sin_addr, addr, n) == NULL)
{
perror("inet_ntop");
return NULL;
}

return addr;
}

这个是设置ip的实用程序(需要root)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
/* file: setip.c */
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#define BUF_SIZE 1024

#include <arpa/inet.h>

int main(int argc, char* argv[])
{
int sock;
struct ifreq ifr;
struct sockaddr_in *sin;
if (argc != 3)
{
exit(1);
}

bzero(&ifr, sizeof(ifr));
strncpy(ifr.ifr_name, argv[1], IFNAMSIZ);

sin = (struct sockaddr_in*)&ifr.ifr_addr;
sin->sin_family = AF_INET;
if (inet_pton(AF_INET, argv[2], &sin->sin_addr) == -1)
{
perror("inet_pton");
exit(errno);
}

if ((sock = socket(AF_INET, SOCK_DGRAM, 0)) == -1)
{
perror("socket");
exit(errno);
}

if (ioctl(sock, SIOCSIFADDR, &ifr) == -1)
{
perror("ioctl");
exit(errno);
}

return 0;
}

附:所有接口相关request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
0x00008910  SIOCGIFNAME                 char []
0x00008911 SIOCSIFLINK void
0x00008912 SIOCGIFCONF struct ifconf * // MORE // I-O
0x00008913 SIOCGIFFLAGS struct ifreq * // I-O
0x00008914 SIOCSIFFLAGS const struct ifreq *
0x00008915 SIOCGIFADDR struct ifreq * // I-O
0x00008916 SIOCSIFADDR const struct ifreq *
0x00008917 SIOCGIFDSTADDR struct ifreq * // I-O
0x00008918 SIOCSIFDSTADDR const struct ifreq *
0x00008919 SIOCGIFBRDADDR struct ifreq * // I-O
0x0000891A SIOCSIFBRDADDR const struct ifreq *
0x0000891B SIOCGIFNETMASK struct ifreq * // I-O
0x0000891C SIOCSIFNETMASK const struct ifreq *
0x0000891D SIOCGIFMETRIC struct ifreq * // I-O
0x0000891E SIOCSIFMETRIC const struct ifreq *
0x0000891F SIOCGIFMEM struct ifreq * // I-O
0x00008920 SIOCSIFMEM const struct ifreq *
0x00008921 SIOCGIFMTU struct ifreq * // I-O
0x00008922 SIOCSIFMTU const struct ifreq *
0x00008923 OLD_SIOCGIFHWADDR struct ifreq * // I-O
0x00008924 SIOCSIFHWADDR const struct ifreq * // MORE
0x00008925 SIOCGIFENCAP int *
0x00008926 SIOCSIFENCAP const int *
0x00008927 SIOCGIFHWADDR struct ifreq * // I-O
0x00008929 SIOCGIFSLAVE void
0x00008930 SIOCSIFSLAVE void
0x00008970 SIOCGIFMAP struct ifreq * // I-O
0x00008971 SIOCSIFMAP const struct ifreq *

两条命令查找程序在哪个软件包

两条命令查找程序在哪个软件包

假如想查找ifconfig在那个软件包中.

1
2
3
4
$ whereis ifconfig
ifconfig: /sbin/ifconfig /usr/share/man/man8/ifconfig.8.gz
$ apt-file search /sbin/ifconfig
net-tools: /sbin/ifconfig

有缩进表示程序的输出. 先用whereis命令大致找出想查询的程序在路径的哪里, 再用apt-file search就可查出程序在哪个包中

arm-linux 汇编(3) – 处理器模式 寄存器

上一篇中给出了在arm体系架构中[用户态]的寄存器, 共有16个通用寄存器r0-r15和一个通用程序状态寄存器(cpsr).

上一篇中给出了在arm体系架构中[用户态]的寄存器, 共有16个通用寄存器r0-r15和一个通用程序状态寄存器(cpsr).

1.这次接着说通用程序状态寄存器

先看图:

image_1bl0ld0ga1lkn16khh2rvg1j3a9.png-116.9kB

cpsr分为4个域, 每个域8位, 分别是标志域, 状态域, 扩展域控制域.(图上画的有点错误, 扩展域画大了, mode和I,F,T都是控制域的)

其中标志域表示运算结果的标志. 控制域中, I为1表示屏蔽掉普通中断, F为1表示屏蔽掉快速中断, T为1表示当前为thumb模式.

Mode域表示了当前CPU的处理器模式.

2.处理器模式

不算上最新安全扩展和虚拟化扩展新加上的模式的话, arm架构共有7个处理器模式:

User mode FIQ mode IRQ mode Supervisor (svc) mode
Abort mode Undefined mode System mode

其中User mode为非特权模式以外, 剩下6个都为特权模式.

当快速中断产生时进入FIQ模式.

当中断产生时进入IRQ模式.

当系统reset或swi(又称svc, 软中断)命令执行时进入svc模式.

当访问内存失败时(分为prefetch abort和data abort)进入Abort模式.

当执行的指令未定义时进入Undefined模式.

是否为特权模式决定了那些寄存器是可用的, 以及cpsr本身的访问权限. 对于特权模式, 对cpsr有完全的访问权限(MSR和MRS指令), 对以非特权模式, 只能读控制域但可读写条件标志域.

3.寄存器

arm架构共有37个寄存器但在不同时刻有20个寄存器是隐藏的(图中阴影部分), 只有当寄存器处于某种特定模式时, 才能访问特定的寄存器(如在abort模式才能访问r13_abt), 另外, 在不同的模式访问cpsr都是同一个, 而spsr_mode用来保存进入特权模式之前的cpsr以便保存异常之前的现场.

image_1bl0lfomu104c13dd161112p81p89m.png-178.8kB

Registers across CPU modes
usr sys svc abt und irq fiq
R0
R1
R2
R3
R4
R5
R6
R7
R8 R8_fiq
R9 R9_fiq
R10 R10_fiq
R11 R11_fiq
R12 R12_fiq
R13 R13_svc R13_abt R13_und R13_irq R13_fiq
R14 R14_svc R14_abt R14_und R14_irq R14_fiq
R15
CPSR
SPSR_svc SPSR_abt SPSR_und SPSR_irq SPSR_fiq

从wiki上copy的一个寄存器的表格

其实这些东西对于写用户程序的话都是透明的了, 但是有利于理解arm体系架构和看懂一些系统级的代码.

下面想要学一学eabi相关的东西了除了指令集就是这个最重要了.

setuid和seteuid

linux下有4种uid, 真实uid(real user id), 有效uid(effective user id), 被保存的uid(saved user id)和文件系统的uid. 本文详细讲解一下相关内容。

linux下有4种uid, 真实uid(real user id), 有效uid(effective user id), 被保存的uid(saved user id)和文件系统的uid.

先说下这两个系统调用, 再说这几种uid:

  • setuid(uid)首先请求内核将本进程的[真实uid],[有效uid]和[被保存的uid]设置成函数指定的uid, 若权限不够则请求将effective uid设置成uid, 再不行则调用失败.
  • seteuid(uid)仅请求内核将本进程的[有效uid]设置成函数指定的uid.

再具体来说setuid函数的话是这样的规则:

  • 当用户具有超级用户权限的时候,setuid 函数设置的id对三者都起效.【规则一】
  • 否则,仅当该id为real user ID 或者saved user ID时,该id对effective user ID起效.【规则二】
  • 否则,setuid函数调用失败.

现在说下前三种uid

real uid表示进程的实际执行者, 只有root才能更改real uid, effective uid用于检测进程在执行时所获得的访问文件的权限(既 但进程访问文件时, 检测effective uid有没有权限访问这个文件), saved uid用于保存effective uid, 以便当effective uid设置成其他id时可以再设置回来(下面着重讲).

一般情况下, 当一个程序执行时(既调用exec), 进程的effective uid会和real uid一致, 但是可执行文件有一个set-user-ID位, 如果这个set-user-ID位被设置了, 那么执行exec后, 进程的effective uid会设置成可执行文件的属主uid, 同时saved uid也会被设置成effective uid.

举例说明: 用户zzz执行了文件a.out, a.out的属主为hzzz且设置了set-user-ID位. 现在本进程的real uid为zzz, effective uid = saved uid = hzzz.

进程执行了一会之后, 突然想用zzz的权限访问一个文件, 于是进程可能会调用setuid(zzz), 此时检测进程的权限, 进程的effective uid是hzzz, 不是root, 所以不能更改real uid(只有root才能更改real uid[setuid规则一不满足]), 所以只能设置effective uid, 发现effective uid可以被设置为zzz(因为real uid是zzz[规则二满足]), 所以函数调用成功, 只将effective uid设置成zzz.

现在进程访问完zzz的文件了, 又想回到hzzz的环境中执行, 所以有可能会调用setuid(hzzz), 这次saved uid的作用就表现出来了, 因为刚刚只是改变了effective uid, 而saved uid还保存着之前的effective uid, 所以可以调用setuid(hzzz)来要回原来的权限([规则二满足]).

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>

int main()
{
printf("real uid is %d\n", getuid());
printf("effective uid is %d\n", geteuid());
getchar();

if (seteuid(1001) == -1) /* zzz = 1001 */
{
perror("seteuid");
return -1;
}

printf("real uid is %d\n", getuid());
printf("effective uid is %d\n", geteuid());
getchar();

if (seteuid(0) == -1) /* root = 1001 */
{
perror("seteuid");
return -1;
}

printf("real uid is %d\n", getuid());
printf("effective uid is %d\n", geteuid());
getchar();
}

然后编译代码, 并把可执行文件的属主改为root, 然后添加上set-user-ID位:

1
2
3
gcc uid.c -o uid
sudo chown root:root uid
sudo chmod u+s uid

执行uid之后会打印出如下:

real uid is 1001
effective uid is 0
real uid is 1001
effective uid is 1001
real uid is 1001
effective uid is 0

可以看出, 我们先把effective uid改成了zzz. 之后又可以改回root.这就是saved uid的作用.
另外这个程序中使用的是seteuid而不是setuid, 这是因为如果改成setuid的话, 执行第一个setuid时, 因为当前effective uid为root, 第一个规则就满足, 所以把real uid, effective uid和saved uid都改成hzzz了, 因为这样更改了saved uid, 我们就回不去root了.

所以, 从这里也可以看出来setuid和seteuid的区别, 分清什么时候用哪个.

参考:

https://www.cppblog.com/converse/archive/2007/12/20/39166.html
https://blog.csdn.net/buaalei/article/details/5344647
https://www.dutor.net/index.php/2010/08/cmd-chmod-set-user-id-set-group-id/
https://hi.baidu.com/zhujian0805/item/cf54470f0bec70c02f4c6b4e

image_1bl0fnhvhft51tdk8q87491gkj9.png-268.8kB

arm-linux 汇编(2) – 调用c函数

上一篇学习了armlinux汇编的helloworld,用到了arm汇编的一些基本指令,如mov、ldr、swi,需要详细的arm体系架构的信息可以去arm官网下载arm_architecture_reference_manual.pdf文件,上面有很详细的信息。本文接着上次,继续讲一下如何用汇编调用c函数。

另外,如果觉得那个手册太冗长,或者是英文苦手的话,可以在本站下载这个短小精悍的reference card(中文版哦):

https://lengzzz.com/download/QRC0001_UAL.pdf

现在进入正题:

上一篇中,重点讲述了在arm-linux下的系统调用的方法,但是更多时候,我们可能会与一些c库进行交互,今天学习了汇编调用c库函数的方法。

先上例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.section .rodata
fmt:
.asciz "2 * 4 = %d\n" @.asciz以NULL结尾的字符串

.section .text
.global _start

_start:
mov r2, #2
mov r3, #4
mul r1, r2, r3 @乘法指令, r1=r2*r3
ldr r0, =fmt
bl printf @bl为函数调用, 跳转到printf并将当前pc保存到lr寄存器
exit:
mov r0, #1
mov r7, #1
swi #0

例子很简单, 功能就是计算24并输出2 * 4 = 8. 先使用mul指令计算出24的值放在r1寄存器中, 然后将printf的格式串fmt放到r0中, 然后调用printf函数.

要汇编这个程序不能用昨天简单的指令了. 因为, printf是libc库中的函数, 必须要和c库进行连接:

1
2
as mul.s -o mul.o
ld -dynamic-linker /lib/ld-linux-armhf.so.3 mul.o -o mul -lc

相比较上一篇中的连接指令, 多了两条, 一个是-dynamic-linker /lib/ld-linux-armhf.so.3`另一个是-lc

第一个是指令在man pages中的介绍是:

–dynamic-linker=file
Set the name of the dynamic linker. This is only meaningful when
generating dynamically linked ELF executables. The default dynamic
linker is normally correct; don’t use this unless you know what you
are doing.

是用来设置动态链接器的, 在不同的设备上ld-linux-armhf.so.3的名字可能都不一样, 但一般都在lib文件夹中并且以ld开头. 如果没有这条指令的话, 运行程序的时候会提示file not found

第二条是-lc, 和gcc的用法一样, 表示连接libc库. 如果不加这个指令的话连接时会提示undefined “printf”.

现在介绍一下bl指令. bl是branch(分支) link(连接)的缩写(和x86架构的call ret相比意思太含糊了), branch的意思就是跳转(分支)到其他地方执行, link的意思是将当前的pc(Program Counter程序计数器, 保存着当前程序运行到哪里了)寄存器保存到lr(Link Register连接寄存器), 以便调用函数之后函数可以返回回来. 当printf函数运行结束时, 会调用mov pc, lr来返回.

再来说一说arm的寄存器吧. 在用户态, arm的寄存器一共有这几个:

r0 r1 r2 r3 r4 r5 r6 r7
r8 r9 r10 r11 r12 r13(sp) r14(lr) r15(pc)
CPSR

算是比较多的. 其中sp表示堆栈指针(Stack Pointer), lr代表连接寄存器(Link Register), pc表示程序计数器(Program Counter).

具体函数调用时参数传递和返回值用那些寄存器在eabi中都有介绍. 之后将会进行学习.

arm-linux 汇编(1) – Helloworld

linux下一般使用c语言编程,但其实也可以直接使用汇编语言。谈谈在 Linux 下使用汇编编写应用程序。

linux下一般使用c语言编程,但其实也可以直接使用汇编语言。

linux下的汇编工具也是由GNU提供的,叫做as(汇编器)和ld(连接器,c语言也要用它)

在写第一个汇编程序之前我们先看看工具的使用:

假如我们的汇编源码文件叫做hello.s,我们需要先将其汇编成hello.o,再将hello.o连接成hello

1
2
as hello.s -o hello.o
ld hello.o -o hello

简单吧,和上文说的一样,需要两步。

然后让我们先写第一个小程序,功能很简单,就是退出程序,并返回状态码1:

1
2
3
4
5
6
.section .text		@伪指令.section, 说明下面代码在text段
.global _start @伪指令.global或.globl, 向外部暴露出_start符号
_start:
mov r0, #1 @将立即数1存入寄存器r0, 作为_exit系统调用的参数
mov r7, #1 @将系统调用号存入r7
swi #0 @软中断, 陷入内核来调用系统调用

在arm-linux汇编中 . 开头的表示伪指令, @ 表示单行注释. 代码的注释里已经把每一句的功能写的很清楚了, 现在说一些没说清楚的.

在汇编中 _start 是一个程序的开始(貌似x86和arm都是从_start开始执行)所以不能像c语言中写main函数了, 要写_start标号.

然后将我们要调用的系统调用的参数存入寄存器, 因为arm体系架构中, 寄存器的数量相当多, 所以当参数的个数不多时, 一般使用寄存器来传递. 我们要调用的_exit只有一个参数, 就是程序的返回值, 所以我们把1存入r0中.

第二步, 我们必须告知内核我们要调用哪个系统调用, 每个系统调用都有一个系统调用号, 在arm-linux中, 系统调用号保存在/usr/include/arm-linux-gnueabifh/asm/unistd.h中可以看到有很多__NR开头的宏, 后面写着系统调用的名字, 然后就是我们要的系统调用号. 经过寻找, 我们发现_exit调用的调用号是1.所以我们把1存入r7中.

最后一步, swi指令发起软中断(中断号为0), 使程序陷入内核, 然后内核进行系统调用.

这里要说一些arm-linux系统调用的历史, 在cortex之前吧(大概是), arm-linux系统调用的方式一直是这样, swi #(0x900000 + 系统调用号), 既在中断号中传递系统调用号(0x900000是一个magic number), 这种方式现在称作oabi(old ABI), 现在的方式就是在r7中传递系统调用号, 称作eabi. 现在的系统大多都是采用eabi的方式.

好的, 现在汇编, 连接程序, 然后运行. 发现程序结束了, 然后使用echo $?可以查看刚刚结束的程序的返回值, 可以发现返回值为1. 说明程序没有问题.

现在来看第二个例子, helloworld.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.section .data		@data段
hello:
.ascii "hello world\n" @ascii伪指令,以ascii码格式来存储
.equ len, . - hello @equ伪指令,令len=.-hello .代表当前地址
@.-hello就代表hello字符串的长度

.section .text
.global _start

_start:
mov r0, #1 @stdout
ldr r1, =hello @将hello的地址保存在r1
mov r2, #len @将长度保存在r2
mov r7, #4 @系统调用号
swi #0 @发起系统调用

exit:
mov r0, #0
mov r7, #1
swi #0

这个的注释也比较清楚就不说明了, 汇编连接执行后会在屏幕上看见helloworld的大字.

EOF

Proudly powered by Hexo and Theme by Hacker
© 2021 wastecat