Guia de rastreamento dinâmico Solaris

Capítulo 7 Structs e uniões

As coleções de variáveis relacionadas podem ser agrupadas em objetos de dados compostos chamados structs e uniões. Você pode definir esses objetos em D criando novas definições de tipo para eles. Você pode usar seus novos tipos para quaisquer variáveis de D, incluindo valores de matriz de associação. Este capítulo explora a sintaxe e a semântica para a criação e manipulação desses tipos compostos e os operadores de D que interagem com eles. A sintaxe das structs e das uniões é ilustrada através de vários programas de exemplo que demonstram o uso dos provedores fbt e pid do DTrace.

Structs

A palavra-chave de D struct, abreviação de structure (estrutura) é usada para introduzir um novo tipo composto de um grupo de outros tipos. O novo tipo struct pode ser usado como o tipo de variáveis e matrizes de D, permitindo que você defina grupos de variáveis relacionadas sob um único nome. As structs de D são o mesmo que a construção correspondente em C e C++. Caso você tenha programado na linguagem de programação Java, pense em uma struct de D como uma classe, mas uma classe que tenha apenas membros de dados, não métodos.

Vamos supor que você queira criar um programa de rastreio de chamada do sistema mais sofisticado em D, que registre inúmeras informações sobre cada chamada do sistema a read(2) e write(2) executada pelo shell, tal como o tempo decorrido, o número de chamadas e a maior contagem de bytes passada como um argumento. Você poderia escrever uma cláusula de D para registrar essas propriedades em três matrizes de associação separadas, como mostrado no exemplo seguinte:

syscall::read:entry, syscall::write:entry
/pid == 12345/
{
	ts[probefunc] = timestamp;
	calls[probefunc]++;
	maxbytes[probefunc] = arg2 > maxbytes[probefunc] ?
	    arg2 : maxbytes[probefunc];
}

Entretanto, esta cláusula é ineficiente porque o DTrace deve criar três matrizes de associação separadas e armazenar cópias separadas dos valores de tupla idênticos correspondentes a probefunc para cada uma. Em vez disso, você pode conservar espaço e facilitar a leitura e a manutenção do seu programa usando uma struct. Primeiro, declare um novo tipo de struct no início do arquivo-fonte do programa:

struct callinfo {
	uint64_t ts;      /* timestamp of last syscall entry */
	uint64_t elapsed; /* total elapsed time in nanoseconds */
	uint64_t calls;   /* number of calls made */
	size_t maxbytes;  /* maximum byte count argument */
};

A palavra-chave struct é seguida por um identificador opcional usado para fazer referência ao nosso novo tipo, que agora é conhecido como struct callinfo. Os membros de struct são colocados entre um conjunto de chaves { } e a declaração inteira é terminada por um ponto-e-vírgula (; ). Cada membro de struct é definido através da mesma sintaxe que uma declaração de variável de D, com o tipo do membro listado primeiro, seguido por um identificador que nomeia o membro e outro ponto-e-vírgula (;).

A própria declaração struct simplesmente define o novo tipo; ela não cria quaisquer variáveis ou aloca qualquer armazenamento no DTrace. Quando declarada, você pode usar struct callinfo como um tipo no restante do seu programa em D, e cada variável de tipo struct callinfo armazenará uma cópia das quatro variáveis descritas por nosso modelo de estrutura. Os membros serão organizados na memória na ordem da lista de membros, com espaço de preenchimento introduzido entre membros, conforme necessário, para fins de alinhamento de objeto de dados.

Você pode usar os nomes de identificador de membro para acessar valores de membro individual usando o operador “.” escrevendo uma expressão do formato:

nome da variável. nome do membro

O exemplo seguinte é um programa aprimorado que usa o novo tipo de estrutura. Em um editor, digite o seguinte programa em D e salve-o em um arquivo chamado rwinfo.d:


Exemplo 7–1 rwinfo.d: coletar estatísticas de read(2) e write(2)

struct callinfo {
	uint64_t ts;      /* timestamp of last syscall entry */
	uint64_t elapsed; /* total elapsed time in nanoseconds */
	uint64_t calls;   /* number of calls made */
	size_t maxbytes;  /* maximum byte count argument */
};

struct callinfo i[string];	/* declare i as an associative array */

syscall::read:entry, syscall::write:entry
/pid == $1/
{
	i[probefunc].ts = timestamp;
	i[probefunc].calls++;
	i[probefunc].maxbytes = arg2 > i[probefunc].maxbytes ?
		arg2 : i[probefunc].maxbytes;
}

