이동 언어 스케줄러 소스 코드 다섯 번째 시나리오 분석 : 어셈블리 지침

다음 콘텐츠는 https://mp.weixin.qq.com/s/fuox6st_iXg_rpklxWXXRA 에서 재현되었습니다. 

Awa는 원래 프로그램을 작성하는 것을 좋아합니다 Zhang  Source Travels  2019-04-20

어셈블리 언어는 모든 백엔드 프로그래머가 마스터해야하는 언어입니다. 어셈블리 언어를 배웠으므로 프로그램을 디버깅하거나 컴퓨터의 기본 작동 원리를 연구하고 이해하는 것이 매우 중요하므로 다음을 수행하는 것이 좋습니다. 관심을 가지십시오 독자들은 그것을 잘 배우기 위해 더 많은 시간을 할애 할 수 있습니다.

고급 프로그래밍 언어와 마찬가지로 어셈블리 언어도 완전한 컴퓨터 프로그래밍 언어이며 많은 지식 내용이 포함됩니다. 다행히도 우리의 주요 목표는 사용이 아닌이 섹션의 연구를 통해 어셈블리 코드를 읽고 이해할 수있는 것입니다. 코드를 작성하는 어셈블리 언어이므로이 섹션에서는 어셈블리 언어를 완전히 소개하지는 않지만 소개 할 어셈블리 언어 어셈블리 지침의 하위 집합 만 선택합니다. 그러나 여기 소개는 간단 해졌지만 독자들은이 지식을 능숙하게 사용하면이 책이 분석 할 고 루틴 스케줄러의 어셈블리 코드를 처리하기에 충분하다는 것을 확신 할 수 있습니다.

어셈블리 명령어라고하면 기계 명령어를 언급해야합니다. 머신 명령어의 바이너리 형식은 CPU가 이해할 수있는 언어입니다. 바이너리 형식이기 때문에 CPU가 파싱하고 실행하는 것이 매우 편리합니다. 사람의 읽기 및 의사 소통에 도움이되지 않으므로 기계 지침 일대일에 해당하는 조립 지침을 사용할 수 있습니다. 조립 지침은 기호를 사용하여 기계 지침을 나타냅니다. 다음 예는이 두 지침의 차이점을 설명하는 데 매우 직관적입니다. :

0x40054d : % rdx, % rax // 어셈블리 명령 추가

(gdb) x / 3xb 0x40054d
0x40054d : 0x48 0x01 0xd0 // 기계 명령어
(gdb)

rdx 및 rax 레지스터에 값을 추가하는 것도 마찬가지입니다. 어셈블리 명령어는 add % rdx, % rax이고 기계 명령어는 0x48 0x01 0xd0의 세 숫자입니다. 분명히 어셈블리 명령어는 인간에게 더 친숙합니다. , 더 기억하기 쉽고 읽기 쉽고 쓰기 쉽습니다.

조립 지침 형식

다른 CPU는 다른 기계 명령을 지원하기 때문에 어셈블리 명령도 다릅니다. 동일한 CPU라도 다른 어셈블리 도구와 플랫폼은 다른 어셈블리 명령 형식을 사용합니다.이 책은 AMD64 Linux 플랫폼에 초점을 맞추기 때문에 이동 스케줄러에서 AT & T 만 소개합니다. 이 플랫폼에서 사용되는 형식 어셈블리 지침 AT & T 어셈블리 지침의 기본 형식은 다음과 같습니다.

Opcode   [ 피연산자 ]

각 어셈블리 명령어는 일반적으로 두 부분으로 구성되어 있음을 알 수 있습니다.

  • Opcode : opcode는 더하기, 빼기 또는 메모리 읽기 및 쓰기와 같이 수행 할 작업을 CPU에 알려줍니다. 모든 명령어에는 opcode가 있어야합니다.

  • 피연산자 : 피연산자는 연산의 객체입니다. 예를 들어 더하기 연산에는 두 개의 추가가 필요하며이 두 개의 추가는이 명령어의 피연산자입니다. 피연산자의 수는 일반적으로 0, 1 또는 2입니다.

조립 지침의 몇 가지 예를 살펴보십시오.

% rdx, % rax 추가

