Application Compatibility in Windows CE 6.0
Posted by: Upender Sandadi
One of the goals for Windows CE 6.0 design was full backward compatibility at the binary level for ISV applications. We have gone to great lengths to maintain binary level compatibility by:
a) Maintaining the same exports from the standard core libraries (for ex: in coredll.dll)
b) Maintaining the same API signatures for all the exported functions.
c) Maintaining the same API functionality unless a change is warranted because of the new memory architecture or stricter security guidelines.
d) Leaving the function exported from coredll even if the API is deprecated. This lets an application to succeed on load time; it might fail at runtime but that is a very small percentage if it happens to use any of the deprecated APIs which we will discuss later.
Given this, we expect minimal impact to developers when they port their applications to CE 6.0. In most cases the application should just work on CE 6.0 without any porting. The level of impact to an application depends on how *well behaved* the application is. A well-behaved application is typically written using only SDK functions and doesn’t use any of the undocumented features or any OAL functions or myriad of other things mentioned in this topic (like passing handle values, passing memory pointers, assumptions about internal workings of a component such as memory mapped files etc.). By the way just to be clear when I say SDK functions, I mean those functions which are available in an installed SDK (for ex: Windows Mobile PPC SDK) or in an exported SDK (for ex: SDK exported from an OS design in Platform Builder). In both cases if the application uses only those functions which are documented as part of the SDK, then we expect minimal or no changes to the application to be able to run on CE 6.0 OS.
Now let us look at some of the changes which we had to make in CE 6.0 to support the new memory layout and what impact that might have on ISV applications.
There are three main categories of API changes:
a) Deprecated APIs: This list of APIs is completely deprecated in CE 6.0. The APIs are still exported from coredll but they mostly turn into no-ops or return failure when called. Some of the examples of the deprecated APIs are:
- Memory pointer based APIs like MapCallerPtr: Since each process now gets its own mapping of 2GB address space (not including 2GB which is kernel space), mapping a memory pointer from one process to another involves reading process memory or making a virtual copy of the process memory. In both cases there are standard SDK functions to achieve this instead of the slot based APIs like MapCallerPtr.
- Process index based APIs like GetProcessIndexFromId: These APIs no longer scale as CE 6.0 supports upto 32K process (pre-6.0 supported only 32 processes).
- Permission based APIs like SetProcPermissions: Since we don’t have any process slots anymore this doesn’t apply anymore. You would need to use OpenProcess to get a valid process handle and then use standard SDK functions to read/write process memory.
All the deprecated APIs will now result in a debug check if a particular debug zone in coredll is enabled (DBGDEPRE == 0x00000040).
b) Kernel mode only APIs: This list of APIs is callable only by code loaded above 0x80000000 (i.e. code loaded in kernel space). This should not have much impact for applications as most of these APIs are hardware related like interrupt APIs, physical page mapping APIs, cross process memory allocation / de-allocation APIs to name a few. Some of these will impact drivers which are covered in a separate blog entry.
c) Usage discouraged APIs: This list of APIs has an alternate API one can call to get the same result as the original API or better. Some of the APIs in this list include checking for a PSL server (WaitForAPIReady is now preferred over IsAPIReady since WaitForAPIReady is a non-polling API and supports full 128 API sets), file mapping (CreateFile/CreateFileMapping is now preferred over CreateFileForMapping) etc.
Platform Builder ships a desktop side tool which you can run on your applications (exes and dlls) to see what APIs a particular module is using and to list out any API hits it registers from the above three buckets. The tool details are as follows:
Usage: Run the tool from an OS build window to scan a particular dll/exe or all files within a folder.
Results: At the end of the run, the tool will generate a HTML file which will have all the API hits from the given module(s) and any recommendations for API usage.
This tool works at the binary level scanning the import table of a given module for list of APIs the module is calling. As a result it cannot detect API usage if the API is called via a function pointer obtained from GetProcAddress.
Now let us look at some of the core changes which have gone in CE 6.0 which might influence how applications behave or interact with other modules on the system.
Pre-6.0 behavior: Handles are global which means handles are accessible from any application.
6.0 behavior: Handles are per process which means a handle value in one application means nothing (zippo, zero, shunya etc.) in another process space unless that handle is duplicated to the second process using DuplicateHandle API call.
Impact to your application: If you have an application which is receiving a handle value from another process, you would need to call DuplicateHandle() on that handle value to create a copy of that object in your process space before you can access that handle object. You have to use DuplicateHandle to get access to that handle object in your own process space.
How come no one told me this before? DuplicateHandle() API was available in pre-6.0 OS also for duplicating the following handles types: mutex, semaphore, event. In CE 6.0 this API has been extended to duplicate any handle (for ex: API handle, message queue handles, process/thread handles to name a few) as long as the source handle value points to a valid handle object in the source process.
Pre-6.0 behavior: Virtual Memory (VM) was based on a single memory model where all applications VM is carved out of single 4GB address space. Kernel had 2GB VM reserved and in the lower 2GB we had space for shared memory region and 32MB slots for all the applications. Given this model, it was easy to map a memory pointer from one process slot to another process slot as any virtual address maps to a unique physical page.
6.0 behavior: Virtual Memory (VM) is based on multiple memory model where all applications still are bound by 4GB address space of which 2GB is still reserved for kernel. But the lower 2GB is now mapped separately for each process. There is a small shared heap area and dll code pages which we will ignore for now (as they have the VM mapping for all processes). Other than that rest of the 2GB in user VM is mapped differently for each process. In other words virtual address in one process space is not identical to another process. As a result you cannot pass memory pointers from one process to another and expect it to work!
Impact to your application: Well if you are passing memory pointers from one process to another and are using MapCalletPtr related functions to translate the virtual address in different process slots, you guessed it right. It won’t work in CE 6.0 anymore. Proper fix for this is to directly read/write process memory using ReadProcessMemory or WriteProcessMemory SDK functions. These APIs give you access to VM of another process (as long as you can open a handle to another process using OpenProcess SDK API call).
How come no one told me this before? This is primarily due to the memory re-architecture where 32MB VM limitation for a process was blown out of the window. You could have used SDK functions to read/write process memory in pre-6.0 also but hey who wouldn’t like shortcuts!
One small note: Don’t use SDK APIs to read/write kernel process memory. For obvious reasons user applications cannot read/write to kernel memory space (upper 2GB starting at 0x80000000). Actually applications cannot write to VM starting from 0x70000000 where the shared heap region starts. Shared heap VM region is from 0x70000000 – 0x80000000 and is user read-only and kernel read-write.
Pre-6.0 behavior: If an application passes invalid arguments to a PSL server, kernel would simply forward the call to the PSL server and the behavior of the thread at that point is at the discretion of the PSL server. Some of the possible scenarios could be that the PSL server throws an exception or it returns failure code to the caller.
6.0 behavior: If an application passes invalid arguments to an API call, the call will be rejected at the kernel layer and what happens next depends on whether the PSL server (the server component which handles the particular API your application is trying to call) registered an API error handler or not. If the PSL server did not register an API error handler (this is new for CE 6.0), an exception will be raised back to the caller. If the caller doesn’t have any exception handler installed in that code path, then the calling thread will be terminated. On the other hand, if the PSL server registered an error handler API with the kernel, then kernel would forward the call to the error handler and in this case the behavior of the API call might be similar to what would happen in pre-6.0 depending on what PSL error handler API does.
Impact to your application: Well if you start seeing strange crashes on API calls, please check all the parameters you are passing to the API calls. One thing to note is that most Win32 APIs (Kernel is the PSL server for these APIs) will gracefully return an error on invalid arguments instead of throwing exceptions. But for some of the Win32 APIs we have explicitly made the decision of faulting the caller rather than continuing on an invalid API call so that any incorrect behavior can be caught by the application at the time of the call rather than some malicious effect downstream.
Trust check APIs
This is not a backward compatibility issue but I thought I will mention it as all the trust check APIs (CeGetCallerTrust and CeGetCurrentTrust) always return full trust in Windows CE 6.0. The only way an OEM can *lock-down* a device is by enabling certification module in the image (using SYSGEN_CERTMOD); this is a topic by itself which we will explore in depth in future articles. So as far as applications are concerned, they can still call the trust APIs but for Windows CE 6.0 understand that these APIs will always return full trust.
Pre-6.0 behavior: Any application can call the interrupt APIs; in Windows Mobile world this is limited to trusted applications.
6.0 behavior: The interrupt APIs are limited to either kernel mode components (dlls loaded in nk.exe process space) or user mode drivers. Calls to these APIs from any other user mode component will simply return FALSE.
Impact to your application: Impact should be minimal since most components which need access to these APIs are drivers and not ISV applications. If you have to call these functions outside of kernel mode or user mode drivers, then the only option is to write a driver (user mode driver is preferred) which can call these APIs on behalf of an application.
Mapping Physical Memory
Pre-6.0 behavior: Any application (mainly drivers) could map physical memory to virtual memory using VirtualCopy or MmMapIoSpace function calls. Again in Windows Mobile, this is limited to trusted components.
6.0 behavior: These calls are limited to either code running in kernel mode or code running in user mode drivers. This might be a limitation for some of the ISV applications. The design decision behind this was to offer stability in the OS and at the same time some flexibility for those who really need this infrastructure.
Impact to your application: It is possible to expose these APIs to user applications via a kernel mode driver or extending a public component which we ship and is included in all images: oalioctl.dll. This is explained next.
OAL Ioctl Codes
Pre-6.0 behavior: User mode code could pass any valid OAL ioctl code when calling KernelIoControl or KernelLibIoControl function calls.
6.0 behavior: To provide better security, the list of OAL ioctl codes callable from user mode code is pre-defined and is limited to a small subset of all ioctl codes supported by OAL. This list of ioctl codes callable by user mode code can be extended by updating a dll which gets shipped as public code with CE 6.0. The code for this lives in public\common\oak\oalioctl. Its main purpose is to intercept all OEMIoctl calls coming from user mode before they are routed to OAL code.
Impact to your application: Your application might return failure when calling certain OAL ioctl codes (even if your application is trusted; remember there is no concept of application trust in CE 6.0). OEM of the particular device would have to explicitly allow certain ioctl codes to be callable from user applications. For example if your application was calling IOCTL_HAL_REBOOT, in CE 6.0 that call would fail unless OEM has explicitly added this ioctl to the user application callable ioctl list. By default in CE 6.0, this ioctl is not callable from user applications. The default OAL ioctls callable from user mode are listed below. As you can see this is not much as we limited the list to the same list that would have been callable by un-trusted applications in Windows Mobile.
Memory Mapped Files
Pre-6.0 behavior: VM associated with memory mapped files in pre-6.0 was always allocated outside of the application memory slot. As a result the memory mapped file objects were uniquely identifiable by a virtual address accessible and visible to all applications. Another distinct feature is that when user creates a mapping object, the access permissions on the view returned could be different from what is requested if there is an existing mapping object by the same name.
6.0 behavior: VM associated with memory mapped files is always allocated in the process VM range (lower 2GB address space). As a result memory mapped file objects in one process are not accessible in another process unless the same object is opened using memory mapped file SDK functions. Also in 6.0, access permissions on the view are purely governed by the caller irrespective of whether the mapping object exists before the call or not.
Impact to your application: If your application was passing memory mapped file handles to other processes, other processes won’t be able to access the memory mapped file object using those handles. You would need to pass offsets to the memory mapped files so that the other process can open the same memory mapped file object (identified by a name) and use the given offset to read/write to the same memory mapped file object. Regarding the view permissions, it is probably easier to explain with an example. If an application calls to open a memory-backed map file with R/O (read-only) access and suppose there is already an existing map file with the same name opened with R/W (read-write) access. In this case in pre-6.0 OS, the new call to open the memory mapped file will get an R/W access to the map file whereas in CE 6.0 the application will get an R/O access to the map file. So if your application was opening a memory mapped file as a read-only but writing to it, this might have worked in pre-6.0 but in CE 6.0, this will result in a fault in your application. So check your CreateFileMapping and MapViewOfFile API calls if you are running into issues with memory mapped files.
In addition to this, there is lot of great info out there on CE 6.0. Here are some of the recent blog entries on CE 6.0 which you might find useful to go through to get an overview on CE 6.0.
Thanks to Sue for reviewing this article.