← 전체 글
SECS/GEM/약 12분 읽기/— 조회

item header는 16 MB를 쓸 수 있는데 socket은 65,537 byte에서 끊겼다

PPBODY item header의 length byte 3개는 16 MB를 쓴다. 그런데 socket은 frame 65,537 byte에서 끊겼다. 경계를 byte로 봤다.

SECS/GEMMESSCADA문제 해결

70 KB짜리 recipe를 S7F3에 담아 보냈다. send()는 70,031 byte를 다 썼다고 돌려준다. 그리고 답이 없다. T3가 터지기도 전에 다음 send()가 EPIPE로 깨지고, 설비 쪽 packet log를 열어 보면 그 message가 아예 없다. Reject.req도 없고 S9F7도 없다. 기록 자체가 없다.

item header 탓이 아니다. SECS-II(E5) item header는 length byte를 1개에서 3개까지 선언할 수 있고, 3개면 16,777,215 byte를 길이로 적을 수 있다. 16 MB다. 문제는 그 item을 태우고 가는 HSMS frame이 먼저 죽는다는 것. 이 listener에서 경계는 message length field 65,536이고, 65,537에서 socket이 RST로 끊겼다. 선언할 수 있는 상한과 도착할 수 있는 상한은 다른 숫자다.

PPBODY item header의 length byte가 1개, 2개, 3개일 때 적을 수 있는 최대 길이는 255, 65,535, 16,777,215 byte이고 HSMS frame 상한 65,536 byte가 세 번째를 잘라낸다 S7F3 PPBODY — E5 item header format code 8 = binary format byte length byte 2190 220190 23000190 length byte 1개 length byte 2개 length byte 3개 255 65,535 16,777,215 HSMS frame 상한 65,536 byte

length byte 수는 선언하는 값이다

E5 item header의 첫 byte는 두 조각으로 나뉜다. 위쪽 6 bit가 format code, 아래쪽 2 bit가 length byte가 몇 개 따라오는지다. binary item의 format code는 8이라서, 첫 byte는 length byte 1개면 21, 2개면 22, 3개면 23이 된다. 그 다음에 선언한 개수만큼 big-endian 길이가 오고, 그 길이가 item 본문 byte 수다.

여기서 host 개발자가 자주 넘어가는 지점: 개수는 길이에서 계산되는 게 아니라 선언된다. 400 byte를 2 length byte로 적어도 되고 3 length byte로 적어도 된다. 둘 다 적법하고, 둘 다 통한다. 아래 capture의 2번과 3번이 그걸 한 socket에서 확인한 것이다.

capture — 같은 payload, length byte 개수만 다르게

세 개의 real socket으로 listener를 밖에서 두드렸다. hex는 host 쪽에서 직접 frame을 만들고 받은 그대로고, 설비 쪽 packet log와 교차 확인했다.

1. Select.req → Select.rsp, 그리고 S1F13 → S1F14

H→E  00 00 00 0A 00 0B 00 00 00 01 00 00 01 01
E→H  00 00 00 0A 00 0B 00 00 00 02 00 00 01 01
H→E  00 00 00 0C 00 0B 81 0D 00 00 00 00 01 02 01 00
E→H  00 00 00 37 00 0B 01 0E 00 00 00 00 01 02 01 02
     21 01 00 01 02 41 15 ...

여기는 준비 작업이다. S1F13을 먼저 안 보내면 이 listener는 data message에 SxF0을 돌려주고 끝난다. 이 부분은 E30 통신 gate 쪽 얘기고, 지금 보려는 건 그 다음이다.

2. PPBODY 400 byte, length byte 2개 → S7F4

H→E  00 00 01 AA 00 0B 87 03 00 00 00 00 01 03
     01 02 41 09 45 54 43 48 5F 42 41 53 45 22 01 90 ...  (wire 430 byte)
E→H  00 00 00 0D 00 0B 07 04 00 00 00 00 01 03 21 01 00

length field 00 00 01 AA = 426 = header 10 + body 416이다. body는 L[2] 2 byte, PPID A "ETCH_BASE" 11 byte, PPBODY item header 22 01 90 3 byte, payload 400 byte. 22가 length byte 2개를 선언하고 01 90이 400이다. 답은 21 01 00, ACKC7 0.

3. 같은 400 byte, length byte 3개 → 역시 S7F4

H→E  00 00 01 AB 00 0B 87 03 00 00 00 00 01 05
     01 02 41 09 45 54 43 48 5F 42 41 53 45 23 00 01 90 ...  (wire 431 byte)
E→H  00 00 00 0D 00 0B 07 04 00 00 00 00 01 05 21 01 00

payload는 2번과 같은 400 byte인데 wire는 1 byte 길어졌다. item header가 22 01 90에서 23 00 01 90으로 늘었기 때문이고, 그게 전부다. 답은 2번과 같은 ACKC7 0. decoder는 길이를 보고 추측하지 않는다 — 선언을 읽는다.

4. 경계: length field 65,536은 통과한다

H→E  00 01 00 00 00 0B 87 03 00 00 00 00 01 04
     01 02 41 09 45 54 43 48 5F 45 44 47 45 22 FF E6 ...  (wire 65,540 byte)
