0. 前言
最近需要在嵌入式系统上调试驱动程序,需要在用户态下频繁读取ARM的寄存器的值。
为了方便测试,发现可以在用户态下,通过mmap函数将设备节点/dev/mem进行映射,实现在用户态下将物理地址映射到虚拟地址,并通过对虚拟地址的修改来实现寄存器的修改。
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函数原型:
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记录。
注:获取系统页面大小的方式:
#include <unistd.h>
long page_size = sysconf(_SC_PAGESIZE);
1.2.2 munmap函数
munmap函数是mmap函数的反过程,取消对某地址的映射。
函数原型:
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则说明打开成功:
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:
//↓获取页面大小
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函数的参数:
- start = NULL = 0,由系统决定映射区起始地址
- length = page_size = sysconf(_SC_PAGESIZE),即一整个内存页大小。由于这个参数是按照内存页大小向上取整,如果这个参数小于一个页面也按照一个页面处理。
- prot = PROT_READ | PROT_WRITE,页面内存可读可写
- flags = MAP_SHARED,与其它所有映射这个对象的进程共享映射空间。对共享区的写入,相当于输出到文件。
- fd = dev_fd:文件描述符
- offset = addr,这里的offset是要写入的物理地址。这个地址需要按照页面大小对齐,否则会出错。如果要操作的地址不能被页面大小整除,则可将该地址所在的页进行mmap,之后根据偏移值对指定地址进行修改。
例如,要对0x01C4001C这个地址进行操作:
首先:我们根据页面大小算出该地址的页面首地址为0x01C40000,偏移量为0x0000001C
接下来对0x01C40000这个物理地址进行mmap得到虚拟地址map_base
就可以通过map_base+0x1C的方式对该物理地址进行访问。
2.1.3 对地址进行操作
获取到映射的虚拟地址后,就可以很方便的进行读写操作了。
由于我们操作的平台是32位,于是下面的说明都按照无符号整型进行操作。
以无符号整型方式读取偏移量为offset的地址的值:
printf("Before Modify : 0x%08X\r\n",*(volatile unsigned int *)(map_base+offset));
以无符号整型方式将data写入偏移量为offset的地址的值:
*(volatile unsigned int *)(map_base + offset) = data;
2.1.4 反映射操作
操作结束后需要munmap反映射并关闭设备文件:
munmap(map_base,page_size); //反映射,这里的map_base是前面获取的映射地址,page_size是映射的大小
close(fd); //关闭/dev/mem文件
2.2 完整代码
整个程序的完整代码如下。
本程序可以实现对某块物理地址的读取和写入,方便嵌入式在用户态调试寄存器。
#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放入板子中。
显示使用方法:
[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字节的数据:
[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的:
[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)
[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