syscall::read:return, syscall::write:return
/i[probefunc].ts != 0 && pid == $1/
{
	i[probefunc].elapsed += timestamp - i[probefunc].ts;
}

END
{
	printf("        calls  max bytes  elapsed nsecs\n");
	printf("------  -----  ---------  -------------\n");
	printf("  read  %5d  %9d  %d\n",
	    i["read"].calls, i["read"].maxbytes, i["read"].elapsed);
	printf(" write  %5d  %9d  %d\n",
	    i["write"].calls, i["write"].maxbytes, i["write"].elapsed);
}

Depois que você digitar o programa, execute o dtrace -q -s rwinfo.d, especificando um dos processos do shell. Em seguida, digite alguns comandos no shell e, quando terminar de inserir os comandos do shell, digite Control-C no terminal do dtrace para acionar o teste END e imprimir os resultados:


# dtrace -q -s rwinfo.d `pgrep -n ksh`
^C
        calls  max bytes  elapsed nsecs
------  -----  ---------  -------------
  read     36       1024  3588283144
 write     35         59  14945541
#

Ponteiros para structs

Fazer referência a structs usando ponteiros é muito comum em C e em D. Você pode usar o operador -> para acessar membros de struct através de um ponteiro. Se uma struct s tiver um membro m e você possuir um ponteiro para esse struct chamado sp (ou seja, sp é uma variável do tipo struct s *), use o operador * para primeiro cancelar a referência do ponteiro sp a fim de acessar o membro:

struct s *sp;

(*sp).m

ou use o operador -> como um atalho para essa notação. Os dois fragmentos seguintes de D terão significados equivalentes, se sp for um ponteiro para uma struct:

(*sp).m				sp->m

O DTrace oferece algumas variáveis incorporadas que são ponteiros para structs, incluindo curpsinfo e curlwpsinfo. Esses ponteiros se referem respectivamente às structs psinfo e lwpsinfo, e seu conteúdo fornece um instantâneo de informações sobre o estado do processo atual e o processo leve (LWP) associado ao segmento que acionou o teste atual. Um Solaris LWP é a representação do kernel de um segmento do usuário, sobre o qual os segmentos do Solaris e as interfaces dos segmentos do POSIX são construídos. Por questões de conveniência, o DTrace exporta as informações no mesmo formato que os arquivos do sistema de arquivos /proc /proc/ pid/psinfo e /proc/pid/lwps/ lwpid/lwpsinfo. As estruturas /proc são usadas por ferramentas de observação e de depuração, tais como ps(1), pgrep(1) e truss(1) e são definidas no arquivo de cabeçalho do sistema de arquivos <sys/procfs.h> e são descritas na página do manual proc(4). Aqui estão algumas expressões de exemplo que usam curpsinfo, seus tipos e significados:

curpsinfo->pr_pid

pid_t

ID do processo atual 

curpsinfo->pr_fname

char []

nome do arquivo executável 

curpsinfo->pr_psargs

char []

argumentos iniciais da linha de comando 

Você deve revisar a definição de estrutura completa mais tarde, examinando o arquivo de cabeçalho <sys/procfs.h> e as descrições correspondentes em proc(4). O próximo exemplo usa o membro pr_psargs para identificar um processo do seu interesse, coincidindo os argumentos da linha de comando.

As structs são usadas freqüentemente para criar estruturas de dados complexas de programas em C,sendo assim, a capacidade de descrever e referenciar structs a partir de D também oferece um recurso poderoso de observação do funcionamento interno do kernel do sistema operacional Solaris e suas interfaces de sistema. Além disso, para usar a struct curpsinfo, mencionada anteriormente, o próximo exemplo também examina algumas structs do kernel, observando a relação entre o driver de ksyms(7D) e as solicitações de read(2) O driver usa duas structs comuns, conhecidas como uio(9S) and iovec(9S), para responder a solicitações de leitura a partir do arquivo de dispositivo de caractere /dev/ksyms.

A struct uio, acessada através do nome struct uio ou do alias de tipo uio_t, é descrita na página do manual uio(9S) e é usada para descrever uma solicitação de E/S que envolve a cópia de dados entre o kernel e um processo do usuário. uio, por sua vez, contém uma matriz de uma ou mais estruturas iovec(9S), cada uma descrevendo uma parte da E/S solicitada, caso vários blocos sejam solicitados por meio das chamadas do sistemareadv(2) ou writev(2) Uma das rotinas de interface de driver de dispositivo (DDI) do kernel que opera na struct uio é a função uiomove(9F), que é uma das famílias que os drivers do kernel de funções usam para responder às solicitações read(2) do processo do usuário e copiar os dados de volta para os processos do usuário.