이 명령어의 opcode는 add, 즉 더하기 연산을 수행하는 것을 의미하며 두 개의 피연산자, rdx 및 rax가 있습니다. 명령어에 두 개의 피연산자가있는 경우 첫 번째 피연산자는 소스 피연산자, 두 번째 피연산자는 대상 피연산자라고합니다. 이름에서 알 수 있듯이 대상 피연산자는 실행 후이 명령어의 결과를 저장해야하는 위치를 나타냅니다. 따라서 위의 명령은 rax 및 rdx 레지스터의 값을 합산하고 결과를 rax 레지스터에 저장하는 것을 의미합니다. 사실,이 명령어의 두 번째 피연산자 rax 레지스터는 소스 피연산자이자 대상 피연산자입니다. rax는 더하기 연산의 두 가산 자 중 하나 일뿐만 아니라 더하기 연산의 결과이기 때문입니다. 이 명령어가 실행 된 후 rax 레지스터의 값이 변경되었습니다. 명령어 이전의 값이 덮어 쓰여지고 손실됩니다. rax 레지스터의 이전 값이 여전히 유용하면 명령어를 사용하여 다른 레지스터에 저장하거나 메모리.

피연산자가 하나만있는 예를 살펴 보겠습니다.

callq 0x400526  

이 명령어의 opcode는 callq이며, 이는 함수가 호출됨을 의미하고 피연산자는 0x400526 (호출 된 함수의 주소)입니다.

마지막으로 피연산자가없는 명령어를 살펴 보겠습니다.

retq

이 명령어에는 opcode retq 만 있습니다. 즉, 호출 된 함수에서 호출 함수로 실행이 계속됩니다.

AT & T 형식 조립 지침을 더 잘 이해하기 위해 다음은 형식에 대한 간략한 설명입니다.

1. AT & T 형식 어셈블리 명령어에서 레지스터 이름 앞에 %를 붙여야합니다.

2. 두 개의 피연산자가있는 명령어에서 첫 번째 피연산자는 소스 피연산자이고 두 번째 피연산자는 대상 피연산자입니다. 방금 논의했지만 해당 명령어의 소스와 대상이 명확하지 않습니다. 하나의 To를 살펴 보겠습니다. 간단합니다. mov % eax, % esi,이 명령어는 eax 레지스터의 값을 esi로 복사하는 것을 의미합니다.이 명령어의 소스와 대상은 매우 명확합니다.

3. 직접 피연산자 앞에는 "mov $ 0x1 % rdi"와 같이 $ 기호를 붙여야합니다.이 명령어의 첫 번째 피연산자는 레지스터 나 메모리 주소가 아니라 명령어에 직접 쓰여진 상수입니다. 피연산자의 즉시 피연산자 라고 합니다 . 이 명령어는 0x1 값을 rdi 레지스터에 넣는 것을 의미합니다.

4. 레지스터 간접 주소 지정의 형식은 오프셋 (% register) 이며, 오프셋이 0이면 오프셋을 생략하고 쓰기없이 (% register)로 직접 쓸 수 있습니다. 간접 주소 지정이란 무엇입니까? 실제로 명령어의 레지스터가 실제 소스 피연산자 또는 대상 피연산자가 아님을 의미합니다. 레지스터 값은 메모리 주소입니다.이 주소에 해당하는 메모리는 실제 소스 또는 대상 피연산자입니다 (예 : mov %). rax, (% rsp)이 명령어에서 두 번째 피연산자 (% rsp)의 레지스터 이름은 괄호로 묶여 간접 주소 지정을 나타냅니다. rsp의 값은 메모리 주소입니다.이 명령어의 실제 의도는 변경하는 것입니다. rax 레지스터의 값 rsp 레지스터 (메모리 주소)의 값에 해당하는 메모리에 값이 할당됩니다. rsp 레지스터 자체의 값은 수정되지 않습니다. 비교를 위해 mov % rax, %를 살펴 보겠습니다. rsp 명령어. 여기서 두 번째 피연산자 만 누락 됨 괄호가 직접 주소 지정이되었으며 의미가 완전히 다릅니다.이 명령어는 rsp에 rax의 값을 할당하여 rsp 레지스터의 값을 동일한 값으로 수정하는 것을 의미합니다. rax 레지스터로. 다음 두 그림은 이러한 두 주소 지정 모드의 차이점을 보여줍니다.

영상

mov % rax, % rsp 명령어를 실행하기 전에 rsp 레지스터의 값은 x이고 rax 레지스터의 값은 y입니다. 명령어가 실행 된 후 rax 레지스터의 값은 rsp 레지스터에 복사됩니다. rsp 레지스터의 값이 y가됩니다. 직접 주소 지정 모드를 채택 할 때 대상 피연산자 rsp 레지스터의 값이 명령 실행 전후에 변경되었으며 소스 피연산자가 변경되지 않았 음을 알 수 있습니다. 간접 주소 지정의 개략도를보십시오.

 

영상

 

