Análise de um artigo - explicando o método de detecção de vazamento de memória do Linux por meio de exemplos

Primeiro, mtrace analisa vazamentos de memória

mtrace (rastreamento de memória) é uma ferramenta de detecção de problemas de memória que vem com o GNU Glibc. Ele pode ser usado para ajudar a localizar problemas de vazamento de memória. Seu código-fonte de implementação está no diretório malloc do código-fonte da glibc. Seu princípio básico de design é projetar uma função void mtrace (). A função rastreia as chamadas de malloc/free e outras funções na biblioteca libc, detectando assim se há é um vazamento de memória. mtrace é uma função C, declarada e definida em <mcheck.h>. O protótipo da função é:

void mtrace(void);

princípio mtrace

mtrace() A função instalará funções "hook" para as funções relacionadas à alocação dinâmica de memória (como malloc(), realloc(), memalign() e free()).Essas funções de gancho registrarão toda a alocação de memória relevante e as informações de rastreamento liberadas , e muntrace() descarregará a função de gancho correspondente.

Com base nas informações de rastreamento de depuração geradas por essas funções de gancho, podemos analisar se há problemas como "vazamentos de memória".

Definir caminho de geração de log

O mecanismo mtrace exige que realmente executemos o programa antes que ele possa gerar logs de rastreamento, mas mais uma coisa a fazer antes de realmente executar o programa é informar ao mtrace (a função de gancho mencionada acima) o caminho para gerar o arquivo de log.

Existem duas maneiras de definir o caminho de geração do log, uma é definir a variável de ambiente: export MALLOC_TRACE=./test.log // 当前目录下 a outra é defini-la no nível do código: setenv("MALLOC_TRACE", "output_file_name", 1);``output_file_nameé o nome do arquivo que armazena os resultados da detecção.

Exemplo de teste

#include <mcheck.h>
#include <stdlib.h>
#include <stdio.h>

int main(int argc, char **argv)
{
    mtrace();  // 开始跟踪

    char *p = (char *)malloc(100);
    free(p);
    p = NULL;
    p = (char *)malloc(100);

    muntrace();   // 结束跟踪,并生成日志信息
    return 0;
}

A partir do código acima, esperamos poder verificar se há um vazamento de memória do início ao fim do programa. O exemplo é simples. Você pode ver rapidamente que há um vazamento de memória, então precisamos verifique se o mtrace pode verificar vazamentos de memória e verifique como analisar e posicionar os resultados. gcc -g test.c -o testGere arquivo executável.

registro

Após a conclusão da execução do programa, o arquivo test.log será gerado no diretório atual. Ao abri-lo, você poderá ver o seguinte conteúdo:

= Start
@ ./test:[0x400624] + 0x21ed450 0x64
@ ./test:[0x400634] - 0x21ed450
@ ./test:[0x400646] + 0x21ed450 0x64
= End

Neste arquivo, podemos ver que as três linhas do meio correspondem às operações malloc -> free -> malloc no código-fonte. Interpretação : ./test refere-se ao nome do programa que executamos, [0x400624] é a informação de endereço no código de máquina da primeira chamada para a função malloc, + significa solicitar memória (- significa liberar), 0x21ed450 é o endereço informações solicitadas pela função malloc, 0x64 representa o tamanho da memória solicitada. A partir desta análise, o primeiro aplicativo foi lançado, mas o segundo aplicativo não foi lançado e há um problema de vazamento de memória.

Análise de vazamento

Use a ferramenta addr2line para localizar o local do código-fonte

Usando a ferramenta de comando "addr2line", você pode obter o número da linha do arquivo fonte (você pode usar isso para localizar o local específico do código-fonte com base no endereço do código da máquina)

# addr2line -e test 0x400624
/home/test.c:9

Use a ferramenta mtrace para analisar informações de log

mtrace + caminho do arquivo executável + caminho do arquivo de log  mtrace test ./test.logsão executados e as seguintes informações são geradas:

Memory not freed:
-----------------
           Address     Size     Caller
0x00000000021ed450     0x64  at /home/test.c:14

2. Valgrind analisa vazamentos de memória

Introdução à ferramenta Valgrind

Valgrind é uma coleção de ferramentas de simulação e depuração de código aberto (GPL V2) no Linux. Valgrind consiste em um núcleo e outras ferramentas de depuração baseadas no núcleo. O kernel é semelhante a um framework, que simula um ambiente de CPU e fornece serviços para outras ferramentas; outras ferramentas são semelhantes a plug-ins, usando os serviços fornecidos pelo kernel para completar várias tarefas específicas de depuração de memória. A arquitetura do Valgrind é mostrada na figura abaixo

1、Verificação de memória

A ferramenta mais comumente usada é usada para detectar problemas de memória em programas. Todas as leituras e gravações na memória serão detectadas e todas as chamadas para malloc() / free() / new / delete serão capturadas.

Portanto, ele pode detectar os seguintes problemas: uso de memória não inicializada; leitura/gravação de blocos de memória liberados; leitura/gravação de blocos de memória além da alocação de malloc; leitura/gravação de blocos de memória inadequados na pilha; vazamentos de memória, apontando para um bloco Ponteiros de memória são perdido para sempre; correspondência incorreta de malloc/free ou new/delete; ponteiros dst e src em funções relacionadas a memcpy() se sobrepõem.

2. Callgrind

Uma ferramenta de análise semelhante ao gprof, mas é mais detalhada na observação do funcionamento do programa e pode nos fornecer mais informações. Ao contrário do gprof, ele não requer opções especiais ao compilar o código-fonte, mas é recomendado adicionar opções de depuração.

Callgrind coleta alguns dados quando o programa está em execução, constrói um gráfico de chamada de função e pode, opcionalmente, realizar simulação de cache. Ao final da execução, ele grava os dados da análise em um arquivo. callgrind_annotate pode converter o conteúdo deste arquivo em um formato legível.

3. Cachegrind

O analisador de cache, que simula o cache de primeiro nível I1, Dl e o cache de segundo nível na CPU, pode apontar com precisão erros e acertos de cache no programa. Se necessário, também pode nos fornecer o número de falhas de cache, o número de referências de memória e o número de instruções geradas por cada linha de código, cada função, cada módulo e o programa inteiro. Esta é uma grande ajuda para otimizar programas.

4、Helgrind

É usado principalmente para verificar problemas de concorrência que ocorrem em programas multithread. Helgrind procura áreas de memória que são acessadas por vários threads e não estão bloqueadas de forma consistente. Essas áreas são frequentemente onde a sincronização entre threads é perdida e podem levar a erros difíceis de encontrar.

Helgrind implementou um algoritmo de detecção de corrida chamado "Eraser" e fez melhorias adicionais para reduzir o número de erros relatados. No entanto, Helgrind ainda está em fase experimental.

5、Maciço

O analisador de pilha, que mede quanta memória um programa usa na pilha, nos informa o tamanho dos blocos de heap, dos blocos de gerenciamento de heap e da pilha.

Massif pode nos ajudar a reduzir o uso de memória. Em sistemas modernos com memória virtual, também pode acelerar a execução de nossos programas e reduzir a chance de o programa permanecer na área de troca.

Além disso, lacaio e nulgrind serão fornecidos. Lackey é uma ferramenta pequena que raramente é usada; Nulgrind apenas mostra aos desenvolvedores como criar uma ferramenta.

 Information Direct: rota de aprendizado da tecnologia de código-fonte do kernel Linux + tutorial em vídeo do código-fonte do kernel

Learning Express: Código-fonte do kernel Linux Ajuste de memória Sistema de arquivos Gerenciamento de processos Driver de dispositivo/pilha de protocolo de rede

