1. 程式人生 > >Linux下直接讀寫物理地址內存

Linux下直接讀寫物理地址內存

ann erro 分頁 .... gpio 細心 key 單位 開發

虛擬 轉 物理地址 virt_to_phys( *addr );
物理 轉 虛擬地址 phys_to_virt( *addr );

如:

    unsigned long pProtectVA;
    phys_addr_t ProtectPA;

    gM4U_ProtectVA = pProtectVA;
    ProtectPA = virt_to_phys((void *)pProtectVA);


-------------------------------------------------
Linux內核裏提供的/dev/mem驅動,為我們讀寫內存物理地址,提供了一個渠道。下面講述2種利用mem設備文件進行物理地址讀寫的方法,一種是設備驅動的方法,另一種是系統調用的方法。

首先我們看下mem這個設備文件,/dev/mem是linux下的一個字符設備,源文件是~/drivers/char/mem.c,這個設備文件是專門用來讀寫物理地址用的。裏面的內容是所有物理內存的地址以及內容信息。通常只有root用戶對其有讀寫權限。

1.設備驅動的方法

下面是mem.c文件裏定義的file_operations結構,提供了llseek,read,write,mmap以及open等方法。

static struct file_operations mem_fops =
{
.llseek = memory_lseek,
.read = read_mem,
.write = write_mem,
.mmap = mmap_mem,
.open = open_mem,
};

因此我們可以通過一般驅動的使用方法,將內存完全當作一個設備來對對待。應用程序如下:

#include <stdio.h>
#include <fcntl.h>
int main(void)
{
int fd;
char *rdbuf;
char *wrbuf = "butterfly";
int i;
fd = open("/dev/mem",O_RDWR);
if(fd < 0)
{
printf("open /dev/mem failed.");
}
read(fd,rdbuf,10);

for(i = 0;i < 10;i++)
{
printf("old mem[%d]:%c\n",i,*(rdbuf + i));
}
lseek(fd,5,0);
write(fd,wrbuf,10);
lseek(fd,0,0);//move f_ops to the front
read(fd,rdbuf,10);
for(i = 0;i < 10;i++)
{
printf("new mem[%d]:%c\n",i,*(rdbuf + i));
}

return 0;
}

執行結果如下:將內存最開始10個字節的內容進行替換。

[[email protected] app]# ./memtest
old mem[0]:b
old mem[1]:u
old mem[2]:t
old mem[3]:t
old mem[4]:e
old mem[5]:r
old mem[6]:f
old mem[7]:l
old mem[8]:y
old mem[9]:!
new mem[0]:b
new mem[1]:u
new mem[2]:t
new mem[3]:t
new mem[4]:e
new mem[5]:b
new mem[6]:u
new mem[7]:t
new mem[8]:t
new mem[9]:e

2.系統調用的方法

細心的你可能會發現,既然你前面說了這個文件裏存放的就是內存的地址及內容信息,那我可不可以直接查看到呢,答案是:可以的。linux內核的開發者為我 們提供了一個命令hexedit,通過它就可以將/dev/mem的內容顯示出來(如果你使用cat /dev/mem將會看到亂碼),執行hexedit /dev/mem的結果如下:

00000000 62 75 74 74 65 62 75 74 74 65 72 66 6C 79 21 20 butterfly!
00000010 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
00000020 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20 20
00000030 6F EF 00 F0 6F EF 00 F0 57 EF 00 F0 6F EF 00 F0 o...o...W...o...
00000040 02 11 00 C0 4D F8 00 F0 41 F8 00 F0 34 85 00 F0 ....M...A...4...
00000050 39 E7 00 F0 59 F8 00 F0 2E E8 00 F0 D2 EF 00 F0 9...Y...........
00000060 A4 E7 00 F0 F2 E6 00 F0 6E FE 00 F0 53 FF 00 F0 ........n...S...
00000070 53 FF 00 F0 A4 F0 00 F0 C7 EF 00 F0 1C 42 00 C0 S............B..

從上圖可見,最左邊顯示的是地址,接下來24列顯示的是各內存字節單元內容的ASCII碼信息,最右邊顯示的是對應的字符信息。讓人欣慰的是,這個文件可 以直接修改,按下tab鍵進入修改模式,修改過程中修改內容會以粗體顯示,按下F2保存後粗體消失。上面的butterfly就是通過這種方式修改的。

既然內存的地址以及內容信息全部被保存在mem這個設備文件裏,那麽我們可以想到通過另外一種方式來實現對物理地址的讀寫了。那就是將mem設備文件和 mmap系統調用結合起來使用,將文件裏的物理內存地址映射到進程的地址空間,從而實現對內存物理地址的讀寫。下面談一下mmap系統調用。

mmap的函數原型為:void *mmap(void *start,size_t length,int prot,int flags,int fd,off_t offset),該函數定義在/usr/include/sys/mman.h中,使用時要包含:#include<sys /mman.h>,mmap()用來將某個文件中的內容映射到進程的地址空間,對該空間的存取即是對該文件內容的讀寫。參數說明如下:

start:指向欲映射到的地址空間的起始地址,通常設為null或者0.表示讓系統融自動選定地址,映射成功後該地址會返回。

length:表示映射的文件內容的大小,以字節為單位。

prot:表示映射區域的保護方式,有如下四種組合:
--PROT_EXEC 映射區域可執行 ,
--PROT_READ 映射區域可讀 ,
--PROT_WRITE 映射區域可寫,
--PROT_NONE 映射區域不能被訪問

flags:映射區域的一些特性,主要有:
--MAP_FIXED 如果映射不成功則出錯返回,
--MAP_SHARED 對映射區域的寫入數據會寫回到原來的文件
--MAP_PRIVATE 對映射區域的寫入數據不會寫回原來的文件
--MAP_ANONYMOUS
--MAP_DENYWRITE 只允許對映射區域的寫入操作,其他對文件直接寫入的操作將被拒絕
--MAP_LOCKED 鎖定映射區域

在調用mmap()時,必須要指定MAP_SHARED或MAP_PRIVATE。

fd:open()返回的文件描述符。
offset:為被映射文件的偏移量,表示從文件的哪個地方開始映射,一般設置為0,表示從文件的最開始位置開始映射。offset必須是分頁大小(4096字節)的整數倍。

應用程序如下:

