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.
Tipo |
Memória (bits) |
Registo (bits) |
|---|---|---|
|
8 |
16 |
|
16 |
16 |
|
16 |
16 |
|
32 |
32 |
|
8 |
16 |
|
16 |
16 |
|
32 |
32 |
|
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.
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.
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 array – lsr r1, r1, #1.
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).
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.
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).
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.
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.
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.
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)
int sum(int a, int b) { return a + b; }
uint16_t multiply(uint8_t multiplying, uint8_t multiplier) { uint16_t product = 0; while (multiplier > 0) { product += multiplying; multiplier--; } return product; }