Jump to content

“Hello world” em nasm no Linux x86


Fernando Mercês

Recommended Posts

Esta é uma maneira antiga de usar as chamas de sistemas do Linux, mas ainda funciona e tem fins didáticos :-)

; # apt install nasm
; $ nasm -f elf32 hello.asm
; $ ld -m elf_i386 -o hello hello.o
; $ ./hello
 
    section .rodata              ; seção .rodata do ELF, onde ficam os dados somente-leitura
msg: db "Mente Binária", 10      ; nossa string que será impressa, seguida de um \n
len: equ $-msg                   ; "$" significa "aqui" -> posição atual menos posição do texto. len terá o tamanho da string.
 
    section .text                ; seção .text do ELF, onde fica o código
global _start                    ; faz o label "_start" visível ao linker (ld)

_start:
    mov edx,len                  ; arg3 da syscall write(), quantidade de bytes para imprimir (tamanho)
    mov ecx,msg                  ; arg2, pointeiro para o endereço da string
    mov ebx,1                    ; arg1, em qual file descriptor (fd) escrever. 1 é stdout
    mov eax,4                    ; 4 é o código da syscall write()
    int 0x80                     ; interrupção 0x80 do kernel (executa a syscall apontada em eax)
	 
    mov ebx,0                    ; arg1 da syscall exit(). 0 significa execução com sucesso
    mov eax,1                    ; 1 é o código da syscall exit()
    int 0x80                     ; executa a syscall apontada em eax, que vai sair do programa

 

Link to comment
Share on other sites

  • 1 month later...
  • 1 month later...

Em CPUs de 64 bits com sistemas Linux costumam suportar a instrução SYSCALL, assim não é necessário usar instruçoes emuladas como int 0x80. Um ponto importante é que no uso desta instrução, todos os parâmetros e números da system call devem ser passados por Registadores de 64 bits (Registadores - Assim chamado em Portugal xD), como nos diz o código do Kernel do Linux.

Outro detalhe é que com o uso desta instrução o número da syscall das funções é diferente, para write em vez de ser mov eax, 4 será mov rax, 1.

Link to comment
Share on other sites

Bem, começando do início, uma system call basicamente será uma "interface" entre o User e Kernel Space.

CPUs de 64 bits e com o uso de sistemas Linux costuma ser suportado a instrução SYSCALL, assim não sendo necessário usar instruçoes emuladas como int 0x80. (Todas as system calls estão disponíveis em funções da libc, pois elas raramente serão chamadas a partir de puro assembly após o código compilado).

De uma maneira resumida, a instrução SYSCALL age como se fosse se fosse uma interrupção (no caso é uma interrupção de software, tal como a int 0x80, agindo diferentemente mas  de uma forma bem mais performática), criando uma excepção que fará com que o controlo de execução da CPU seja transferido para um tratador de excepção (exception handler) residente no Kernel (mais concretamente Kernel Code... xD). A maneira com que o Kernel consegue passar o controle de execução para o tratador de excepção correto para a syscall em questão é a partir de uma tabela (syscall table), cujo é representada pela array "sys_call_table" presente no Kernel do Linux que é definida em "arch/x86/entry/syscall_64.c" a partir deste código: 

asmlinkage const sys_call_ptr_t sys_call_table[__NR_syscall_max+1] = {
    [0 ... __NR_syscall_max] = &sys_ni_syscall,
    #include <asm/syscalls_64.h>
};

Isto quer dizer que a array sys_call_table é uma array de "__NR_syscall_max + 1" de tamanho, onde "__NR_syscall_max",  que é uma macro, representa o máximo número de system calls existentes na arquitetura em questão. Sem ter a certeza, penso que atualmente o valor de __NR_syscall_max é 322. Podemos ver a definição desta Macro no arquivo gerado durante a compilação do Kernel em "include/generated/asm-offsets.h":   #define __NR_syscall_max 322.

Outro tipo aprensentado no código de "/syscall_64.c" é "sys_call_ptr_t" que nada mais é do que um ponteiro para para a tabela de system calls (syscall table). Este é definido como um typedef para um ponteiro de uma função que nao retorna nada nem recebe parâmetros:

typedef void (*sys_call_ptr_t)(void);

Para não deixar muito longo a explicação sobre o código da syscall table vou apenas falar muito resumidamente sobre "sys_ni_syscall", que basicamente representa syscalls que nao foram implementadas diretamente. (OBS: Todos os elementos de sys_call_table apontam para estas syscalls não implementadas), o valor de retorno de "sys_ni_syscall" é  "-errno" ou "-ENOSYS" em certos casos.