Princípio de verificação de memória

O foco deste artigo é detectar vazamentos de memória, então não vou explicar muito sobre outras ferramentas do valgrind, mas explicarei principalmente o trabalho do Memcheck. O princípio do Memcheck para detectar problemas de memória é mostrado na figura abaixo:

A chave para a capacidade do Memcheck de detectar problemas de memória é que ele cria duas tabelas globais.

  • A tabela Valid-Value possui 8 bits correspondentes a cada byte em todo o espaço de endereçamento do processo; existe também um vetor de bits correspondente para cada registro da CPU. Esses bits são responsáveis ​​por registrar se o valor do byte ou do registro possui um valor válido e inicializado.
  • A tabela Valid-Address possui um bit correspondente para cada byte em todo o espaço de endereçamento do processo, que é responsável por registrar se o endereço pode ser lido ou escrito.
  • Princípio de detecção: Quando você deseja ler ou escrever um byte na memória, primeiro verifique o bit A na tabela de endereços válidos correspondente a este byte. Se o bit A mostrar que o local é inválido, o memcheck relata um erro de leitura e gravação. O núcleo é semelhante a um ambiente de CPU virtual, portanto, quando um determinado byte da memória é carregado na CPU real, o bit V na tabela de valores válidos correspondente ao byte também é carregado no ambiente de CPU virtual. Uma vez que o valor no registro seja usado para gerar um endereço de memória, ou o valor possa afetar a saída do programa, o memcheck verificará os bits V correspondentes.Se o valor não tiver sido inicializado, um erro de memória não inicializada será relatado.

Tipo de vazamento de memória

valgrind divide vazamentos de memória em 4 categorias:

  • Definitivamente perdido: a memória não foi liberada, mas não há nenhum ponteiro apontando para a memória e a memória não pode ser acessada. Está estabelecido que a memória em execução vazada precisa fortemente ser corrigida.
  • Vazamento indireto (perdido indiretamente): O ponteiro de memória vazado é armazenado na memória que vazou. Como a memória que vazou não pode ser acessada, a memória que causou o vazamento indireto não pode ser acessada. Por exemplo:
struct list {
 struct list *next;
};

int main(int argc, char **argv)
{
 struct list *root;
 root = (struct list *)malloc(sizeof(struct list));
 root->next = (struct list *)malloc(sizeof(struct list));
 printf("root %p roop->next %p\n", root, root->next);
 root = NULL;
 return 0;
}

O que falta aqui é o ponteiro raiz (que é o tipo de vazamento estabelecido), fazendo com que o próximo ponteiro armazenado na raiz se torne um vazamento indireto. A memória vazada indiretamente definitivamente precisará ser corrigida, mas geralmente será corrigida junto com a correção do vazamento estabelecido.

  • Possivelmente perdido: a agulha não aponta para o endereço do cabeçalho da memória, mas para o local dentro da memória. Valgrind muitas vezes suspeita que pode haver um vazamento porque os ponteiros já estão tendenciosos e não estão direcionados para o cabeçote da memória, mas sim para as partes internas da memória. Em alguns casos, isso não é um vazamento, porque este programa é projetado dessa forma, por exemplo, para obter alinhamento de memória, memória adicional de processamento do aplicativo é devolvida ao endereço de memória alinhado.
  • Ainda acessível: O ponteiro está sempre presente e inclinado em direção ao topo da memória, e a memória não foi liberada até que o programa seja encerrado.

