분류 전체보기 (34)
2024-11-21 10:47:01

이번 글에서는 솔리디티 프로그래밍과 보안을 입문하기 위한 이더리움의 기본 개념과 특징을 서술한다.

1. 솔리디티란?

이더리움에서 제공하는 스마트 컨트랙트 개발 언어. 이더 [ETH]를 보내고 받는 데에 이용된다. 여기서 등장하는 용어는 낯설 수 있지만 추후에 다시 설명한다.

  • EVM [Ethereum Virtual Machine]을 타깃으로 디자인되었고
  • 튜링 완전성: 어떤 프로그래밍 언어 및 머신이 튜링 머신과 동일하 계산 능력으로 문제를 풀고 해결할 수 있다는 의미.
    • 솔리디티는 자체적인 튜링 완전 언어들을 지원하기 때문에 사실상 가능한 모든 형태의 거래를 코드로 작성할 수 있다.

2. 이더리움이란?

암호화폐라고 하면 떠오르는 것이 크게 두 개 있다.

  1. 비트코인
  2. 이더리움

거래를 해본 사람도 있을 것이고 들어만 본 사람도 있을 것이다. 적어도 프로그래밍 및 블록체인을 몰라도 이름은 알 것이라 생각한다. 사실 이더리움을 개념적으로 이해하는 것은 상당히 복잡하다(

적어도 나는 그렇다

)

우선 암호화폐를 정의할 필요가 있겠다.
암호화폐는 공개키 암호화를 통해 안전하게 전송하고 해시 함수를 이용해 쉽게 소유권을 증명해낼 수 있는 자산이다. 다르게 말하면 검산은 쉬운데 역산은 어렵다는 것이다.

이더리움도 비트코인과 같은 암호화폐이다.
그러나 비트코인은

    1. 튜링 불완전성: 스크립트로 작성된 비트코인은 비교적 단순한 편이라 화폐 이상의 기능을 수행하기 어렵다.
    1. 상태표현제한: 표현 가능한 상태가 사용완료 or 안함 둘뿐이라 잔액을 일부 남기고 계약이 불가하다.
    1. Blockchain-blindness: UTXO(비트코인 잔액)이 블록헤더 데이터들을 해독하지 못해서 화폐 기능 이외 다른 분야의 어플리케이션을 만들기 어렵다.

위처럼 비트코인은 단순하고 제한적이다.
이러한 배경에서 더 세련된 언어로 분산 어플리케이션 [Decentralized Application; dApp] 을 이용할 수 있는 플랫폼을 만들자! 는 시도에서 나온 게 블록체인이다.

이것들을 구현하기 위해 사용되는 기술들이 있다.
비트코인은 스크립트를 사용하여 튜링불완전한데,
이더리움은

  • Solidity
  • Serpent
    등을 이용하기 떄문에 튜링완전하고, 이게 곧 Smart Contract를 가능하게 한다.

3. 이더리움의 기술

GAS

이더리움 플랫폼에서는 이더(ether)라는 자체 화폐토큰이 있고 이더를 가지고 GAS라는 어플리케이션을 구입한다. 이것은 이더리움이 Smart Contract를 하는 데 연산력과 저장공간 제공을 위한 연료로 쓰인다.

Smart Contract

블록체인 기술을 활용해 제3의 인증기관 없이 개인 간 계약이 이루어질 수 있도록 하는 기술이다.

스마트 컨트랙트는 다음과 같은 성질을 가진다.

    1. 관측가능성(obsevability): 서로의 계약 이행 가능성을 관찰하거나 성과를 입증할 수 있어야 함
    1. 검증가능성(verfifiability): 계약을 이행하거나 위반할 경우 계약 당사자들이 이를 알 수 있어야함
    1. 프라이버시(privity): 계약 내용은 계약에 필요한 당사자만 알 수 있어야 함
    1. 강제 가능성(enforceability): 계약이 이루어질 수 있도록 구속력이 있어야 함

4. 이더리움의 블록

이더리움의 블록은 함수처럼 작동한다.

124번 블록이 상태 1에서 2로 바꿔주는 역할을 하는데,
여기서 상태 1은 블록1

123이 연결된 블록체인 상태를 의미하고, 상태 2는 1

124가 연결된 블록체인 상태이다.
이가 의미하는 건 블록이 추가될 때마다 상태가 계속해서 변한다는 것이다.

이러한 상태 변환 연산을 수만 개의 노드가 하나의 컴퓨터처럼 연산한다. 수만 개의 노드가 똑같은 연산을 중복하여 똑같은 데이터를 가지고 항상 동일한 상태의 합의에 도달한다.

이에 필요한 연산력을 아까 말했던 GAS의 구입을 통해 제공받는다.

5. 이더리움과 계좌

이더리움의 구성은
이더리움 = 이더 + 계좌 + GAS
로 표현될 수 있다.

