在Linux用户层使用MMAP对寄存器进行读写

23 | 03 | 2021

0. 前言

最近需要在嵌入式系统上调试驱动程序,需要在用户态下频繁读取ARM的寄存器的值。

为了方便测试,发现可以在用户态下,通过mmap函数将设备节点/dev/mem进行映射,实现在用户态下将物理地址映射到虚拟地址,并通过对虚拟地址的修改来实现寄存器的修改。

本文介绍如何在Linux用户层使用MMAP函数对寄存器进行读写。

1. 原理

1.1 /dev/mem设备节点

简单一点说,/dev/mem是Linux系统下的物理内存的全镜像,可通过该节点实现对物理内存的访问。

一般用于在嵌入式中以用户态形式直接访问寄存器/物理IO设备等。

通常用法是open这个设备节点文件,然后mmap进行内存映射,就可以使用map之后的地址访问物理内存。

1.2 linux下的mmap函数和munmap函数

1.2.1 mmap函数

mmap函数可以将一个文件或者其它对象映射进内存。

这里我们使用mmap函数将设备节点/dev/mem映射到内存中。

在操作结束后,需要使用munmap函数反映射。

函数头文件:<sys/mman.h>

mmap函数原型:

1
void* mmap(void* start,size_t length,int prot,int flags,int fd,off_t offset);

关键参数如下:

  • start:映射区的开始地址,设置为0时表示由系统决定映射区的起始地址。需要按照页面大小对齐,否则会出错。
  • length:映射区的长度。长度单位是以字节为单位,不足一内存页按一内存页处理
  • prot:期望的内存保护标志,类似于可读可写等
  • flags:指定映射对象的类型
  • fd:文件标识符
  • offset:被映射对象内容的起点,需要按照页面大小对齐,否则会出错。

返回值:成功执行时,mmap()返回被映射区的指针,失败时,mmap()返回MAP_FAILED[其值为(void *)-1],错误原因会被errno记录。

注:获取系统页面大小的方式:

1
2
#include <unistd.h>
long page_size = sysconf(_SC_PAGESIZE);

1.2.2 munmap函数

munmap函数是mmap函数的反过程,取消对某地址的映射。

函数原型:

1
int munmap(void* start,size_t length);

参数:

  • start:虚拟内存起始地址
  • length:映射长度

返回值:

成功返回0,失败返回-1。错误原因也会被errno记录。

2. 代码及解析

2.1 使用mmap的关键流程分析

使用mmap函数映射一块物理地址并进行读写操作的基本流程如下:

2.1.1 使用open打开设备文件

首先要打开/dev/mem文件,才能通过mmap对物理地址进行操作。

打开文件的代码如下,如果fd大于0则说明打开成功:

1
2
3
4
5
6
int fd = open("/dev/mem", O_RDWR | O_NDELAY);
if (fd < 0)  
{
    printf("open(/dev/mem) failed.");    
    return 0;
}

2.1.2 使用mmap进行地址的映射

有了/dev/mem文件的文件描述符,就可以对其进行mmap操作了。

具体操作代码如下,将物理地址addr转换成虚拟地址map_base:

1
2
3
4
5
6
//↓获取页面大小
long page_size = sysconf(_SC_PAGESIZE);
//↓将指定物理地址addr映射为虚拟地址,并作为uchar型指针
unsigned char *map_base = (unsigned char * )mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );
//↓指针类型转换为uint型指针
unsigned int *map_base_i =  (unsigned int * )map_base;

这里注意看mmap函数的参数:

  1. start = NULL = 0,由系统决定映射区起始地址
  2. length = page_size = sysconf(_SC_PAGESIZE),即一整个内存页大小。由于这个参数是按照内存页大小向上取整,如果这个参数小于一个页面也按照一个页面处理。
  3. prot = PROT_READ | PROT_WRITE,页面内存可读可写
  4. flags = MAP_SHARED,与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。
  5. fd = dev_fd:文件描述符
  6. offset = addr,这里的offset是要写入的物理地址。这个地址需要按照页面大小对齐,否则会出错。如果要操作的地址不能被页面大小整除,则可将该地址所在的页进行mmap,之后根据偏移值对指定地址进行修改。

例如,要对0x01C4001C这个地址进行操作:
首先:我们根据页面大小算出该地址的页面首地址为0x01C40000,偏移量为0x0000001C
接下来对0x01C40000这个物理地址进行mmap得到虚拟地址map_base
就可以通过map_base+0x1C的方式对该物理地址进行访问。

2.1.3 对地址进行操作

获取到映射的虚拟地址后,就可以很方便的进行读写操作了。

