Programação em assembly x86-64

Suporte à linguagem C

Valores

A arquitetura x86-64 realiza operações com palavras de 8, 16, 32 ou 64 bits. Os registos tem nomes próprios que definem as diferentes porções. Por exemplo, AL, AX, EAX e RAX.

Para valores operados em memória as instruções aceitam um indicador, em sufixo, que define a dimensão da palavra.

b

byte

8 bits

w

word

16 bits

l

double word

32 bits

q

quad word

64 bits

Existem instruções que estendem a representação num número inferior de bits para um número superior de bits.

movs-- src, dst

extensão com o bit de sinal

os tracinhos são substituídos pelos indicadores acima

o primeiro indica a dimensão da origem e o segundo a do destino

movz-- src, dst

extensão com o valor zero

Expressões

Alguns exemplos de calculo de expressões numéricas.

Considerando a um valor expresso a 64 bit, previamente carregado em RAX.

Deslocar p posições para a esquerda

a << p;
mov     p(%rip), %cl
shl     %cl, %rax

Afetar o bit da posição p com zero

a & ~(1 << p);
mov     p(%rip), %cl
mov     $1, %rdx
shl     %cl, %rdx
not     %rdx
and     %rdx, %rax

Afetar o bit da posição p com um

a | 1 << p;
mov     p(%rip), %cl
mov     $1, %rdx
shl     %cl, %rdx
or      %rdx, %rax

Testar o valor do bit da posição p

if (a & (1 << p))
mov     p(%rip), %cl %cl
mov     $1, %rdx
shl     %cl, %rdx
test    %rdx, %rax
jz      label

Obter o campo de n bits a começar na posição p.

(a >> p) & ~(~0 << n);
mov     $~0, %rdx
mov     n(%rip), %cl
shl     %cl, %rdx
not     %rdx
mov     p(%rip), %cl
shr     %cl, %rax
and     %rdx, %rax

Multiplicação por constante

a * 13;
# rax * 13  = (rax * 3) * 4 + rax

lea   (%rax, %rax, 2), %rdx
lea   (%rax, %rdx, 4), %eax

Considerando a um valor expresso a 128 bit previamente carregado em RDX:RAX.

Deslocar 1 posição para a esquerda

a <<= 1;
shl    $1, %rax
rcl    $1, %rdx

Deslocar 1 posição para a direita

a >>= 1;
shr    $1, %rdx
rcr    $1, %rax

Deslocar p posições para a esquerda

a <<= p;
mov     p(%rip), %cl
shld    %cl, %rax, %rdx
shl     $cl, %rax

Deslocar N posições para a direita

a >>= N;
shrd    $N, %rdx, %rax
shr     $N, %rdx

Controlo da execução

if

if sem else

if (c == 0)
	d = 2;

(a)

        .text
        cmp     $0, %eax
        jne     if_end
        mov     $2, %ecx
if_end:

(b)

if com else

if (c == 0)
	d = 2;
else
	d = 3;

(a)

        .text
        cmp     $0, %eax
        jne     if_else
        mov     $2, %ecx
        jmp     if_end
if_else:
        mov     $3, %ecx
if_end:

(b)

switch

int v, a;

switch (v) {
	case 1:
		a = 11;
		break;
	case 10:
		a = 111;
		break;
	default:
		a = 0;
};

(a)

# v - eax       a - ecx
        .text
switch:
        cmp     $1, %eax
        jne     switch_case_10
        mov     $11, %ecx
        jmp     switch_break
switch_case_10:
        cmp     $10, %eax
        jne     switch_default
        mov     $111, %ecx
        jmp     switch_break
switch_default:
        mov     $0, %ecx
switch_break:

(b)

do while

int v, z;

do {
	v >>= 1;
	z += 1;
} while (v != 0);

(a)

# v - eax	z - ecx
	.text
do_while:
	shr	$1, %eax
	inc	%ecx
	and	%eax, %eax
	jne	do_while

(b)

while

É codificado como a diferença entre o endereço da instrução e a label a e é calculado adicionando este valor ao RIP. A utilização que se faz do endereço da variável depende da semântica da instrução. No caso de movb    $0, a(%rip) é realizada a escrita do valor 0 na posição de memória com esse endereço.

No caso de incq    i(%rip) é realizada uma leitura e uma escrita do valor depois de incrementado. Um valor do tipo int é representado em memória p | | | | (a) | (b) | (c) | +———————————————————————-+———————————————————————-+———————————————————————-+

for

int i, a, n;

for (i = 0, a = 1; i < n; ++i) {
	a <<= 1;
}

(a)

# i - eax       a - ecx d - edx
        .text
for:
        mov     $0, %eax
        mov     $1, %ecx
        jmp     for_cond
for_do:
        shl     $1, %ecx
        inc     %eax
for_cond:
        cmp     %eax, %edx
        jl      for_do

(b)

Funções

A chamada a função é realizada com a instrução call e o retorno é realizado com a instrução ret.

A instrução call empilha o endereço da próxima instrução no topo do stack. Nessa altura RIP já contém o endereço da instrução seguinte, designado por endereço de retorno. Depois executa um salto para o endereço de início da função chamada (callee).

A instrução call <endereço> é equivalente à sequência push rip; jmp <endereço. Exemplos:

  • call label salto relativo; a distância até à label é embutida no código da instrução.

  • call *%rax salto absoluto; o registo RAX contém o endereço da função

  • call *(%rax) salto absoluto; o registo RAX contém o endereço da posição de memória ende se encontra o endereço da função

Para retornar à função chamadora (caller), a função chamada executa, em último lugar, a instrução ret. Esta instrução desempilha para o registo RIP, o endereço empilhado pela última instrução call, provocando o regresso à função chamadora.

A instrução ret é equivalente a pop rip

Função sem parâmetros

Listagem 20 delay.c
void delay(void) {
	for (int i = 0; i < 100; ++i)
		;
}
Listagem 21 delay_asm.s
	.text
	.global	delay
delay:
	mov	$0, %eax
	jmp	delay_for_cond
delay_for:
	inc	%eax
delay_for_cond:
	cmp	$100, %ecx 
	jl	delay_for:

	ret
Listagem 22 use_delay.c
void delay(void);

int main() {
	delay();
}
Listagem 23 use_delay_asm.s
	.text
	.global	main