mov % rax, (% rsp) 명령어를 실행하기 전에 rax 레지스터의 값은 y이고 rsp 레지스터의 값은 위 그림과 같이 메모리 주소 인 X입니다. 빨간색 화살표를 사용했습니다. rsp 레지스터에서 가리 키기 메모리 주소는 X입니다. 명령어가 실행 된 후 rsp 레지스터의 값은 변경되지 않았지만이 명령어의 대상 피연산자가 간접을 사용하기 때문에 rsp가 가리키는 메모리의 값이 변경되었습니다. 주소 지정 (% rsp), 명령 실행의 결과는 rax 레지스터의 값이 rsp 레지스터에 저장된 주소에 해당하는 8 개의 메모리 유닛에 복사됩니다. 또한 명령에 나타나는 메모리 주소는 시작 주소 일뿐입니다.이 주소를 시작 주소로 사용하는 여러 연속 메모리 장치의 특정 동작은 mov % rax와 같은 특정 명령에 따라 달라집니다. (% rsp), 소스 피연산자가 64 비트 레지스터이므로이 명령어는 rax에 저장된 8 바이트를 주소 X, X + 1, X + 2, X + 3, X + 4, X로 복사합니다. +5, X + 6, X + 7이 8 개의 메모리 셀.

 

간접 주소 지정 형식 오프셋 (% register)의 이전 오프셋은 -0x8 (% rbp)과 같은 오프셋을 의미하고, -0x8은 오프셋이며, 전체는 rbp 레지스터에 저장된 주소 값이 처음에서 8을 뺀 값임을 의미합니다. 오프셋은 음수입니다. 8) 획득 한 주소에 해당하는 메모리.

 

5.  메모리와 관련된 일부 명령어의 opcode는 movl $ 0x0, -0x8 (% rbp) 명령어와 같이 작동 메모리가 1, 2, 4 또는 8 바이트인지 여부를 나타내는 b, w, l 및 q 문자를 추가합니다. ,이 명령어 opcode movl의 접미사 l은 주소 -0x8 (% rbp)에서 시작하는 4 개의 메모리 유닛에 0을 할당하려고 함을 나타냅니다. 일부 독자는 3 개 또는 5 개의 메모리 셀을 작동하려면 어떻게해야합니까? CPU가 해당 단일 명령을 제공하지 않는 것은 유감입니다. 여러 명령을 결합하여 목표를 달성 할 수 있습니다.

 

일반적인 명령에 대한 자세한 설명

 

수천 개의 x86-64 어셈블리 지침이 있습니다. 여기서 각각에 대해 자세히 설명하지는 않겠습니다. 독자는 어셈블리 언어 자습서를 참조 할 수 있습니다. 우리는 매우 일반적인 몇 가지 지침에 초점을 맞추거나 프로그램의 작동 메커니즘을 이해하는 데 도움을 줄 수 있습니다.

 

  • mov 명령

mov 소스 작업 번호의 피연산자

이 명령어는 소스 피연산자를 대상 피연산자로 복사합니다. 예:

mov % rsp, % rbp # 직접 주소 지정, rsp 값을 rbp로 복사합니다. 이는 rbp = rsp와 같습니다.
mov -0x8 (% rbp), % edx # 소스 피연산자는 간접적으로 주소가 지정되고 대상 피연산자는 직접 주소가 지정됩니다. 메모리에서 edx 레지스터로 4 바이트 읽기
mov % rsi, -0x8 (% rbp) # 소스 피연산자는 직접 주소가 지정되고 대상 피연산자는 간접적으로 주소가 지정됩니다. rsi 레지스터의 8 바이트 값을 메모리에 씁니다.
  • 추가 / 구독 지침

소스 연산 번호의 피연산자 추가
하위 소스 연산 번호의 피연산자

더하기 및 빼기 지침. 예:

sub $ 0x350, % rsp # 소스 피연산자는 즉시 피연산자이고 대상 피연산자는 직접 주소가 지정됩니다. rsp = rsp-0x350
add % rdx, % rax # 직접 주소 지정. rax = rax + rdx
addl $ 0x1, -0x8 (% rbp) # 소스 피연산자는 즉시 피연산자이고 대상 피연산자는 간접적으로 주소가 지정됩니다. 메모리의 값이 1 씩 증가합니다 (addl 접미사 문자 l은 메모리에서 4 바이트의 연산을 나타냄).
  • 콜 / 리트 지시

전화 목적지 주소
권리

호출 명령어는 함수 호출을 수행합니다. CPU가 호출 명령을 실행하면 먼저 립 레지스터의 값을 스택에 넣은 다음 립 값을 대상 주소로 설정합니다. 립 레지스터는 실행할 다음 명령을 결정 하므로 CPU가 실행될 때 점프합니다. 현재 호출 명령 실행을 완료합니다. 실행할 대상 주소로 이동합니다.

ret 명령어는 호출 된 함수에서 호출 함수로 반환됩니다. 실현 원리는 호출 명령어의 반환 주소를 스택의 립 레지스터로 팝하는 것입니다.