Configurações de parâmetros Valgrind

  • --leak-check=<no|summary|yes|full> Se definido como sim ou completo, após o término do programa chamado, valgrind descreverá cada vazamento de memória em detalhes. O padrão é resumo, que relata apenas vários vazamentos de memória.
  • --log-fd= [padrão: 2, stderr] valgrind imprime logs e os despeja no arquivo especificado ou no descritor de arquivo. Sem este parâmetro, os logs do valgrind serão exibidos junto com os logs do programa do usuário, o que parecerá muito confuso.
  • --trace-children=<yes | no> [default: no] Se deseja rastrear processos filhos.Se for um programa multiprocesso, é recomendado usar esta função. No entanto, não terá muito impacto se um único processo estiver habilitado.
  • --keep-debuginfo=<yes | no> [padrão: no] Se o programa usar uma biblioteca carregada dinamicamente (dlopen), as informações de depuração serão apagadas quando a biblioteca dinâmica for descarregada (dlclose). Após habilitar esta opção, as informações da pilha de chamadas serão retidas mesmo se a biblioteca dinâmica for descarregada.
  • --keep-stacktraces=<alloc | free | alloc-and-free | alloc-then-free | none> [padrão: alloc-and-free] Vazamentos de memória nada mais são do que incompatibilidade de aplicativo e versão, e a chamada de função pilha é apenas Gravar ao aplicar, ou gravar ao solicitar lançamento. Se nos concentrarmos apenas em vazamentos de memória, na verdade não há necessidade de registrar ambos ao solicitar lançamento, pois isso ocupará muita memória extra e mais consumo de CPU, fazendo com que a execução já lenta do programa acrescenta insulto à injúria.
  • --freelist-vol= Quando um programa cliente usa free ou delete para liberar um bloco de memória, o bloco de memória não estará imediatamente disponível para realocação. Ele será apenas colocado em uma fila de blocos livres (freelist) e marcado como indisponível. Acesso , o que é útil para detectar erros quando o programa cliente acessa o bloco liberado após um período de tempo muito importante. Esta opção especifica o tamanho do bloco de bytes ocupado pela fila. O padrão é 20MB. Aumentar esta opção aumentará a sobrecarga de memória do memcheck, mas a capacidade de detectar tais erros também será melhorada.
  • --freelist-big-blocks= Ao retirar blocos de memória disponíveis da fila freelist para realocação, o memcheck retirará um bloco de acordo com a prioridade daqueles blocos de memória maiores que o número. Esta opção evita chamadas frequentes para pequenos blocos de memória na lista livre.Esta opção aumenta a probabilidade de detecção de erros de ponteiro selvagem para pequenos blocos de memória. Se esta opção for definida como 0, todos os blocos serão realocados primeiro a entrar, primeiro a sair. O padrão é 1 milhão. Referência: Introdução ao valgrind (ferramenta de verificação de memória)

Parâmetros de compilação recomendados

Para imprimir detalhadamente as informações de desempilhamento quando ocorre um problema, é melhor adicionar a opção -g ao compilar o programa. Se houver uma biblioteca carregada dinamicamente, ela deverá ser adicionada  --keep-debuginfo=yes , caso contrário, se for descoberto que a biblioteca carregada dinamicamente vazou, a tabela de símbolos não poderá ser encontrada porque a biblioteca dinâmica foi desinstalada. Otimização do compilador de código, não é recomendado usar -O2 e superior. -O0 provavelmente retardará a operação, por isso é recomendado usar -O1.

Descrição do exemplo de detecção

Solicite memória sem liberá-la

#include <stdlib.h>
#include <stdio.h>
void func()
{
  //只申请内存而不释放
    void *p=malloc(sizeof(int));
}
int main()
{
    func();
    return 0;
}

Use o comando valgrind para executar o programa e enviar o log para um arquivo

valgrind --log-file=valReport --leak-check=full --show-reachable=yes --leak-resolution=low ./a.out

