cmemorylinux-kernellinux-device-driverembedded-linux

How can I convert virtual address from user space to physical address?(in linux device driver)


I'm testing a simple character device driver and an application using the driver. So I wanted to pass the address of an array and see the array address and the first array element value in the driver as the first step(By the way, the first element is a pointer itself).

app.c

uint64_t __attribute__(( aligned(64) )) args[32]; // set to enough size

printf("args = %p\n", args);
printf("app : args[0] = %p\n", ((uint64_t *)args)[0]);

ioctl(fd, CallSetBareMetalMode, uint64_t)args);

driver.c

static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    switch(cmd) {
         case CallSetBareMetalMode:
            printk("driver:cmd = %x, arg = %p\n", cmd, (uint64_t *)arg);  // line 177
            printk("driver:arg[0] = %llx\n", ((uint64_t *)arg)[0]);

When executed, the print shows like this.

args = 0x442f40
app : args[0] = 443040
[12603.293899] driver:cmd = 40086142, arg = 0000000030dec968
[12603.294509] Unable to handle kernel access to user memory outside uaccess routines at virtual address 0000000000442f40

Q1 : This isn't my main question, but why is the args value printed 0000000030dec968 in the driver?
But if I change the print in line 177,

printk("driver:cmd = %x, arg = %lx\n", cmd, (uint64_t *)arg);  // line 177

for the print, the arg is at least printed correct.

args = 0x442f40
app : args[0] = 443040
[13204.162435] driver:cmd = 40086142, arg = 442f40
[13204.162886] Unable to handle kernel access to user memory outside uaccess routines at virtual address 0000000000442f40

Now, I found I should use copy_from_user function to use address from user space. So I changed line 178 to

copy_from_user(&value, (void __user *)arg, 8);
printk("value (arg[0]) = %llx\n", value);

and the driver prints

[13812.691957] driver:cmd = 40086142, arg = 442f40
[13812.693277] value (arg[0]) = 443040

Ok, the virtual address of the user space was obtained. Now to my real question..
Q2 : I have to change the virtual address from the user space to "physical address" and write this value to register or memory. How can I change this user space virtual address to physical address? (remember I'm in kernel space because I'm in the driver, right?) and if I write this converted physical addresses to the memory locations using copy_to_user function (of course at the user virtual addresses, only the written data is physical addresses), is it going to be the same physical page shared with the original application? Explanation with a simple code will be really appreciated.

ADD

I write 77 to test_val and pass the address of test_val to args[2]. and I pass the address of args to the driver as before.

app.c

uint64_t __attribute__(( aligned(64) )) args[32]; 
uint64_t test_val = 77;
args[2] = (uint64_t) &test_val; // let's see it's changed to 78
printf("app : args[2] = %p, *args[2] = %lld\n", args[2], *(uint64_t *)args[2]);
ioctl(fd, CallSetBareMetalMode, (uint64_t)args);

driver.c

static long my_ioctl(struct file *file, unsigned int cmd, unsigned long arg)
{
    switch(cmd) {
    case CallSetBareMetalMode:
        printk("driver:cmd = %x, arg = %lx\n", cmd, arg);
        copy_from_user(&args, (void __user *)arg, 8*3);
        printk("args[2] = %llx\n", args[2]);
        offs = args[2] % 4096;

        down_read(&current->mm->mmap_sem);
        res = get_user_pages( (unsigned long)args[2], 1, 1, &pages, NULL);
        printk("get_user_pages done! args[2] = %px\n", args[2]);

        if (res) {
            printk(KERN_INFO "Got mmaped.\n");
            kvpaddr = kmap(pages);
            printk("kmap done!\n");
            printk(KERN_INFO "kvpaddr = %px, ofs = %x\n", kvpaddr, offs);
            printk("xx = %llx\n", ((unsigned long long int)(kvpaddr)+offs));
            *(uint64_t *)((unsigned long long int)(kvpaddr)+offs) = 78;
            printk(KERN_INFO "changed value : %lld\n",\
             *(uint64_t *)((unsigned int)(kvpaddr)+offs));
            put_page(pages); //page_cache_release(page);
            printk("put_page done!\n");
        }
        else {
            printk("get_user_pages failed!\n");
        }
        up_read(&current->mm->mmap_sem);

run output

args = 0x442f40
app : args[0] = 0x443040
app : args[2] = 0xffffd2e44d98, *args[2] = 77
[85194.544029] driver:cmd = 40086142, arg = 442f40
[85194.544822] args[2] = ffffd2e44d98
[85194.545613] get_user_pages done! args[2] = 0000ffffd2e44d98
[85194.546004] Got mmaped.
[85194.546248] kmap done!
[85194.546536] kvpaddr = ffff00001f7c0000, ofs = d98
[85194.546976] kvaddr = ffff00001f7c0d98
[85194.548645] Unable to handle kernel paging request at virtual address 000000001f7c0d98
[85194.549245] Mem abort info:
[85194.549513]   ESR = 0x96000006
[85194.549929]   EC = 0x25: DABT (current EL), IL = 32 bits
[85194.550364]   SET = 0, FnV = 0
[85194.550719]   EA = 0, S1PTW = 0
[85194.551008] Data abort info:
[85194.551555]   ISV = 0, ISS = 0x00000006
[85194.551938]   CM = 0, WnR = 0
[85194.552609] user pgtable: 4k pages, 48-bit VAs, pgdp=00000000568fb000
[85194.553441] [000000001f7c0d98] pgd=0000000056209003, pud=0000000047b97003, pmd=0000000000000000
[85194.555783] Internal error: Oops: 96000006 [#16] SMP
[85194.556761] Modules linked in: chr_drv_ex1(OE) nls_iso8859_1 dm_multipath scsi_dh_rdac scsi_dh_emc scsi_dh_alua qemu_fw_cfg sch_fq_codel ppdev lp parport drm ip_tables x_tables autofs4 btrfs zstd_compress raid10 raid456 async_raid6_recov async_memcpy async_pq async_xor async_tx xor xor_neon raid6_pq libcrc32c raid1 raid0 multipath linear crct10dif_ce ghash_ce sm4_ce sm4_generic sm3_ce sm3_generic sha3_ce sha3_generic sha512_ce sha512_arm64 sha2_ce sha256_arm64 sha1_ce virtio_net net_failover virtio_blk failover aes_neon_bs aes_neon_blk aes_ce_blk crypto_simd cryptd aes_ce_cipher [last unloaded: chr_drv_ex1]
[85194.563741] CPU: 2 PID: 4258 Comm: test_axpu_app Tainted: G      D W  OE     5.4.0-77-generic #86-Ubuntu
[85194.564638] Hardware name: QEMU QEMU Ab21q Virtual Machine, BIOS 0.0.0 02/06/2015
[85194.565687] pstate: 60400005 (nZCv daif +PAN -UAO)
[85194.568143] pc : my_ioctl+0x310/0x370 [chr_drv_ex1]
[85194.568854] lr : my_ioctl+0x300/0x370 [chr_drv_ex1]
[85194.569473] sp : ffff80001372bd30
[85194.569838] x29: ffff80001372bd30 x28: ffff000009f7bc00 
[85194.570354] x27: 0000000000000000 x26: 0000000000000000 
[85194.570796] x25: 0000000056000000 x24: ffff00000d3c35e0 
[85194.571308] x23: ffff000016875600 x22: 0000000000000d98 
[85194.571870] x21: 000000001f7c0d98 x20: 0000000000442f40 
[85194.572273] x19: ffff00001f7c0000 x18: 0000000000000010 
[85194.572669] x17: 0000000000000000 x16: 0000000000000000 
[85194.573126] x15: ffff000009f7c128 x14: ffffffffffffffff 
[85194.573632] x13: ffff80009372ba77 x12: ffff80001372ba7f 
[85194.574127] x11: ffff800011b9e000 x10: 0000000000000000 
[85194.574578] x9 : ffff800011db4000 x8 : 00000000000005f2 
[85194.575193] x7 : 0000000000000017 x6 : ffff800011db39d4 
[85194.575700] x5 : 0000000000000000 x4 : ffff00001feb5250 
[85194.576139] x3 : ffff00001fec56c8 x2 : 0000000000000000 
[85194.576550] x1 : 0000000000000000 x0 : ffff80000924b220 
[85194.577379] Call trace:
[85194.577805]  my_ioctl+0x310/0x370 [chr_drv_ex1]
[85194.579423]  do_vfs_ioctl+0xc64/0xe60
[85194.579846]  ksys_ioctl+0x88/0xb8
[85194.580116]  __arm64_sys_ioctl+0x2c/0x228
[85194.580479]  el0_svc_common.constprop.0+0xe4/0x1f0
[85194.580929]  el0_svc_handler+0x38/0xa8
[85194.581293]  el0_svc+0x10/0x2c8
[85194.582065] Code: d28009c0 f8336ac0 b0000000 91088000 (f94002a1) 
[85194.583646] ---[ end trace bd1ac75ca265aec2 ]---
[85194.590696] Device File closed..
Segmentation fault (core dumped)

I thought I changed the user virtual address to kernel virtual address, but writing to the kernel virtual address causes trap. Can anyone help me here?


Solution

  • First, virt_to_phys() is only supposed to be used with kernel lowmem (directly mapped RAM) region, so you can't get physical address of user space - it's not allowed even with kernel space regions such as from vmalloc(). It's only allowed with lowmem region since it's at fixed offset of PAGE_OFFSET.

    If I'm correct, you try to write directly to physical memory and hope your result will be seen in user space app. If this is what you want there is no way to do that (at least, no easy way).


    The hack would be like: you will want to view the page tables of the app and record where this address is located in physical RAM. On x86, page tables address is stored in CR3 register. The next problem is that the CPU is already set in protected mode with PG flag set in CR0 (ie; using paging) which make the MMU read the address as virtual and do the conversion itself to physical (you can't interfere). So to access physical memory you must switch to real mode with paging disabled (which must be if you are using real mode). This will probably means that you will have to call BIOS routines to do the hard work for you.