asmlinkage long sys_ni_syscall(void)
{
    return -ENOSYS;
}

O erro -ENOSYS diz nos que - Function not Implemented (POSIX.1)

Mais uma nota: É possível iniciar a sys_call_table a partir de uma extensão do GCC chamada Designated Initializers, que permite a inicialização de elementos de forma não ordenada. Como foi visto no fim de "syscall_64.c" foi incluido o header asm/syscalls_64.h, que foi incialmente gerado pelo script arch"/x86/entry/syscalls/syscalltbl.sh", "syscalls_64.h", contém definidos macros que serão utilizados.

Isto foi apenas uma curiosidade sobre syscalls em linux, é certo que é um assunto bastante extenso que seria praticamente impossível descreve-lo por completo aqui (Nem eu próprio o sei kkkk), mas já foi uma introdução, para os interessados sugiro a procura pelo assunto em documentação oficial, etc...

---------------------------------------------------------------------------------------------------------------------------------------------

Para verificar a disponibilidade desta instrução na CPU em questão basta usar a instrução CPUID, a qual a descrição está fora do escopo. Mas o código para tal será este:

mov rax,1
cpuid
test edx,0x800  ; O bit 11 de EDX será "testado" e se ZF = 0 então a instrução SYSCALL é suportada.

 

Segundo a documentação todos os parâmetros de uma syscall devem ser passados por Registadores e não pela Stack (Pilha). Segue a convenção no uso destes:

RDI, RSI, RDX, R10, R8, R9 - Nestes 6 Registadores devem ser passados os parâmetros da syscall executada, notar que terão de ser passados nesta ordem específica de Registadores.

RAX - Usado para indicar o número da syscall que será executada. (Levar em consideração que o valor que será colocado em RAX ou EAX para indicar a função derivará do uso da instrução SYSCALL para o uso de uma instrução emulada como int 0x80).

(OBS: O Registador R11 não deve ser utilizado em syscalls pois o contexto de RFLAGS será guarda nele durante o retorno da chamada).

O retorno da syscall será colocado em em RAX, como um endereço linear para o ponteiro dessa função (pelo menos no caso de algumas funções). Caso a execução da syscall gere algum erro, o próprio será colocado também em RAX como valor de retorno.

Voltando atrás, à parte da passagem do número da syscall por RAX, pode ser obtida uma lista de syscalls no dirétorio e arquivos "arch/x86/entry/syscalls/syscalls_32.tbl" e "arch/x86/entry/syscalls/syscalls_64.tbl" (para arquiteturas 32 e 64 bits respetivamente (o número de syscalls contidas nestes arquivos depende do valor de "__NR_syscall_max", como visto anteriormente)). Nas manpages do linux também se encontram disponíveis listas e informaçoes sobre syscalls de uma forma mais aprofundada. Junto Envio:

http://man7.org/linux/man-pages/man2/syscalls.2.html

Algumas manpages sobre funções específicas contêm também informações sobre a respetiva syscall.

--------------------------------------------------------------------------------------------------------------------------------------------

Um outro detalhe é que no Windows a instrução syscall também pode ser usada (não tao comum e viável, segundo documentação), caso não esteja dísponivel sempre pode ser usada a interrupção int 0x2e.

------------------------------------------------------------------------------------------------------------------------------------------------

Segue  o exemplo da instrução SYSCALL, e como pedido pelo Fernando um Hello World :D:

------------------------------------------------------------------------------------------------------------------------------------------------

bits 64                 ;informar que as instuções devem ser codificadas para a arquitetura 64 bits.

section .data     ; Secção do arquivo executável final que contém dados globais inicializados.

msg  db "Hello World", 0x0a      ; mensagem que vai ser "printada" .
len equ $ - msg                               ; tamanho da mensagem (Parâmetro que a função com que vamos printar a mensagem recebe).
FILENO_STDOUT equ 1                 ;Descritor de arquivos (File Descriptor de output de dados, também será passado como parâmetro à função).

section .text       ;Secção do arquivo executável final que contém as instruções do programa.
    global _start                ; Faz com que o símbolo _start seja vísivel ao Linker (como mencionado pelo Fernando no tópico acima :D)