main:
	call	delay
	mov	$0, %eax
	ret

Geração do executável:

$ gcc -Og -g use_delay.c delay_asm.s -o use_delay

Teste com debugger:

$ insight use_delay

Função com parâmetros

Os argumentos de uma função são passados nos registos RDI, RSI, RDX, RCX, R8 e R9, por esta ordem, até se esgotarem estes registos. Caso seja necessário, os restantes argumentos, são passados em stack. O retorno é feito em AL, AX, EAX, RAX ou em RDX:RAX, conforme o tipo.

No caso de retorno de uma struct com dimensão superior a 128 bits, o chamador passa um argumento extra, que é o endereço de uma zona de memória, reservada pelo chamador, onde a função deposita o resultado.

Listagem 24 packdate.c
short pack_date(int year, int month, int day) {
	short date = day;
	date = date + (month << 5);
	date = date + ((year - 2000) << 9);
	return date;
}
Listagem 25 packdate.s
        .text
        .global pack_date
pack_date:
        mov     %dx, %ax
        sal     $5, %esi
        add     %si, %ax
        sub     $2000, %edi
        sal     $9, %edi
        add     %di, %ax
        ret
Listagem 26 use_packdate.c
short pack_date(int, int, int);

int main() {
	short date = pack_date(2024, 9, 18);
}
Listagem 27 use_packdate_asm.s
        .text
        .global main
main:
        mov     $18, %edx
        mov     $9, %esi
        mov     $2024, %edi
        call    pack_date
        mov     $0, %eax
        ret
Listagem 28 getbits.c
unsigned long getbits(unsigned long x, int p, int n) {
    return (x >> p) & ((1L << n) - 1);
}
Listagem 29 getbits_asm.s
	.text
	.global getbits
getbits:
	mov	$1, %rax
	mov	%dl, %cl
	shl	%cl, %rax
	dec	%rax
	mov	%sil, %cl
	shr	%cl, %rdi
	and	%rdi, %rax
	ret
Listagem 30 use_getbits.c
unsigned long getbits(unsigned long x, int p, int n);

int main() {
	unsigned long b = getbits(450, 4, 3);
}
Listagem 31 use_getbits_asm.s
	.text
	.global main
main:
	mov	$3, %edx
	mov	$4, %esi
	mov	$450, %rdi
	call	getbits
	ret

Geração do executável:

$ gcc -Og -g use_getbits.c getbits_asm.s -o use_getbits

Teste com debugger:

$ insight use_getbits

Acesso a variáveis

As variáveis são alojadas em memória ou em registos. As variáveis alojadas em memória podem ter endereço fixo, é o caso das variáveis globais e locais com atributo static, ou endereço variável, é o caso das variáveis locais alojadas em stack.

O compilador gcc na arquitetura x86_64 utiliza duas formas de acesso a variáves. Endereçamento relativo ao RIP ou endereçamento indireto.

O endereçamento relativo consiste em codificar a distância em número de bytes desde o endereço presente em RIP até ao endereço da variável.

Este modo de endereçamento aplica-se às variáveis globais e ás variáveis locais com atributo static. Estas variáveis são no conjunto designadas por variáveis estáticas e são alocadas em memória na altura da compilação.

Em linguagem C quando se define uma variável estabelece-se um símbolo (no exemplo a ou i) que representa o conteúdo dessa variável.

char a;
int i;

Na tradução para linguagem assembly esse símbolo origina uma label que representa um endereço de memória, ou seja, o endereço da variável.

    .bss
a:
    .byte    0
    .align   4
i:
    .long    0

A notação a(%rip), utilizada nos exemplo seguinte, representa o endereço da variável a. É codificado como a diferença entre o endereço presente em RIP e a label a.

A utilização que se faz do endereço da variável depende da semântica da instrução. No caso de movb    $0, a(%rip) é realizada a escrita do valor 0 na posição de memória com esse endereço. No caso de incq    i(%rip) é realizada uma leitura e uma escrita do valor depois de incrementado.

Um valor do tipo int é representado em memória por uma palavras de quatro bytes, por isso, o acesso processa-se sobre as quatro posições de memória contiguas ao endereço calculado.

A dimensão da palavra operada é definida pela dimensão de um registo operando ou pelo sufixo da instrução (letras b, w, l, q).

a = 0;
movb    $0, a(%rip)
i++;
incl    i(%rip)

Exemplos – variáveis como argumentos

Listagem 32 use_packdate.c
short pack_date(int, int, int);

int year = 2024;
int month = 9;
int day = 18;

int main() {
	short date = pack_date(year, month, day);
}
Listagem 33 use_packdate_asm.s
        .data
year:
        .long    2024
month:
        .long    9
day:
        .long    18

        .text
        .global main
main:
        mov     day(%rip), %edx
        mov     month(%rip), %esi
        mov     year(%rip), %edi
        call    pack_date
        mov     $0, %eax
        ret


        .section        .note.GNU-stack
Listagem 34 use_getbits.c
long x = 30;
long y = 0;

long getbits(long x, int p, int n);

int main() {
	y = getbits(x, 4, 3);
}
Listagem 35 use_getbits_asm.s
        .data
x:
        .quad   30

        .bss
y:
        .zero   8

        .text
        .global main
main:
        sub     $8, %rsp
        mov     $3, %edx
        mov     $4, %esi
        mov     x(%rip), %rdi
        call    getbits
        mov     %rax, y(%rip)
        mov     $0, %eax
        add     $8, %rsp
        ret

Array

Em linguagem C quando se define um array estabelece-se um símbolo (no exemplo seguinte, ca) equivalente ao ponteiro para o primeiro elemento do array.

char ca[10];

Na tradução para linguagem assembly, este símbolo origina uma label que representa o endereço inicial da zona de memória onde o array está alojado.

ca:
    .space   10

Para reservar uma zona de memória utiliza-se a diretiva .space cujos parâmetros são, respetivamente, a dimensão da zona de memória a reservar, medida em número de bytes, e o valor inicial de cada byte.

No caso de um array de tipo char a dimensão de memória a reservar é igual ao número de elementos do array porque op elemento do tipo char ocupa 1 byte em memória.

Generalizando, a dimensão da memória a reservar é igual ao número de elementos do array vezes a dimensão do elemento.

