9. Convenções de programação de funções

Com vista à estruturação dos programas de modo a fazer-se uma boa utilização dos recursos (memória e processador) e à reutilização e análise de partes de programas (funções e variáveis), é conveniente usarem-se convenções de programação. Designadamente, representação dos tipos de dados, parâmetros de funções, retorno de valor de funções e vocação dos registos.

Nos exemplos de programa apresentados são utilizadas as convenções descritas seguidamente.

9.1. Tipos de dados

Os tipos numéricos são codificados em código binário natural ou em código dos complementos para dois, usando 8, 16, 32 ou 64 bits. Os dados dos programas são representados segundo estes tipos em valores simples ou em array.

Tabela 9.1 Representação dos tipos numéricos

Tipo

Memória (bits)

Registo (bits)

char

8

16

short

16

16

int

16

16

long

32

32

uint8_t e int8_t

8

16

uint16_t e int16_t

16

16

uint32_t e int32_t

32

32

uint64_t e int64_t

64

64

Na Tabela 9.1 apresentam-se as dimensões em número de bits com que são representados os tipos numéricos em função do suporte material: memória principal ou registo do processador.

Os tipos representados na memória com 8 bits são representados nos registos do processador com 16 bits. Esta diferença visa preparar os valores para serem operados pelo P16, que apenas realiza operações na dimensão dos registos.

O tipo char é um tipo para números naturais (unsigned) e os tipos short, int e long são tipos para números relativos (signed).

Os tipos representados em memória com dimensões superiores a 16 bits ocupam os registos necessários até perfazer a totalidade da dimensão ou são processados por partes.

9.2. Passagem de argumentos

As funções que comportam parâmetros recebem os argumentos nos registos do processador, ocupando a quantidade necessária, por ordem: R0, R1, R2 e R3.

void f(uint16_t a,  int8_t b,  uint8_t c,  char d);
               r0         r1          r2       r3

Os argumentos dos parâmetros de tipos representados a 8 bits – char, uint8_t ou int8_t – são convertidos para representação a 16 bits.

Tabela 9.2 Passagem de argumentos de vários tipos
uint16_t x = 20;
int8_t y = -4;

f(x, y, 10, 'a');

(a)

 1    .data
 2x:
 3    .word    20
 4y:
 5    .byte    -4
 6
 7    .text
 8    ldr    r0, x_addr
 9    ldr    r0, [r0]
10    ldr    r1, y_addr
11    ldrb   r1, [r1]
12    lsl    r1, r1, #8
13    asr    r1, r1, #8
14    mov    r2, #10
15    mov    r3, #'a'
16    bl     f
17
18x_addr:
19   .word   x
20y_addr:
21   .word   y

(b)

Argumento a 32 bits

Se o tipo do parâmetro for um valor codificado a 32 bits, o argumento é colocado em dois registos consecutivos. Cabendo ao registo de menor índice a parte de menor peso do argumento.

void f(uint8_t a,   int32_t b,  int c);
              r0        r2:r1      r3

No programa (b) da Tabela 9.3, a variável x é definida na linha 3 com a diretiva .word. Os dois argumentos têm o efeito de reservar espaço para duas words em memória. A word de endereço menor (a primeira no sentido da escrita) recebe a parte de menor peso e a seguinte recebe a parte de maior peso. Como o número é negativo a parte de maior peso tem todos os bits a 1, por isso se utilizou a expressão ~0 para a inicializar.

Tabela 9.3 Passagem de argumento codificado a 32 bits
int32_t x = -20;
int y = 10000;

f(-4, x, y);

(a)

 1    .data
 2x:
 3    .word    -20, ~0
 4y:
 5    .word    10000
 6
 7    .text
 8    mov    r0, #-4 & 0xff
 9    movt   r0, #-4 >> 8