_start:         ;simbolo start (primeira função a ser executada no programa que irá executar funções construturas até chegar à famosa "main")
   

     Protótipo de write() - ssize_t write(int fd, void *buffer, size_t count);

     mov rax, 1           ; Número da função syscall que será executada, neste caso 1 = write() em syscall, com int 0x80, 4 = write().
     mov rdi, FILENO_STDOUT    ; Passar como parâmetro da função Write o descritor de ficheiros pelo Registador RDI
     mov rsi, msg         ;Passar a mensagem a ser printada como parâmetro por RSI
     mov rdx, len           ; Passar o tamanho da mensagem a ser printada como parâmetro por RDX
     syscall                      ; Finalmente executar a instrução SYSCALL que executará RAX = 4, ou seja write() com todos os parâmetros passados por registadores acima.

    mov rax, 60       ;Número da função da syscall que será executada, neste caso 60 = exit()
    mov rdi, 0x0      ; Passar o valor de retorno de exit como parâmetro por registador RDI
    syscall                 ;Executar a instrução syscall que executará RAX = 60, ou seja exit() como todos os parâmetros passados por Registadores acima.

 

OBS: Deve ser mantida a ordem de registadores usados para passagem de parâmetros para funções, assim seguindo a convenção de chamada das syscalls

Peço desculpa pela bagunça no código xD.

Aqui fica uma introdução às System Calls. Se me lembrar de algo essencial que me tenha esquecido volto a editar, caso tenha cometido algum erro, agradeco a correção.

Ricardo Santos.

Link to comment
Share on other sites

  • 4 weeks later...

Eu mudaria pouca coisa:

  1. Mover valores imediatos para registradores de 32 bits automaticamente zera os 32 bits superiores;
  2. Por motivoo de clareza, usar LEA ao invés de MOV para inicializar registradores com ponteiros;
  3. Offsets relativos a RIP são menores que os tradicionais;
  4. Valores constantes, incluindo strings, deveriam ser colocados em .rodata, não .data.
  bits 64
  default rel

  section .rodata

  ; Se usar 'crase' como delimitador de strings,
  ; pode usar sequências de escape! Aspas, simples
  ; ou duplas, não fazem isso no NASM.
msg:
  db `Hello!\n`
msg_len equ $ - msg

  section .text

  global _start
_start:
  mov eax,1           ; syscall 1: write
  mov edi,eax         ; STDOUT_FILENO
  lea rsi,[msg]       ; offsets relativos a RIP são menores.
  mov edx,msg_len
  syscall

  mov eax,60          ; syscall 60: exit
  xor edi,edi
  syscall

Assim as instruções ficam pequenas (apenas LEA, acima, tem o prefixo REX).

Link to comment
Share on other sites

@Rick Santos sua explicação foi incrível. Muito obrigado! Compilei seu programa e funciona perfeitamente.

Já que o @fredericopissarra falou da .rodata, andei dando uma olhada e achei bem legal o tratamento que o NASM dá dependendo do nome da seção/segmento:

$ objdump -h hello.o

hello.o:     file format elf64-x86-64

Sections:
Idx Name          Size      VMA               LMA               File off  Algn
  0 .data         00000007  0000000000000000  0000000000000000  00000380  2**2
                  CONTENTS, ALLOC, LOAD, DATA
  1 .rodata       00000007  0000000000000000  0000000000000000  00000390  2**2
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  2 .rodata666    00000007  0000000000000000  0000000000000000  000003a0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  3 .mentebin     00000007  0000000000000000  0000000000000000  000003b0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  4 rodata        00000007  0000000000000000  0000000000000000  000003c0  2**0
                  CONTENTS, ALLOC, LOAD, READONLY, DATA
  5 .comment      00000005  0000000000000000  0000000000000000  000003d0  2**0
                  CONTENTS, READONLY
  6 .bss          00000041  0000000000000000  0000000000000000  000003e0  2**2
                  ALLOC
  7 .text         0000001e  0000000000000000  0000000000000000  000003e0  2**4
                  CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE

Conforme visto, para qualquer nome fora do padrão do formato (seções 2, 3 e 4), o comportamento padrão é uma seção de dados somente leitura. No entanto, ao usar um nome suportado, o tratamento é outro.

Usar .rodata faz a string ficar numa seção somente leitura (a .data tem permissão de escrita), mas tem mais uma coisa: o alinhamento tá em 2**2 (contra 2**0 das outras seções "fora do padrão"). Casa exatamente com o que a documentação diz sobre o alinhamento das seções, onde 2**2 = 4 bytes.

Em tempo, esta thread tá excelente. Muito obrigado por compartilharem conhecimento!

Abraços,

Fernando

Link to comment
Share on other sites

  • 3 weeks later...