O array de valores inteiros int ia[10] é concretizado em assembly por:

ia:
    .space    10 * 4

porque um elemento do tipo int ocupa 4 bytes em memória.

Em programação assembly, para acesso aos elementos do array é necessário fazer corresponder os índices dos elementos no array ao endereço dos elementos na memória.

O acesso utilizando operador indexação ca[i] é concretizado como um endereçamento indireto, na forma de dois registos, um para o endereço base – rdx e outro para o índice – rcx.

ca[i] = 0;
movb   $0, (%rdx, %rcx)

No caso do array do tipo int, a utilização do operador indexação ia[i] é semelhante ao caso do array do tipo char, com diferença do fator de escala a aplicar ao registo índice.

No calculo do endereço do elemento do array, o dígito 4 na instrução (%rdx, %rcx, 4) significa que o registo RCX é multiplicado por 4 antes de ser adicionado a RDX.

ia[i] = 0;
movq   $0, (%rdx, %rcx, 4)

Exemplos – acesso a elementos de array

Listagem 36 strlen.c
#include <stdlib.h>

size_t strlen(char string[]) {
	size_t len;
	for (len = 0; string[len] != '\0'; ++len)
		;
	return len;
}
Listagem 37 strlen_asm.s
        .text
        .global strlen
strlen:
        mov     $0, %eax
        jmp     strlen_for_cond
strlen_for_do:
        add     $1, %rax
strlen_for_cond:
        cmpb    $0, (%rdi,%rax)
        jne     strlen_for_do
        ret
Listagem 38 use_strlen.c
#include <string.h>

int main(int argc, char *argv[]) {
	return strlen(argv[0]);
}
Listagem 39 use_strlen_asm.s
	.text
	.globl	main
main:
	movq	(%rsi), %rdi
	call	strlen
	ret

Geração do executável:

$ gcc -Og -g use_strlen.c strlen_asm.s -o use_strlen

Teste com debugger:

$ insight use_strlen
Listagem 40 use_strlen2.c
size_t i = 0;

char message[] = "abcdef";

int main() {
	i = my_strlen(message);
}
Listagem 41 use_strlen2_asm.s
        .data
message:
        .string "abcdef"

        .bss
i:
        .zero     8

        .text
        .global main
main:
        sub     $8, %rsp
        lea     message(%rip), %rdi
        call    my_strlen
        mov     %rax, i(%rip)
        mov     $0, %eax
        add     $8, %rsp
        ret
Listagem 42 findbigger.c
#include <stddef.h>

int find_bigger(int array[],
                size_t array_size) {
    int bigger = array[0];
    for (size_t i = 1; i < array_size; ++i)
        if (array[i] > bigger)
            bigger = array[i];
    return bigger;
}
Listagem 43 findbigger_asm.s
        .text
        .global find_bigger
find_bigger:
        mov     (%rdi), %ecx
        mov     $1, %eax
        jmp     for_cond
for_do:
        add     $1, %rax
for_cond:
        cmp     %rsi, %rax
        jnb     for_end
        mov     (%rdi,%rax,4), %edx
        cmp     %ecx, %edx
        jle     for_do
        mov     %edx, %ecx
        jmp     for_do
for_end:
        mov     %ecx, %eax
        ret
Listagem 44 use_findbigger.c
#include <stddef.h>

int find_bigger(int array[], size_t array_size);

int a[] = { 10, 40, 30, 5};

int main() {
	int b = find_bigger(a, 4);
}
Listagem 45 use_findbigger_asm.s
        .text
        .global  main
main:
        sub     $8, %rsp
        mov     $4, %esi
        lea     a(%rip), %rdi
        call    find_bigger@PLT
        mov     $0, %eax
        add     $8, %rsp
        ret

        .data
a:
        .long   10
        .long   40
        .long   30
        .long   5

Ponteiros

Considere-se a seguinte definição de variáveis e a respetiva tradução para linguagem assembly.

char a, b;
char *cp;
int i, j;
int *ip;
a:
    .byte    0
b:
    .byte    0
    .align   8
cp:
    .quad    0
i:
    .long    0
j:
    .long    0
ip:
    .quad    0

O operador & aplicado a uma variável dá o ponteiro para essa variável. Um ponteiro para uma variável é concretizado como o endereço de memória dessa variável.

A expressão cp = &a; afeta a variável cp, do tipo ponteiro para char, com o ponteiro para a variável a.

Na notação da linguagem assembly a expressão a(%rip) representa o endereço da label a. A instrução lea   a(%rip), %rax calcula o endereço definido por a(%rip) e afeta RAX com o endereço calculado. O cálculo é realizado pela adição do valor atual de RIP com a distância, calculada em compilação, da instrução corrente até à label a.

cp = &a;
lea    a(%rip), %rax
mov    %rax, cp(%rip)

Comparando a instrução lea   a(%rip), %rax com a instrução mov   %rax, cp(%rip). Ambas cálculam endereços de memória. A primeira afeta o endereço calculado ao registo RAX, a segunda escreve o conteúdo de RAX nas posições de memória definidas pelo endereço calculado. Posições, porque se trata de escrever o conteúdo de RAX que contém um endereço de memória (8 bytes).

A obtenção do ponteiro para uma variável, é indiferente ao tipo dessa variável. No exemplo abaixo, pode verificar-se que a expressão ip = &i; é traduzida para assembly com o mesmo padrão de código que a expressão cp = &a;.

ip = &i;
lea    i(%rip), %rax
mov    %rax, ip(%rip)

O acesso a uma variável por desreferenciação de ponteiro, programa-se em dois passos. Primeiro carrega-se o ponteiro num registo, em seguida aplica-se endereçamento indireto com base nesse registo para efetivar o acesso à variável.

*cp = 'a';
mov    cp(%rip), %rax
movb   $'a', (%rax)

No exemplo *cp = 'a'; o valor do ponteiros cp é carregado no registo RAX, como uma variável comum – mov    cp(%rip), %rax. Em seguida escreve-se o valor 'a' na memória por endereçamento indireto simples – movb $0'a', (%rax). O sufixo b na instrução movb é necessário para definir a dimensão da palavra. Por se tratar do tipo char é apenas um byte.

b = *cp;
mov    cp(%rip), %rax
mov    (%rax), %dl
mov    %dl, b(%rip)