아까도 언급했지만

  • 이더: 이더리움의 자체토큰
  • 계좌: 말그대로 이더를 가지고 있는 계좌
  • GAS: 연산력에 필요한 연료와 같은 것

이 중 계좌는 다음과 같이 다시 구분된다.

외부 소유계좌(EOA)

  • 개인키를 가지고 통제하는 계좌
  • 이더를 주고받을 수 있는 일반적인 은행 계좌와 비슷

계약계좌

  • 개인키가 아니라 계약 코드에 의해서 통제
  • 계약 코드계약 정보 저장 공간을 가지고 있고, 내부 저장공간의 데이터를 읽고 쓸 수 있다.

EOA는 일반적인 은행 계좌와 비슷한 개념이지만, 계약계좌는
GAS를 구입해 계약 코드를 걸어놓으면 코드의 조건이 만족될 때에만 계약계좌가 메시지를 받고 계약의 내용이 실행된다.

예를 들면 '과속을 했을 때' '1ETH를 부과하는 계약 코드'를 걸어놓으면 과속을 했을 때 메시지를 받고 1ETH가 계약계좌에서 빠져나간다.
이러한 계약 코드는 이더리움 플랫폼에서 정해놓은 헌법과도 같아서 누구도 통제가 불가능하다.

'보안 > web3' 카테고리의 다른 글

web3 시작하기  (0) 2024.11.18
2024-11-18 09:34:11
2024-11-12 20:05:18

https://dreamhack.io/wargame/challenges/1364

