| /* |
| * MobiCore Driver Kernel Module. |
| * |
| * This module is written as a Linux device driver. |
| * This driver represents the command proxy on the lowest layer, from the |
| * secure world to the non secure world, and vice versa. |
| * This driver is located in the non secure world (Linux). |
| * This driver offers IOCTL commands, for access to the secure world, and has |
| * the interface from the secure world to the normal world. |
| * The access to the driver is possible with a file descriptor, |
| * which has to be created by the fd = open(/dev/mobicore) command. |
| * |
| * <-- Copyright Giesecke & Devrient GmbH 2009-2012 --> |
| * <-- Copyright Trustonic Limited 2013 --> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License version 2 as |
| * published by the Free Software Foundation. |
| */ |
| #include "main.h" |
| #include "debug.h" |
| #include "mem.h" |
| |
| #include <linux/highmem.h> |
| #include <linux/slab.h> |
| #include <linux/kthread.h> |
| #include <linux/pagemap.h> |
| #include <linux/device.h> |
| |
| |
| /* MobiCore memory context data */ |
| struct mc_mem_context mem_ctx; |
| |
| /* convert L2 PTE to page pointer */ |
| static inline struct page *l2_pte_to_page(pte_t pte) |
| { |
| unsigned long phys_page_addr = ((unsigned long)pte & PAGE_MASK); |
| unsigned int pfn = phys_page_addr >> PAGE_SHIFT; |
| struct page *page = pfn_to_page(pfn); |
| return page; |
| } |
| |
| /* convert page pointer to L2 PTE */ |
| static inline pte_t page_to_l2_pte(struct page *page) |
| { |
| unsigned long pfn = page_to_pfn(page); |
| unsigned long phys_addr = (pfn << PAGE_SHIFT); |
| pte_t pte = (pte_t)(phys_addr & PAGE_MASK); |
| return pte; |
| } |
| |
| static inline void release_page(struct page *page) |
| { |
| SetPageDirty(page); |
| |
| page_cache_release(page); |
| } |
| |
| static int lock_pages(struct task_struct *task, void *virt_start_page_addr, |
| int pages_no, struct page **pages) |
| { |
| int locked_pages; |
| |
| /* lock user pages, must hold the mmap_sem to do this. */ |
| down_read(&(task->mm->mmap_sem)); |
| locked_pages = get_user_pages( |
| task, |
| task->mm, |
| (unsigned long)virt_start_page_addr, |
| pages_no, |
| 1, /* write access */ |
| 0, |
| pages, |
| NULL); |
| up_read(&(task->mm->mmap_sem)); |
| |
| /* check if we could lock all pages. */ |
| if (locked_pages != pages_no) { |
| MCDRV_DBG_ERROR(mcd, "get_user_pages() failed, locked_pages=%d", |
| locked_pages); |
| if (locked_pages > 0) { |
| /* release all locked pages. */ |
| release_pages(pages, locked_pages, 0); |
| } |
| return -ENOMEM; |
| } |
| |
| return 0; |
| } |
| |
| /* Get kernel pointer to shared L2 table given a per-process reference */ |
| struct l2table *get_l2_table_kernel_virt(struct mc_l2_table *table) |
| { |
| if (WARN(!table, "Invalid L2 table")) |
| return NULL; |
| |
| if (WARN(!table->set, "Invalid L2 table set")) |
| return NULL; |
| |
| if (WARN(!table->set->kernel_virt, "Invalid L2 pointer")) |
| return NULL; |
| |
| return &(table->set->kernel_virt->table[table->idx]); |
| } |
| |
| /* Get physical address of a shared L2 table given a per-process reference */ |
| struct l2table *get_l2_table_phys(struct mc_l2_table *table) |
| { |
| if (WARN(!table, "Invalid L2 table")) |
| return NULL; |
| if (WARN(!table->set, "Invalid L2 table set")) |
| return NULL; |
| if (WARN(!table->set->kernel_virt, "Invalid L2 phys pointer")) |
| return NULL; |
| |
| return &(table->set->phys->table[table->idx]); |
| } |
| |
| static inline int in_use(struct mc_l2_table *table) |
| { |
| return atomic_read(&table->usage) > 0; |
| } |
| |
| /* |
| * Search the list of used l2 tables and return the one with the handle. |
| * Assumes the table_lock is taken. |
| */ |
| struct mc_l2_table *find_l2_table(unsigned int handle) |
| { |
| struct mc_l2_table *table; |
| |
| list_for_each_entry(table, &mem_ctx.l2_tables, list) { |
| if (table->handle == handle) |
| return table; |
| } |
| return NULL; |
| } |
| |
| /* |
| * Allocate a new l2 table store plus L2_TABLES_PER_PAGE in the l2 free tables |
| * list. Assumes the table_lock is already taken by the caller above. |
| */ |
| static int alloc_table_store(void) |
| { |
| unsigned long store; |
| struct mc_l2_tables_set *l2table_set; |
| struct mc_l2_table *l2table, *l2table2; |
| struct page *page; |
| int ret = 0, i; |
| /* temp list for holding the l2 tables */ |
| LIST_HEAD(temp); |
| |
| store = get_zeroed_page(GFP_KERNEL); |
| if (!store) |
| return -ENOMEM; |
| |
| /* |
| * Actually, locking is not necessary, because kernel |
| * memory is not supposed to get swapped out. But we |
| * play safe.... |
| */ |
| page = virt_to_page(store); |
| SetPageReserved(page); |
| |
| /* add all the descriptors to the free descriptors list */ |
| l2table_set = kmalloc(sizeof(*l2table_set), GFP_KERNEL | __GFP_ZERO); |
| if (l2table_set == NULL) { |
| ret = -ENOMEM; |
| goto free_store; |
| } |
| /* initialize */ |
| l2table_set->kernel_virt = (void *)store; |
| l2table_set->page = page; |
| l2table_set->phys = (void *)virt_to_phys((void *)store); |
| /* the set is not yet used */ |
| atomic_set(&l2table_set->used_tables, 0); |
| |
| /* init add to list. */ |
| INIT_LIST_HEAD(&(l2table_set->list)); |
| list_add(&l2table_set->list, &mem_ctx.l2_tables_sets); |
| |
| for (i = 0; i < L2_TABLES_PER_PAGE; i++) { |
| /* allocate a WSM L2 descriptor */ |
| l2table = kmalloc(sizeof(*l2table), GFP_KERNEL | __GFP_ZERO); |
| if (l2table == NULL) { |
| ret = -ENOMEM; |
| MCDRV_DBG_ERROR(mcd, "out of memory\n"); |
| /* Free the full temp list and the store in this case */ |
| goto free_temp_list; |
| } |
| |
| /* set set reference */ |
| l2table->set = l2table_set; |
| l2table->idx = i; |
| l2table->virt = get_l2_table_kernel_virt(l2table); |
| l2table->phys = (unsigned long)get_l2_table_phys(l2table); |
| atomic_set(&l2table->usage, 0); |
| |
| /* add to temp list. */ |
| INIT_LIST_HEAD(&l2table->list); |
| list_add_tail(&l2table->list, &temp); |
| } |
| |
| /* |
| * If everything went ok then merge the temp list with the global |
| * free list |
| */ |
| list_splice_tail(&temp, &mem_ctx.free_l2_tables); |
| return 0; |
| free_temp_list: |
| list_for_each_entry_safe(l2table, l2table2, &temp, list) { |
| kfree(l2table); |
| } |
| |
| list_del(&l2table_set->list); |
| |
| free_store: |
| free_page(store); |
| return ret; |
| |
| } |
| /* |
| * Get a l2 table from the free tables list or allocate a new one and |
| * initialize it. Assumes the table_lock is already taken. |
| */ |
| static struct mc_l2_table *alloc_l2_table(struct mc_instance *instance) |
| { |
| int ret = 0; |
| struct mc_l2_table *table = NULL; |
| |
| if (list_empty(&mem_ctx.free_l2_tables)) { |
| ret = alloc_table_store(); |
| if (ret) { |
| MCDRV_DBG_ERROR(mcd, "Failed to allocate new store!"); |
| return ERR_PTR(-ENOMEM); |
| } |
| /* if it's still empty something wrong has happened */ |
| if (list_empty(&mem_ctx.free_l2_tables)) { |
| MCDRV_DBG_ERROR(mcd, |
| "Free list not updated correctly!"); |
| return ERR_PTR(-EFAULT); |
| } |
| } |
| |
| /* get a WSM L2 descriptor */ |
| table = list_first_entry(&mem_ctx.free_l2_tables, |
| struct mc_l2_table, list); |
| if (table == NULL) { |
| MCDRV_DBG_ERROR(mcd, "out of memory\n"); |
| return ERR_PTR(-ENOMEM); |
| } |
| /* Move it to the used l2 tables list */ |
| list_move_tail(&table->list, &mem_ctx.l2_tables); |
| |
| table->handle = get_unique_id(); |
| table->owner = instance; |
| |
| atomic_inc(&table->set->used_tables); |
| atomic_inc(&table->usage); |
| |
| MCDRV_DBG_VERBOSE(mcd, |
| "chunkPhys=%p,idx=%d", table->set->phys, table->idx); |
| |
| return table; |
| } |
| |
| /* |
| * Frees the object associated with a l2 table. Initially the object is moved |
| * to the free tables list, but if all the 4 lists of the store are free |
| * then the store is also released. |
| * Assumes the table_lock is already taken. |
| */ |
| static void free_l2_table(struct mc_l2_table *table) |
| { |
| struct mc_l2_tables_set *l2table_set; |
| |
| if (WARN(!table, "Invalid table")) |
| return; |
| |
| l2table_set = table->set; |
| if (WARN(!l2table_set, "Invalid table set")) |
| return; |
| |
| list_move_tail(&table->list, &mem_ctx.free_l2_tables); |
| |
| /* if nobody uses this set, we can release it. */ |
| if (atomic_dec_and_test(&l2table_set->used_tables)) { |
| struct mc_l2_table *tmp; |
| |
| /* remove from list */ |
| list_del(&l2table_set->list); |
| /* |
| * All the l2 tables are in the free list for this set |
| * so we can just remove them from there |
| */ |
| list_for_each_entry_safe(table, tmp, &mem_ctx.free_l2_tables, |
| list) { |
| if (table->set == l2table_set) { |
| list_del(&table->list); |
| kfree(table); |
| } |
| } /* end while */ |
| |
| /* |
| * We shouldn't recover from this since it was some data |
| * corruption before |
| */ |
| BUG_ON(!l2table_set->page); |
| ClearPageReserved(l2table_set->page); |
| |
| BUG_ON(!l2table_set->kernel_virt); |
| free_page((unsigned long)l2table_set->kernel_virt); |
| |
| kfree(l2table_set); |
| } |
| } |
| |
| /* |
| * Create a L2 table in a WSM container that has been allocates previously. |
| * Assumes the table lock is already taken or there is no need to take like |
| * when first creating the l2 table the full list is locked. |
| * |
| * @task pointer to task owning WSM |
| * @wsm_buffer user space WSM start |
| * @wsm_len WSM length |
| * @table Pointer to L2 table details |
| */ |
| static int map_buffer(struct task_struct *task, void *wsm_buffer, |
| unsigned int wsm_len, struct mc_l2_table *table) |
| { |
| int ret = 0; |
| unsigned int i, nr_of_pages; |
| /* start address of the 4 KiB page of wsm_buffer */ |
| void *virt_addr_page; |
| struct page *page; |
| struct l2table *l2table; |
| struct page **l2table_as_array_of_pointers_to_page; |
| /* page offset in wsm buffer */ |
| unsigned int offset; |
| |
| if (WARN(!wsm_buffer, "Invalid WSM buffer pointer")) |
| return -EINVAL; |
| |
| if (WARN(wsm_len == 0, "Invalid WSM buffer length")) |
| return -EINVAL; |
| |
| if (WARN(!table, "Invalid mapping table for WSM")) |
| return -EINVAL; |
| |
| /* no size > 1Mib supported */ |
| if (wsm_len > SZ_1M) { |
| MCDRV_DBG_ERROR(mcd, "size > 1 MiB\n"); |
| return -EINVAL; |
| } |
| |
| MCDRV_DBG_VERBOSE(mcd, "WSM addr=0x%p, len=0x%08x\n", wsm_buffer, |
| wsm_len); |
| |
| |
| /* calculate page usage */ |
| virt_addr_page = (void *)(((unsigned long)(wsm_buffer)) & PAGE_MASK); |
| offset = (unsigned int) (((unsigned long)(wsm_buffer)) & (~PAGE_MASK)); |
| nr_of_pages = PAGE_ALIGN(offset + wsm_len) / PAGE_SIZE; |
| |
| MCDRV_DBG_VERBOSE(mcd, "virt addr page start=0x%p, pages=%d\n", |
| virt_addr_page, nr_of_pages); |
| |
| /* L2 table can hold max 1MiB in 256 pages. */ |
| if ((nr_of_pages * PAGE_SIZE) > SZ_1M) { |
| MCDRV_DBG_ERROR(mcd, "WSM paged exceed 1 MiB\n"); |
| return -EINVAL; |
| } |
| |
| l2table = table->virt; |
| /* |
| * We use the memory for the L2 table to hold the pointer |
| * and convert them later. This works, as everything comes |
| * down to a 32 bit value. |
| */ |
| l2table_as_array_of_pointers_to_page = (struct page **)l2table; |
| |
| /* Request comes from user space */ |
| if (task != NULL && !is_vmalloc_addr(wsm_buffer)) { |
| /* |
| * lock user page in memory, so they do not get swapped |
| * out. |
| * REV axh: Kernel 2.6.27 added a new get_user_pages_fast() |
| * function, maybe it is called fast_gup() in some versions. |
| * handle user process doing a fork(). |
| * Child should not get things. |
| * http://osdir.com/ml/linux-media/2009-07/msg00813.html |
| * http://lwn.net/Articles/275808/ |
| */ |
| ret = lock_pages(task, virt_addr_page, nr_of_pages, |
| l2table_as_array_of_pointers_to_page); |
| if (ret != 0) { |
| MCDRV_DBG_ERROR(mcd, "lock_user_pages() failed\n"); |
| return ret; |
| } |
| } |
| /* Request comes from kernel space(cont buffer) */ |
| else if (task == NULL && !is_vmalloc_addr(wsm_buffer)) { |
| void *uaddr = wsm_buffer; |
| for (i = 0; i < nr_of_pages; i++) { |
| page = virt_to_page(uaddr); |
| if (!page) { |
| MCDRV_DBG_ERROR(mcd, "failed to map address"); |
| return -EINVAL; |
| } |
| get_page(page); |
| l2table_as_array_of_pointers_to_page[i] = page; |
| uaddr += PAGE_SIZE; |
| } |
| } |
| /* Request comes from kernel space(vmalloc buffer) */ |
| else { |
| void *uaddr = wsm_buffer; |
| for (i = 0; i < nr_of_pages; i++) { |
| page = vmalloc_to_page(uaddr); |
| if (!page) { |
| MCDRV_DBG_ERROR(mcd, "failed to map address"); |
| return -EINVAL; |
| } |
| get_page(page); |
| l2table_as_array_of_pointers_to_page[i] = page; |
| uaddr += PAGE_SIZE; |
| } |
| } |
| |
| table->pages = nr_of_pages; |
| |
| /* |
| * create L2 Table entries. |
| * used_l2table->table contains a list of page pointers here. |
| * For a proper cleanup we have to ensure that the following |
| * code either works and used_l2table contains a valid L2 table |
| * - or fails and used_l2table->table contains the list of page |
| * pointers. |
| * Any mixed contents will make cleanup difficult. |
| */ |
| for (i = 0; i < nr_of_pages; i++) { |
| pte_t pte; |
| page = l2table_as_array_of_pointers_to_page[i]; |
| |
| /* |
| * create L2 table entry, see ARM MMU docu for details |
| * about flags stored in the lowest 12 bits. |
| * As a side reference, the Article |
| * "ARM's multiply-mapped memory mess" |
| * found in the collection at |
| * http://lwn.net/Articles/409032/ |
| * is also worth reading. |
| */ |
| pte = page_to_l2_pte(page) |
| | PTE_EXT_AP1 | PTE_EXT_AP0 |
| | PTE_CACHEABLE | PTE_BUFFERABLE |
| | PTE_TYPE_SMALL | PTE_TYPE_EXT | PTE_EXT_NG; |
| /* |
| * Linux uses different mappings for SMP systems(the |
| * sharing flag is set for the pte. In order not to |
| * confuse things too much in Mobicore make sure the |
| * shared buffers have the same flags. |
| * This should also be done in SWD side |
| */ |
| #ifdef CONFIG_SMP |
| pte |= PTE_EXT_SHARED | PTE_EXT_TEX(1); |
| #endif |
| |
| l2table->table_entries[i] = pte; |
| MCDRV_DBG_VERBOSE(mcd, "L2 entry %d: 0x%08x\n", i, |
| (unsigned int)(pte)); |
| } |
| |
| /* ensure rest of table is empty */ |
| while (i < 255) |
| l2table->table_entries[i++] = (pte_t)0; |
| |
| |
| return ret; |
| } |
| |
| /* |
| * Remove a L2 table in a WSM container. Afterwards the container may be |
| * released. Assumes the table_lock and the lock is taken. |
| */ |
| static void unmap_buffers(struct mc_l2_table *table) |
| { |
| struct l2table *l2table; |
| int i; |
| |
| if (WARN_ON(!table)) |
| return; |
| |
| /* found the table, now release the resources. */ |
| MCDRV_DBG_VERBOSE(mcd, "clear L2 table, phys_base=%p, nr_of_pages=%d\n", |
| (void *)table->phys, table->pages); |
| |
| l2table = table->virt; |
| |
| /* release all locked user space pages */ |
| for (i = 0; i < table->pages; i++) { |
| /* convert physical entries from L2 table to page pointers */ |
| pte_t pte = l2table->table_entries[i]; |
| struct page *page = l2_pte_to_page(pte); |
| release_page(page); |
| } |
| |
| /* remember that all pages have been freed */ |
| table->pages = 0; |
| } |
| |
| /* Delete a used l2 table. Assumes the table_lock and the lock is taken */ |
| static void unmap_l2_table(struct mc_l2_table *table) |
| { |
| /* Check if it's not locked by other processes too! */ |
| if (!atomic_dec_and_test(&table->usage)) |
| return; |
| |
| /* release if Nwd and Swd/MC do no longer use it. */ |
| unmap_buffers(table); |
| free_l2_table(table); |
| } |
| |
| int mc_free_l2_table(struct mc_instance *instance, uint32_t handle) |
| { |
| struct mc_l2_table *table; |
| int ret = 0; |
| |
| if (WARN(!instance, "No instance data available")) |
| return -EFAULT; |
| |
| mutex_lock(&mem_ctx.table_lock); |
| table = find_l2_table(handle); |
| |
| if (table == NULL) { |
| MCDRV_DBG_VERBOSE(mcd, "entry not found"); |
| ret = -EINVAL; |
| goto err_unlock; |
| } |
| if (instance != table->owner && !is_daemon(instance)) { |
| MCDRV_DBG_ERROR(mcd, "instance does no own it"); |
| ret = -EPERM; |
| goto err_unlock; |
| } |
| /* free table (if no further locks exist) */ |
| unmap_l2_table(table); |
| err_unlock: |
| mutex_unlock(&mem_ctx.table_lock); |
| |
| return ret; |
| } |
| |
| int mc_lock_l2_table(struct mc_instance *instance, uint32_t handle) |
| { |
| int ret = 0; |
| struct mc_l2_table *table = NULL; |
| |
| if (WARN(!instance, "No instance data available")) |
| return -EFAULT; |
| |
| mutex_lock(&mem_ctx.table_lock); |
| table = find_l2_table(handle); |
| |
| if (table == NULL) { |
| MCDRV_DBG_VERBOSE(mcd, "entry not found %u\n", handle); |
| ret = -EINVAL; |
| goto table_err; |
| } |
| if (instance != table->owner && !is_daemon(instance)) { |
| MCDRV_DBG_ERROR(mcd, "instance does no own it\n"); |
| ret = -EPERM; |
| goto table_err; |
| } |
| |
| /* lock entry */ |
| atomic_inc(&table->usage); |
| table_err: |
| mutex_unlock(&mem_ctx.table_lock); |
| return ret; |
| } |
| /* |
| * Allocate L2 table and map buffer into it. |
| * That is, create respective table entries. |
| * Must hold Semaphore mem_ctx.wsm_l2_sem |
| */ |
| struct mc_l2_table *mc_alloc_l2_table(struct mc_instance *instance, |
| struct task_struct *task, void *wsm_buffer, unsigned int wsm_len) |
| { |
| int ret = 0; |
| struct mc_l2_table *table; |
| |
| if (WARN(!instance, "No instance data available")) |
| return ERR_PTR(-EFAULT); |
| |
| mutex_lock(&mem_ctx.table_lock); |
| table = alloc_l2_table(instance); |
| if (IS_ERR(table)) { |
| MCDRV_DBG_ERROR(mcd, "allocate_used_l2_table() failed\n"); |
| ret = -ENOMEM; |
| goto err_no_mem; |
| } |
| |
| /* create the L2 page for the WSM */ |
| ret = map_buffer(task, wsm_buffer, wsm_len, table); |
| |
| if (ret != 0) { |
| MCDRV_DBG_ERROR(mcd, "map_buffer() failed\n"); |
| unmap_l2_table(table); |
| goto err_no_mem; |
| } |
| MCDRV_DBG(mcd, "mapped buffer %p to table with handle %d @ %lx", |
| wsm_buffer, table->handle, table->phys); |
| |
| mutex_unlock(&mem_ctx.table_lock); |
| return table; |
| err_no_mem: |
| mutex_unlock(&mem_ctx.table_lock); |
| return ERR_PTR(ret); |
| } |
| |
| uint32_t mc_find_l2_table(uint32_t handle, int32_t fd) |
| { |
| uint32_t ret = 0; |
| struct mc_l2_table *table = NULL; |
| |
| mutex_lock(&mem_ctx.table_lock); |
| table = find_l2_table(handle); |
| |
| if (table == NULL) { |
| MCDRV_DBG_ERROR(mcd, "entry not found %u\n", handle); |
| ret = 0; |
| goto table_err; |
| } |
| |
| /* It's safe here not to lock the instance since the owner of |
| * the table will be cleared only with the table lock taken */ |
| if (!mc_check_owner_fd(table->owner, fd)) { |
| MCDRV_DBG_ERROR(mcd, "not valid owner%u\n", handle); |
| ret = 0; |
| goto table_err; |
| } |
| |
| ret = table->phys; |
| table_err: |
| mutex_unlock(&mem_ctx.table_lock); |
| return ret; |
| } |
| |
| void mc_clean_l2_tables(void) |
| { |
| struct mc_l2_table *table, *tmp; |
| |
| mutex_lock(&mem_ctx.table_lock); |
| /* Check if some WSM is orphaned. */ |
| list_for_each_entry_safe(table, tmp, &mem_ctx.l2_tables, list) { |
| if (table->owner == NULL) { |
| MCDRV_DBG(mcd, |
| "clearing orphaned WSM L2: p=%lx pages=%d\n", |
| table->phys, table->pages); |
| unmap_l2_table(table); |
| } |
| } |
| mutex_unlock(&mem_ctx.table_lock); |
| } |
| |
| void mc_clear_l2_tables(struct mc_instance *instance) |
| { |
| struct mc_l2_table *table, *tmp; |
| |
| mutex_lock(&mem_ctx.table_lock); |
| /* Check if some WSM is still in use. */ |
| list_for_each_entry_safe(table, tmp, &mem_ctx.l2_tables, list) { |
| if (table->owner == instance) { |
| MCDRV_DBG(mcd, "release WSM L2: p=%lx pages=%d\n", |
| table->phys, table->pages); |
| /* unlock app usage and free or mark it as orphan */ |
| table->owner = NULL; |
| unmap_l2_table(table); |
| } |
| } |
| mutex_unlock(&mem_ctx.table_lock); |
| } |
| |
| int mc_init_l2_tables(void) |
| { |
| /* init list for WSM L2 chunks. */ |
| INIT_LIST_HEAD(&mem_ctx.l2_tables_sets); |
| |
| /* L2 table descriptor list. */ |
| INIT_LIST_HEAD(&mem_ctx.l2_tables); |
| |
| /* L2 table descriptor list. */ |
| INIT_LIST_HEAD(&mem_ctx.free_l2_tables); |
| |
| mutex_init(&mem_ctx.table_lock); |
| |
| return 0; |
| } |
| |
| void mc_release_l2_tables() |
| { |
| struct mc_l2_table *table; |
| /* Check if some WSM is still in use. */ |
| list_for_each_entry(table, &mem_ctx.l2_tables, list) { |
| WARN(1, "WSM L2 still in use: phys=%lx ,nr_of_pages=%d\n", |
| table->phys, table->pages); |
| } |
| } |