No exemplo b = *cp;, depois de carregado o ponteiro cp em RAX, a instrução mov   (%rax), %dl descarrega o valor apontado por cp no registo DL. Através da designação DL o assembler dispensa a utilização de sufixo b.

*ip = j;
mov    j(%rip), %ecx
mov    ip(%rip), %rax
mov    %ecx, (%rax)

No exemplo *ip = j;, depois de carregado o ponteiro ip em RAX, a instrução mov   %ecx, (%rax) escreve o valor da variável j, entretanto carregado em ECX, na memória apontada por ip. Através da designação ECX o assembler dispensa a utilização de sufixo l.

Exemplo – ponteiros como argumentos

Listagem 46 unpackdate.c
void unpack_date(short date, int *year, int *month, int *day) {
	*year = (date >> 9 & 0b1111111) + 2000;
	*month = date >> 5 & 0b1111;
	*day = date & 0b11111;
}
Listagem 47 unpackdate_asm.s
        .text
        .global unpack_date
unpack_date:
        mov     %edi, %eax
        shr     $9, %ax
        cwtl
        add     $2000, %eax
        mov     %eax, (%rsi)
        mov     %edi, %eax
        sar     $5, %ax
        and     $15, %eax
        mov     %eax, (%rdx)
        and     $31, %edi
        mov     %edi, (%rcx)
        ret
Listagem 48 use_unpackdate.c
int year;
int month;
int day;

void unpack_date(short, int *, int *, int *);

int main() {
	unpack_date(2024 - 2000 << 9 | 10 << 5 | 8,
				&year, &month, &day);
}
Listagem 49 use_unpackdate_asm.s
        .text
        .global main
main:
        sub     $8, %rsp
        lea     day(%rip), %rcx
        lea     month(%rip), %rdx
        lea     year(%rip), %rsi
        mov     $2024 - 2000 << 9 | 10 << 5 | 8, %edi
        call    unpack_date
        mov     $0, %eax
        add     $8, %rsp
        ret

        .bss
day:
        .zero   4
month:
        .zero   4
year:
        .zero   4

Aritmética de ponteiros

char ca[10];
int ia[10];
char *cp;
int *ip;
ca:
    .space   10
    .align   4
ia:
    .space   10 * 4
    .align   8
cp:
    .quad    0
ip:
    .quad    0

A aritmética de ponterios engloba dois casos: adição de inteiro a ponteiro e diferença de ponteiros. Estas operações fazem sentido quando os ponteiros apontam para arrays. A adição de um inteiro significa deslocar o ponteiro um número de posições. A subtração de um ponteiro a outro ponteiro significa obter o número de posições entre esses ponteiros.

Na linguagem assembly um ponteiro é concretizado como um endereço de memória. Quando se traduz a adição de um valor inteiro a um ponteiro é necessário aplicar o fator de escala que consiste em multiplicar o valor inteiro pela dimensão do elemento apontado, antes de efetuar a adição.

Nos exemplos seguintes, a operação cp++ implica adicionar uma unidade à variàvel cp; a operação ip++ implica adicionar 4 unidades à variável ip.

cp++
incq   cp(%rip)
ip++
addq   $4, ip(%rip)

Na expressão ip + i é necessário múltiplicar o valor de i por 4 – instrução shl   $2, %rax – antes da instrução add  %rax, ip(%rip)

ip = ip + i
mov    i(%rip), %eax
shl    $2, %rax
add    %rax, ip(%rip)

A diferença de dois ponteiros faz sentido se ambos os ponteiros apontarem para elementos do mesmo array e significa o número de elementos entre as posições apontadas.

Em linguagem assembly, como os ponteiros são concretizados por endereços, a diferença de endereços é um número de posições de memória.

Para converter para número de elementos é necessário dividir a diferença de endereços pela dimensão do elemento. Tratando-se de um array de int, a instrução shr   $2, %rax divide por 4 a diferença de endereços.

j = ip - iq
mov    iq(%rip), %rax
sub    ip(%rip), %rax
shr    $2, %rax
mov    %eax, j(%rip)

A notação de indexação ip[i] é equivalente à notação *(p + i).

j = *(ip + i);
j = ip[i];
mov    ip(%rip), %rax
mov    i(%rip), %esi
mov    (%rax, %rsi, 4), %eax
mov    %eax, j(%rip)

Struct

Considere-se a seguinte definição da struct y e da variável x desse mesmo tipo:

struct y {
    char a;
    int b;
    short c;
} x = {
     .a = '1',
     .b = 1000,
};

A definição da variável x em linguagem assembly consiste na definição de uma label na direção do primeiro campo e da reserva de memória para alojamento de todos os campos.

x:
    .align   4
    .byte    '1'         # campo a - distância 0
    .align   4
    .long    1000    # campo b - distância 4
    .word    0       # campo c - distância 8

A forma comum de aceder aos campos de uma variável do tipo struct é por endereçamento indireto. Coloca-se o endereço da variável num registo e pela adição da distância do campo ao início da struct define-se o endereço do campo.

No exemplo x.c = 222; a instrução lea    x(%rip), %rax coloca em RAX o endereço da label x que corresponde ao endereço do primeiro campo – do campo a. Na instrução mov    $222, 8(rax) o valor 8 corresponde à distância do campo c desde o início da struct. O endereço do campo c na variável x, é obtido pela adição de 8 ao registo RAX.

Exemplo – Array de struct

Listagem 50 getlighter.c
#include <stddef.h>

typedef struct person {
    char name[20];
    int age;
    int weight;
    float height;
} Person;

Person *get_lighter(Person *people, size_t n_people) {
	size_t lighter = 0;
	for (size_t i = 1; i < n_people; ++i)
		if (people[i].weight < people[lighter].weight)
			lighter = i;
	return &people[lighter];
}
Listagem 51 getlighter_asm.s
        .text
        .globl  get_lighter
get_lighter:
        mov     $1, %edx             # for (size_t i = 1; ...
        mov     $0, %r8d             # size_t lighter = 0;
        jmp     for_cond
for_do:
        add     $1, %rdx             # for (... ; ... ; ++i)