[datestring

Description Time is gold... but shell is diamond. Flag Format: DH{...}

dreamhack.io](https://dreamhack.io/wargame/challenges/1364)

파일을 다운받으면 따로 주어진 소스코드는 없다.

실행시

./datestring
Calendar v0.1
Year: 2024
Month: 1 
Day: 12
Hour: 19
Minute: 46
Second: 19
Formatted date: Fri Jan 12 19:46:19 2024

상단과 같이 나옴.

먼저 gdb에 넣어봤었는데 flag라는 함수가 있다는 걸 찾음.

flag호출 시 쉘을 취득가능한듯함.

flag호출 조건을 ida로 좀 더 편하게 볼 수 있는데 결론적으로

printf("Formatted date: %s", v16);
  if ( monthminusone == 11 && date_1 == 25 && !v15 && valuezero )
  {
    puts("A Present for Admin!");
    flag("A Present for Admin!");
  }

변수명을 조금 바꿔보면 month: 12, date: 25이고, 처리해야되는 부분은 !v15 이면서 valuezero를 0이 아닌 값으로 바꾸기.

 v15 = (year / -100 + year / 4 + 23 * month / 9 + date + 4 + year / 400) % 7;
  calendar(v16, v11);

v15는 이 부분을 처리해줘야함. 저게 0이 되면 된다.
해석하면 12/25가 일요일인 연도를 찾는 문제다

그런데 아무 연도나 입력하면 valuezero값이 안 덮어져서 플래그를 얻을 수 없다.
calendar를 이용해 버퍼오버플로우를 일으켜서 0이 아니도록 덮어버리면 될 일이다.
따라서 특정 연도라기보단 매우 크면서 12/25가 일요일인 연도 찾기를 하면됨
100000000 이상이면 무관하다.

가장 작은 친구로는

Year: 100000005
Month: 12
Day: 25
Hour: 2
Minute: 2
Second: 2
Formatted date: Sun Dec 25 02:02:02 100000005
A Present for Admin!

가 있겠다.
저 문구가 나오면 쉘을 취득했으니 플래그를 cat 하면 끝

2024-11-04 02:13:56

현재 필자는 NvChad를 이용중이다.

Mason을 통해 asm-lsp를 설치하고 다른 언어와 마찬가지로 lsp서버를 세팅에 추가하였으나 서버에 접속이 안 된다는 문구가 뜸.

레딧에서 구글링을 좀 해본 결과 Mason자체가 문제인 것은 아닌 듯하다.

asm-lsp는 다른 lsp들과 좀 다른데, git으로 관리되지 않으면 사용이 안된다는 듯하다.

그런데 나의 경우 이 부분에 대해 아무런 문제가 없었다. 원래부터 깃으로 관리되고 있어서 git init 하나 던진다고 해결이 되는 게 아니었음.

결과적으로

https://github.com/bergercookie/asm-lsp

레포에 직접 찾아가서 Mason이 아니라 cargo를 통해 다시 다운받아주었는데 이게 필수적인지는 잘 모르겠다.

어째거나 원인을 모르니 그렇게 했고,

lspconfig.lua에서

-- lspconfig.lua
local on_attach = require("nvchad.configs.lspconfig").on_attach
local on_init = require("nvchad.configs.lspconfig").on_init
local capabilities = require("nvchad.configs.lspconfig").capabilities

local lspconfig = require "lspconfig"
local servers = { "html", "cssls", "clangd", "tsserver", "pyright", "eslint", "gopls" }

-- lsps with default config
for _, lsp in ipairs(servers) do
  lspconfig[lsp].setup {
    on_attach = on_attach,
    on_init = on_init,
    capabilities = capabilities,
  }
end

lspconfig.asm_lsp.setup {
  cmd = { "asm-lsp" }, -- asm-lsp 명령어
  filetypes = { "asm", "s" }, -- 어셈블리 파일 유형
  root_dir = lspconfig.util.root_pattern(".git", vim.fn.getcwd()), -- 프로젝트 루트를 설정
}

아래 부분을 추가해주었다.
이제 asm확장자 붙이면 정상 작동되는 걸 확인함. 만세~~~~~~~

'잡담' 카테고리의 다른 글

runcat 설치  (0) 2024.08.10
2024-10-25 14:42:44

이 시리즈는 go언어로 인터프리터를 만드는 과정을 소개한다. 밑바닥부터 만드는 인터프리터 in Go를 참고하였으며, 말그대로 인터프리터를 밑바닥부터 만드는 과정을 보여주며, 완성되는대로 공백 단순 무시+세미콜론이 있는 c스타일 인터프리터 언어, 이후에 go로 만드는 파이썬 스타일 인터프리터를 깃허브에 올릴 생각이다. 현재 글은 c스타일에 가까운 인터프리터 언어를 만드는 과정이며, 파이썬 스타일에서는 공백 처리 등의 전처리가 다르고, 실제 사용 언어와 같이 만들고자 한다면 더욱 섬세한 처리가 필요하다.

Lexing

  • 렉싱 <lexing>   : 소스코드를 토큰 열로 변환하는 것
  • 위와 같은 과정을 lexer라고 하는 녀석이 수행한다. 이 친구는 토크나이저 또는 스캐너로도 불림

인터프리터는 소스코드->토큰 으로 변환된 이후 토큰을 추상구문트리  <Abstract Syntax Tree>  로 바꾸는 2차 변환 작업이 일어난다.

예를 들면,

let x = 5 + 5

와 같은 소스코드는

[
    LET,
    IDENTIFIER(“x”)
    EQUAL_SIGN,
    INTEGER,
    PLUS_SIGN,
    INTEGER(5),
    SEMICOLON
]

과 같이 변환한다. 앞서 말한대로, 이러한 과정은 렉서가 수행한다.
토큰을 어떻게 구성할지는 렉서의 구현에 따라 다르다. 공백의 길이를 처리하거나 크게 영향을 갖지 않을 수도 있다. 예컨대 공백을 단순한 구분자로만 쓴다면

let    x    =     5      +     5

도 똑같이 처리될 것이란 말이다.

 

그러나 파이썬과 같은 언어에서는 공백의 길이가 중요하게 작동한다. 알다시피 중괄호 대신 블럭 형태, 즉 공백이 무의미한 구분자가 아니라 중괄호같은 역할을 하기 때문에 그냥 공백을 일괄적으로 처리해버리면 안된다는 뜻이다.

여담이지만 나는 이러한 블럭 형태의 구분을 별로 좋아하지 않는다


토큰 정의하기

그러면 인터프리터의 첫 단계인 렉싱을 수행하기 위해 우리가 가장 먼저 해야할 일은 토큰 정의하기이다. 아래 코드를 보면서 어떤 식으로 토큰을 정의해야할지 생각해보자.

let five = 5;
let ten = 10;
let add = fn(x, y) {
    x + y;
} 
let result = add(five, ten);

 

어떤 토큰이 필요할까?
직관적으로는

  • 숫자 토큰
  • 변수명 x, y, result ...
  • 변수가 아닌 문자열: fn, let
  • 특수 문자: 괄호 등

를 들어볼 수 있겠다.
사실 토큰화하는 과정에서 ’값‘은 신경 쓸 필요가 없다. 렉서가 할 일은 저게 뭔지 구분해서 쪼개놓을 뿐이지, 사실 저게 5냐 10이냐는 전혀 알 바가 아니라는 거다. 토큰화는 그저 쪼갤 뿐이므로, 5와 10은 어차피 같은 방식으로 처리되기 때문이다.

 

다른 것도 마찬가지로, 렉서는 저 문자열이 ‘변수명’인지를 고려하지 변수명이 무엇인지는 중요하지 않다. 여러분이 a, b와 같은 알아보기 힘든 변수명을 써서 상사나 교수님한테 혼나더라도, 우리 착한 렉서는 그런 건 아무 신경 쓰지 않는다.
이러한 변수명은 식별자(identifier)  라는 명칭으로 그저 통일되어 구분된다.

 

그런데 일괄적으로 처리되지 않아야할, 그러니까 식별자가 아닌 문자열도 있지 않은가?
바로 let, fn, 뭐 다른 언어에선 def같은 이런 특수한 문자열들 말이다. 이것들은 식별자와 기능적으로 구분되어야하기 때문이다. 이런 문자열들은 언어 의 일부이다. 따라서 우리는 이것들을 예약어(keywords)  라고 부른다.

 

추가적으로 특수 문자에 대한 처리가 필요할 수 있겠다. 특수 문자는 기능적으로 함수의 적용 범위 등을 규정할 수 있으므로 또 하나의 기능적으로 다른 친구이다. 또한 소스코드에 괄호의 여부에 따라 처리 과정이 크게 달라지곤 한다고 한다. 중요한 것은 이놈을 별도로 처리하자는 것.

토큰 자료구조 정의하기

그럼 이제 토큰의 자료구조를 정의할 수 있겠다.
이젠 뭐가 필요한가?
토큰은 ‘타입’ 속성이 필요하다. 그래야 정수와 특수 문자, 이런 것들의 구분이 가능하다.
더해, 토큰은 토큰 자체의 문자값(리터럴) 을 가질 필드가 필요하다. 소스코드 자체를 쪼갤 때는 5인지 10인지가 중요하지 않지만, 이후 과정에서는 쓰여야한다. 이러한 필드가 있어야 값을 잃어버리지 않는다.

package token
type TokenType string //토큰의 타입을 필요한만큼 정의해서 쓸 수가 있다!
//성능이 중요하다면 알아서 int, byte쓸 것…
type Token Struct {
    Type TokenType
    Literal string
}

타입리터럴 필드가 필요하다고 했으니 저런 식으로 구현하면 되겠다.
그렇다면 좀 더 나아가 아래 코드를 추가할 수 있겠다.

const (
    ILLEGAL = “ILLEGAL” //어떤 토큰이나 문자를 렉서가 알 수 없음
    EOF = “EOF” //파일의 끝

    //식별자, 리터럴
    IDENT = “IDENT”
    INT = “INT”

    //연산자
    ASSIGN = “,”
    PLUS = “+”

    //구분자
    COMMA = “,”
    SEMICOLON = “;”

    LPAREN = “(“
    RPAREN = “)”
    LBRACE = “{“
    RBRACE = “}”

    //예약어
    FUNCTION = “FUNCTION”
    LET = “LET”
)

다른 것들은 그렇다치고, EOF와 ILLEGAL은 뭐 하는 놈들인가?
뭐 코멘트에도 달려있다만

  • ILLEGAL: 알 수 없는 토큰이나 문자
  • EOF: end of file이라는 뜻인데, 나중에 파서에게 여기가 끝이니 멈춰라- 할 때 쓰인다.

어느정도 토큰을 정의하였다. 당연하지만 실제 인터프리터 언어로 쓰일 정도면 이보다 훨씬 확장되어야한다—그러나 여기서는 이만 멈추고, 렉서를 작성해보자.


렉서

이제 렉서를 작성할 차례다.
렉서가 뭐 하는 놈이던가? 이 녀석은 소스코드를 입력으로 받고, 소스코드를 토큰 열로 쪼갠 다음 이를 뱉어낼 것이다. 그런데 이 렉서는 버퍼도 필요 없고 토큰을 저장할 공간을 따로 만들지 않아도 된다.


이건 소스코드를 보고 토큰으로 쪼갤 때마다 그 토큰을 바로바로 뱉어내기 때문에, 렉서 안에 저장 공간이 있을 필요는 없다. 대신 나중에 뱉은 것을 다른 친구가 잘 주워담야겠지..그게 이따 볼 NextToken이라는 친구인데, 일단 넘어가자.

 

따라서 렉서를 초기화할 때 소스코드를 인자로 전달하고, 초기화 이후에 NextToken을 호출하면서 소스코드를 토큰 단위->문자 단위로 훑는 작업을 할 것이다.


여기서는 그냥 string타입으로 다룰 거지만, 실제 작업 시에는 파일 이름, 행 번호도 추가하여 작업해야한다. 이런 식의 간단한 작업은 에러 처리에서 매우 불리하다..


그러나, 인터프리터 구조 살펴보기라면 이 정도로도 충분하다.

//lexer_test.go

package lexer

import (
    “testing”
    “lang/token” //이건 참고 책에서는 monkey라고 하는데, 그냥 만들 언어의 이름인것이다
)

func TestNextToken(t *testing, T) {
    input := ‘=+(){},;’
    tests := []struct {
        expectedType token.TokenType
        expectedLiteral string
    } {
        {token.ASSIGN, “=“},
        {token.PLUS, “+”},
        {token.LPAREN, “(“},
        {token.RPREN, “)”},
        {token.COMMA, “,”},
        {token.SEMICOLON, “;”},
        {token.EOF, “”},
    }
    l := New(input)
    for i, tt := range tests {
        tok := l.NextToken()
        if tok.Type != tt.expectedType {
            t.Fatal(“tests[%d] - tokentype wrong, expeced=%q, got=%q”,
            i, tt.expectedType, tok.Type)
        }
        if tok.Literal != tt.expectedLiteral {
            t.Fatal(“tests[%d] - literal wrong. expected=%q, got=%q”,
            i, tt.expectedLiteral, tok.Literal)
        }
    }
}

와 같이 야심차게 작성한 후 실행하면! 당연히 실패한다. 이유는 코드가 없으므로.

아래는 *Lexer를 반환하는 New 함수이다.

//lexer.go
package lexer

type Lexer struct {
    input string
    position int //입력에서 현재 위치
    readPosition int //입력에서 현재 읽는 위치
    ch byte //현재 조사하고 있는 문자
}
func New(input string) *Lexer {
    l := &Lexer{input: input}
    return l
}

Lexer 구조체는 input, position, readPosition, ch로 되어있는데, 이 중 input은 그냥 들어온 입력, 즉 소스코드일 것이고, 나머지 세 개가 무엇인가 하면. 사실 주석에 전부 있긴 하다.


그렇지만 조금 헷갈릴 수 있는 부분이 position과 readPositio의 차이 정도일 것이다.
둘 다 시점은 현재인데, 현재 위치현재 읽는 위치는 뭐가 다른 것인가?

둘 모두 입력 문자열의 문자에 인덱스로 접근하기 위해 사용되는데, 둘 모두 사용되는 이유는 다음 문자를 미리 살펴봄과 동시에 현재 문자를 보존해야하기 때문이다. 그러므로,

  • readPosition: 다음 문자를 가리킴
  • position: 현재 문자를 가리킴

그렇다. 현재 읽음 이라는 것의 의미는 다음 인덱스에 있는 문자를 미리 살펴본다는 것이다.

헬퍼 메서드

readChar

func (l *Lexer) readChar() {
    if l.readPosition >= len(l.input()) {
        l.ch = 0
    } else {
        l.ch = l.input[l.readPosition]
    }
    l.position = l.readPosition
    l.readPosition += 1
}

readChar 메서드는 다음 문자에 접근할 수 있도록 만들어준다.
하나하나 쪼개서 보자면

if l.readPosition >= len(l.input()) {
    l.ch = 0
}

문자열 input의 끝에 도달했는지 확인한다. 만약 끝이라면 l.ch에 0을 넣게 되는데,
0은 ‘아직 아무것도 읽지 않은 상태’ 혹은 ‘EOF’(파일의 끝)을 의미한다.

else {
    l.ch = l.input[l.readPosition]
}

만약 input문자열의 끝에 도달하지 못했다면, l.readPosition을 통해 l.ch에 다음 문자를 저장한다.

l.position = l.readPosition
l.readPosition += 1

아까 position은 실제 현재 위치 readPosition은 다음 문자라고 말했다. 따라서 위치를 업데이트하기 위해 미리 살펴본 문자의 위치로 position을 다시 세팅해주고, readPosition은 1 늘려주면 된다.

 

아까 코드가 없어서 New함수가 작동하지 않는다고 했다. 인풋(소스코드드가 들어와야하는데, 그것을 읽을 수 있지 않기 때문이다. 그래서 readChar를 써주면 인풋에서의 문제점은 사라진다.

func New(input string) {
    l := &Lexer{input: input}
    l.readChar()
    return l
}

리마인드하자면 우리가 해야할 것은

  1. 소스코드를 인자로 전달하고
  2. NextToken을 호출하여 토큰 단위->문자 단위로 훑는다-

이 중에 1번이 readChar를 통해 해결되었다.

NextToken

위의 과정에서 NextToken이 없었으므로, 이것을 만들 것이다.

package lexer
import “lang/token”

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    swtich l.ch {
    case ‘=‘:
        tok = newToken(token.ASSIGN, l.ch)
    case ‘;’:
        tok = newToken(token.SEMICOLON, l.ch)
    case ‘(‘: 
        tok = newToken(token.LPAREN, l.ch)
    case ‘)’:
        tok = newToken(token.RPAREN, l.ch)
    case ‘,’:
        tok = newToken(token.COMMA, l.ch)
    case ‘+’:
        tok = newToken(token.PLUS, l.ch)
    case ‘{‘:
        tok = newToken(token.LBRACE, l.ch)
    case ‘}’:
        tok = newToken(token.RBRACE, l.ch)
    case 0:
        tok.Literal = “”
        tok.Type = token.EOF
    }
    l.readChar()
    return tok
}

func newToken(tokenType token.Tokentype, ch byte) token.Token {
    return token.Token{Type: tokentype, Literal: string(ch)}
}

위 코드가 NextToken 메서드의 기본 구조이다.
현재 검사하고 있는 문자 l.ch를 보고 이에 대응하는 토큰을 반환한다. 토큰을 반환하기 전에 입력 문자열의 인덱스인 position과 readPosition을 증가시킨다.

테스트 케이스를 확장해서,

func TestNextToken(t *testing.T) {
    input := let five = 5;

    let ten = 10;
    let add = fn(x, y) {
        x + y;
    };
    let result = add(five, ten);

    tests := []struct {
        expectedType token.TokenType
        expectedLiteral string
    } {
        {token.LET, “let”},
        {token.IDENT, “five”},
        {token.ASSIGN, “=“},
        {token.INT, “5”},
        {token.SEMICOLON, “;”},
        {token.LET, “let”},
        {token.INT, “10”},
        {token.SEMICOLON, “;”},
        {token.LET, “let”},
        {token.IDENT, “add”},
        {token.ASSIGN, “=“},
        {token.FUNCTION, “fn”},
        {token.LPAREN, “(“},
        {token.IDENT, “x”},
        {token.COMMA, ”,”},
        {token.IDENT, “y”},
         // …
    }
}

뭐가 엄청 많다…
위와 같이 성공적으로 토큰화할 수도 있지만 문제가 될 수 있는 상황이 있다.

 

만약 순서가 식별자(identifier)-예약어(keywords)-숫자의 형태라면 정확한 토큰화에 실패하게 된다. 왜냐하면 현재 로직은 글자인 문자, 글자가 아닌 문자로 구분하여 글자가 아닌 문자까지 도달할 때까지 이를 한 번에 읽어버리게 되는데,
예약어들은 지정되어있으므로 이것들이 먼저 있으면 처음-끝을 파악 가능하지만 식별자-예약어가 되면 예약어의 문자열이 식별자로 들어가게 될 수 있다. 식별자+예약어, 숫자로 읽어들이게 될 수 있는 것이다.

 

그렇다면 이것을 읽어낸 후 구분할 필요가 있다는 것이다.

import “lang/token”

func (l *Lexer) NextToken() token.Token {
    var tok token.Token
    switch l.ch {
     // defualt분기: 앞선 분기문에서 처리되지 않은 문자일때 식별자로 처리
     default:
         // 글자일 때
         if isLetter(l.ch) {
             // readIdentifier를 호출하여 현재 토큰의 Literal필드를 채움
            tok.Literal = l.readIdentifier()
            return tok
        } else {
            //처리되지 않은 문자, 즉 ILLEGAL
            tok = newToken(token.ILLEGAL, l.ch)
        }
    }
}
// 식별자를 하나 읽어들이고 렉서의 position과 readPosition을 글자가 아닌 문자를 만날 때까지 증가시킨다.
func (l *Lexer) readIdentifier() string {
    position := l.position
    for isLetter(l.ch) {
        l.readChar()
    }
    return l.input[position:l.position]
}
// 전달받은 인자가 글자인지 아닌지 판단한다. 
func isLetter(ch byte) bool {
    return ‘a’ <= ch && ch <= ‘z’ || ‘A’ <= ch && ch <= ‘Z’ || ch == ‘_’ // _를 문자 처리. under_var같은 것도 변수명으로 이용 가능해졌다.
}

아, 그리고 글자인 문자라는 것은 숫자와 특수문자가 아닌 자연어?문자. 여기서는 알파벳을 말하는 것이다. char라는 자료형 자체는 1,2,3,%$^이런 것도 다 포함하고 있기에, 알파벳인지 판단하는 것.


물론 여기서는 확장 가능하기 때문에 알파벳뿐 아니라 한글 등 다양하게 확장 가능하지만 현재로서는 isalpha()와 비슷한 놈이다.

앞서 필드에는 Type, Literal가 필요하다고 밝혔다.


위 코드에서 readIdentifier를 호출하여 Literal필드를 해결하였다. 이제는 Type필드를 처리할 차례이다.

이미 let, fn과 같은 식별자는 읽어놓은 상태이므로, 토큰 리터럴에 맞는 TokenType반환 함수를 하나 정의해야한다. 사용자 정의 식별자와 예약어도 처리해야하기 때문.

var keywords = map[string]TokenType {
    “fn”: FUNCTION,
    “let”: LET
}
//keywords 테이블을 검사해서 주어진 식별자가 예약어인지 아닌지 살펴본다.
func LookupIdent(ident string) TokenType {
    //예약어라면 예약어에 맞는 TokenType반환
    if tok, ok := keywords[ident]; ok {
        return tok    
    }
    //아니라면 식별자 반환
    return IDENT
}

위 과정을 통해 식별자와 예약어 렉싱이 마무리되었다.

func (l *Lexer) NextToken() token.Token {
    var tok token.Token

    switch l.ch
    // …
    default:
        if isLetter(l.ch) {
            tok.Literal = l.readIdentifier()
            tok.Type = token.LookupIdent(tok.Literal)
            //아랫줄을 통해 조기 종료하게 된다.
            //switch문 이후에 readChar는 다시 호출할 필요가 없다.
            return tok
        } else {
            tok = newToken(token.ILLEGAL, l.ch)
        }
}

그러면 이대로 마무리되는가?
아니다. 공백에 대한 처리가 제대로 이루어지지 않았다.
현재 만들고자 하는 것은 공백을 그냥 토큰 구분자로 사용할 뿐, 그냥 넘어가야하는데, 현재는 그렇지 않은 것.

func (l *Lexer) NextToken() token.Token {
    var tok token.Token
    l.skipwhitespace()
    switch l.ch {…}
}
func (l *Lexer) skipWhitespace() {
    for l.ch == ‘ ‘ || l.ch == ‘\t’ || l.ch == ‘\n’ || l.ch == ‘\r’ {
        l.readChar()
    }
}

공백 처리 함수를 넣어주었다. 이를 적절히 수정하면 공백이 아닌 다른 문자 처리에서도 활용할 수 있다. 마참내 let five = 5;에서 숫자5를 읽을 수 있게 된 것

그럼 된 것인가?


아니다. 무언가 잊어버리지 않았는가?
우리는 식별자와 예약어를 구분할 수도 있고, 숫자 즉 글자가 아닌 문자와 글자인 문자도 구분할 수 있지만 렉서는 숫자를 토큰으로 바꾸는 법을 모른다. 그냥 추가하면 그만이다.

swtich l.ch {
    //…
    default:
        if isLetter(l.ch) {
        tok.Literal = l.readIdentifier()
        tok.Type = token.LookupIdent(tok.Literal)
        return tok
        } else if isDigit(l.ch) {
            tok.Type = token.INT
            tok.Literal = l.readNumber()
            return tok
        } else {
            tok = newToken(token.ILLEGAL, l.ch)
        }
}

func (l *Lexer) readNumber() string {
    position := l.position
    for isDigit(l.ch) {
        l.readChar()
    }
    return l.input[position:l.position]
}
func isDigit(ch byte) bool {
    return ‘0’ <= ch && ch <= ‘9’
}

간단하다. 전에 isalpha함수랑 비슷한 isLetter함수를 소개했듯 마찬가지로 isDigit을 추가하고 검사한다. 글자가 아닌 것 중 숫자인 것이 else if분기문에서 한 번 더 걸러지는것이다. 이제 숫자는 ILLEGAL이 아니다!!

2024-09-26 12:01:45

문제 설명

문제

선영이는 주말에 할 일이 없어서 새로운 언어 AC를 만들었다. AC는 정수 배열에 연산을 하기 위해 만든 언어이다. 이 언어에는 두 가지 함수 R(뒤집기)과 D(버리기)가 있다.

함수 R은 배열에 있는 수의 순서를 뒤집는 함수이고, D는 첫 번째 수를 버리는 함수이다. 배열이 비어있는데 D를 사용한 경우에는 에러가 발생한다.

함수는 조합해서 한 번에 사용할 수 있다. 예를 들어, "AB"는 A를 수행한 다음에 바로 이어서 B를 수행하는 함수이다. 예를 들어, "RDD"는 배열을 뒤집은 다음 처음 두 수를 버리는 함수이다.

배열의 초기값과 수행할 함수가 주어졌을 때, 최종 결과를 구하는 프로그램을 작성하시오.

입력

첫째 줄에 테스트 케이스의 개수 T가 주어진다. T는 최대 100이다.

각 테스트 케이스의 첫째 줄에는 수행할 함수 p가 주어진다. p의 길이는 1보다 크거나 같고, 100,000보다 작거나 같다.

다음 줄에는 배열에 들어있는 수의 개수 n이 주어진다. (0 ≤ n ≤ 100,000)

다음 줄에는 [x1,...,xn]과 같은 형태로 배열에 들어있는 정수가 주어진다. (1 ≤ xi ≤ 100)

전체 테스트 케이스에 주어지는 p의 길이의 합과 n의 합은 70만을 넘지 않는다.

출력

각 테스트 케이스에 대해서, 입력으로 주어진 정수 배열에 함수를 수행한 결과를 출력한다. 만약, 에러가 발생한 경우에는 error를 출력한다.


풀이

문제 분석

  • 각각의 테스트케이스별로 입력의 한 세트가 명령어, 배열 요소 수, 배열(대괄호와 쉼표로 구성) 이 들어온다.
  • 출력값은 크게 봐서 정상적 명령어 수행 후의 배열 or error 출력임.
  • error처리는 배열이 비었을 때 D가 들어오기만 하면 되므로 간단하다.
  • 입력값을 처리하기 좋은 형태로 바꿔주고, 정상적 명령어 수행 후의 배열 출력만 좀 신경쓰면 될듯하다.

입력값 처리

보면 알겠지만 입력을 main함수, 출력을 void AC에 뒀다.

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  int t;
  cin >> t;
  while (t--) {
    string ac_func;
    int length;
    string arr;

    cin >> ac_func >> length >> arr;

    deque<int> dq;
    if (length > 0) {
      // 배열의 값을 정수로 변환
      arr = arr.substr(1, arr.size() - 2); // '[', ']' 제거
      stringstream ss(arr);
      string token;
      while (getline(ss, token, ',')) {
        dq.push_back(stoi(token));
      }
    }

    AC(ac_func, length, dq);
  }

  return 0;
}
  • string ac_func: 명령어
  • int length: 배열 길이
  • string arr: 배열 처음 받을 때. 괄호와 쉼표 제거 후 덱에 집어넣는다.

출력값 처리

void AC(const string &func, int n, deque<int> &dq) {
  bool reverse = false, error = false;

  for (char command : func) {
    if (command == 'R') {
      reverse = !reverse;
    } else { // command == 'D'
      if (dq.empty()) {
        cout << "error" << '\n';
        error = true;
        break;
      }
      if (reverse)
        dq.pop_back();
      else
        dq.pop_front();
    }
  }

  if (!error) {
    cout << '[';
    if (reverse) {
      for (auto it = dq.rbegin(); it != dq.rend(); ++it) {
        if (it != dq.rbegin())
          cout << ',';
        cout << *it;
      }
    } else {
      for (auto it = dq.begin(); it != dq.end(); ++it) {
        if (it != dq.begin())
          cout << ',';
        cout << *it;
      }
    }
    cout << ']' << '\n';
  }
}
  • R 처리: 실제로 뒤집는 대신 reverse라는 플래그 처리로 대신한다.
    • R을 받을 때마다 reverse값이 바뀜. 두 번 받으면 원래대로 돌아온다.
    • 플래그 값에 따라 D명령어에서 끝 부분 중 어디를 제거할지, 출력을 어디부터 할지가 달라진다.
    • 만약 뒤집어져 있다면 끝 부분의 요소를 제거하고 아니라면 앞 부분을 제거하면 되는것.
    • 최종 출력에서 뒤집힘 여부를 확인하여 요소를 출력한다.
  • 최종 출력
    • error인 경우 특별한 포맷 없이 에러만 출력하면 된다.
    • 따라서 이것도 플래그로 처리하고 error가 아닌 경우에 괄호와 쉼표로 구분된 배열을 출력하게 함.
    • 뒤집힌 배열은 뒤에서부터 읽고 그렇지 않으면 처음부터 읽으면 된다.

전체 코드

#include <deque>
#include <iostream>
#include <sstream>
#include <string>
using namespace std;

void AC(const string &func, int n, deque<int> &dq) {
  bool reverse = false, error = false;

  for (char command : func) {
    if (command == 'R') {
      reverse = !reverse;
    } else { // command == 'D'
      if (dq.empty()) {
        cout << "error" << '\n';
        error = true;
        break;
      }
      if (reverse)
        dq.pop_back();
      else
        dq.pop_front();
    }
  }

  if (!error) {
    cout << '[';
    if (reverse) {
      for (auto it = dq.rbegin(); it != dq.rend(); ++it) {
        if (it != dq.rbegin())
          cout << ',';
        cout << *it;
      }
    } else {
      for (auto it = dq.begin(); it != dq.end(); ++it) {
        if (it != dq.begin())
          cout << ',';
        cout << *it;
      }
    }
    cout << ']' << '\n';
  }
}

int main(void) {
  ios::sync_with_stdio(0);
  cin.tie(0);

  int t;
  cin >> t;
  while (t--) {
    string ac_func;
    int length;
    string arr;

    cin >> ac_func >> length >> arr;

    deque<int> dq;
    if (length > 0) {
      // 배열의 값을 정수로 변환
      arr = arr.substr(1, arr.size() - 2); // '[', ']' 제거
      stringstream ss(arr);
      string token;
      while (getline(ss, token, ',')) {
        dq.push_back(stoi(token));
      }
    }

    AC(ac_func, length, dq);
  }

  return 0;
}

'알고리즘 > 백준' 카테고리의 다른 글

백준 1011 c++  (0) 2024.09.14
백준 13172 c++  (0) 2024.09.03
백준 11005 [C언어]  (0) 2024.07.17
2024-09-19 00:31:36

보호글입니다.
비밀번호를 입력하시면
내용을 보실 수 있습니다.


2024-09-17 00:31:45

보호글입니다.
비밀번호를 입력하시면
내용을 보실 수 있습니다.


2024-09-16 22:53:52

보호글입니다.
비밀번호를 입력하시면
내용을 보실 수 있습니다.


2024-09-16 19:13:12

보호글입니다.
비밀번호를 입력하시면
내용을 보실 수 있습니다.