Análise de cenário do quinto código-fonte do agendador de linguagem go: instruções de montagem

O conteúdo a seguir é reproduzido de  https://mp.weixin.qq.com/s/fuox6st_iXg_rpklxWXXRA

Awa adora escrever programas originais Zhang  fonte Viagens  20/04/2019

A linguagem assembly é uma linguagem que todo programador de back-end deve dominar. Como você aprendeu a linguagem assembly, é muito importante para nós depurar programas ou estudar e compreender alguns dos princípios operacionais subjacentes do computador, por isso é recomendado Fique interessado Os leitores podem dedicar mais tempo para aprender bem.

Assim como as linguagens de programação de alto nível, a linguagem assembly também é uma linguagem de programação de computador completa e envolve muito conteúdo de conhecimento. Felizmente, nosso principal objetivo é ser capaz de ler e compreender o código assembly por meio do estudo desta seção, e não do uso linguagem assembly para escrever código, portanto, esta seção não irá apresentar totalmente a linguagem assembly, mas apenas selecionar um subconjunto de instruções assembly-assembly para apresentar. No entanto, embora a introdução aqui tenha sido simplificada, os leitores podem ter certeza de que o uso qualificado desse conhecimento é suficiente para lidar com o código de montagem no programador goroutine que este livro analisará.

Por falar em instruções de montagem, devo mencionar as instruções de máquina. O formato binário das instruções de máquina é a linguagem que a CPU pode entender. Por estar no formato binário, é muito conveniente para a CPU analisar e executar, mas é não propício à leitura humana e à comunicação, portanto As instruções de montagem que correspondem às instruções da máquina um a um estão disponíveis. As instruções de montagem usam símbolos para representar as instruções da máquina. O exemplo a seguir é muito intuitivo para ilustrar a diferença entre essas duas instruções :

0x40054d: adicione% rdx,% rax // instruções de montagem 

(gdb) x / 3xb 0x40054d 
0x40054d: 0x48 0x01 0xd0 // instruções da máquina 
(gdb)

O mesmo é adicionar os valores nos registradores rdx e rax. A instrução de montagem é: add% rdx,% rax, enquanto a instrução de máquina tem três números: 0x48 0x01 0xd0. Obviamente, a instrução de montagem é mais amigável para os humanos , e é mais fácil de lembrar, fácil de ler e fácil de escrever.

Formato de instrução de montagem

Como diferentes CPUs suportam diferentes instruções de máquina, suas instruções de montagem também são diferentes. Mesmo para a mesma CPU, diferentes ferramentas de montagem e plataformas usam diferentes formatos de instrução de montagem. Como este livro se concentra na plataforma AMD64 Linux sob o agendador go, apresentamos apenas o AT&T formato das instruções de montagem usadas nesta plataforma. O formato básico das instruções de montagem da AT&T é:

Opcode   [ operando ]

Você pode ver que cada instrução de montagem geralmente consiste em duas partes:

  • Opcode : O opcode diz à CPU qual operação realizar, como realizar adição, subtração ou leitura e gravação de memória. Cada instrução deve ter um opcode.

  • Operando : O operando é o objeto da operação.Por exemplo, uma operação de adição requer dois adendos, e esses dois adendos são os operandos desta instrução. O número de operandos é geralmente 0, 1 ou 2.

Veja alguns exemplos de instruções de montagem

adicione% rdx,% rax

O opcode desta instrução é add, que significa realizar uma operação de adição, e possui dois operandos, rdx e rax. Se uma instrução possui dois operandos, então o primeiro operando é denominado operando fonte e o segundo operando é denominado operando destino.Como o próprio nome sugere, o operando destino representa o local onde o resultado desta instrução deve ser salvo após a execução. Portanto, a instrução acima significa somar os valores nos registradores rax e rdx e salvar o resultado no registrador rax. Na verdade, o segundo operando rax registrador desta instrução é o operando de origem e o operando de destino, porque rax não é apenas um dos dois adendos da operação de adição, mas também o resultado da operação de adição. Depois que esta instrução é executada, o valor do registro rax mudou. O valor antes da instrução é sobrescrito e perdido. Se o valor anterior do registro rax ainda for útil, então a instrução deve ser usada para salvá-lo em outros registros ou memória.

Vejamos um exemplo com apenas um operando:

callq 0x400526  

O opcode desta instrução é callq, o que significa que a função é chamada e o operando é 0x400526, que é o endereço da função chamada.

Finalmente, vamos olhar para uma instrução sem operandos:

retq

Esta instrução possui apenas o opcode retq, o que significa que a execução continua da função chamada para a função chamadora.

Para entender melhor as instruções de montagem do formato AT&T, aqui está uma breve descrição de seu formato:

1. Nas instruções de montagem em formato AT&T, o nome do registro precisa ser prefixado com%, o que vimos antes;

2. Em uma instrução com dois operandos, o primeiro operando é o operando de origem e o segundo é o operando de destino. Eu discuti isso agora, mas a origem e o destino nessa instrução não são tão claros. Vejamos um Para seja direto, mov% eax,% esi, esta instrução significa copiar o valor no registrador eax para esi, a origem e o destino desta instrução são muito claros;

3. O operando imediato precisa ser prefixado com um sinal $, como "mov $ 0x1% rdi". O primeiro operando nesta instrução não é um registrador ou um endereço de memória, mas uma constante escrita diretamente na instrução. Este tipo de operando é chamado de operando imediato . Esta instrução significa colocar o valor 0x1 no registrador rdi.

4. O formato do endereçamento indireto do registro é deslocado (% registro) .Se o deslocamento for 0, você pode omitir o deslocamento e escrevê-lo diretamente como (% registro) sem escrever. O que é endereçamento indireto? Na verdade, isso significa que o registro na instrução não é o operando de origem real ou o operando de destino. O valor do registro é um endereço de memória. A memória correspondente a este endereço é o operando de origem ou destino real, como mov% rax, (% rsp) Nesta instrução, o nome do registro no segundo operando (% rsp) é colocado entre parênteses, indicando endereçamento indireto. O valor de rsp é um endereço de memória. A real intenção desta instrução é alterar o valor do registro rax O valor é atribuído à memória correspondente ao valor do registro rsp (endereço de memória). O valor do próprio registro rsp não será modificado. Para comparação, vamos olhar para mov% rax,% instrução rsp. Aqui só falta o segundo operando Os parênteses tornaram-se o endereçamento direto e o significado é completamente diferente. Esta instrução significa atribuir o valor de rax a rsp, de modo que o valor do registrador rsp seja modificado para o mesmo valor como o registro rax. As duas imagens a seguir mostram a diferença entre esses dois modos de endereçamento:

imagem

Antes de executar a instrução mov% rax,% rsp, o valor do registrador rsp é xe o valor do registrador rax é y. Depois que a instrução é executada, o valor do registrador rax é copiado para o registrador rsp, então o o valor do registrador rsp passa a ser y. Pode-se observar que quando o modo de endereçamento direto é adotado, o valor do registrador rsp do operando destino mudou antes e depois da execução da instrução, e o operando fonte não mudou. Observe o diagrama esquemático do endereçamento indireto:

 

imagem

 

Antes de executar a instrução mov% rax, (% rsp), o valor do registro rax é y, e o valor do registro rsp é X, que é um endereço de memória, conforme mostrado na figura acima, usamos uma seta vermelha para apontar do registrador rsp O endereço de memória é X; após a execução da instrução, o valor do registrador rsp não mudou, mas o valor na memória apontado por rsp mudou, porque o operando de destino desta instrução usa indireta endereçamento (% rsp), o resultado da execução da instrução é que o valor no registro rax é copiado para as 8 unidades de memória correspondentes ao endereço armazenado no registro rsp. Além disso, deve-se notar que o endereço de memória que aparece na instrução é apenas o endereço inicial. A operação específica de várias unidades de memória consecutivas com este endereço como endereço inicial depende da instrução específica, como mov% rax, no figura acima. (% rsp), como o operando de origem é um registrador de 64 bits, esta instrução irá copiar os 8 bytes armazenados em rax para os endereços X, X + 1, X + 2, X + 3, X + 4, X +5, X + 6, X + 7 essas 8 células de memória.

 

O deslocamento anterior no deslocamento do formato de endereçamento indireto (% registro) significa o deslocamento, como -0x8 (% rbp), -0x8 é o deslocamento, o todo significa que o valor do endereço salvo no registro rbp é o primeiro menos 8 (porque o deslocamento é Negativo 8) A memória correspondente ao endereço obtido.

 

5.  Os opcodes de algumas instruções relacionadas à memória adicionarão as letras b, w, l e q para indicar se a memória operacional tem 1, 2, 4 ou 8 bytes, como a instrução movl $ 0x0, -0x8 (% rbp) , a letra l do sufixo deste opcode movl de instrução indica que queremos atribuir 0 às 4 unidades de memória a partir do endereço -0x8 (% rbp). Alguns leitores podem perguntar, e se eu quiser operar 3 ou 5 células de memória? É uma pena que a cpu não forneça a instrução única correspondente, só podemos atingir o objetivo combinando várias instruções.

 

Explicação detalhada dos comandos comuns

 

Existem milhares de instruções de montagem x86-64. Não vou explicar cada uma em detalhes aqui. Os leitores podem consultar os tutoriais em linguagem de montagem se estiverem interessados. Estamos aqui para nos concentrar em algumas instruções muito comuns ou para nos ajudar a entender o mecanismo de operação do programa.

 

  • instrução mov

Operando do número de operação da fonte mov

Esta instrução copia o operando de origem para o operando de destino. exemplo:

mov% rsp,% rbp # Endereçamento direto, copie o valor de rsp para rbp, equivalente a rbp = rsp 
mov -0x8 (% rbp),% edx # Endereçamento indireto do operando de origem, endereçamento direto do operando de destino. Lê 4 bytes da memória para o registro edx 
mov% rsi, -0x8 (% rbp) # O operando de origem é endereçado diretamente, e o operando de destino é endereçado indiretamente. Grave o valor de 8 bytes no registro rsi na memória
  • adicionar / sub instruções

adicionar operando do número de 
operações de origem suboperando do número de operações de origem

Instruções de adição e subtração. exemplo:

sub $ 0x350,% rsp # O operando de origem é um operando imediato e o operando de destino é endereçado diretamente. rsp = rsp-0x350 
add% rdx,% rax # Endereçamento direto. rax = rax + rdx 
addl $ 0x1, -0x8 (% rbp) # O operando de origem é um operando imediato e o operando de destino é indiretamente endereçado. O valor na memória é aumentado em 1 (a letra l do sufixo adicional representa a operação de 4 bytes na memória)
  • instrução call / ret

endereço de destino de chamada 
ret

A instrução de chamada executa uma chamada de função. Quando a CPU executa a instrução de chamada, ela primeiro coloca o valor no registro de rip na pilha e, em seguida, define o valor de rip para o endereço de destino. Como o registro de rip determina a próxima instrução a ser executada , ele pulará quando a CPU termina de executar a instrução de chamada atual. Vá para o endereço de destino para executar.

A instrução ret retorna à função de chamada a partir da função chamada. Seu princípio de realização é colocar o endereço de retorno da instrução de chamada na pilha do registrador rip.

Os exemplos a seguir ilustram os princípios dessas duas instruções.


#Calling function fragment 0x0000000000400559: callq 0x400526 <sum> 
0x000000000040055e: mov% eax, -0x4 (% rbp) #Called 

function fragment 
0x0000000000400526: push% rbp 
...... 
0x000000000040053f: retq  