for_cond:
        cmp     %rsi, %rdx           # for ( ...; i < n_people; ...)
        jnb     for_end
        mov     %rdx, %rcx
        sal     $5, %rcx             # r8 = i * sizeof people[0]
        mov     %r8, %rax
        sal     $5, %rax             # rax = lighter * sizeof people[0]
        mov     24(%rdi,%rax), %eax  # eax = people[lighter].weight
        cmp     %eax, 24(%rdi,%rcx)  # if (people[i].weight < eax)
        jge     for_do
        mov     %rdx, %r8            # lighter = i;
        jmp     for_do
for_end:
        sal     $5, %r8              # r8 = lighter * sizeof people[0]
        lea     (%rdi,%r8), %rax     # r8 = &people[lighter]
        ret
Listagem 52 use_getlighter.c
#include <stddef.h>

#define ARRAY_SIZE(a)	(sizeof(a) / sizeof(a)[0])

typedef struct person {
    char name[20];
    int age;
    int weight;
    float height;
} Person;

Person *get_lighter(Person *people, size_t n_people);

Person people[] = {
	{"Ana", 30, 60, 1.75},
	{"Manuel", 30, 70, 2.0}
};

int main() {
    char *lighter_name = get_lighter(
               people, ARRAY_SIZE(people))->name;
}
Listagem 53 use_getlighter_asm.s
        .text
        .global main
main:
        sub     $8, %rsp
        mov     $2, %esi
        lea     people(%rip), %rdi
        call    get_lighter
        mov     $0, %eax
        add     $8, %rsp
        ret

        .data
people:
        .string "Ana"
        .zero   16
        .long   30
        .long   60
        .long   0b00111111111000000000000000000000
        .string "Manuel"
        .zero   13
        .long   30
        .long   70
        .long   0b01000000000000000000000000000000

Convenções de utilização de registos

Os conteúdos dos registos RAX, RCX, RDX, RSI, RDI, R8, R9, R10 e R11 podem ser modificados pela função chamada, os conteúdos dos registos RBX, RBP, R12, R13, R14 e R15 devem ser mantidos. Manter não implica a não possam ser utilizados. Implica que devem apresentaser à saída da função os mesmos conteúdos que tinham à entrada. Se forem utilizados, os seus conteúdos devem ser resguardadados, por exemplo em stack.

../_images/register_rules.svg

A cadeia de chamadas a funções num programa pode ser visualizada como uma árvore em que a função main se situa na posição da raiz. As funções que são chamadas e que também chamam outras funções, situam-se nas posições dos ramos e são designadas por "funções ramo"; as funções que apenas são chamadas situam-se nas posições das folhas e são designadas por "funções folha".

Para efeitos de escolha dos registos a utilizar, interessa classificar as funções como "funções folha" ou como "funções ramo".

Função folha

  • Deve-se operar os argumentos diretamente no registos que os transportam.

  • Deve-se preferir utilizar os registos caller saved.

  • Se tiver que se utilizar os registos callee saved deve-se assegurar à saída da função o mesmo conteúdo que tinham à entrada.

As funções get_lighter, find_bigger, strlen e unpack_date usadas em exemplos anteriores são casos de funções folha, programadas segundo os critérios enumerados acima.

Função ramo

  • Reutiliza os registos de parâmetros na chamada a outras funções.

  • Deve-se salvar os argumentos recebidos em registos callee saved ou em stack.

  • Deve-se alojar variáveis locais em registos callee saved ou em stack.

  • Se se optar por utilizar registos caller saved ou manter os argumentos recebidos no registos originais deve-se salvar esses registos antes de proceder à chamada de outra função.

Listagem 54 sort.c
void sort(int array[], int dim) {
    for (int i = 0; i < dim - 1; ++i)
        for (int j = 0; j < dim - 1 - i; ++j)
            if (array[j] > array[j + 1])
                swap(&array[j], &array[j + 1]);
}
Listagem 55 sort_asm.s
	.text
	.global	sort
 sort:
	push	%r14
	push	%r12
	push	%rbp
	push	%rbx
	mov	%rdi,%rbp	/*  array   */
	mov	%esi,%r14d	/*  dim  */
	dec	%r14d		/*  dim - 1  */
	mov	$0x0,%r12d	/*  i = 0  */
	jmp	sort_for1_cond
sort_for1:
	mov	$0x0, %ebx	/*  j = 0  */
	jmp	sort_for2_cond
sort_for2:
	movslq	%ebx,%rax
	lea	0x0(%rbp,%rax,4),%rdi
	lea	0x4(%rbp,%rax,4),%rsi
	mov	(%rsi),%eax
	cmp	%eax,(%rdi)	/* if (array[j] > array[j + 1])  */
	jle	sort_if_end
	call	swap
sort_if_end:
	inc	%ebx		/* ++j */
sort_for2_cond:
	mov	%r14d, %eax	/* j < dim - 1 - i  */
	sub	%r12d, %eax
	cmp	%ebx, %eax
	jg	sort_for2
	inc	%r12d		/* ++i  */
sort_for1_cond:
	cmp	%r12d,%r14d	/* i < dim - 1  */
	jg	sort_for1
 sort_for1_end:
	pop	%rbx
	pop	%rbp
	pop	%r12
	pop	%r14
	ret   

Organização da stack frame

Na stack frame alojam-se os conteúdos dos registos a preservar, as variáveis locais e argumentos de chamada a outras funções.

../_images/stack_frame.svg

Variáveis locais em stack

As variáveis locais são alojadas em stack se:

  • a sua quantidade excede o número de registos disponíveis;

  • a sua dimensão não permite o alojamento em registo – é o caso dos arrays;

  • é necessário aceder a essas variáveis através de ponteiros.

Listagem 56 use_unpackdate.c

void unpack_date(short, int *, int *, int *);

int main() {
	int year;
	int month;
	int day;
	short adate = 2024 - 2000 << 9 | 10 << 5 | 8;
	unpack_date(adate, &year, &month, &day);
}
Listagem 57 use_unpackdate_asm.s
 1        .text
 2        .global main
 3main:
 4        sub     $24, %rsp
 5        lea     4(%rsp), %rcx
 6        lea     8(%rsp), %rdx
 7        lea     12(%rsp), %rsi
 8        mov     $12616, %edi
 9        call    unpack_date
10        mov     $0, %eax
11        add     $24, %rsp
12        ret
../_images/stack4.svg

À entrada da função o registo RSP apresenta o endereço de memória 0x7fffffffdd58. A instrução sub $24, %rsp ao subtrair 24 a RSP, reserva espaço para alojar as variáveis year, month e day. Para alojar três variáveis do tipo int são necessários 12 bytes. Como RSP deve estar alinhado num endereço múltiplo de 16 na chamada a uma função, são subtraídas 24 posições de memória. As posições de memória entre 0x7fffffffdd40 e 0x7fffffffdd43 e entre 0x7fffffffdd50 e 0x7fffffffdd57 não são utilizadas. Nas linhas 5, 6 e 7 são preparados os argumentos para a chamada à função unpack_date, que são os ponteiros para as variáveis year, month e day. A instrução add $24, %rsp reposiciona RSP no endereço inicial.

Array de dimensão variável

Alojamento de array local de dimensão variável em stack. (Sem proteção de stack clash.)

Consideremos a função get_year que extrai a componente ano, na forma de inteiro, de uma data representada numa string com o formato «2020-9-3».

Listagem 58 getyear.c
int get_year(const char *date) {
    char buffer[strlen(date) + 1];
    strcpy(buffer, date);
    return atoi(strtok(buffer, "-/ "));
}
Listagem 59 getyear_asm.s
 1        .section .rodata
 2sep:    .asciz  "/- "
 3
 4        .text
 5        .global get_year
 6get_year:
 7        push    %rbp
 8        mov     %rsp, %rbp
 9        push    %rbx
10        mov     %rdi, %rbx
11        sub     $8, %rsp
12        call    strlen
13        inc     %eax
14        add     $15, %eax
15        and     $-16, %eax
16        sub     %rax, %rsp
17        mov     %rsp, %rdi
18        mov     %rbx, %rsi
19        call    strcpy
20        mov     %rsp, %rdi
21        lea     sep(%rip), %rsi
22        call    strtok
23        mov     %rax, %rdi
24        call    atoi
25        mov     -8(%rbp), %rbx
26        mov     %rbp, %rsp
27        pop     %rbp
28        ret

A reserva de espaço para o array local buffer é realizada nas linhas 14, 15 e 16. A dimensão necessária é estabelecida na linha 13 – valor retornado por strlen mais um.

Na linha 14 e 15 essa dimensão em EAX é arredondada por excesso para um valor múltiplo de 16. ((EAX + 15) / 16) * 16 ( o sinal / representa divisão inteira).

Na linha 16 esse valor é subtraído a RSP consumando a reserva de espaço de memória para o array local buffer. No final da função, RSP é restabelecido com o valor de RBP – linha 26. Sendo uma solução simples para de reajuste do RSP e libertação do espaço de memória reservado. Nas circunstâncias em que o espaço de memória a reservar em stack é variável, o gcc por omissão gera código de mitigação do efeito stack clash. O código apresentado acima foi gerado pelo gcc sob o efeito da opção -fno-stack-clash-protection.

Passagem de argumentos em stack

Na invocação de funções com mais de seis parâmetros, os argumentos para além do sexto são passados em stack. Estes argumentos são empilhados no stack pela ordem inversa da lista de parâmetros da função. O argumento correspondente ao último parâmetro é o primeiro a ser empilhado e consequentemente ocupará o endereço mais alto. O argumento do parâmetro mais à esquerda é o que fica no topo do stack.

Invocação da função

Listagem 60 use_func8args.c
void func8args(long a1, long *a1p, int a2, int *a2p,
               short a3, short *a3p, char a4, char *a4p);

long x1 = 1;
int x2 = 2;
short x3 = 3;
char x4 = -4;