O driver ksyms gerencia um arquivo de dispositivo de caractere chamado /dev/ksyms, que parece ser um arquivo ELF que contém informações sobre a tabela de símbolo do kernel, mas que é, de fato, uma ilusão criada pelo driver usando o conjunto de módulos que estão carregados no momento no kernel. O driver usa a rotina uiomove(9F) para responder às solicitações de read(2). O próximo exemplo ilustra que os argumentos e as chamadas para read(2) de /dev/ksyms correspondem às chamadas do driver para uiomove(9F) a fim de copiar os resultados de volta para o espaço de endereço do usuário no local especificado para read(2).

Podemos usar o utilitário strings(1) com a opção -a para forçar várias leituras de /dev/ksyms . Tente executar strings -a /dev/ksyms em seu shell e veja qual saída é produzida. Em um editor, digite a primeira cláusula do script de exemplo e salve-a em um arquivo chamado ksyms.d:

syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
}

Esta primeira cláusula usa a expressão curpsinfo->pr_psargs para acessar e coincidir os argumentos da linha de comando do nosso comando strings(1), para que o script selecione as solicitações corretas de read(2) antes de rastrear os argumentos. Observe que usando o operador == com um argumento esquerdo que seja uma matriz de char e um argumento direito que seja uma seqüência, o compilador de D deduz que o argumento esquerdo deve ser promovido para uma seqüência e uma comparação de seqüência deve ser realizada. Digite e execute o comando dtrace -q -s ksyms.d em um shell e, em seguida, digite o comando strings -a /dev/ksyms em outro shell. Enquanto strings(1) é executado, você verá a saída do DTrace semelhante ao exemplo seguinte:


# dtrace -q -s ksyms.d
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
read 8192 bytes to user address 80639fc
...
^C
#

Este exemplo pode ser estendido com uma técnica de programação comum em D para seguir um segmento a partir desta solicitação de read(2) inicial até um local mais profundo no kernel. Na entrada do kernel em syscall::read:entry, o próximo script define uma variável de sinalizador local de segmento, indicando que esse segmento é do seu interesse, e limpa esse sinalizador em syscall::read:return. Quando é definido, o sinalizador pode ser usado como um predicado em outros testes para instrumentar as funções do kernel, tal como uiomove(9F). O provedor de rastreio de limite de função do DTrace ( fbt) publica os testes de entrada e retorna para as funções definidas no kernel, incluindo as da DDI. Digite o seguinte código-fonte, que usa o provedor fbt para instrumentaruiomove(9F) e salve-o novamente no arquivo ksyms.d:


Exemplo 7–2 ksyms.d: rastreio de relação de read(2) e uiomove(9F)

/*
 * When our strings(1) invocation starts a read(2), set a watched flag on
 * the current thread.  When the read(2) finishes, clear the watched flag.
 */
syscall::read:entry
/curpsinfo->pr_psargs == "strings -a /dev/ksyms"/
{
	printf("read %u bytes to user address %x\n", arg2, arg1);
	self->watched = 1;
}

syscall::read:return
/self->watched/
{
	self->watched = 0;
}

/*
 * Instrument uiomove(9F).  The prototype for this function is as follows:
 * int uiomove(caddr_t addr, size_t nbytes, enum uio_rw rwflag, uio_t *uio);
 */
fbt::uiomove:entry
/self->watched/
{
	this->iov = args[3]->uio_iov;

	printf("uiomove %u bytes to %p in pid %d\n",
	    this->iov->iov_len, this->iov->iov_base, pid);
}

A cláusula final do exemplo usa a variável local de segmento self->watched para identificar quando um segmento do kernel de seu interesse entra na rotina DDI uiomove(9F). Uma vez lá, o script usa a matriz incorporada args para acessar o quarto argumento (args[3]) para uiomove(), que é um ponteiro para a struct uio que representa a solicitação. O compilador de D associa automaticamente cada membro da matriz args com o tipo correspondente ao protótipo da função de C da rotina do kernel instrumentado. O membro uio_iov contém um ponteiro para struct iovec da solicitação. Uma cópia deste ponteiro é salva para que seja usada na nossa cláusula na variável local de cláusula this->iov. Na declaração final, o script cancela a referência de this->iov para acessar os membros iovec, iov_len e iov_base, que representam o tamanho em bytes e o endereço base de destino de uiomove(9F), respectivamente. Esses valores devem corresponder aos parâmetros de entrada para a chamada do sistema de read(2) emitida no driver. Vá para o shell, execute dtrace -q -s ksyms.d e insira novamente o comando strings -a /dev/ksyms em outro shell. Você deve ver uma saída semelhante ao exemplo seguinte:


# dtrace -q -s ksyms.d
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
read 8192 bytes at user address 80639fc
uiomove 8192 bytes to 80639fc in pid 101038
...
^C
#

Os endereços e os IDs de processo serão diferentes na sua saída, mas você deve observar que os argumentos de entrada para read(2) correspondem aos parâmetros passados para uiomove(9F) pelo driver ksyms.

Uniões

As uniões são outra espécie de tipo composto aceito por ANSI-C e D, e estão fortemente relacionadas às structs. Uma união é um tipo composto onde um conjunto de membros de tipos diferentes são definidos e todos os objetos de membro ocupam a mesma região de armazenamento. Uma união é, portanto, um objeto de tipo variado, onde somente um membro é válido a qualquer momento determinado, dependendo de como a união foi atribuída. Geralmente, alguma outra variável ou parte de estado é usada para indicar que o membro da união é válido no momento. O tamanho de uma união é o tamanho do maior membro, e o alinhamento de memória usado para a união é o alinhamento máximo necessário para os membros da união.

A estrutura kstat do Solaris define uma struct contendo uma união que é usada no exemplo seguinte para ilustrar e observar as uniões de C e de D. A estrutura kstat é usada para exportar um conjunto de contadores nomeados representando as estatísticas do kernel, tal como o uso da memória e o throughput de E/S. A estrutura é usada para implementar utilitários como mpstat(1M) e iostat(1M) Esta estrutura usa struct kstat_named para representar um contador nomeado e seu valor, e é definido da seguinte forma:

struct kstat_named {
	char name[KSTAT_STRLEN]; /* name of counter */
	uchar_t data_type;	/* data type */
	union {
		char c[16];
		int32_t i32;
		uint32_t ui32;
		long l;
		ulong_t ul;
		...
	} value;	/* value of counter */
};	

A declaração de inspeção é abreviada para fins ilustrativos. A definição de estrutura completa pode ser encontrada no arquivo de cabeçalho <sys/kstat.h> e é descrita em kstat_named(9S). A declaração acima é válida em ANSI-C e em D, e define uma struct contendo, como um de seus membros, um valor de união com membros de vários tipos, dependendo do tipo do contador. Observe que, já que a própria união é declarada dentro de outro tipo, struct kstat_named, um nome formal do tipo de união é omitido. Este estilo de declaração é conhecido como união anônima. O membro nomeado valor é de um tipo de união descrito pela declaração precedente, mas esse tipo de união não tem nome porque ele não precisa ser usado em nenhum outro local. É atribuído ao membro struct data_type um valor que indica qual membro da união é válido para cada objeto do tipo struct kstat_named. Um conjunto de símbolos de pré-processadores de C são definidos para os valores de data_type. Por exemplo, o símbolo KSTAT_DATA_CHAR é igual a zero e indica que o membro value.c se encontra onde o valor está armazenado no momento.

O Exemplo 7–3 demonstra o acesso à união kstat_named.value rastreando um processo do usuário. Os contadores kstat podem ser exemplificados a partir de um processo do usuário por meio da função kstat_data_lookup(3KSTAT), que retorna um ponteiro para struct kstat_named. O utilitário mpstat(1M) chama esta função repetidamente enquanto é executado, a fim de exemplificar os valores de contador mais recentes. Vá para o seu shell e tente executar mpstat 1 e observe a saída. Pressione Control-C em seu shell para anular mpstat após alguns segundos. Para observar a amostra do contador, gostaríamos de ativar um teste que seja acionado cada vez que o comando mpstat chamar a função kstat_data_lookup(3KSTAT) em libkstat. Para fazê-lo, usaremos um novo provedor do DTrace: pid. O provedor pid permite que você crie os testes dinamicamente nos processos do usuário em locais de símbolo de C, tais como pontos de entrada de função. Você pode pedir ao provedor pid para criar um teste em uma entrada de função do usuário e retornar os sites, escrevendo as descrições do teste no formato:

pidID do processo:nome do objeto:nome da função:entry pidID do processo:nome do objeto:nome da função:return

