Ir para conteúdo

“Hello world” em nasm no Linux x86


Fernando Mercês

Posts Recomendados

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 para o comentário
Compartilhar em outros sites

  • 1 mês depois...
  • 1 mês depois...

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 para o comentário
Compartilhar em outros 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 para o comentário
Compartilhar em outros sites

  • 4 semanas depois...

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 para o comentário
Compartilhar em outros 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 para o comentário
Compartilhar em outros sites

  • 3 semanas depois...

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 para o comentário
Compartilhar em outros sites

  • 4 meses depois...

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 para o comentário
Compartilhar em outros 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 para o comentário
Compartilhar em outros 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 para o comentário
Compartilhar em outros sites

  • 8 meses depois...
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 para o comentário
Compartilhar em outros sites

Arquivado

Este tópico foi arquivado e está fechado para novas respostas.

  • Quem Está Navegando   0 membros estão online

    • Nenhum usuário registrado visualizando esta página.
×
×
  • Criar Novo...