long use_func8args() {
    func8args(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
	return (x1 + x2) * (x3 - x4);
}
Listagem 61 use_func8args_asm.s
 1        .text
 2        .global use_func8args
 3use_func8args:
 4        sub     $8, %rsp
 5        lea     x4(%rip), %rax
 6        push    %rax
 7        movsbl  x4(%rip), %eax
 8        push    %rax
 9        lea     x3(%rip), %r9
10        movswl  x3(%rip), %r8d
11        lea     x2(%rip), %rcx
12        mov     x2(%rip), %edx
13        lea     x1(%rip), %rsi
14        mov     x1(%rip), %rdi
15        call    func8args
16        movslq  x2(%rip), %rax
17        add     x1(%rip), %rax
18        movswl  x3(%rip), %edx
19        movsbl  x4(%rip), %ecx
20        sub     %ecx, %edx
21        movslq  %edx, %rdx
22        imul    %rdx, %rax
23        add     $24, %rsp
24        ret
25
26        .data
27x4:
28        .byte   -4
29        .align  2
30x3:
31        .word   3
32        .align  4
33x2:
34        .long   2
35        .align  8
36x1:
37        .quad   1
../_images/stack1.svg

Na linha 6 empilha-se o oitavo argumento – o ponteiro para x4. Na linha 8 empilha-se o sétimo argumento – o valor de x4. Note-se que apesar de ser do tipo char a passagem é feita numa palavra de 64 bits. Entre as linhas 9 e 14 procede-se à passagem em registo dos restantes seis argumentos.

A convenção de chamada a funções define que na altura da execução da instrução call o registo RSP deve estar alinhado num endereço múltiplo de 16. Como consequência, à entrada de uma função, o RSP está sempre desalinhado de endereço múltiplo de 16. Assim, a função atual pode basear-se neste pressuposto para efeito de alinhamento do RSP ao realizar outras chamadas. A instrução sub  $8, %rsp na linha 4 serve para cumprir esta convenção. Até à instrução call na linha 15, o RSP vai ser decrementado de 24 ficando alinhado num endereço múltiplo de 16.

Acesso aos argumentos em stack

Listagem 62 func8args.c
void func8args(long a1, long *a1p, int a2, int *a2p,
               short a3, short *a3p, char a4, char *a4p) {
    *a1p += a1;
    *a2p += a2;
    *a3p += a3;
    *a4p += a4;
}
Listagem 63 func8args_asm.s
 1        .text
 2        .global func8args
 3func8args:
 4        mov     16(%rsp), %rax
 5        add     %rdi, (%rsi)
 6        add     %edx, (%rcx)
 7        add     %r8w, (%r9)
 8        mov     8(%rsp), %edx
 9        add     %dl, (%rax)
10        ret
../_images/stack2.svg

No início da execução de uma função o endereço de retorno apresenta-se no topo do stack, depois dos argumentos da função. O acesso aos argumentos é realizado com base em RSP. O acesso a a4p é realizado na linha 4. 16(%rsp) equivale ao endereço 0x7fffffffdd60 que é o local do stack onde ser encontra o argumento &x4.

O acesso a a4 é realizado na linha 8. 8(%rsp) equivale ao endereço 0x7fffffffdd58 que é o local do stack onde se encontra o argumento x4.

Argumentos em stack e variáveis locais

Neste exemplo vai ser mostrada uma utilização do stack mais abrangente. Além de utilizado na passagem de argumentos vai também ser utilizado para alojamento de variáveis locais. O exemplo é semelhante ao anterior com a diferença das variáveis x1, x2, x3 e x4 serem locais à função use_func8args.

Listagem 64 use2_func8args.c
 1void func8args(long a1, long *a1p, int a2, int *a2p,
 2               short a3, short *a3p, char a4, char *a4p);
 3
 4long use_func8args() {
 5
 6        long x1 = 1;
 7        int x2 = 2;
 8        short x3 = 3;
 9        char x4 = -4;
10
11        func8args(x1, &x1, x2, &x2, x3, &x3, x4, &x4);
12                return (x1 + x2) * (x3 - x4);
13}
Listagem 65 use2_func8args_asm.s
 1        .text
 2        .global use_func8args
 3use_func8args:
 4        push    %rbp
 5        mov     %rsp, %rbp
 6        sub     $16, %rsp
 7        movq    $1, -8(%rbp)
 8        movl    $2, -12(%rbp)
 9        movw    $3, -14(%rbp)
10        movb    $-4, -15(%rbp)
11        lea     -15(%rbp), %r8
12        push    %r8
13        movb    -15(%rbp), %dil
14        push    %rdi
15        lea     -14(%rbp), %r9
16        movw    -14(%rbp), %r8w
17        lea     -12(%rbp), %rcx
18        movl    -12(%rbp), %edx
19        lea     -8(%rbp), %rsi
20        movq    -8(%rbp), %rdi
21        call    proc
22        movslq  -12(%rbp), %rdx
23        movq    -8(%rbp), %rcx
24        add     %rdx, %rcx
25        movswl  -14(%rbp), %edx
26        movsbl  -15(%rbp), %eax
27        sub     %eax, %edx
28        mov     %edx, %eax
29        cltq
30        imul    %rcx, %rax
31        mov     %rbp, %rsp
32        pop     %rbp
33        ret
../_images/stack5.svg

O bloco de código inicial linhas 4 a 6 designa-se por preâmbulo da função. No preâmbulo, linhas 4 e 5, o registo RBP é preparado para acesso aos valores em stack – argumentos e variáveis locais. Na linha 6 a adição de 16 a RSP reserva espaço de memória em stack para as variáveis locais.

O registo RSP sofre um decremento de 40 unidades até à instrução call o que garante o alinhamento a múltiplo de 16.

O registo RBP indica sempre a mesma posição do stack – a seguir ao endereço de retorno, onde é salvo o conteúdo de RBP da função chamadora – e mantém-se fixo durante a execução da função.

Para aceder aos parâmetros são usados deslocamentos positivos em relação a RBP: 8(%rbp) corresponde ao primeiro argumento em stack, 16(%rbp) corresponde ao segundo argumento e assim sucessivamente.

Para aceder às variáveis locais são usados deslocamentos negativos em relação a RBP: -8(%rbp) corresponde à primeira variável local – x1, -12(%rbp) corresponde à segunda variável local – x2, -14(%rbp) corresponde à terceira variável local – x3 e -15(%rbp) corresponde à quarta variável local – x4.

Ao retornar da função é necessário repor RSP exatamente no estado inicial – libertar o espaço usado para passagem de argumentos, libertar o espaço reservado para variáveis locais e repor em RBP o conteúdo original.

O código responsável por esta operação designa-se por epílogo e neste caso é formado pelas instruções das linhas 31 e 32. A instrução mov    %rsp, %rbp ao reposicionar RSP na posição de RBP, liberta simultaneamente o espaço ocupado em stack quer no alojamento de variáveis locais, quer na passagem de argumentos.

Red zone

A read zone é a área de stack, livre, além da posição indicada pelo registo SP (endereços inferiores, na arquitetura x86_64). Esta área tem a dimensão de 128 bytes e está resguardada de modificações por rotinas de atendimento de interrupções ou de excepções. Pode ser utilizada para armazenamento temporário de dados que não sejam necessários entre chamadas a funções. Nas funções folha pode ser utilizada para alojar toda a stack frame sem necessidade de ajustar do registo SP.

Listagem 66 mfd.c
#include <stdlib.h>

int must_frequent_digit(char *str) {
	int digits[10] = {0};
	while (*str++)
		if (*str >= '0' && *str <= '9')
			digits[*str - '0']++;
	size_t index = 0;
	for (size_t i = 1; i < sizeof digits / sizeof digits[0]; ++i)
		if (digits[index] < digits[i])
			index = i;
	return digits[index];
}
Listagem 67 mfd_asm.s
 1	.text
 2	.globl	must_frequent_digit
 3must_frequent_digit:
 4	endbr64
 5	mov	$0, %eax		# Inicializar o array digits com o valor zero
 6	jmp	mfd_for1_cond		# O array digits é alojado na red zone
 7mfd_for1_do:
 8	movl	$0, -40(%rsp,%rax,4)
 9	add	$1, %rax
10mfd_for1_cond:
11	cmp	$9, %rax
12	jbe	mfd_for1_do
13mfd_while_do:
14	cmpb	$0, (%rdi)		# While (*str++)
15	je	mfd_while_exit
16	inc	%rdi
17	movzbl	(%rdi), %eax
18	sub	$'0', %eax
19	cmp	$9, %al			# if (*str >= '0' && *str <= '9')
20	ja	mfd_while_do
21	movsbl	(%rdi), %eax		# digits[*str - '0']++;
22	sub	$'0', %eax
23	cltq
24	incl	-40(%rsp,%rax,4)
25	jmp	mfd_while_do
26mfd_while_exit:
27	mov	$1, %edx		# for (size_t i = 1; ... ; ...)
28	mov	$0, %eax
29	jmp	mfd_for2_cond
30mfd_for2_do:
31	add	$1, %rdx		# for (... ; ... ; ++i)
32mfd_for2_cond:			# for (... ; i < sizeof digits / sizeof digits[0]; ...)
33	cmp	$9, %rdx		# if (digits[index] < digits[i])
34	ja	mfd_for2_exit		
35	mov	-40(%rsp,%rdx,4), %esi
36	cmp	%esi, -40(%rsp,%rax,4)
37	jge	mfd_for2_do
38	mov	%rdx, %rax		# index = i;
39	jmp	mfd_for2_do
40mfd_for2_exit:
41	mov	-40(%rsp,%rax,4), %eax
42	ret
43
44	.section .note.GNU-stack

Buffer overflow

A situação conhecida como buffer overflow caracteriza-se pelo acesso a posições de memória além dos limites das variáveis. O caso mais conhecido passa-se com a função gets.

char* gets( char *str );

A função gets lê caracteres do standard input e escreve-os no array passado em parâmetro até ler uma marca de fim de linha – \n. Se o array apontado por str tiver uma dimensão inferior ao número de caracteres lidos, a função gets escreve-os para além do limite do array, corrompendo informação armazenada na vizinhança.

A função gets foi retirada da biblioteca normalizada da linguagem C pelo potencial de falha que permite introduzir num programa.

No exemplo, se o número de caracteres lido for superior a 7, irá ocorrer falha. Quais as consequências dessa falha?

Listagem 68 secret.c
#include <stdio.h>
#include <string.h>

void print_secret(int n) {
	static const char *secret[] = {
		"Tfhsfep!ef!qpmjdijofmp\v",
		"Ugitgfq\"fg\"cdgnjc\f",
		"Vhjuhgr#gh#Idwlpd"
	};
	const char *s = secret[n - 1];
	while (*s)
		putchar(*s++ - n);
}

void secrets() {
	struct {
		int a;
		char buffer[7];
		short b;
	} x;

	x.a = 'a';
	x.b = 'b';

	gets(x.buffer);

	if (x.b == 'B')
		print_secret(1);

	if (x.a == 'A')
		print_secret(2);
}

int main() {
	printf("%p\n", main);
	printf("Secrets\n");
	secrets();
}
Listagem 69 secret_asm.s
 1	.text
 2secrets:
 3	pushq	%rbp
 4	movq	%rsp, %rbp
 5	subq	$16, %rsp
 6	movb	$97, -1(%rbp)
 7	movb	$98, -2(%rbp)
 8	leaq	-9(%rbp), %rax
 9	movq	%rax, %rdi
10	movl	$0, %eax
11	call	gets@PLT
12	cmpb	$65, -1(%rbp)
13	jne	.L5
14	movl	$1, %edi
15	call	print_secret
16.L5:
17	cmpb	$66, -2(%rbp)
18	jne	.L7
19	movl	$2, %edi
20	call	print_secret
21.L7:
22	nop
23	leave
24	ret
25	mov	$3, %rdi

Geração do executável:

$ gcc secret.c -fno-stack-protector -o secret

Exercícios

  1. Fazer com que a sequência de caracteres introduzida provoque a execução de print_secret(1).

  2. Fazer com que a sequência de caracteres introduzida provoque a execução de print_secret(2).

Stack protector

O gcc dispõe de um meio de deteção da ocorrência de buffer overflow. À entrada na função é posicionada uma marcação no limite da stack frame; à saída da função verifica-se se a marcação foi corrompida. Esta marcação é designada por "canário".

Este método não completamente eficaz. Se o acesso se der para além do canário sem o modificar, não será detetado.

Esta funcionalidade é ativada/desativada através das opções seguintes. Por omissão, é ativada.

-fstack-protector
-fno-stack-protector
Listagem 70 stack_protector.c
void f() {
	char array[10];
	array[28] = 33;
}

Listagem 71 stack_protector_asm.s
 1        .text
 2f:
 3        endbr64
 4        pushq   %rbp
 5        movq    %rsp, %rbp
 6        subq    $32, %rsp
 7        movq    %fs:40, %rax
 8        movq    %rax, -8(%rbp)
 9        xorl    %eax, %eax
10        movb    $33, 10(%rbp)
11        nop
12        movq    -8(%rbp), %rax
13        xorq    %fs:40, %rax
14        je      .L2
15        call    __stack_chk_fail@PLT
16.L2:
17        leave
18        ret

Stack clash

O stack clash acontece quando a dimensão de uma variável local depende de um parâmetro (a dimensão é variável). As variáveis locais são alojadas no stack por deslocação do stack pointer. Se esta deslocação ultrapassar certos limites pode colidir com outras zonas de memória.

O gcc dispõe de uma opção de compilação para ativar/desativar a geração de código de mitigação de stack clash.

-fstack-clash-protection
-fno-stack-clash-protection

Recomendações para escrita em assembly

Na escrita de programas em geral, usam-se convenções de formatação para facilitar a leitura do programa por parte do humano. Em seguida lista-se um conjunto de regras geralmente utilizadas na programação em linguagem assembly e que são aplicadas nos programas de exemplo.

  • O texto do programa é escrito em letra minúscula, exceto os identificadores de constantes, que são escritos em letra maiúscula.

  • Nos identificadores formados por várias palavras usa-se como separador o carácter ‘_’ (sublinhado).

  • O texto do programa é disposto na forma de uma tabela de quatro colunas. Na primeira coluna insere-se apenas a label, se existir; na segunda coluna a mnemónica da instrução ou a diretiva; na terceira coluna os parâmetros da instrução ou da diretiva; na quarta coluna os comentários até ao fim da linha (começados por ';' ou envolvidos por /* */).

  • Cada linha contém apenas uma label, uma instrução ou uma diretiva.

  • Para definir as colunas deve usar-se o carácter TAB configurado com a largura de oito espaços.

  • A terceira coluna ­– a dos parâmetros ­– pode ocupar mais que um espaçamento.

  • As linhas com label não devem conter nenhum outro elemento. Isso permite usar labels compridas sem desalinhar a tabulação e criar separações na sequência de instruções, que ajudam na interpretação do programa.

../_images/assembly_layout.svg

Referências