Por exemplo, se você quisesse criar um teste no ID do processo 3 que é acionado na entrada para kstat_data_lookup(3KSTAT), você escreveria a seguinte descrição de teste:

pid12345:libkstat:kstat_data_lookup:entry

O provedor pid insere a instrumentação dinâmica no processo do usuário especificado no local do programa correspondente à descrição do teste. A implementação do teste força cada segmento do usuário que alcança o local do programa instrumentado a entrar no kernel do sistema operacional e no DTrace, acionando o teste correspondente. Sendo assim, embora o local da instrumentação esteja associado a um processo do usuário, os predicados e as ações do DTrace que você especificar ainda serão executados no contexto do kernel do sistema operacional. O provedor pid é descrito em maiores detalhes no Capítulo 30Provedor pid.

Em vez de ter que editar o fonte do programa em D toda vez que desejar aplicar seu programa a um processo diferente, você pode inserir identificadores chamados variáveis de macro em seu programa que são avaliados no momento em que o programa é compilado e substituído pelos argumentos da linha de comando adicionais do dtrace. As variáveis de macro são especificadas por meio do sinal de dólar $ seguido por um identificador ou dígito. Se você executar o comando dtrace -s script foo bar baz, o compilador de D definirá automaticamente as variáveis de macro $1, $2 e $3 aos símbolos foo, bar e baz, respectivamente. Você pode usar as variáveis de macro nas expressões de programa em D ou em descrições de teste. Por exemplo, as descrições de teste seguintes instrumentam qualquer ID de processo que seja especificado como um argumento adicional para dtrace:

pid$1:libkstat:kstat_data_lookup:entry
{
	self->ksname = arg1;
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 != NULL/
{
	this->ksp = (kstat_named_t *)copyin(arg1, sizeof (kstat_named_t));
	printf("%s has ui64 value %u\n", copyinstr(self->ksname),
	    this->ksp->value.ui64);
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 == NULL/
{
	self->ksname = NULL;
}

As variáveis de macro e os scripts reutilizáveis são descritos em maiores detalhes no Capítulo 15Script. Agora que sabemos como instrumentar os processos do usuário usando o ID de processo deles, vamos retornar às uniões de amostra. Vá para o editor e digite o código-fonte do nosso exemplo completo e salve-o em um arquivo chamado kstat.d:


Exemplo 7–3 kstat.d: rastrear chamadas para kstat_data_lookup(3KSTAT )

pid$1:libkstat:kstat_data_lookup:entry
{
	self->ksname = arg1;
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 != NULL/
{
	this->ksp = (kstat_named_t *) copyin(arg1, sizeof (kstat_named_t));
	printf("%s has ui64 value %u\n",
	    copyinstr(self->ksname), this->ksp->value.ui64);
}

pid$1:libkstat:kstat_data_lookup:return
/self->ksname != NULL && arg1 == NULL/
{
	self->ksname = NULL;
}

Agora vá para um dos seus shells e execute o comando mpstat 1 para iniciar o mpstat(1M) sendo executado em um modo em que ele faz amostra de estatísticas e informa-as uma por segundo. Quando o mpstat estiver em execução, execute o comando dtrace -q -s kstat.d `pgrep mpstat` em seu outro shell. Você verá a saída correspondente às estatísticas que estão sendo acessadas. Pressione Control-C para anular dtrace e retornar para o prompt do shell.


# dtrace -q -s kstat.d `pgrep mpstat`
cpu_ticks_idle has ui64 value 41154176
cpu_ticks_user has ui64 value 1137
cpu_ticks_kernel has ui64 value 12310
cpu_ticks_wait has ui64 value 903
hat_fault has ui64 value 0
as_fault has ui64 value 48053
maj_fault has ui64 value 1144
xcalls has ui64 value 123832170
intr has ui64 value 165264090
intrthread has ui64 value 124094974
pswitch has ui64 value 840625
inv_swtch has ui64 value 1484
cpumigrate has ui64 value 36284
mutex_adenters has ui64 value 35574
rw_rdfails has ui64 value 2
rw_wrfails has ui64 value 2
...
^C
#

Se capturar a saída em cada janela do terminal e subtrair cada valor do valor informado pela iteração anterior através das estatísticas, você deve ser capaz de correlacionar a saída do dtrace com a saída do mpstat. O programa de exemplo registra o ponteiro do nome do contador na entrada para a função de consulta e, em seguida, realiza a maior parte do trabalho de rastreio no retorno de kstat_data_lookup(3KSTAT). As funções incorporadas de D copyinstr() e copyin() copiam os resultados da função do processo do usuário para o DTrace quando arg1 (o valor de retorno) não é NULL. Quando os dados de kstat tiverem sido copiados, o exemplo informará o valor do contador ui64 a partir da união. Este exemplo simplificado assume que mpstat faz amostras de contadores que usam o membro value.ui64. Como um exercício, tente registrar kstat.d para usar vários predicados e imprimir a união correspondente ao membro data_type. Você também pode tentar criar uma versão de kstat.d que calcule a diferença entre valores de dados sucessivos e produza realmente uma saída semelhante a mpstat.

Deslocamentos e tamanhos de membro

Você pode determinar o tamanho em bytes de qualquer tipo ou expressão de D, incluindo uma struct ou união, usando o operador sizeof. O operador sizeof pode ser aplicado a uma expressão ou ao nome de um tipo entre parênteses, como ilustrado pelos dois exemplos seguintes:

sizeof expression				sizeof (type-name)

Por exemplo, a expressão sizeof (uint64_t) retornaria o valor 8, e a expressão sizeof (callinfo.ts) retornaria 8, se inserida no código-fonte do nosso programa de exemplo acima. O tipo de retorno formal do operador sizeof é o alias de tipo size_t, que é definido como um inteiro não-assinado do mesmo tamanho que um ponteiro no modelo de dados atual, e é usado para representar as contagens de byte. Quando o operador sizeof é aplicado a uma expressão, a expressão é validada pelo compilador de D, mas o tamanho de objeto resultante é calculado no momento da compilação e nenhum código da expressão é gerado. Você pode usar sizeof em qualquer lugar que uma constante de inteiro é necessária.

Você pode usar o operador acompanhante offsetof para determinar o desvio em bytes de um membro de struct ou de união a partir do início do armazenamento associado a qualquer objeto do tipo de união ou de struct. O operador offsetof é usado em uma expressão do seguinte formato:

offsetof (type-name, member-name)

Aqui nome do tipo é o nome de qualquer tipo de struct ou de união ou alias de tipo, e nome do membro é o identificador que nomeia um membro dessa struct ou união. Semelhante a sizeof, offsetof retorna size_t e pode ser usado em qualquer lugar em um programa em D onde uma constante de inteiro possa ser usada.

Campos de bit

D também permite a definição de membros de struct de inteiro e de união com números de bits arbitrários, conhecidos como campos de bit. Um campo de bit é declarado pela especificação de um tipo base de inteiro assinado ou não assinado, um nome de membro e um sufixo indicando o número de bits a ser atribuído ao campo, como mostrado no exemplo seguinte:

struct s {
	int a : 1;
	int b : 3;
	int c : 12;
};

A largura do campo de bit é uma constante de inteiro separada do nome do membro por uma vírgula à direita. A largura do campo de bit deve ser positiva e deve ter um número de bits que não seja maior que a largura do tipo base de inteiro correspondente. Os campos de bit maiores que 64 bits não podem ser declarados em D. Os campos de bits de D fornecem compatibilidade e acesso ao recurso ANSI-C correspondente. Os campos de bit são geralmente usados em situações em que o armazenamento de memória seja pelo menos premium ou quando um layout de struct deve corresponder ao layout de registro de hardware.

Um campo de bits é uma construção de compilador que automatiza o layout de um inteiro e um conjunto de máscaras para extrair os valores de membro. O mesmo resultado pode ser atingido pela simples definição de máscaras e pelo uso do operador &. Os compiladores de C e de D tentam empacotar bits, da forma mais eficiente possível, mas eles estão livres para fazê-lo em qualquer ordem ou de qualquer maneira, sendo assim, não há garantias de que os campos de bit produzam layouts de bit idênticos entre compiladores ou arquiteturas diferentes. Se você precisa de layout de bit estável, construa as máscaras de bits e extraia os valores usando o operador &.

Um membro de campo de bit é acessado pela simples especificação de seu nome em combinação com os operadores “.” ou ->, como qualquer outra struct ou membro de união. O campo de bit é automaticamente promovido para o próximo tipo de inteiro maior a fim de ser usado em quaisquer expressões. Como o armazenamento de campo de bit pode não estar alinhado em um limite de bytes ou ser um número de bytes arredondado em tamanho, você não pode aplicar os operadores sizeof ou offsetof a um membro de campo de bits. O compilador de D também proíbe você de pegar o endereço de um membro de campo de bit usando o operador &.