Notem que modifiquei o código original que postei, mostrando as chamadas SYSCALL... O NASM aceita o uso de delimitadores de string com "crases", que permite o uso de sequências de escape como \n, \r, \0, \t, ...

Outra coisa... ao usar a diretiva "default rel", não precisa se preocupar com o uso do tipo de endereçamento relativo ao RIP, este torna-se o default.

Link to comment
Share on other sites

  • 4 months later...

Boa noite a todos,

No meu caso para funcionar, estou utilizando o kali linux instalado dentro do windows 10 (Microsoft Store tem para baixar e instalar).

A plataforma que utilizo é de 64 bits e utilizei a seguinte variação para funcionar:

hello.asm

section .data
msg db "Mente Binária Rocks!"
 
section .text
global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, 13
syscall
mov rax, 60
mov rdi, 0

syscall

 

nasm -f elf64 -o hello.o hello.asm

ld -o hello hello.o

./hello

Link to comment
Share on other sites

Em 09/11/2017 em 12:46, Rick Santos disse:

Outro detalhe a se considerar é que o retorno da syscall executada é acedido por RAX, que contém o endereço linear do ponteiro de retorno da função (syscall) executada.

Existe outro detalhe com o uso de SYSCALL... Os Flags (quando retornados) são colocados em R11.
Embora isso não seja problemático no Linux...

Em 19/05/2018 em 22:27, fransalles disse:

Boa noite a todos,

No meu caso para funcionar, estou utilizando o kali linux instalado dentro do windows 10 (Microsoft Store tem para baixar e instalar).

A plataforma que utilizo é de 64 bits e utilizei a seguinte variação para funcionar:

hello.asm

section .data
msg db "Mente Binária Rocks!"
 
section .text
global _start
_start:
mov rax, 1
mov rdi, 1
mov rsi, msg
mov rdx, 13
syscall
mov rax, 60
mov rdi, 0

syscall

 

nasm -f elf64 -o hello.o hello.asm

ld -o hello hello.o

./hello

É sempre interessante informar o modelo usado com 'bits 64' ou 'bits 32' no código.
Algumas instruções são codificadas de acordo com essas diretivas.

O argymento -f elf64 diz ao NASM apenas qual é o formato do arquivo ELF.

Link to comment
Share on other sites

Em 21/05/2018 em 08:16, fredericopissarra disse:

Existe outro detalhe com o uso de SYSCALL... Os Flags (quando retornados) são colocados em R11.
Embora isso não seja problemático no Linux...

É sempre interessante informar o modelo usado com 'bits 64' ou 'bits 32' no código.
Algumas instruções são codificadas de acordo com essas diretivas.

O argymento -f elf64 diz ao NASM apenas qual é o formato do arquivo ELF.

Com certeza Frederico, eu cheguei a fazer uma observação sobre os flags: (OBS: O Registador R11 não deve ser utilizado em syscalls pois o contexto de EFLAGS será guarda nele durante o retorno da chamada). Obrigado pela informação.

Link to comment
Share on other sites

  • 8 months later...
Em 09/08/2017 em 21:35, Fernando Mercês disse:

Esta é uma maneira antiga de usar as chamas de sistemas do Linux, mas ainda funciona e tem fins didáticos ?


; # apt install nasm
; $ nasm -f elf32 hello.asm
; $ ld -m elf_i386 -o hello hello.o
; $ ./hello
 
    section .rodata              ; seção .rodata do ELF, onde ficam os dados somente-leitura
msg: db "Mente Binária", 10      ; nossa string que será impressa, seguida de um \n
len: equ $-msg                   ; "$" significa "aqui" -> posição atual menos posição do texto. len terá o tamanho da string.
 
    section .text                ; seção .text do ELF, onde fica o código
global _start                    ; faz o label "_start" visível ao linker (ld)

_start:
    mov edx,len                  ; arg3 da syscall write(), quantidade de bytes para imprimir (tamanho)
    mov ecx,msg                  ; arg2, pointeiro para o endereço da string
    mov ebx,1                    ; arg1, em qual file descriptor (fd) escrever. 1 é stdout
    mov eax,4                    ; 4 é o código da syscall write()
    int 0x80                     ; interrupção 0x80 do kernel (executa a syscall apontada em eax)
	 
    mov ebx,0                    ; arg1 da syscall exit(). 0 significa execução com sucesso
    mov eax,1                    ; 1 é o código da syscall exit()
    int 0x80                     ; executa a syscall apontada em eax, que vai sair do programa

 

Parabéns! Código bastante útil...

Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

  • Recently Browsing   0 members

    • No registered users viewing this page.
×
×
  • Create New...