由于我们操作的平台是32位,于是下面的说明都按照无符号整型进行操作。

以无符号整型方式读取偏移量为offset的地址的值:

1
printf("Before Modify : 0x%08X\r\n",*(volatile unsigned int *)(map_base+offset));

以无符号整型方式将data写入偏移量为offset的地址的值:

1
*(volatile unsigned int *)(map_base + offset) = data;

2.1.4 反映射操作

操作结束后需要munmap反映射并关闭设备文件:

1
2
munmap(map_base,page_size); //反映射,这里的map_base是前面获取的映射地址,page_size是映射的大小
close(fd); //关闭/dev/mem文件

2.2 完整代码

整个程序的完整代码如下。

本程序可以实现对某块物理地址的读取和写入,方便嵌入式在用户态调试寄存器。

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <time.h>
#include <unistd.h>
#include <string.h>

#define ARGV_ADDR_POS 2
#define ARGV_DATA_POS 3
#define ARGV_WR_OFF_POS 3
#define ARGV_WR_DATA_POS 4
#define ARGV_CMD_POS 1

//显示使用方法
void printUsage()
{
    printf("Usage: \r\n Read address as Byte: mymm r [ADDR] [LEN] \r\n Read address as int: mymm i [ADDR] [LEN] \r\n");
    printf(" Write to address: mymm w [ADDR] [OFF] [DATA]\r\n");
}

//读取某个基地址addr+byte字节的数据并逐个字节显示。
//从DataSheet上看,页大小为0x400,因此这里的基地址addr必须是能够被0x400整除,否则Segment Error.
void readMMap(int dev_fd,unsigned int addr,unsigned int byte)
{
    int mmapByte = (byte/4*4+4),iloop;
    int realByte = (byte/4*4);
    //这里的mmapByte计算多此一举,因为这里只要进行映射都会按照页面大小向上取整,一般不用担心超出地址的问题
    unsigned char *map_base = (unsigned char * )mmap(NULL, mmapByte, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );  //使用MMAP直接映射基地址的内存数据到map_base
    int i,j;
    if(map_base == (unsigned char *)-1)
    {
        printf("MMAP Error.\r\n");
        return;
    }
    printf("           | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F\r\n");
    printf("------------------------------------------------------------\r\n");
    iloop = realByte/16+((realByte%16)?1:0);
    //printf("iloop is %d\r\n",iloop);
    for(i = 0;i < iloop;i++)
    {
        int loopbyte = (byte-i*16 > 16)?16:(byte-i*16);
        //printf("loopbyte = %d\n",loopbyte);ARGV_WR_OFF_POS
        printf("0x%08X | ",addr+i*16);
        for(j = 0;j < loopbyte;j++)
        {
            printf("%02X ",*(volatile unsigned char *)(map_base+i*16+j));
        }
        printf("\r\n");
    }
    munmap(map_base,mmapByte);
}

//读取某个基地址addr+byte字节的数据并按照逐个int显示。
//从DataSheet上看,页大小为0x400,因此这里的基地址addr必须是能够被0x400整除,否则Segment Error.
void readMMapByUINT(int dev_fd,unsigned int addr,unsigned int byte)
{
    int mmapByte = (byte/4*4+4),iloop;
    int realByte = (byte/4*4);
    unsigned char *map_base = (unsigned char * )mmap(NULL, mmapByte, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );
    unsigned int *map_base_i =  (unsigned int * )map_base;
    int i,j;
    if(map_base == (unsigned char *)-1)
    {
        printf("MMAP Error.\r\n");
        return;
    }
    printf("           | +0x00      +0x04      +0x08      +0x0C         \r\n");
    printf("------------------------------------------------------------\r\n");
    iloop = realByte/16+((realByte%16 > 0)?1:0);
    //printf("iloop = %d\r\n",iloop);
    for(i = 0;i < iloop;i++)
    {
        int loopcnt = (byte-i*4 > 4)?4:byte-i*4;
        //printf("loopbyte = %d\n",loopbyte);
        printf("0x%08X | ",addr+i*16);
        for(j = 0;j < loopcnt;j++)
        {
            printf("0x%08X ",*(volatile unsigned int *)(map_base_i+i*4+j));
        }
        printf("\r\n");
    }
    munmap(map_base,mmapByte);
}

