Writing Device Drivers

Appendix F Making a Device Driver 64-Bit Ready

The Solaris kernel is capable of running in 64-bit mode on suitable hardware and supports both 32-bit and 64-bit applications. This chapter is a guide to updating a device driver to run in a 64-bit environment. Previous releases of the Solaris operating environment were only 32-bit.

Applications that need 64-bit capabilities may need to be updated to run on 64-bit Solaris. Device drivers will require at least minimal conversion to run in the 64-bit kernel. For those applications that continue to use 32-bit Solaris on 64-bit hardware, 32-bit device drivers will continue to work without recompilation. The information in this chapter will enable drivers to be written to use common code for the 32-bit and 64-bit environments.

How 64-Bit Drivers Differ From 32-Bit Drivers

Before starting to convert a device driver for the 64-bit environment, it is useful to understand how the 32-bit environment differs from the 64-bit environment. In particular, it is important to become familiar with the C language data type models ILP32 and LP64, and to be aware of driver-specific issues.

C Language Data Type Models: LP64 and ILP32

The 32-bit Solaris C language data model, called ILP32, defines int, long, and pointers as 32 bits, short as 16 bits, and char as 8 bits. The C data type model chosen for the 64-bit operating system is LP64. This data model defines long and pointers as 64 bits, int as 32 bits, short as 16 bits, and char as 8 bits.

In LP64, only longs and pointers change size; the other C data types stay the same size as in the ILP32 model. Table F-1 lists the standard C data types and their corresponding sizes in bits for ILP32 and LP64.

Table F-1 C Data Type Sizes

C Type 













long long 






In addition to the data model changes, some system-derived types, such as size_t, have been expanded to be 64-bit quantities when compiled in the 64-bit environment.

Potential Problems to Avoid

To run in a 64-bit environment, drivers may need to be converted to use the LP64 data model. There are several potential problem areas caused by the change in size of long and pointers:

  1. Source code that assumes that int, long, and pointer types are the same size are incorrect for 64-bit Solaris.

  2. Type casts may need updating, since the underlying data types may have changed.

  3. Data structures containing long types and pointers will need to be checked for different offset values than expected. This is caused by alignment differences that occur when long and pointer fields grow to 64 bits.

Overview of Driver-Specific Issues

In addition to general code cleanup to support the data model changes for LP64, driver writers have several driver-specific issues to consider:

General Conversion Steps

The sections below provide information on converting drivers to run in a 64-bit environment. Driver writers may need to do one or more of the following:

Convert Driver Code to Be 64-Bit Clean

As a first step in converting a device driver to run in a 64-bit kernel, make the driver 64-bit clean by converting to the LP64 data model. For example, make sure that the driver's use of long variables remains consistent between data models.

To enable source code to be both 32-bit and 64-bit safe, the Solaris 2.6 system provides new fixed-width integer types, derived types, constants, and macros in the files <sys/types.h> and <sys/inttypes.h>. The fixed-width types include both signed and unsigned integer types such as int8_t, uint8_t, uint32_t, uint64_t, as well as constants that specify their limits.

Note the following when converting to LP64:

  1. System-derived types, such as size_t, should be used for type declarations whenever possible.

    Using derived types will help make driver code 32-bit and 64-bit safe, since the derived types themselves are safe for both IPL32 and LP64 data models. In addition, it is a good idea to use system-derived types for definitions to allow for future change.

  2. Fixed-width types, such as uint32_t, should be used where appropriate to clearly specify type declarations.

    Fixed-width integer types are useful for representing explicit sizes of binary data structures or hardware registers, while fundamental C language data types, such as int, can still be used for loop counters or file descriptors. In particular, make sure that use of variables of type long remains accurate. As a long is 64 bits in LP64, change variables that represent 32-bit data and that are currently defined as long to fixed-width 32-bit types, such as uint32_t.

  3. The new derived types uintptr_t or intptr_t should be used as the integral type for pointers.

    Pointers are 64 bits in size in the Solaris 64-bit environment. Although pointers could be recast to long, the new derived system types should be used instead, particularly when pointers are used to do address arithmetic or alignment. In addition, check for code that assumes that int and pointer variables are the same size, and use uintptr_t instead of int for variables that are cast to pointers. For example, change:

    		char *p;
     	p = (char *)((int)p & PAGEOFFSET);


    		p = (char *)((uintptr_t)p & PAGEOFFSET);