Descrição do parâmetro:

  • –log-file=valReport especifica para gerar um arquivo de log de análise no diretório de execução atual e o nome do arquivo é valReport
  • –leak-check=full exibe detalhes de cada vazamento
  • –show-reachable=yes Se deve detectar vazamentos fora do intervalo de controle, como ponteiros globais, ponteiros estáticos, etc., e exibir todos os tipos de vazamento de memória
  • –leak-resolution = baixo nível de mesclagem do relatório de vazamento de memória
  • –track-origins=yes significa ativar a função de detecção de “uso de memória não inicializada” e abrir resultados detalhados. Se não existir tal sentença, a detecção nesta área será realizada por padrão, mas os resultados detalhados não serão impressos. Após a execução da saída, o relatório é interpretado. 54017 refere-se ao número do processo. Se o programa for executado usando vários processos, o conteúdo de vários processos será exibido.
==54017== Memcheck, a memory error detector
==54017== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==54017== Using Valgrind-3.15.0 and LibVEX; rerun with -h for copyright info
==54017== Command: ./a.out
==54017== Parent PID: 52130

O segundo parágrafo é um resumo da alocação de memória heap e menciona que o programa solicitou memória uma vez, dos quais 0 foram liberados e 4 bytes foram alocados ( 1 allocs, 0 frees, 4 bytes allocated).

No resumo principal, há a quantidade total de memória heap usada pelo programa, o número de tempos de alocação de memória e o número de tempos de liberação de memória.Se o número de tempos de alocação de memória e os tempos de liberação de memória forem inconsistentes, significa que há um vazamento de memória.

==54017== HEAP SUMMARY:
==54017==   in use at exit: 4 bytes in 1 blocks
==54017==   total heap usage: 1 allocs, 0 frees, 4 bytes allocated

O terceiro parágrafo descreve as informações específicas do vazamento de memória. Há um pedaço de memória que ocupa 4 bytes ( 4 bytes in 1 blocks). Ele é alocado chamando malloc. Você pode ver na pilha de chamadas que a função func finalmente chamou malloc, então esta informação é relativamente preciso. Isso localiza onde nossa memória vazada é solicitada.

==54017== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1
==54017==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==54017==    by 0x40057E: func() (in /home/oceanstar/CLionProjects/Share/src/a.out)
==54017==    by 0x40058D: main (in /home/oceanstar/CLionProjects/Share/src/a.out)

O último parágrafo é um resumo, um vazamento de memória de 4 bytes.

==54017== LEAK SUMMARY:
==54017==    definitely lost: 4 bytes in 1 blocks  // 确立泄露
==54017==    indirectly lost: 0 bytes in 0 blocks  // 间接性泄露
==54017==    possibly lost: 0 bytes in 0 blocks   // 很有可能泄露
==54017==    still reachable: 0 bytes in 0 blocks // 仍可访达
==54017==    suppressed: 0 bytes in 0 blocks

Ler e escrever além dos limites

#include <stdio.h>
#include <iostream>
int main()
{
    int len = 5;
    int *pt = (int*)malloc(len*sizeof(int)); //problem1: not freed
    int *p = pt;
    for (int i = 0; i < len; i++){
        p++;
    }
    *p = 5; //problem2: heap block overrun
    printf("%d\n", *p); //problem3: heap block overrun
    // free(pt);
    return 0;
}

problema1: O ponteiro pt solicitou espaço, mas não foi liberado; problema2: pt solicitou o espaço de 5 ints, e quando p atingiu a posição de p[5] após 5 ciclos, o acesso estava fora dos limites (a escrita foi fora dos limites)  *p = 5. (Escrita inválida de tamanho 4 no relatório valgrind abaixo)

==58261== Invalid write of size 4
==58261==    at 0x400707: main (main.cpp:12)
==58261==  Address 0x5a23054 is 0 bytes after a block of size 20 alloc'd
==58261==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==58261==    by 0x4006DC: main (main.cpp:7)

problema1: leitura fora dos limites (leitura inválida de tamanho 4 no relatório valgrind abaixo)

==58261== Invalid read of size 4
==58261==    at 0x400711: main (main.cpp:13)
==58261==  Address 0x5a23054 is 0 bytes after a block of size 20 alloc'd
==58261==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==58261==    by 0x4006DC: main (main.cpp:7)