E→H  00 00 00 0D 00 0B 07 04 00 00 00 00 01 04 21 01 00

PPBODY 65,510 byte, length field 00 01 00 00 = 65,536 = 정확히 64 KiB다. 통과하고 ACKC7 0이 돌아온다. 참고로 이 body는 65,535 이하라서 아직 length byte 2개(22 FF E6)로 충분하다.

5. 한 byte 더: 65,537에서 socket이 끊긴다

H→E  00 01 00 01 00 0B 87 03 00 00 00 00 02 03
     01 02 41 09 45 54 43 48 5F 45 44 47 45 22 FF E7 ...  (wire 65,541 byte)
E→H  (없음 — ECONNRESET)

PPBODY를 1 byte 늘려 65,511로 만들었다. length field는 65,537. send()는 65,541 byte를 다 썼다고 하고, recv()는 ECONNRESET를 던진다. 같은 socket에 Linktest.req를 하나 더 보내려 하면 EPIPE다. 이 message는 설비 쪽 packet log에 RX로 남지 않았다 — 같은 연결의 Select.req와 S1F13은 남아 있고, S7F3만 없다. listener는 4 byte 길이 prefix를 읽은 시점에 socket을 버린다. body가 도착하기도 전이고, 그래서 기록할 message가 없다.

6. 70,000 byte PPBODY, length byte 3개 → 역시 RST

H→E  00 01 11 8B 00 0B 87 03 00 00 00 00 03 03
     01 02 41 09 45 54 43 48 5F 48 55 47 45 23 01 11 70 ...  (wire 70,031 byte)
E→H  (없음 — ECONNRESET)

여기서 처음으로 length byte 3개가 필요해서 쓰였다. 23 01 11 70 = 70,000이고, 16 MB까지 쓸 수 있는 header다. 그런데 length field가 70,027이라 5번과 같은 자리에서 죽는다. 이 listener에서 3 length byte item은 선언만 가능하고 도착은 불가능하다 — 65,535를 넘는 item은 그 자체로 frame을 65,536 위로 밀어 올리기 때문이다.

65,536은 이 listener의 숫자다

분명히 해 두자. 64 KiB는 구현이 고른 값이고 SEMI 숫자가 아니다. E37은 message 앞에 4 byte length field를 두고, 그 field의 폭 자체는 훨씬 큰 message를 표현한다. 실제 상한은 설비 벤더가 코드에 박아 둔 값이고, 나는 이 listener의 값(65,536 통과 / 65,537 RST)만 byte로 확인했다. 다른 설비가 어디서 끊는지는 확인하지 못했다. SECS-I(E4)로 올라온 tool은 또 다른 천장을 가지는데 그건 이번 run에서 건드리지 않았다.

그리고 S7F4의 ACKC7 0도 그대로 믿으면 안 된다. 이 listener는 data message에 고정된 ack를 돌려주는 reply table을 쓴다(코드에서 읽었다). 그러니까 위 2·3·4번의 21 01 00이 증명하는 건 frame이 받아들여져 stream 7 function 3으로 올바르게 decode됐다는 것까지다. 진짜 설비가 그 recipe를 저장하겠다는 뜻이 아니다. ACKC7 값의 의미는 E5 표와 벤더 문서에서 가져와야 하고, 둘이 다르면 tool이 맞다.

host 쪽에서 할 일

  • interface spec에서 최대 message size를 숫자로 받아 둔다. "HSMS 지원"은 답이 아니다. 끊는 자리가 적혀 있지 않으면 통합 시험 첫날에 recipe로 그걸 알아내게 된다.
  • PPBODY 길이를 보내기 전에 검사한다. 상한을 넘으면 socket을 죽이는 게 아니라 host 안에서 거절해야 한다. socket이 죽으면 그 연결에 걸려 있던 다른 transaction도 같이 죽는다.
  • 큰 recipe는 S7F3로 나르지 않는다. E5에는 host가 긴 PPBODY를 쪼개 보내는 표준 수단이 없다. tool이 그 frame을 받거나, recipe가 다른 경로로 가거나 둘 중 하나고, 이건 host stack이 고칠 문제가 아니라 interface spec에서 정할 문제다.
  • 진단 로그에 length field를 남긴다. "no reply"와 "connection reset"은 다른 증상이다. 뒤쪽이면 T3를 기다릴 이유가 없고, 설비 로그에는 아무것도 없을 테니 network 탓이라는 소리를 듣게 된다. 내가 보낸 length field 숫자가 그 주장을 끝내는 유일한 증거다.

큰 PPBODY 전에 S7F1로 승인을 먼저 받는 쪽이 왜 싼지는 recipe는 S7F3부터 보내는 게 아니다에 따로 적었다. 다만 S7F1도 frame 상한은 구해주지 못한다. 그건 S7F3가 socket에 올라간 뒤의 일이다.

같은 경계를 직접 때려 보고 싶으면 SECS/GEM simulator에 passive listener를 띄우고 length field만 1씩 올려 보면 된다. 65,536에서 ack가 오고 65,537에서 socket이 사라지는 걸 보는 데 1분이면 된다.