The new fixed width data types in <sys/inttypes.h> are included in <sys/ddi.h>.

Caution - Caution -

Caution: Do not ignore compilation or lint warnings during conversion for LP64, because even those that were safe to ignore previously in the ILP32 environment may now indicate a more serious problem.

Useful Tools to Check Data Model Conversion

Running driver code through the Sun WorkShop Compiler C 4.2 lint utility can help find problems with data models. This version of lint provides warnings about potential 64-bit problems. It prints the line number of the problem code and a warning message describing the problem, indicates whether a pointer was involved, and provides information about the size of the data types.

To use lint(1B) to check for data model conversion problems, use the -errchk=longptr64 option.

Update Data Structures to Preserve 32-Bit Data in Register Layouts

In the 64-bit data model, data structures that use long to define the type of arguments might be incorrect if the argument needs to define a 32-bit quantity. For example, some drivers currently use long to define 32-bit fields in a hardware register layout. To make a driver 64-bit safe, update data structures where necessary to use int32_t or uint32_t, defined in <sys/inttypes.h>, instead of long for 32-bit data. This preserves the binary layout of 32-bit data structures.

For example, change:

struct reg {
 	ulong_t			addr;
 	uint_t			count;


struct reg {
 	uint32_t			addr;
 	uint32_t			count;

Check Use of Derived Types That Change Size Between 32-Bit and 64-Bit Environments

System derived types, such as size_t, should be used where possible so that the resulting variables make sense when passed between functions. The new derived types uintptr_t or intptr_t should be used as the integral type for pointers.

Fixed-width integer types are useful for representing explicit sizes of binary data structures or hardware registers, while fundamental C language data types, such as int, can still be used for loop counters or file descriptors.

Some system derived types represent 32-bit quantities on a 32-bit system but represent 64-bit quantities on a 64-bit system. Derived types that change size in this way include:

Drivers that use these derived types should pay particular attention to their use, particularly if they are assigning these values to variables of another derived type, such as a fixed-width type.

Change Common Access Functions to Fixed-Width Versions

Previously, DDI common access functions specified the size of data in terms of bytes, words, and so on. For example, ddi_getl(9F) was used to access 32-bit quantities. This function will not work to access a 32-bit quantity in the 64-bit environment because long is now a 64-bit quantity.

In Solaris 2.6, new common access functions that use fixed-width types have been added. These functions have been named to reflect the actual data size. For example, in a 64-bit environment, a driver must use ddi_get32(9F) to access 32-bit data rather than ddi_getl(9F).

	uint32_t ddi_get32(ddi_acc_handle_t hdl, uint32_t *dev_addr); 

To make a device driver 64-bit safe, replace all Common Access functions with the new fixed-width versions.

Table F-2 shows a subset of the new common access functions. For a complete list of the new functions, see Appendix B, Interface Transition List. For a brief description of the new interfaces, see Appendix C, Summary of Solaris 7 DDI/DKI Services.

Table F-2 Solaris 7 Common Access Functions


reads 8 bits from device address 


reads 16 bits from device address 


reads 32 bits from device address 


reads 64 bits from device address 


writes 8 bits to device address 


writes 16 bits to device address 


writes 32 bits to device address 


writes 64 bits to device address 

Modify Routines That Handle Data Sharing

If a device driver shares data structures with an application using ioctl(9E), devmap(9E), or mmap(9E), and the driver is recompiled for a 64-bit kernel but the application that uses the interface is a 32-bit program, the binary layout of data structures will be incompatible if they contain long types or pointers.

If a data structure is defined in terms of type long, but there is no actual need for 64-bit data items, the data structure should be changed to use fundamental types that remain 32 bits in LP64 (int and unsigned int) or the new fixed-width 32-bit types in <sys/inttypes.h>. In the remaining cases, where the data structures contain pointers or structure fields that need to be long (32-bits in an ILP32 kernel and 64-bits in an LP64 kernel), the driver needs to be aware of the different structure shapes for ILP32 and LP64 and determine whether there is a model mismatch between the application and the kernel.

To handle potential data model differences, the ioctl(9E), devmap(9E) and mmap(9E) driver entry points, which are passed arguments from user applications, need to be written to determine whether the argument came from an application using the same data type model as the kernel. The new DDI function ddi_model_convert_from(9F) enables drivers to determine this.


This function takes the data type model of the user application as an argument and returns the following values:

DDI_MODEL_NONE is returned if no data conversion is necessary. This is the case when application and driver have the same data model (both are ILP32 or LP64). DDI_MODEL_ILP32 is returned if the driver is compiled to the LP64 data model and is communicating with a 32-bit application. Typically, the code that returns the application data model is conditionally compiled depending on the _MULTI_DATAMODEL macro. This macro is defined by the system when the driver supports multiple data models.

If the driver supports multiple data models, it will switch on the return value of ddi_model_convert_from(9F). The DDI_MODEL_ILP32 case should define a 32-bit version of the structure being passed in. Use ddi_copyin(9F) to copy the structure from user space to the 32-bit version of the structure, and then assign each field in the 32-bit structure to the 64-bit version. Otherwise, the code should be unchanged.

The sections that follow show code examples of the use of ddi_model_convert_from(9F).


In a 32-bit system, the ioctl(9E) entry point takes an int as the argument to pass a 32-bit value or user address to the device driver. In a 64-bit system, this argument must handle 64-bit values and addresses. Therefore, the ioctl(9E) function prototype has changed from:

	int (*cb_ioctl)(dev_t dev, int cmd, int arg, int mode,
 							cred_t *credp, int *rvalp);


	int (*cb_ioctl)(dev_t dev, int cmd, intptr_t arg, int mode,
 							cred_t *credp, int *rvalp);

Note that intptr_t arg remains 32-bits when compiled in the ILP32 kernel.

To determine whether there is a model mismatch between the application and the driver, the driver uses the FMODELS mask to determine the model type from the ioctl(9E) mode argument. The following values are passed in mode to identify the application data model:

The driver passes the data model type to ddi_model_convert_from(9F), which determines if adjustments are needed to the application data structures.

Example F-1 demonstrates the use of the _MULTI_DATAMODEL macro and the ddi_model_convert_from(9F) function.

Example F-1 ioctl(9E)

struct passargs {
	int		len;
	caddr_t		addr
} pa;

xxioctl(dev_t dev, int cmd, intptr_t arg, int mode, cred_t
			int *rvalp)
	switch (ddi_model_convert_from(mode & FMODELS)) {
	case DDI_MODEL_ILP32:
	  	struct passargs32 {
			int			len;
			uint32_t			*addr;
	  	} pa32;

		(void) ddi_copyin((void *)arg, &pa32,
					sizeof (struct passargs32), mode);
		pa.len = pa32.len;
		pa.addr = pa32.address;
		(void) ddi_copyin((void *)arg, &pa,
					sizeof (struct passargs), mode);
#else /* ! _MULTI_DATAMODEL */
	(void) ddi_copyin((void *)arg, &pa,
				sizeof (struct passargs), mode);
#endif /* ! _MULTI_DATAMODEL */

Data structure macros are another method of referring to the data structure from the user application. The macros effectively hide the difference between the data model of the user application and the driver. For more information see "Data Structure Macros".


To enable a 64-bit driver and a 32-bit application to share memory, the binary layout generated by the 64-bit driver must be the same as consumed by the 32-bit application.

To determine whether there is a model mismatch, devmap(9E) uses the model parameter to pass the data model type expected by the application. model is set to one of the following:

Example F-2 shows the devmap(9E) model parameter being passed to the ddi_model_convert_from(9F) function.

Example F-2 devmap(9E)

struct data {
	int			len;
	caddr_t			addr;

xxdevmap(dev_t dev, devmap_cookie_t dhp, offset_t offset,
			size_t len, size_t *maplen, uint_t model);
	struct data dtc;  /* local copy for clash resolution */
	struct data *dp = (struct data *)shared_area;

	switch (ddi_model_convert_from(model)) {
	case DDI_MODEL_ILP32:
		struct data32 {
			int			len;
			uint32_t			*addr;
		} *da32p;

		da32p = (struct data32 *)shared_area;
		dp = &dtc;
		dp->len = da32p->len;
		dp->address = da32p->address;
#endif  /* _MULTI_DATAMODEL */
	/* continues along using dp */


Because mmap(9E) does not have a parameter that can be used to pass data model information, the driver's mmap(9E) entry point should be written to use the new DDI function ddi_mmap_get_model(9F). This function returns one of the following values to indicate the application's data type model:

As with ioctl(9E) and devmap(9E), the model bits can be passed to ddi_model_convert_from(9F) to determine whether data conversion is necessary.

Example F-3 shows the use of ddi_mmap_get_model(9F).

Example F-3 mmap(9E)

struct data {
	int			len;
	caddr_t			addr

xxmmap(dev_t dev, off_t off, int prot)
	struct data dtc;  /* local copy for clash resolution */
	struct data *dp = (struct data *)shared_area;

	switch (ddi_model_convert_from(ddi_mmap_get_model())) {
	case DDI_MODEL_ILP32:
		struct data32 {
			int			len;
			uint32_t			*addr
		} *da32p;

		da32p = (struct data32 *)shared_area;
		dp = &dtc;
		dp->len = da32p->len;
		dp->address = da32p->address;

#endif  /* _MULTI_DATAMODEL */
	/* continues along using dp */

Check Changed Fields in DDI Data Structures

The data type of some of the fields in DDI data structures such as buf(9S) have been changed. Drivers that use these data structures should make sure that these fields are being used appropriately. The data structures and the fields that were changed in a significant way are listed below.


size_t   b_bcount;      /* was type unsigned int */
size_t   b_resid;       /* was type unsigned int */
size_t   b_bufsize;      /* was type long */
The fields changed here pertain to transfer size, which can now exceed more than 4GB in future systems.


This structure defines attributes of the DMA engine and the device. Since these attributes specify register sizes, fixed-width data types have been used instead of fundamental types.


uint32_t     dmac_address;    /* was type unsigned long */
size_t     dmac_size;     /* was type u_int */
This structure contains a 32-bit DMA address, so a fixed-width data type has been used to define it. The size has been redefined as size_t.


u_int		pkt_flags;			/* was type u_long */
int			pkt_time;			/* was type long */
ssize_t	pkt_resid;			/* was type long */
u_int		pkt_state;			/* was type u_long */
u_int		pkt_statistics;	/* was type u_long */
Since the flags, state and statistics fields do not need to grow they have been redefined as 32-bit integers. The data transfer size pkt_resid field does grow and has been redefined as ssize_t.

Check Changed Arguments of DDI Functions

Some of the DDI functions argument data types have been changed. These routines are listed below.


struct buf *getrbuf(int sleepflag);
In previous releases, sleepflag was defined as a type long.


 int drv_getparm(unsigned int parm, void *value_p);
In previous releases, value_p was defined to be type unsigned long *.

In the 64-bit kernel, drv_getparm(9F) can be used to fetch quantities that are both 32-bit and 64-bit, yet the interface does not define the data types of these quantities which encourages simple programming errors.

The following new routines offer a safer alternative:

clock_t  ddi_get_lbolt(void);
time_t   ddi_get_time(void);
cred_t   *ddi_get_cred(void);
pid_t    ddi_get_pid(void);
Driver writers are strongly urged to use these routines instead of drv_getparm(9F).

delay(9F) and timeout(9F)

void delay(clock_t ticks);
timeout_id_t timeout(void (*func)(caddr_t), caddr_t arg, clock_t ticks);
The ticks argument to both of these routines has been changed from long to clock_t.

rmallocmap(9F) and rmallocmap_wait(9F)

struct map *rmallocmap(size_t mapsize);
struct map *rmallocmap_wait(size_t mapsize);
The mapsize argument to both of these routines has been changed from ulong_t to size_t.


struct buf *scsi_alloc_consistent_buf(struct scsi_address *ap,
   struct buf *bp, size_t datalen, uint_t bflags,
   int (*callback )(caddr_t), caddr_t arg);
In previous releases, datalen was defined as an int and bflags was defined as a ulong.


int uiomove(caddr_t address, size_t nbytes,
   enum uio_rw rwflag, uio_t *uio_p);
The nbytes argument was defined as a type long, but since it represents a size in bytes, size_t is more appropriate.

cv_timedwait(9F) and cv_timedwait_sig(9F)

 int cv_timedwait(kcondvar_t *cvp, kmutex_t *mp, clock_t timeout); 
int cv_timedwait_sig(kcondvar_t *cvp, kmutex_t *mp, clock_t timeout);
In previous releases, the timeout argument to both of these routines was defined to be of type long. Since they represent time in ticks, clock_t is more appropriate.


 int ddi_device_copy(ddi_acc_handle_t src_handle,
   caddr_t src_addr, ssize_t src_advcnt,
   ddi_acc_handle_t dest_handle, caddr_t dest_addr,
   ssize_t dest_advcnt, size_t bytecount, uint_t dev_datasz);
The src_advcnt, dest_advcnt, dev_datasz arguments have changed type. These were previously defined as long, long and ulong_t respectively.


int ddi_device_zero(ddi_acc_handle_t handle,
   caddr_t dev_addr, size_t bytecount, ssize_t dev_advcnt,
   uint_t dev_datasz):
In previous releases, dev_advcnt was defined as a type long and dev_datasz as a ulong_t.


int ddi_dma_mem_alloc(ddi_dma_handle_t handle,
   size_t length, ddi_device_acc_attr_t *accattrp,
   uint_t flags, int (*waitfp)(caddr_t), caddr_t arg,
   caddr_t *kaddrp, size_t *real_length,
   ddi_acc_handle_t *handlep);
In previous releases, length, flags and real_length were defined with types, uint_t, ulong_t and uint_t *.

Convert Well-Known Ioctl Interfaces

Many ioctl operations are common to the same class of device driver. For example, most disk drivers implement many of the dkio(7I) family of ioctls. Many of these interfaces copy in or copy out data structures from the kernel, and some of these data structures have changed size as a result of the LP64 data model. The following section lists the ioctls that now require explicit conversion in 64-bit driver ioctl routines for the dkio(7I), fdio(7I), fbio(7I),cdio(7I) and mtio(7I) families of ioctls.

Table F-3 Data Structures

ioctl Command 

Affected Data Structure 


 DKIOCGAPARTstruct dk_map()dkio(7I)
 DKIOCSAPARTstruct dk_allmap()
 DKIOGVTOC struct partition()dkio(7I)
 DKIOSVTOCstruct vtoc()
 FBIOPUTCMAPstruct fbcmap()fbio(7I)
 FBIOPUTCMAPIstruct fbcmap_i()fbio(7I)
 FBIOSCURSORstruct fbcursor()fbio(7I)
 CDROMREADMODE1struct cdrom_read()cdio(7I)
 CDROMCDDAstruct cdrom_cdda()cdio(7I)
 CDROMCDXAstruct cdrom_cdxa()cdio(7I)
 CDROMSUBCODEstruct cdrom_subcode()cdio(7I)
 FDIOCMDstruct fd_cmd()fdio(7I)
 FDRAWstruct fd_raw()fdio(7I)
 MTIOCTOPstruct mtop()mtio(7I)
 MTIOCGETstruct mtget()mtio(7I)
 MTIOCGETDRIVETYPEstruct mtdrivetype_request()mtio(7I)
 USCSICMDstruct uscsi_cmd() Undocumented

Device Sizes

The nblocks property is exported by each slice of a block device drivers. It contains the number of 512 byte blocks that each slice of the device can support. The nblocks property is defined as a signed 32-bit quantity, which limits the maximum number of disk blocks to 1Tbyte.

Disk devices that provide more than 1Tbyte of storage per disk, must define the Nblocks property, which should still contain the number of 512 byte blocks that the device can support. However, Nblocks is a signed 64-bit quantity, which removes any practical limit on disk space.

The nblocks property is now deprecated; all disk devices should provide the Nblocks property.

Data Structure Macros

There are two ways to write the ioctl handler of a driver to deal with the possible differences between the data model of the the user application. This section presents one method, using a new set of macros when referring to the data structure from the user application.

While the method shown in Example F-1 will work well for many drivers, this alternate scheme is to use the data structure macros provided in <sys/model.h> to move data between the application and the kernel. These tend to make the code less cluttered, and behave identically from a functional perspective. Here's the first example recoded using the data structures macros.

Example F-4 ioctl(9E)

xxioctl(dev_t dev, int cmd, intprt_t arg, int mode,
				cred_t *cr, int *rval_p)
		STRUCT_DECL(opdata, op);

		if (cmd != OPONE)
			return (ENOTTY);

		STRUCT_INIT(op, mode);

		if (copyin((void *)arg,
				return (EFAULT);

		if (STRUCT_FGET(op,flag) != XXACTIVE ||
			STRUCT_FGET(op, size) > XXSIZE)
				return (EINVAL);

		xxdwork(device_state, STRUCT_FGET(op, size));
		return (O);

How Structure Macros Work

In a 64-bit device driver, these macros do all that is necessary to use the same piece of kernel memory as a buffer for the contents of the native form of the data structure (that is, the LP64 form), and for the ILP32 form of the same structure. This usually means that each structure access is implemented by a conditional expression. When compiled as a 32-bit driver, since there is only one supported data model, there is only native form, so no conditional expression is used.

The 64-bit versions of the macros depend on the definition of a shadow version of the data structure which describes the 32-bit interface using fixed-width types. The name of the shadow data structure is formed by appending "32" to the name of the native data structure. It is convenient to place the definition of the shadow structure in the same file as the native structure to ease future maintenance costs.

The macros take arguments such as:


The structure name (as would appear after the struct keyword) of the native form of the data structure


A flag word containing the user data model, such as FILP32 or FLP64, extracted from the mode parameter of ioctl(9E)


The name used to refer to a particular instance of a structure which is manipulated by these macros


The name of the field within the structure

Declaring and Initializing Structure Handles

STRUCT_DECL(9F) and STRUCT_INIT(9F) can be used to declare and initialize a handle and space for decoding an ioctl on the stack. STRUCT_HANDLE(9F) and STRUCT_SET_HANDLE(9F) declare and initialize a handle without allocating space on the stack. The latter macros can be useful if the structure is very large, or is contained in some other data structure.

Note -

Since the STRUCT_DECL() and STRUCT_HANDLE() macros expand to data structure declarations, they should be grouped with such declarations in C code.

STRUCT_DECL(structname, handle)

Declares a structure handle called handle for a struct structname data structure, and allocated space for its native form on the stack. It is assumed that the native form is larger than or equal to the ILP32 form of the structure.

STRUCT_INIT(handle, umodel)

Initializes the date model for handle to umodel. This macro must be invoked before any access is made to a structure handle declared with STRUCT_DECL(9F).

STRUCT_HANDLE(structname, handle)

Declares a structure handle called handle. Contrast with STRUCT_DECL(9F).

STRUCT_SET_HANDLE(handle, umodel, addr)

Initializes the data model for handle to umodel, and set addr as the buffer used for subsequent manipulation. This macro must be invoked before any access is made to a structure handle declared with STRUCT_HANDLE(9F).

Operations on Structure Handles

size_t STRUCT_SIZE(handle)

Returns the size of the structure referred to by handle according to its embedded data model.

typeof fieldname STRUCT_FGET(handle, fieldname)

Returns the indicated field (non-pointer type) in the data structure referred to by handle.

typeof fieldname STRUCT_FGETP(handle, fieldname)

Returns the indicated field (pointer type) in the data structure referred to by handle.

STRUCT_FSET(handle, fieldname, val)

Sets the indicated field (non-pointer type) in the data structure referred to by handle to value val. The type of val should match the type of fieldname.

STRUCT_FSETP(handle, fieldname, val)

Sets the indicated field (pointer type) in the data structure referred to by handle to value val.

typeof fieldname *STRUCT_FADDR(handle, fieldname)

Returns the address of the indicated field in the data structure referred to by handle.

struct structname *STRUCT_BUF(handle)

Returns a pointer to the native structure described by handle.

Other Operations

size_t SIZEOF_STRUCT(structname, datamodel)

Returns the size of struct_name based on the given data model.

size_t SIZEOF_PTR(datamodel)

Returns the size of a pointer based on the given data model.

When to Use Structure Macros

Macros only allow you to make in-place references to the fields of a data item. They do not provide a way to take separate code paths based on the data model. They should be avoided if the number of fields in the data structure is large or the frequency of references to these fields is high.

Because macros hide many of the differences between data models in the implementation of the macros, code written with this interface is generally easier to read.

When compiled as a 32-bit driver, the resulting code is compact without needing clumsy #ifdefs, while preserving type checking.

Macros are best suited for making in-place references to the fields of a data structure, particularly if the number of fields in the data structure is small and the frequency of references to these fields is low.