#include <stdio.h>
#include <fcntl.h>
#include <sys/mman.h>//mmap head file
int main (void)
{
int i;
int fd;
char *start;
char *buf = "butterfly!";

//open /dev/mem with read and write mode
fd = open ("/dev/mem", O_RDWR);
if (fd < 0)
{
printf("cannot open /dev/mem.");
return -1;
}

//map physical memory 0-10 bytes
start = (char *)mmap(0, 10, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if(start < 0)
{
printf("mmap failed.");
return -1;
}
//Read old value
for (i = 0; i < 10; i++)
{
printf("old mem[%d]:%c\n", i, *(start + i));
}
//write memory
memcpy(start, buf, 10);
//Read new value
for (i = 0;i < 10;i++)
{
printf("new mem[%d]:%c\n", i,*(start + i));
}
munmap(start, 10); //destroy map memory
close(fd); //close file
return 0;
}

程序執行結果如下:

[[email protected] app]# ./rwphy
old mem[0]:b
old mem[1]:u
old mem[2]:t
old mem[3]:t
old mem[4]:e
old mem[5]:b
old mem[6]:u
old mem[7]:t
old mem[8]:t
old mem[9]:e
new mem[0]:b
new mem[1]:u
new mem[2]:t
new mem[3]:t
new mem[4]:e
new mem[5]:r
new mem[6]:f
new mem[7]:l
new mem[8]:y
new mem[9]:!

“/dev/mem是個很好玩的東西,你竟然可以直接訪問物理內存。這在Linux下簡直是太神奇了,這種感覺象一個小偷打算偷一個銀行,可是這個銀行戒備森嚴,正當這個小偷苦無對策時,突然發現在一個不起眼的地方有個後門,這個後門可以直接到銀行的金庫。”

Linux下/dev/mem和/dev/kmem的區別:
/dev/mem: 物理內存的全鏡像。可以用來訪問物理內存。
/dev/kmem: kernel看到的虛擬內存的全鏡像。可以用來訪問kernel的內容。
作用:
/dev/mem用來訪問物理IO設備,比如X用來訪問顯卡的物理內存,或嵌入式中訪問GPIO。用法一般就是open,然後mmap,接著可以使用map之後的地址來訪問物理內存。這 其實就是實現用戶空間驅動的一種方法。
/dev/kmem後者一般可以用來查看kernel的變量,或者用作rootkit之類的。參考1和2描述了用來查看kernel變量這個問題。



/dev/mem用法舉例:
比如驅動(內核空間)中
vir_addr = kmalloc( size, GFP_KERNEL | GFP_DMA )
phy_addr = __pa( vir_addr );
申請了一段內存,然後得到__pa()得到它的物理地址,然後將該物理地址傳到用戶空間
然後在應用程序(用戶空間)中
int map_fd = open("/dev/mem", O_RDWR);
map_addr = mmap(0, size, PROT_READ|PROT_WRITE, MAP_SHARED, map_fd, phy_addr);

這樣就可以得到 在應用程序中 訪問驅動程序中申請的內存的指針map_addr,可以實現應用程序和驅動程序訪問同段內存,節省開銷,實現內存共享

這是一種方法,把內核空間的內存映射到用戶空間,內核空間內存-->物理地址(PA)-->用戶空間 通過/dev/mem 和系統調用mmap

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>

#include <sys/types.h>
#include <sys/stat.h>
#include <sys/poll.h>
#include <sys/mman.h>

int page_size;
#define PAGE_SIZE page_size
#define PAGE_MASK (~(PAGE_SIZE-1))

void get_var (unsigned long addr) {
	off_t ptr = addr & ~(PAGE_MASK);
	off_t offset = addr & PAGE_MASK;
	int i = 0;
	char *map;
	static int kfd = -1;

	kfd = open("/dev/kmem",O_RDONLY);
	if (kfd < 0) {
		perror("open");
		exit(0);
	}

	map = mmap(NULL,PAGE_SIZE,PROT_READ,MAP_SHARED,kfd,offset);
	if (map == MAP_FAILED) {
		perror("mmap");
		exit(-1);
	}
	printf("%s\n",map+ptr);

	return;
}

int main(int argc, char **argv)
{
	FILE *fp;
	char addr_str[11]="0x";
	char var[51];
	unsigned long addr;
	char ch;
	int r;
	
	if (argc != 2) {
		fprintf(stderr,"usage: %s System.map\n",argv[0]);
		exit(-1);
	}

	if ((fp = fopen(argv[1],"r")) == NULL) {
		perror("fopen");
		exit(-1);
	}

	do {
		r = fscanf(fp,"%8s %c %50s\n",&addr_str[2],&ch,var);
		if (strcmp(var,"modprobe_path")==0)
			break;
	} while(r > 0);
	if (r < 0) {
		printf("could not find modprobe_path\n");
		exit(-1);
	}
	page_size = getpagesize();
	addr = strtoul(addr_str,NULL,16);
	printf("found modprobe_path at (%s) %08lx\n",addr_str,addr);
	get_var(addr);
}
[cpp] view plain copy 技術分享技術分享
  1. 外國項目中的一段代碼:
  2. if((fdMem = open("/dev/mem", O_RDWR | O_SYNC)) == -1)
  3. {
  4. fprintf(stderr, "Unable to open the /dev/mem interface !\n");
  5. return;
  6. }
  7. map_base = mmap(0, MAP_SIZE, PROT_READ | PROT_WRITE, MAP_SHARED, fdMem, GPIO_ADDR & ~MAP_MASK);
  8. if (map_base == (void *) -1)
  9. {
  10. fprintf(stderr, "Unable to map 0x%08x address\n", GPIO_ADDR);
  11. close (fdMem);
  12. return;
  13. }
  14. // Add offset for init (VD0) => PORTC8
  15. virt_addr = map_base + 0x20;
  16. // Configure GPIO
  17. read_result = *((unsigned long *) virt_addr);
  18. read_result &= (~(0x03 << 16));
  19. read_result |= 0x01 << 16;
  20. *((unsigned long *) virt_addr) = read_result;
  21. // Add offset for VD0
  22. virt_addr = map_base + 0x24;

Linux下直接讀寫物理地址內存