No trecho de código acima, a função de chamada usa a instrução callq 0x400526 para chamar a função em 0x400526 e 0x400526 é o endereço da primeira instrução da função chamada. A função chamada executa a instrução retq em 0x40053f e retorna à função de chamada para continuar executando a instrução no endereço 0x40055e. Observe que essas duas instruções envolverão operações de empilhamento e popping, portanto, afetarão o valor do registrador rsp.

imagem

Na figura acima, você pode ver que no início da execução da instrução de chamada, o valor do registrador rip é o endereço da instrução imediatamente após a chamada, ou seja, 0x40055e, mas quando a instrução de chamada é concluída, mas antes do a próxima instrução é executada, o valor do registrador rip muda. Torna-se o operando da instrução de chamada, ou seja, o endereço da função chamada é 0x400526, então a CPU irá pular para a função chamada para executar.

Ao mesmo tempo, deve-se notar que quando a instrução de chamada é executada, o endereço 0x40055e da instrução após a instrução de chamada é PUSH na pilha, então uma instrução de chamada modifica o valor de 3 casas: o registrador rip, rsp e a pilha .

Vamos dar uma olhada na instrução ret que é executada quando a função chamada retorna da função chamada. O diagrama esquemático é o seguinte:

imagem

Pode-se observar que a operação realizada pela instrução ret é completamente oposta à operação realizada pela instrução call. Quando a instrução ret começa a ser executada, o valor do registrador rip é o endereço imediatamente após a instrução ret, que é 0x400540 , mas a chamada anterior será chamada durante a execução da instrução ret. O endereço de retorno 0x40055e POP da instrução PUSH para a pilha é dado ao registrador rip, de forma que quando a execução de ret for concluída, ele retornará da chamada função para a próxima instrução da instrução de chamada da função de chamada para continuar a execução. Também deve ser observado aqui que a instrução retq também modificará o valor do registrador rsp.

  • instruções jmp / je / jle / jg / jge, etc. começando com j

Estas são todas as instruções de salto. O endereço para o qual saltar ou o registro com o endereço é seguido diretamente pelo opcode. Essas instruções correspondem a instruções como goto e if em linguagens de programação de alto nível. Exemplo de uso:

jmp 0x4005f2 
jle 0x4005ee 
jl 0x4005b8
  • instruções push / pop

push operando fonte operando 
destino pop

Dedicadas às instruções stack e pop da pilha de chamadas de função, essas duas instruções modificarão automaticamente o registrador rsp .

Ao empurrar para a pilha, o valor do registro rsp é primeiro subtraído por 8 para salvar a posição da pilha e, em seguida, o operando é copiado para a posição apontada por rsp. A instrução push é equivalente a:

sub $ 8, 
operando de origem de mov % rsp , (% rsp)

imagem

 

A instrução push precisa prestar atenção às mudanças do registrador rsp.

Quando o pop é retirado da pilha, os dados no local apontado pelo registro rsp são copiados para o operando de destino e, em seguida, o valor do registro rsp é aumentado em 8. A instrução pop é equivalente a:

mov (% rsp), operando de destino 
add $ 8,% rsp

imagem

 

 

Da mesma forma, a instrução pop também precisa prestar atenção às mudanças no registrador rsp.

 

  • deixar instrução



     

A instrução leave não tem operandos. Geralmente é colocada antes da instrução ret no final da função para ajustar rsp e rbp. Esta instrução é equivalente às duas instruções a seguir:

mov% rbp,% rsp 
pop% rbp

 

Apresentaremos muito do assembly AMD64. Na próxima seção, apresentaremos a linguagem assembly go usada no runtime go. É semelhante ao assembly AMD64 apresentado aqui, mas existem algumas diferenças. Depois de compreender o conteúdo desta seção, o processo de montagem é fácil de entender.

Acho que você gosta

Origin blog.csdn.net/pyf09/article/details/115219384
Recomendado
Clasificación