Redescobrindo Assembly

Como arrays são representados em Assembly e o que muda na passagem de parâmetros em arquiteturas X64

Um array é um conjunto de valores posicionados na memória de maneira sequencial. Percorrer os elementos de um array implica, pura e simplesmente, em acessar essas posições memória.

Esse conceito, aliás, é bem explícito em C++.

#include <iostream>

using namespace std;

extern "C" int compute_sum_asm(const int* values, const int count);

int compute_sum(const int* values, const int count)
{
	auto result = 0;
	for (auto i = 0; i < count; i++)
	{
		result += *values;
		values++;
	}
	return result;
}

int main()
{
	const int values[] { 4, 7, 9, 12, 23, 18, 45 };
	const int count = sizeof(values) / sizeof(int);

	cout << compute_sum(values, count) << endl;
	cout << compute_sum_asm(values, count) << endl;

	return 0;
}

No exemplo, escrito de maneira bem primitiva, vemos que a função recebe dois parâmetros. O primeiro, é um ponteiro para o primeiro elemento no array. O segundo, é um contador indicando quantos elementos o array possui.

A implementação, em Assembly para x86 não se afasta muito do que já fizemos até aqui.

	.model flat, c
	.code

compute_sum_asm proc

	push ebp
	mov ebp, esp
	push ebx

	xor eax, eax

	mov ebx, [ebp + 8]
	mov edx, [ebp + 12]

	cmp edx, 0
	jle done

lp: add eax, [ebx]
	add ebx, 4
	dec edx
	jnz lp

done:
	pop ebx
	pop ebp
	ret

compute_sum_asm endp
	end 

Para X64, entretanto, essa implementação precisaria ser um pouco diferente. Com o número ampliado de registradores (todos que começam com “r” são versões 64 bits), a convenção os utiliza sempre que possível reduzindo a utilização da Stack. Como registradores são os elementos de memória mais velozes disponíveis, o resultado é um incremento de performance.

Convenções para chamada de funções em C++ 64 bits

A convenção para chamada de funções em C/C++ X64 se beneficia do número maior de registradores disponível nessa arquitetura. Assim:

  • Os primeiros quatro parâmetros inteiros ou ponteiros são passados através dos registradores rcx, rdx, r8, e r9.
  • Os primeiros quatro argumentos com ponto-flutuante são passados nos registradores SSE  xmm0xmm3.
  • A função que está “chamando” é responsável por reservar espaço na stack para os argumentos passados como parâmetros. Assim, a função “chamada” pode usar este espaço para “copiar” os valores dos registradores na stack.
  • Parâmetros adicionais são passados via stack.
  • O retorno deve acontecer em rax (lembrando que eax corresponde aos bits menos significativos desse registrador). Retornos com ponto flutuante devem ser retornados em xmm0.
  • rax, rcx, rdx, r8r11 são voláteis (não é necessário garantir que seus valores sejam restaurados quando a função retorna).
  • rbx, rbp, rdi, rsi, r12r15 não são voláteis..

Fonte: Microsoft

Essas convenções, por interoperabilidade, também são respeitadas pelo .NET

No exemplo desse post, como estamos passando um ponteiro e um inteiro, respectivamente, os registradores utilizados  nas passagens de parâmetros são rcx e rdx.

       .code

compute_sum_asm proc
	xor eax, eax

	cmp edx, 0
	jle done
	
lp: add eax, [rcx]
	add rcx, 4
	dec edx
	jnz lp

done: 	
	ret

compute_sum_asm endp

	end 

Os cuidados no projeto dos compiladores em aproveitar as características da arquitetura de execução são expressão de “design caprichoso”.

Elemar Júnior

Microsoft Regional Director e Microsoft MVP. Atua, há mais de duas décadas, desenvolvendo software e negócios digitais de classe mundial. Teve o privilégio de ajudar a mudar a forma como o Brasil vende, projeta e produz móveis através de software. Hoje, seus interesses técnicos são arquiteturas escaláveis. bancos de dados e ferramentas de integração. Além disso, é fascinado por estratégia e organizações exponenciais.

Talvez você goste também

Carregando posts…

Mais posts da série Redescobrindo Assembly

Deixe uma resposta

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *