Ir para conteúdo

Rick Santos

Membros
  • Total de itens

    10
  • Registro em

  • Última visita

Reputação

7 Neutral
  1. Alguns pontos interessantes sobre malloc()

    Neste tópico pretendo realizar uma breve apresentação de diversos pontos que considero importantes relativamente à função malloc() em sistemas Linux. malloc() tem como função alocar memória dinamicamente, criando ou alterando assim um segmento pertencente ao processo chamado Heap. (Alterando também pois durante a carregamento do programa para a memória, tanto o Loader do OS quando a própria libc podem (mas nem 100% das vezes) ter pré-alocado o segmento Heap com páginas suficientes para o processo em questão, para assim, evitar o mapeamento de novas) A função malloc() é declarada em em "stdlib.h" a partir do seguinte protótipo: void *malloc(size_t size); Isto quer dizer que malloc() alocará size quantidade de memória e retornará um ponteiro * do tipo void para o primeiro endereço do segmento alocado. (A Heap não será inicializada com nenhuns dados inicialmente). Quando malloc() é executada com sucesso retorna um ponteiro do tipo void, isto quer dizer que (falando a nível de programação high-level), o ponteiro não tem nenhum tipo associado, sendo assim podendo ser usado por qualquer tipo de variável e ficando sempre com o tamanho de 8 bytes (64 bits), como qualquer ponteiro em arquiteturas atuais. Por outro lado, caso não tenha havido sucesso, ou seja não conseguindo alocar memória, quer dizer que o size = 0. Quando tal acontece malloc() retorna um valor NULL, em certos casos, poderá ser retornado um valor único de ponteiro podendo ser passado futuramente para uma outra função chamada free(). Quando a libc tenta alocar memória pedida por malloc() (atráves de system calls que irei descrever adiante) assume que existe espaço para a criação de novas páginas no espaço requisitado, o é quase sempre verdade devido ao uso do VAS (Virtual Address Space, derivado da Paginação). Mesmo assim é correto e essencial verificar sempre o valor de retorno, e caso não seja possível a alocação decidir o que fazer. Segue um código de exemplo sobre o falado até agora: #include <stdio.h> #include <stdlib.h> // malloc() tem protótipo aqui como: void *malloc(size_t size); int main(int argc, char *argv[]) { void *ptr; //Criação de um ponteiro do tipo void, podia ser de qualquer tipo. //Aqui usou-se a malloc para alocar sizeof(int) memória, neste caso será 4 bytes e caso malloc() retorne null, acontece algo, como coloquei em baixo. if (!!(ptr = malloc(sizeof(int)) == NULL)) { //OBS: O uso de !! foi apenas para o compilador não reclamar de uma atribuição "válida". printf("Não foi possível alocar memória\n"); exit(EXIT_FAILURE); // OBS: EXIT_FAILURE é uma macro neste caso para 0, mas portável para todos os sistemas operativos, enquanto 0 em si não. } free(ptr); //Limpar o espaço alocado por malloc atráves de ptr. (Sempre necessário quando não se vai mais utilizar a memória alocada) return 0; } PS: NULL é definido em C, por vários headers - <stddef.h>, <locale.h>, <stdio.h>, <stdlib.h>, <string.h>, <time.h>. A partir da seguinte forma: #if !defined(NULL) #define NULL ((void*)0) #endif Querendo assim dizer que NULL é uma Macro, que pode ser atribuída a qualquer tipo de dado "void" e que aponta * para o valor 0, ou seja, usar NULL ou 0 é a mesma coisa pois NULL = 0. -------------------------------------------------------------------------------------------------------------------------------------------------------- Algo importante é que o Kernel apenas aloca segmentos de memória (blocos de páginas) com tamanhos múltiplos do tamanho de uma página, que por padrão é 4 KiB (O Linux como outros sistemas podem trabalhar com páginas de tamanhos maiores habilitando a extensão PSE, mas não nos interessa por agora). Isto quer dizer que se o programa tentar alocar menos do que 4 KiB o Kernel continuará a alocar uma página pelo menos e o código só tera acesso ao espaço requisitado dessa página (O que equivale a um desperdício de memória, e assim de performance). Felizmente a própria malloc() e outras funções de alocação, para ganho de performance, tem como objetivo reutilizar páginas já usadas e também aproveitar páginas já criadas mas com necessidade de remapeamento, assim evitando que elas andem "perdidas" pela memória. --------------------------------------------------------------------------------------------------------- Na verdade a malloc() não é nenhuma função do "sistema", ela é apenas uma função usada em C que é definida em "stdlib.h". Na realidade malloc() utiliza duas System Calls para a alocação de memória, chamadas sbrk() e mmap(). A malloc() utiliza a system call sbrk() quando pretende alocar blocos de memória menores que MMAP_THRESHOLD (ou seja 128 KiB (32 páginas)), isto é feito a partir da expansão de memória já alocada para o processo a partir de um ponto chamado "program break". Program Break é o primeiro endereço linear logo após ao segmento BSS (inicializado a zeros) do processo, o program break ao ser aumentado a partir de void *sbrk(intptr_t increment) aumenta também o número de páginas disponíveis que foram requesitadas incialmente para uso da heap por malloc(). (PS: Chamar sbrk() com um incremento de zero vai dar origem à obtenção do endereço do Program Break do processo em questão). Por fim o valor de retorno de sbrk() é um ponteiro para o endereço inicial do novo "segmento" de memória alocado. Caso tenha sido passado o argumento increment = 0 a sbrk(), o valor de retorno será o endereço do Program Break. Caso tenha havido algum erro o valor de retorno será do tipo void * com o valor de -1. (PS: O tipo do argumento passado para sbrk() varia entre sistemas POSIX, podendo ser: int, ssize_t, ptrdiff_t, intptr_t). Para blocos maiores que MMAP_THRESHOLD (ou seja 128 KiB (32 páginas)), malloc() utiliza a system call mmap(), que contém a seguinte definição: void *mmap(void *addr, size_t lengthint " prot ", int " flags , int fd, off_t offset); Como podem ver é uma função longa, não vou falar detalhadamente dela mas sim cobri-la no geral como fiz com sbrk(). mmap() aloca novas páginas não acessíveis no user-space com tamanho passado como argumento "length" e a partir de um endereço inicial passado em "addr". Caso o argumento addr seja passado como NULL o Kernel tenta alocar memória a partir do endereço que ele achar mais "performático" alocar, caso contrário (seja passado algum endereço em addr), o Kernel tenta alocar memória a partir daquele endereço passado (A passagem do parâmetro addr é apenas uma "dica", agora não quer dizer que seja sempre cumprida). O argumento "prot" de mmap está relacionado com algumas "opções" de proteções das páginas criadas cujo a descrição está fora do escopo do tópico. Por fim, o valor de retorno de mmap() é o endereço da primeira posição do segmento de memória alocado por esta função. PS: A função (system call) mmap() pode ser utilizada diretamente em C, assim tornando-se mais vantajo-so trabalhar com esta quando se quer ter um maior controle sobre páginas do que usar malloc(), que nos dará "recursos limitados". Basta incluir o seguinte header: #include <sys/mman.h> ---------------------------------------------------------------------------- Bem, esta foi apenas a apresentação de um pontos básicos que considero importantes acerca da tão conhecida função malloc() em C. Ricardo Santos
  2. Ponteiros em C

    Só para acrescentar um ponto relevante relacionado à aritmética de ponteiros: Ao nos referenciar-mo-nos a ptr[3] estamos a referir-nos ao endereço de ponteiro como base mais um offset(deslocamento) entre [], quando falamos em endereços, para obter-mos o endereço correto de uma referência como a de cima teremos de multiplicar o offset pelo sizeof(tipo_de_ponteiro) que está a ser usado. Exemplo: ptr[3] é igual a *(ptr + 3), o que na realidade vai fazer referência a *(ptr + 3 * sizeof(ptr)), como referi acima, se ptr apontar por exemplo para 0x11 e usarmos ptr[3] na realidade vamos nos referir ao endereço 0x1d, pois foi somado a ponteiro 3 * sizeof(ptr). Um outro detalhe interessante, é o uso da propriedade comutativa na aritmética de ponteiros... Exemplo: Ter ptr[3] ou 3[ptr] é exatamente a mesma coisa, pois ptr[3] significa em termos de programação *(ptr + 3) e 3[ptr] significa *(3 + ptr), e seguindo a propriedade comutativa da matemática a + b = b + a, querendo assim dizer que ptr + 3 <=> 3 + ptr. Resumindo ptr[3] e 3[ptr] são iguais, a unica consideração a se levar é que por vezes o compilador pode gerar um aviso em relação à segunda durante o processo de compilação. Por fim, este uso esquisito de aritmética de ponteiros também pode ser usado com arrays, considerado que o símbolo pelo qual a array é representado, por exemplo "a", onde "a" é igual a "&a[0]" (ou seja "a" é um ponteiro para o endereço de memória do primeiro "item" do array), assim qualquer deslocamento numa array, por exemplo a[5], pode também ser representado por *(a + 5) ou *(5 + a) devido à propriedade de comutativa que demonstrei acima. Foi apenas um acréscimo ao tópico. ### SEM QUERER MULTICITEI O CONTEÚDO ###
  3. Um pouco sobre Excepções

    Neste tópico irei falar um pouco sobre excepções, espero que seja util para quem desconhece este assunto. (Mais informações podem ser encontradas no manual da Intel, Volume 3, Capítulo 6). Excepções podem ser consideradas um tipo de interrupção gerada pela CPU após algum acontecimento inesperado de execução. Elas podem causar um grande custo de performace. Certos aspetos que irei falar aqui encontram-se melhor explicados em livros e documentação oficial, como tal, este conteúdo é apenas uma introdução. Uma ultima dica é observar o código fonte do relacionado ao que falo aqui, nele encontram-se todas as estruturas, etc... Existem 3 tipos principais de Excepções (pelo menos conhecidos ), estes são (por ordem de "gravidade"): Faults/Faltas (Em Português é comum chama-las "Falhas"): Simplesmente acontecimentos inesperados que continuam a permitir o fluxo de execução normal sem causar grandes custos de processamento (Mas é sempre preferível evita-las ). Traps/Armadilhas (Nunca vi nenhuma tradução em Português para esta por isso pode ser armadilhas ): São um tipo de interrupção (como todas as outras) e são reportadas imediatamente após à instrução que gerou a excepção. Aborts/Abortos - Excepções de nível grave, onde muitas das vezes torna-se impossível continuar a operação do sistema dando-se assim o término do processo que gerou o aborto, e por vezes do sistema operativo em si, sendo necessário o reboot da máquina. OBS: Alguns Abortos ao afetarem o Kernel podem gerar algo chamado Kernel Panic (normalmente originado a partir de um "fatal error") que no caso do Windows, pode dar-se o famoso "Blue Screen of Death" . PS: Por convenção, a nível de arquitetura, excepções tal como interrupções podem ser representadas por "excepção/interrupção + #", como demonstrado a baixo. Algumas excepções como General Protection Fault (GP#) , Page Fault (PF#) e Double Fault (DF#) (entre muitas outras) podem colocar um código de erro específico da determinada excepção na Stack (Pilha) pertencente ao processo que gerou o "erro". Normalmente este código de erro é empilhado após o empilhamento de diveros registadores, na ordem: SS, RSP, CS, RIP (SS é empilhado apenas por questões de optimização, segundo consta ). Estes registadores e códigos de erro devem-se manter empilhados até ao momento do tratador de interrupção encontrar alguma instrução do tipo IRET que recuperá todos os conteúdos que terão sido colocados na pilha antes do Kernel passar o controle de execução para o tratador de excepção corresponde à mesma. Por fim após os valores "desempilhados", o tratador ainda tem a função se direcionar o controle de execução para CS:RIP (Par de registadores para o qual a CPU executa código). ------------------------------------------------------------------------------------------------------------------------------------------------------------ Aqui irei fazer uma breve introdução ao tratamento de excepções no modo x64 do Linux. A primeira coisa a entender é que uma excepção, em certos aspectos, é idêntica a uma interrupção, e como tal, necessita de ser tratada a partir de de uma determinada rotina que é escolhida a partir do tratador de excepção (Exception Handler). OBS: Caso exista, por exemplo uma excepção devido a uma divisão por zero na CPU, o exception handler pode mandar um signal (sinal) chamado SIGFPE ao processo que causou o "erro". Isto é, o processo interrompe o fluxo de execução atual e atende o signal a partir de uma rotina de tratamento de sinais, caso tal não seja possível pode se dar o aborto do processo, sendo assim encerrado. Dependendo da tratador de excepção escolhido o sinal enviado ao processo pode ser diferente, por vezes sim ou nao tratável... (Para uma boa leitura sobre sinais recomendo o livro: Richard W. Stevens (“Advanced Programming in the UNIX Environment, Third Edition”)) De uma forma bastante resumida o ciclo de tratamento de excepções pode ser divido em 3 passos: 1º - O contexto de determinados registadores são guardados na pilha (Dependo da excepção pode ser colocado também um código de erro após estes). 2º - A excepção é tratada a partir de uma função escrita em C. 3º - A "saída" do tratador de interrupção (exception handler), dá se a partir da função "ret_from_exception(), que termina todas as "excepções", excepto as emuladas como int 0x80 (interrupção de software). ----------------------------------------------------------------------------------------------------------------------------------------------------------- Primeirante a IDT (Interrupt Descriptor Table) deve-se encontrar inicializada com um exception handler para excepção conhecida. Isto é trabalho da função "trap_init()", que tem como "função" colocar todas a rotinas de tratamento de excepção nas entradas da IDT que se referem às próprias excepções e também, em alguns casos, às NMI (Non Maskable Interrupt). O primeiro ponto durante o ciclo de tratamento de excepção, como referido acima, consiste no empilhamento do contexto de certos registadores, cujo a ordem é: SS, RSP, CS, RIP. Esta "preparação" antes do tratamento da excepção propriamente dito, é feita de um arquivo assembly presente em "arch/x86/entry/entry_64.S" que o seu código fonte também pode ser visto aqui: https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/entry/entry_64.S Nele está definido uma Macro chamada "idtentry" que indica o "Entry Point" das excepções na IDT. Esta Macro tem como função preparar o pré-tratamento de excepções a partir da preservamento dos registadores na pilha. Idtentry irá "alocar espaço para os registadores na pilha, como definido na estrutura "pt_regs" de "/entry_64.S", por fim também poderá colocar o código de erro respetivo à excepção na pilha, caso este código não exista o seletor colocado no registador de segmentos CS é "checado" (assim, acessando o descriptor correspondente) e dá-se a troca de estado de código, do user-space para o kernel-space ou vice-versa, dependendo do valor de DPL (Descriptor Privilege Level) do descritor apontado por CS (Existe também uma cópia de DPL em SS, mas não nos interessa por agora). Após tudo isto, o idtentry terá de fazer uma chamada ao exception handler respetivo, assim fazendo com que o Kernel direcione o controle de execução para ele. Por fim, já depois de o exception handler ter terminado as suas "tarefas", a macro idtentry ainda tem a função de "desempilhar" os registadores da pilha executando a instrução IRET, como descrito mais acima no texto. Todos os Handlers presentes na IDT (Interrupt Descriptor Table) são definidos em "arch/x86/kernel/traps.c" a partir de um macro chamado "DO_ERROR", neste arquivo para além de muitas outras coisas também está indicado qual o signal que pode ou não ser enviado pelo handler ao processo que gerou a excepção. A descrição de estruturas adiantes torna-se bastante extenso, recomendo aos leitores procurarem sobre tal em documentação oficial, etc... DO_ERROR(X86_TRAP_DE, SIGFPE, "divide error", divide_error) DO_ERROR(X86_TRAP_OF, SIGSEGV, "overflow", overflow) DO_ERROR(X86_TRAP_UD, SIGILL, "invalid opcode", invalid_op) DO_ERROR(X86_TRAP_OLD_MF, SIGFPE, "coprocessor segment overrun", coprocessor_segment_overrun) DO_ERROR(X86_TRAP_TS, SIGSEGV, "invalid TSS", invalid_TSS) DO_ERROR(X86_TRAP_NP, SIGBUS, "segment not present", segment_not_present) DO_ERROR(X86_TRAP_SS, SIGBUS, "stack segment", stack_segment) DO_ERROR(X86_TRAP_AC, SIGBUS, "alignment check", alignment_check) //Número //Sinal //String identificadora // Ponto de Entrada do tratador de excepção do vetor que será da excepção da interrupção enviado ao processo Segue o código de fonte de "/traps.c": https://github.com/torvalds/linux/blob/16f73eb02d7e1765ccab3d2018e0bd98eb93d973/arch/x86/kernel/traps.c ----------------------------------------------------------------------------------------------------------------------------------------------------------- Agora para terminar (mais suave ), vou colocar em baixo alguns (nem perto de todos ) tipos de excepções, referindo o seu grupo e fazendo uma breve descrição sobre elas. (As traduções para Português são hilárias e nunca vi o uso delas ). ------------------------------------------------------------------------------------------------------------------------------------------------------------ Faults/Falhas: Divide-by-Zero Error/Divisão por Zero - A CPU lança esta excepção ao dividir algum número por zero (matemáticamente impossível), a partir de instruções de divisão (como DIV ou IDIV, por exemplo). RIP, que terá sido salvo na pilha com visto anteriormente, irá conter o apontar (conter o endereço) para a instrução que gerou a excepção. OBS: Certas vezes Software Developers "abusam" desta excepção com o intuito de testar o funcionamento dos tratadores da mesma (exception handlers). Invalid Opcode/Código de operação inválido - Esta excepção é lançada pela CPU na execução de uma instrução não existente (não definida na CPU), em certos casos também pode ser gerada devido a uma instrução exceder os 15 bytes de tamanho. RIP salvo na pilha irá apontar para a instrução que causou a excepção. Device Not Available/Dispositivo não disponível - (Apesar de ter um nome que se refere a dispositivos nada tem a ver com eles, estranhamente... ) Esta excepção é gerada quando é executada algum tipo de instrução estendida não estando elas habilitadas (isto é se certas as flags do registador de controle CR0 (flags as quais habilitam instruções como FPU/MMX/SSE) estiverem a zero). Novamente o salvo RIP apontará para a instrução que causou a excepção. Segment not Present/Segmento não Presente - Esta excepção é gerada quando é tentado o acesso ou o carregamento de um segmento de memória cujo o descriptor contém o campo "P (Present) = 0", que significa que tal segmento não se encontra "presente". Ao contrário das excepções descritas acima, esta coloca um código de erro na pilha (da maneira como foi descrito a meio do tópico) que é um indíce de um seletor de segmentos (Segment Selector Index). O RIP salvo na pilha, apontará para a instrução que gerou a excepção (mais uma vez ). General Protection Fault/Falta de proteção Geral - (Talvez uma das excepções mais famosas) Esta excepção pode ser gerada a partir de dezenas (sem exagero) de casos. Alguns deles podem ser: O erro de algum segmento (seja por parte de não correspondência de Privilégios, tipo, limites ou leitura/escrita); A execução de uma instrução priviligiada enquanto CPL (Current Privilege level) do processo em questão é diferente de zero (significa que o código não se encontra em Kernel-Mode); Acessar a partir de Seletores algum tipo de "NULL Descriptor", como por exemplo a primeira entrada de GDT (Global Descriptor Table); Escrever o valor 1 em registadores reservados; Escrever um valor de diferente de zero (0x0) no registador EOIR (End of Interruption Register), presente na LAPIC de cada processador lógico (Caso a CPU em questão faça uso desta)................... Quando a excepção GP# é gerada a partir de algum problema relacionado com um segmento de memória, é empilhado um código de erro na pilha que é um seletor de segmentos (Segment Selector Index), por outro lado apenas é colocado o valor 0 na pilha. O RIP apontará para a instrução que gerou a excepção. Page Fault/Falta da Página - Esta excepção é gerada devido a algum acontecimento inesperado relacionado com uma página. Uma página é uma região de memória (criada pelo Kernel) de 4 KiB ou KB que permite o uso de memória virtual. As páginas devem ser mapeadas atráves de uma hierarquia de tabelas cujo sua informação estão fora do escopo deste tópico, pode ser encontrada muito boa informação nos manuais da Intel sobre Paginação e Memória Virtual. Alguns acontecimentos que dão origem à excepção Page Fault são: Referir ou Acessar uma página que não foi mapeada (logo não se encontra presente na memória); Escrever numa página Read-Only (Esta informação encontra-se na tabela da página em questão); O RIP apontar para uma página marcada como não executável (bit "XD" ativado na tabela da página); Tentar acessar uma página quando o nível de privilégio atual de execução do processo (CPL) é maior do que o da página, ou seja menos priviligiado; Escrever em bits reservados da página (normalmente os 12 inferiores para impedir o transbordamento de dados entre páginas); Escrever em bits reservados das tabelas de páginas............................. A excepção Page Fault coloca um código de erro na pilha conhecido por mim como "PWURI" (não vou falar sobre ele aqui, sinceramente também nunca li muito sobre ele ). Por fim o registador de controle CR2 vai apontar para o endereço virtual que causou a excepção. ----------------------------------------------------------------------------------------------------------------------------------------------------- Traps/Armadilhas: BreakPoint/Ponto de Paragem: Esta é uma excepção que ocorre devido à execução de uma instrução "INT3". Esta instrução é bastante utilizada para fins de debbug. Quando marcamos o famoso "breakpoint" num determinado local de um programa a partir de Debugger, apenas estamos a pedir ao Debugger que coloque uma instrução INT3 naquele determinado local, assim fazendo com que o fluxo de execução do processo para naquele ponto. O RIP que terá sido salvo na pilha irá apontar (conter o endereço) para/do primeiro endereço de memória logo após a instrução INT3 colocada pelo Debugger. Overflow/Não encontro tradução que me satisfaça - Esta excepção é gerada quando o bit "overflow" (também conhecido como V Flag) de RFLAGS/EFLAGS se encontra setado (valor 1) e é executada a instrução INTO. Essa flag de RFLAGS é utilizada para indicar o overflow gerado a partir de alguma instrução aritmética (Na nossa conhecida ALU). Para além deste caso, a excepção overflow também pode resultar do resultado do resultado de uma divisão entre dois operandos for maior em tamanho (normalmente bits/bytes) do que estes mesmos, por exemplo, se numa divisão entre dois operandos de 32 bits for gerado um resultado final de 64 bits irá ser lançada uma excepção de overflow. O RIP salvo na pilha, caso a excepção seja gerada a partir da instrução INTO, irá apontar para o primeiro endereço de memória logo após a instrução que gerou o overflow (neste caso a INTO). Por outro lado, caso a excepção se gere por meio de um resultado final de uma divisão maior que os operandos da mesma, RIP apontará para a instrução causadora da excepção. ------------------------------------------------------------------------------------------------------------------------------------------------------------ Aborts/Abortos: Double Fault/Falta Dupla - Esta excepção é gerada a partir de uma outra excepção que terá sido gerada e que não pode ser tratada. Outro caso que pode "lançar" esta excepção é uma excepção ser gerada quando a CPU está a tentar "chamar" o exception handler para uma outra excepção gerada... Pode ter ficado confuso, mas vou dar um exemplo bem intressante e intuitivo que li à uns tempos num livro. Imaginem que ocorre um Page Fault, cujo o exception handler se encontra numa página marcada como não presente (P = 0), neste caso quando a CPU fosse procurar pelo tratador de interrupção para chama-lo iria causa uma outra page fault pois tal página estaria marca como não presente, e por fim seria gerado um Double Fault. Double Fault gera sempre o código de erro 0, empilhando-o assim na pilha. Neste ponto RIP encontra-se inacessível, querendo assim dizer que o processo têm de ser terminado. (Daqui a gravidade dos abortos relativamente à perda de performace, por vezes até o sistema precisará de ser reiniciado, como no caso da Triple Fault por exemplo). Triple Fault/Falta Tripla - Esta não tem nenhum exception handler associado, por isso sendo por muitos não considerada uma excepção, ela basicamente ocorre quando uma excepção é gerada durante o processo de tratamento da excepção Double Fault. Neste ponto, chegando a um estado critíco de execução, o processador precisa de ser reiniciado. A unica maneira de tentar prevenir as Triple Faults é fornecer uma estrutura TSS separada das outras especificamente à excepção Double Fault e usar Task Gates para este tipo de excepção. (Sinceramente não sei ao certo se a Triple Fault gera algum código de erro, se alguem souber agradeço que me informem ). -------------------------------------------------------------------------------------------------------------------------------------------------------- Bem, esta foi apenas uma "pequena" introdução às excepções, é um assunto bastante extenso que não pode ser cobrido aqui. Aqui encontram-se pelo menos as bases de uma matéria tão importante na ciência da computação. Ricardo Santos
  4. “Hello world” em nasm no Linux x86

    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 : ------------------------------------------------------------------------------------------------------------------------------------------------ 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.
  5. “Hello world” em nasm no Linux x86

    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.
  6. “Hello world” em nasm no Linux x86

    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.
  7. Código em ASM com gcc

    Acho que ambos devem conhecer, mas talvez alguem não conheça. É possível nessa listagem pedir ao GCC que também adicione comentários ao código referentes ao programa, usando o parâmetro -fverbose-asm. Outra dica é em vez de escrever -m64/-m32, pode se escrever -march=native, para o compilador optimizar o código para a arquitetura da CPU em questão, assim caso a CPU do computador onde o arquivo será compilado, suportar por exemplo instruções estendidas, o GCC irá usa-las.
  8. [Python] Ping em segmentos diferentes

    Sem ter a certeza, eu recomendaria procurar sobre módulos que permitem o MultiThreading (Se não souber o que é recomendo aprender) em Python (como por exemplo o módulo Thread), assim talvez conseguisse estabelecer multiplas ligações à máquina B (ao mesmo tempo) e pingar em todas em alvos diferentes. MultiThreading: http://wiki.osdev.org/Symmetric_Multiprocessing http://wiki.osdev.org/Multiprocessing http://wiki.osdev.org/Multitasking_Systems https://en.wikipedia.org/wiki/Computer_multitasking Por fim recomendo também o capítulo que fala sobre Threads do livro "C e Assembly" do Frederico Pissara. Junto Envio. MultiThreading in Python: https://docs.python.org/3/library/threading.html https://www.tutorialspoint.com/python/python_multithreading.htm Entre toda a outro documentação oficial do Python. Peço desculpa não poder ajudar diretamente, tenho pouquissimo conhecimento em Python. C_e_Assembly_x86-64_v0_33.9.pdf
  9. Biblioteca em Python para instrumentação de binários

    Obrigado pela partilha Fernando , com certeza irei explorar. Aproveito que durante o tópico apareceu algo chamado "Rich Header", e deixo aqui um artigo bastante interessante. http://www.ntcore.com/files/richsign.htm
×