Lançamento repetido

#include <stdio.h>
#include <iostream>
int main()
{
    int *x;
    x = static_cast<int *>(malloc(8 * sizeof(int)));
    x = static_cast<int *>(malloc(8 * sizeof(int)));
    free(x);
    free(x);
    return 0;
}

O relatório é o seguinte,Invalid free() / delete / delete[] / realloc()

==59602== Invalid free() / delete / delete[] / realloc()
==59602==    at 0x4C2B06D: free (vg_replace_malloc.c:540)
==59602==    by 0x4006FE: main (main.cpp:10)
==59602==  Address 0x5a230a0 is 0 bytes inside a block of size 32 free'd
==59602==    at 0x4C2B06D: free (vg_replace_malloc.c:540)
==59602==    by 0x4006F2: main (main.cpp:9)
==59602==  Block was alloc'd at
==59602==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==59602==    by 0x4006E2: main (main.cpp:8)

A interface de lançamento do aplicativo não corresponde

O relatório de incompatibilidade entre aplicativos e interfaces de liberação é o seguinte: o ponteiro para solicitar espaço usando malloc é liberado usando free; o espaço solicitado para usar new é liberado usando delete() Mismatched free() / delete / delete []:

==61950== Mismatched free() / delete / delete []
==61950==    at 0x4C2BB8F: operator delete[](void*) (vg_replace_malloc.c:651)
==61950==    by 0x4006E8: main (main.cpp:8)
==61950==  Address 0x5a23040 is 0 bytes inside a block of size 5 alloc'd
==61950==    at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==61950==    by 0x4006D1: main (main.cpp:7)

sobrescrever memória

int main()
{
    char str[11];
    for (int i = 0; i < 11; i++){
        str[i] = i;
    }
    memcpy(str + 1, str, 5);
    char x[5] = "abcd";
    strncpy(x + 2, x, 3);
}

O problema está no memcpy. Copiar 5 caracteres começando da posição do ponteiro str para o espaço apontado por str+1 causará a substituição da memória. O mesmo vale para strncpy. O relatório é o seguinte Source and destination overlap:

==61609== Source and destination overlap in memcpy(0x1ffefffe31, 0x1ffefffe30, 5)
==61609==    at 0x4C2E81D: memcpy@@GLIBC_2.14 (vg_replace_strmem.c:1035)
==61609==    by 0x400721: main (main.cpp:11)
==61609== 
==61609== Source and destination overlap in strncpy(0x1ffefffe25, 0x1ffefffe23, 3)
==61609==    at 0x4C2D453: strncpy (vg_replace_strmem.c:552)
==61609==    by 0x400748: main (main.cpp:14)

3. Resumo

Existem dois métodos de detecção de memória:

1. Manter uma lista vinculada de operação de memória. Quando há uma operação de aplicação de memória, ela é adicionada a esta lista vinculada. Quando há uma operação de liberação, ela é removida da lista vinculada da operação de aplicação. Se ainda houver conteúdo na lista vinculada após o término do programa, significa que há um vazamento de memória; se a operação de memória a ser liberada não encontrar a operação correspondente na lista vinculada, significa que ela foi liberada várias vezes . Use este método com ferramentas de depuração integradas, Visual Leak Detecter, mtrace, memwatch, debug_new. 2. Simule o espaço de endereço do processo. Após o tratamento das operações de memória do processo pelo sistema operacional, um mapeamento de espaço de endereço é mantido no modo de usuário.Este método requer uma compreensão profunda do processamento de espaços de endereço de processo. Como a distribuição do espaço de endereçamento do processo do Windows não é de código aberto, é difícil de simular, por isso só é compatível com Linux. Aquele que adota essa abordagem é o valgrind.

Autor original: Aprenda integrados juntos

Acho que você gosta

Origin blog.csdn.net/youzhangjing_/article/details/132817245
Recomendado
Clasificación