//将数据data写入基地址addr+offset的位置。
void writeMMap(int dev_fd,unsigned int addr,unsigned int offset,unsigned int data)
{
    unsigned int mmapByte = offset + 0x04;
    unsigned char *map_base = (unsigned char * )mmap(NULL, mmapByte, PROT_READ | PROT_WRITE, MAP_SHARED, dev_fd, addr );  
    if(map_base == (unsigned char *)-1)
    {
        printf("MMAP Error.\r\n");
        return;
    }
    printf("Before Modify : addr 0x%08X = 0x%08X\r\n",(addr+offset),*(volatile unsigned int *)(map_base+offset));
    *(volatile unsigned int *)(map_base + offset) = data;
    printf("After Modify : addr 0x%08X = 0x%08X\r\n",(addr+offset),*(volatile unsigned int *)(map_base+offset));
    munmap(map_base,mmapByte);
}

int main(int argc,char* argv[])
{
    int addr = 0,byte = 0,fd = 0,off = 0;
    if(argc < 4)
    {
        printf("ARG ERROR.\r\n");
        printUsage();
        return 0;
    }
    addr = strtoul(argv[ARGV_ADDR_POS],0,0);
    byte = strtoul(argv[ARGV_DATA_POS],0,0);
   
    if(addr == 0)
    {
        printf("Addr Err.\r\n");
        return;
    }
    fd = open("/dev/mem", O_RDWR | O_NDELAY);
    if (fd < 0)  
    {
        printf("open(/dev/mem) failed.");    
        return 0;
    }
    switch(argv[ARGV_CMD_POS][0])
    {
        case 'r':
            if(byte == 0)
            {
                printf("Byte len Err.\r\n");
                close(fd);
                return 0;
            }
            printf("Now Read Memory at 0x%08X by %d\r\n",addr,byte);
            readMMap(fd,addr,byte);
            break;
        case 'i':
            if(byte == 0)
            {
                printf("Byte len Err.\r\n");
                close(fd);
                return 0;
            }
            printf("Now Read Memory By int at 0x%08X by %d\r\n",addr,byte);
            readMMapByUINT(fd,addr,byte);
            break;
        case 'w':
            if(argc < 5)
            {
                printf("Write ARGC Error.");
                close(fd);
                return 0;
            }
            off = strtoul(argv[ARGV_WR_OFF_POS],0,0);
            byte = strtoul(argv[ARGV_WR_DATA_POS],0,0);
            printf("Now Write Memory By int at 0x%08X + 0x%08X by 0x%08X\r\n",addr,off,byte);
            writeMMap(fd,addr,off,byte);
            break;
        default:
            printf("Error Cmd.\r\n");
            break;
    }
    close(fd);
    return 0;
}

2.3 使用方法和示例

交叉编译并将文件命名为mymm放入板子中。

显示使用方法:

1
2
3
4
5
6
[root@xxxxx root]#/mymm                            
ARG ERROR.
Usage:
 Read address as Byte: mymm r [ADDR] [LEN]
 Read address as int: mymm i [ADDR] [LEN]
 Write to address: mymm w [ADDR] [OFF] [DATA]

按照字节读取基地址为0x01C40000的48字节的数据:

1
2
3
4
5
6
7
[root@xxxxx root]#/mymm r 0x01C40000 0x30
Now Read Memory at 0x01C40000 by 48
           | 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
------------------------------------------------------------
0x01C40000 | 00 00 FD 00 45 01 04 00 DA 00 00 00 FF FF 5A B7
0x01C40010 | 55 C5 44 55 10 00 00 00 05 C0 03 00 03 00 00 00
0x01C40020 | 00 00 00 00 00 00 00 00 2F E0 83 8B CF 41 11 08

按照整型读取基地址为0x01C40000的48字节的数据,可以看出这个CPU是little-endian的:

1
2
3
4
5
6
7
[root@xxxxx root]#/mymm i 0x01C40000 0x30
Now Read Memory By int at 0x01C40000 by 48
           | +0x00      +0x04      +0x08      +0x0C        
------------------------------------------------------------
0x01C40000 | 0x00FD0000 0x00040145 0x000000DA 0xB75AFFFF
0x01C40010 | 0x5544C555 0x00000010 0x0003C005 0x00000003
0x01C40020 | 0x00000000 0x00000000 0x8B83E02F 0x081141CF

修改地址0x01C4000C的数据为0xB75AFFFF:(从上面分析可知,这里的基地址是0x01C40000,偏移量是0x0C)

1
2
3
4
[root@xxxxx root]#/mymm w 0x01C40000 0x0C 0xB75AFFFF
Now Write Memory By int at 0x01C40000 + 0x0000000C by 0xB75AFFFF
Before Modify : addr 0x01C4000C = 0xB35AFFFF
After Modify : addr 0x01C4000C = 0xB75AFFFF