다음 예는이 두 가지 지침의 원리를 보여줍니다.

# 호출 함수 조각
0x0000000000400559 : callq 0x400526 <합계>
0x000000000040055e : 이동 % eax, -0x4 (% rbp)

# 호출 된 함수 조각
0x0000000000400526 : % rbp 푸시
......
0x000000000040053f : retq  

위의 코드 조각에서 호출 함수는 callq 0x400526 명령어를 사용하여 0x400526에서 함수를 호출하고 0x400526은 호출 된 함수의 첫 번째 명령어 주소입니다. 호출 된 함수는 0x40053f에서 retq 명령을 실행하고 호출 함수로 돌아가 주소 0x40055e에서 명령을 계속 실행합니다. 이 두 명령어는 스택 및 팝 작업을 포함하므로 rsp 레지스터의 값에 영향을줍니다.

영상

위의 그림에서 호출 명령 실행이 시작될 때 rip 레지스터의 값은 호출 바로 다음의 명령 주소 인 0x40055e이지만 호출 명령이 완료되면 다음 명령이 실행되면 립 레지스터의 값이 변경됩니다. 호출 명령의 피연산자가됩니다. 즉, 호출 된 함수의 주소가 0x400526이므로 CPU는 호출 된 함수로 점프하여 실행합니다.

동시에 호출 명령이 실행될 때 호출 명령 이후의 명령 주소 0x40055e가 스택에 PUSH되므로 호출 명령은 립 레지스터, rsp 및 스택 .

호출 된 함수가 호출 된 함수에서 반환 될 때 실행되는 ret 명령어를 살펴 보겠습니다. 회로도는 다음과 같습니다.

영상

ret 명령에 의해 수행되는 연산은 call 명령에 의해 수행되는 연산과 완전히 반대임을 알 수 있습니다 .ret 명령이 실행되기 시작하면 rip 레지스터의 값은 ret 명령 바로 뒤의 주소 인 0x400540입니다. 그러나 ret 명령 실행 중에 이전 호출이 호출됩니다. 스택으로의 PUSH 명령의 반환 주소 0x40055e POP가 rip 레지스터에 제공되므로 ret 실행이 완료되면 호출 된 주소에서 반환됩니다. 실행을 계속하려면 호출 함수의 호출 명령어의 다음 명령어에 함수를 추가합니다. 여기서도 retq 명령어는 rsp 레지스터의 값도 수정한다는 점에 유의해야합니다.

  • jmp / je / jle / jg / jge 등 j로 시작하는 명령어

모두 점프 명령어입니다. 점프 할 주소 또는 주소가있는 레지스터 바로 뒤에 opcode가옵니다. 이러한 명령어는 goto 및 if와 같은 명령문에 해당합니다. 사용 예 :

jmp 0x4005f2
jle 0x4005ee
jl 0x4005b8
  • 푸시 / 팝 지침

푸시 소스 피연산자
팝 대상 피연산자

함수 호출 스택의 스택 및 팝 명령어 전용 으로이 두 명령어는 rsp 레지스터를 자동으로 수정합니다 .

스택에 푸시 할 때 먼저 rsp 레지스터의 값을 8로 빼서 스택 위치를 저장 한 다음 피연산자를 rsp가 가리키는 위치에 복사합니다. 푸시 명령어는 다음과 같습니다.

$ 8, % rsp 이하
mov 소스 피연산자, (% rsp)

영상

 

푸시 명령어는 rsp 레지스터의 변경에주의를 기울여야합니다.

스택에서 팝이 팝되면 rsp 레지스터가 가리키는 위치의 데이터가 대상 피연산자로 복사 된 다음 rsp 레지스터의 값이 8 씩 증가합니다. 팝 명령어는 다음과 같습니다.

mov (% rsp), 대상 피연산자
$ 8, % rsp 추가

영상

 

 

마찬가지로, pop 명령어도 rsp 레지스터의 변경 사항에주의를 기울여야합니다.

 

  • 지시를 떠나다



     

leave 명령어에는 피연산자가 없습니다. 일반적으로 rsp 및 rbp를 조정하는 함수 끝에있는 ret 명령어 앞에 배치됩니다.이 명령어는 다음 두 명령어와 동일합니다.

이동 % rbp, % rsp
팝 % rbp

 

많은 AMD64 어셈블리를 소개하겠습니다. 다음 섹션에서는 go 런타임에서 사용되는 go 어셈블리 언어를 소개합니다. 여기서 소개 한 AMD64 어셈블리와 비슷하지만 몇 가지 차이점이 있습니다. 이 섹션의 내용을 이해하면 go assembly가 이해하기 쉽습니다.

추천

출처blog.csdn.net/pyf09/article/details/115219384