10    ldr    r2, x_addr
11    ldr    r1, [r2, #0]
12    ldr    r2, [r2, #2]
13    ldr    r3, y_addr
14    ldrb   r3, [r3]
15    bl     f
16
17x_addr:
18   .word   x
19y_addr:
20   .word   y

(b)

Array como parâmetro

Se o parâmetro for um array, independentemente do tipo dos elementos, o que é passado como argumento é o endereço da primeira posição do array.

void f(uint16_t array[], uint16_t array_size);
                  r0               r1

No programa da Tabela 9.4, o primeiro argumento é o endereço da primeira posição do array. Em assembly corresponde à label array:. É carregado em R0 com a instrução ldr r0, array_addr da forma convencional de carregamento de endereços de variáveis em registo – Carregamento de endereço em registo.

O segundo argumento é o número de elementos do array. Este valor é calculado pela diferença de endereços das labels array_end: e array: (linha 8), que é a dimensão do array em número de bytes, dividida pela dimensão de cada elemento do arraylsr    r1, r1, #1.

Tabela 9.4 Passagem de array como argumento
int16_t array[] = {-20, 0, 10, -15};

f(array, sizeof array / sizeof array[0]);

(a)

 1    .data
 2array:
 3    .word    -20, 0, 10, -15
 4array_end:
 5
 6    .text
 7    ldr    r0, array_addr
 8    mov    r1, #(array_end - array) / 2
 9    bl     f
10
11array_addr:
12    .word   array

(b)

Argumentos em stack

Se os argumentos ocuparem mais que os quatro registos, os restantes são passados no stack. Sendo o argumento que se escreve mais à direita na linguagem C, o primeiro a ser empilhado.

int16_t sum(int8_t a, int16_t b, int8_t c, int16_t d, int8_t e, int16_t f)
                  r0         r1        r2         r3     stack      stack

No programa (b) da Tabela 9.5 começa por se processar os argumentos a passar em stack. Nas linhas 14 a 16 empilha-se o argumento -3. Primeiro -3 é carregado em R0 pelos movs das linhas 14 e 15 e em seguida empilhado no stack com a instrução push r0.

O argumento z sofre um processo semelhante, primeiro o seu conteúdo é carregado em R0 (linhas 17 a 20) e em seguida é empilhado na posição seguinte do stack. Note que este parâmetro (int8_t e) é de tipo representado a 8 bits, mas é passado com representação a 16 bits, tal como acontece com a passagem em registo.

Os restantes argumentos são passados nos registos R0 a R3 da forma convencional (linhas 22 a 29).

Tabela 9.5 Passagem de mais de quatro argumentos
int8_t x = -55;
int16_t y = 2000;
int8_t z = +100;
int16_t w;

w = sum(x, y, 2, 3, z, -3);

(a)

 1    .data
 2x:
 3    .byte    -55
 4y:
 5    .word    2000
 6z:
 7    .byte    +100
 8w:
 9    .word    0
10
11    .text
12    mov    r0, #-3 & 0xff
13    movt   r0, #-3 >> 8
14    push   r0
15    ldr    r0, z_addr
16    ldrb   r0, [r0]
17    lsl    r0, r0, #8
18    asr    r0, r0, #8
19    push   r0
20    ldr    r0, x_addr
21    ldrb   r0, [r0]
22    lsl    r0, r0, #8
23    asr    r0, r0, #8
24    ldr    r1, y_addr
25    ldr    r1, [r1]
26    mov    r2, #2
27    mov    r3, #3
28    bl     sum
29    mov    r1, #4
30    add    sp, r1, sp
31    ldr    r1, w_addr
32    str    r0, [r1]
33
34x_addr:
35    .word  x
36y_addr:
37    .word  y
38z_addr:
39    .word  z
40w_addr:
41    .word  w

(b)

Depois do regresso, a partir da linha 31, é necessário recolocar o registo SP na posição que tinha antes da linha 16 – antes do empilhamento do primeiro argumento.

A solução mais intuitiva seria realizar dois pops para compensar os pushs das linhas 16 e 21. Seria uma solução viável. Como não é necessário recuperar os conteúdos, basta "mover" o SP duas posições para trás. Operação que é realizada pela instrução add  sp, r1, sp, que adiciona quatro unidades ao registo SP.

Tabela 9.6 Receção de mais de quatro argumentos
int16_t sum(int8_t a, int16_t b, int8_t c,
            int16_t d, int8_t e, int16_t f) {
    return a + b + c + d + e + f;
}

(a)

 1    .text
 2sum:
 3    add   r0, r0, r1
 4    add   r0, r0, r2
 5    add   r0, r0, r3
 6    ldr   r1, [sp]
 7    add   r0, r0, r1
 8    ldr   r1, [sp, #2]
 9    add   r0, r0, r1
10    mov   pc, lr

(b)

O programa (b) da Tabela 9.6 mostra como aceder aos argumentos passados em stack, utilizando a instrução ldr com base no registo SP (linhas 6 e 8). Este método dispensa a necessidade de desempilhar os argumentos (pops).

9.3. Valor de retorno

O valor de retorno de uma função, caso exista, é devolvido no registo R0. Se for um valor representado a 32 bits é devolvido no par de registos R1:R0, com a parte de menor peso em R0. Se o valor de retorno for de tipo representado a 8 bits – char, uint8_t ou int8_t – será retornado em R0 com representação a 16 bits.

9.4. Utilização dos registos

Uma função pode utilizar os registos de R0 a R3 sem preservar o seu conteúdo original. Os restantes registos – de R4 a R12 e SP – devem ser preservados.

Na perspetiva de função chamadora, no regresso da invocação a outra função, o conteúdo dos registos R0 a R3, LR e CPSR podem vir alterados; o conteúdo dos registos R4 a R12 e o SP não pode vir alterados.

Na perspectiva inversa, a função chamada, deve salvar e restaurar os registos que utilizar de R4 a R12 e assegurar que, imediatamente após a execução da instrução de retorno, o registo SP aponta a mesma posição de stack que apontava imediatamente antes da execução da instrução de chamada a função.

Os registos R4 a R12 são designados por callee saved porque deve ser a função chamada a preservá-los se os utilizar.

Os registos R0 e R3 são designados por caller saved porque deve ser a função chamadora a preservá-los ao chamar outra função, caso não queira perder o seu conteúdo. Deve-se admitir que a função chamada os modifica.

9.5. Aplicação das convenções

A execução de um programa realiza uma sucessão de chamadas a funções que pode ser representada por um grafo na forma de árvore. As últimas funções na cadeia de chamadas, as que não chamam outras funções, surgem representadas nos extremos do grafo, na posição das folhas da árvore, e são designadas por "funções folha". As restantes funções, as que chamam outras funções, e são representadas nos nós de ligação dos ramos, são designadas por "funções não folha".

No desenvolvimento de programas em assembly é vantajosa a utilização de padrões de programação e critérios de escolha de registos, de acordo com as convenções. Estas práticas facilitam tanto a escrita como a análise dos programas, e também conduzem à produção de programas eficientes.

9.5.1. Função folha

Na programação de uma função folha deve dar-se preferência à utilização dos registos R0 a R3. Este registos podem ser utilizados sem se preservar o seu conteúdo. No caso de não serem suficientes, recorre-se à utilização de registos callee saved (R4 a R12).

Listagem 9.1 Função find_min em linguagem C
1<r0> int16_t find_min(<r0> uint16_t array[], <r1> uint8_t array_size) {
2	<r2> uint16_t min = array[0];
3	for (<r3> uint8_t i = 1; i < array_size; ++i)
4		if (array[i] < min)
5			min = array[i];
6	return min;
7}

Na Listagem 9.1 a função find_min possui dois parâmetros e duas variáveis locais min e i. Os registos que os suportam são assinalados com <rx>. Por exemplo, na linha 1 <r0> uint16_t array[] significa que o argumento deste parâmetro é recebido no registo R0. Os registos R0 e R1 são utilizados para parâmetros. Os registo R2 e R3 são os escolhidos para as variáveis locais min e i.

Listagem 9.2 Função find_min em linguagem assembly
 1find_min:
 2	push	r4
 3	ldr	r2, [r0]	; min = array[0];
 4	mov	r3, #0		; i = 0
 5find_min_for:
 6	cmp	r3, r1		; i < array_size
 7	bhs	find_min_for_end
 8	add	r4, r3, r3
 9	ldr	r4, [r0, r4]	; if (array[i] < min)
10	cmp	r4, r2
11	bhs	find_min_if_end
12	mov	r2, r4		;min = array[i]
13find_min_if_end:
14	add	r3, r3, #1 	; ++i
15	b	find_min_for
16find_min_for_end:
17	mov	r0, r2		; return min
18	pop	r4
19	mov	pc, lr

Na Listagem 9.2, o registo R3 suporta a variável i que é usada como índice de acesso ao array. No cálculo do endereço de cada posição array[i], é necessário multiplicar R3 por dois, porque os elementos do array ocupam duas posições de memória. A instrução add  r4, r3, r3 (linha 9) realiza essa multiplicação afetando R4 com R3 * 2.

Como os registos R0 a R3 estão todos a ser utilizados, teve que se recorrer a R4 para este cálculo intermédio. Segundo a convenção – Utilização dos registos – o conteúdo deste registo deve ser preservado, o que justifica a utilização das instruções push r4 e pop r4.

9.5.2. Função não folha

A função não folha caracteriza-se por conter chamadas a outras funções. Para realizar estas chamadas são necessários os registos R0 a R3 para passar os argumentos a essas funções. Esses registos surgem, ao início da função, ocupados pelos seus próprio argumentos. Para libertar este registos e preservar os argumentos, necessários ao longo da função, transfere-se o seu conteúdo para registos callee saved, logo à entrada da função. Assim, os registos R0 a R3 ficam disponíveis para passar argumentos às funções chamadas, como também, para serem utilizados em operações intermédias.

A utilização de registos callee saved (R4 a R12), tanto para manter argumentos, como para suportar variáveis locais garante a manutenção destes dados durante o tempo de vida da função. Pela aplicação das convenções, as funções chamadas encarregam-se de preservar o conteúdo destes registos caso necessitem de os utilizar.

Listagem 9.3 Função array_square em linguagem C
1uint16_t multiply(uint8_t, uint8_t);
2
3void array_square(<r0><r4> uint16_t result[], <r1><r5> uint8_t array[], <r2><r6> uint16_t array_size) {
4	for (<r7> uint16_t i = 0; i < array_size; ++i)
5		result[i] = multiply(array[i], array[i]);
6}

A função array_square apresentada na Listagem 9.3, é uma função não folha, pois invoca a função multiply. Os argumentos result, array e array_size, assim como a variável local i, constituem dados estáveis que devem ser mantidos durante toda a execução da função. Os registos R4, R5 e R6 são escolhidos para os argumentos e o registo R7 para a variável local. Estes registos foram os escolhidos porque, segundo a convenção – Utilização dos registos – a função multiply não os vai alterar, o que dispensa a função array_square de qualquer ação de preservação dos seus conteúdos.

Listagem 9.4 Função array_square em linguagem assembly
 1array_square:
 2	push	lr
 3	push	r4
 4	push	r5
 5	push	r6
 6	push	r7
 7	mov	r4, r0	; uint16_t result[]
 8	mov	r5, r1	; uint8_t array[]
 9	mov	r6, r2	; uint16_t array_size
10	mov	r7, #0	; uint16_t i = 0
11array_square_for:
12	cmp	r7, r6
13	bhs	array_square_for_end
14	ldrb	r0, [r5, r7]
15	mov	r1, r0
16	bl	multiply
17	add	r1, r7, r7
18	str	r0, [r4, r1]
19	add	r7, r7, #1
20	b	array_square_for
21array_square_for_end:
22	pop	r7
23	pop	r6
24	pop	r5
25	pop	r4
26	pop	pc

Nas linhas 7, 8 e 9, os conteúdo dos argumentos são transferidos para os registos onde vão permanecer durante toda a execução da função. Na linha 10 a variável i é inicializada com zero.

A sequência de pushs e pops nas linha 3 a 6 e 22 a 25, tratam de salvar e restaurar os conteúdos originais dos registos R4 a R7. Esta ação resulta da necessidade de cumprir a convenção, em relação à função chamadora de array_square. Essa função ao aplicar o mesmo critério, utiliza também esses registos para os seus dados estáveis.

Uma solução alternativa seria a de envolver o código de chamada com pushs e pops dos registos com dados estáveis. Essa solução seria pior porque no caso da chamada à função ser repetida, como no caso da chamada a multiply, essas instruções seriam executadas em todas as repetições.

No P16 a instrução BL guarda o endereço de retorno no registo LR. À entrada de uma função este registo contém o endereço de retorno dessa função. Se no seu interior for realizada a chamada a outra função como é o caso da chamada de bl multiply, o registo LR vai receber novo endereço de retorno. Não sendo tomada nenhuma ação, o endereço de retorno original seria perdido. É isso que justifica a instrução push lr na linha 1 para salvar o endereço de retorno original e a instrução pop pc na linha 26 para a respetiva reposição. Como a reposição é para o registo PC, tem também como efeito o retorno à função chamadora.

9.6. Exercícios

Este conjunto de exercícios destina-se a exercitar a programação de funções, em linguagem assembly do P16.

O objetivo de cada exercício é traduzir para assembly, o código da função definida em linguagem C.

Devem ser aplicadas as convenções de utilização dos registos na passagem de argumentos, assim como na concretização de variáveis locais. (criar link para a respetiva secção)

  1. int sum(int a, int b)
    {
        return a + b;
    }
    
  2. uint16_t multiply(uint8_t multiplying, uint8_t multiplier)
    {
        uint16_t product = 0;
        while (multiplier > 0) {
            product += multiplying;
            multiplier--;
        }
        return product;
    }