openlang

상태 플래그 하나를 |=로 지웠더니 전부 지워졌다

쓰기 1로 지우는 상태 레지스터에 |=를 쓰면 이름 붙인 비트보다 더 많이 지워진다.

#include <stdint.h>

#define UART_SR   (*(volatile uint32_t *)0x40004000u)
#define SR_ORE    (1u << 3) /* overrun, write 1 to clear */

void clear_overrun_wrong(void) { UART_SR |= SR_ORE; }
void clear_overrun(void)       { UART_SR = SR_ORE; }

오버런 플래그는 1을 써야 지워진다. 0을 쓰면 아무 일도 없다. UART_SR |= SR_ORE는 먼저 레지스터 전체를 읽는다. 그 순간 걸려 있던 쓰기 1 지우기 플래그는 전부 1로 읽히고 그대로 1로 다시 쓰이니 전부 지워진다. 처리기가 아직 보지 않은 프레이밍 오류도 함께 사라진다.

Cortex-M에서 -O2로 보면 틀린 쪽은 ldr, orr, str이다. 맞는 쪽은 마스크를 한 번 str하는 것이 전부다. volatile은 두 접근을 프로그램에 남긴다. 다시 써 넣은 비트를 하드웨어가 어떻게 다루는지는 말해 주지 않는다.

이어지는 대화 5

서로의 근거를 따라
  1. 고유 링크

    C 쪽에서 규칙은 흔히 기억하는 것보다 좁습니다. E1 |= E2에 대해 표준은 E1 = E1 | E2와 다른 점이 E1을 한 번만 평가한다는 것뿐이라고 말합니다. E1이 volatile이라도 여전히 읽기 한 번과 쓰기 한 번, 두 번의 접근이며, 표준은 둘 다 일어나기를 요구합니다. 그것이 명령어 하나여야 한다고 요구하지 않고, 원자성에 대해서는 아무 말도 하지 않습니다. 따라서 여기서 읽고 수정하고 쓰는 동작은 언어가 요구하는 그대로이고, 어떤 컴파일러 설정도 그것을 레지스터에 필요한 단순 저장으로 바꾸지 않습니다.

  2. 고유 링크

    맞다. 명령어 하나여도 구해 주지 못한다. Cortex-M3의 비트 밴딩이 흔한 함정이다. 비트 밴드 쓰기는 코드에서 저장 하나로 보이지만, 버스는 여전히 워드 전체를 읽고 쓴다. 그러니 다른 플래그도 똑같이 지워진다. 고칠 곳은 소스다. 마스크를 =로 쓰고, 플래그 종류가 섞인 레지스터라면 먼저 레퍼런스 매뉴얼에서 비트마다 접근 유형을 확인한다.

  3. 고유 링크

    재밌네요. 머릿속에서 타이밍을 바꿔 봤어요. 수신 쪽이 ldr 뒤, str 전에 새 오류 플래그를 올린다고 쳐요. 읽기는 그걸 못 봤으니 그 비트는 0으로 다시 쓰이고, 0을 쓰면 아무 일도 없으니 새 플래그는 살아남아요. 그러면 |= 버전은 이미 걸려 있던 플래그만 정확히 잃고, 그 틈에 들어온 건 지키는 건가요? 경쟁 구간 쪽이 오히려 무해하다니, 제가 짐작했던 것과 거의 반대예요.

  4. 고유 링크

    쓰기 1로 지우는 비트에 대해서는 말씀대로입니다. 새 플래그는 두 버전 모두에서 살아남습니다. 다만 단순 저장에도 대가가 있고, 그것은 레지스터의 나머지에 달려 있습니다. UART_SR = SR_ORE는 다른 모든 비트에 0을 씁니다. 그 비트들이 모두 0 쓰기를 무시할 때만 무해합니다. 같은 레지스터에 활성화 비트나 모드 비트 같은 일반 읽기 쓰기 필드가 있다면 단순 저장이 그것을 지웁니다. 레퍼런스 매뉴얼은 비트마다 접근 유형을 적어 두며, 레지스터의 모든 비트를 그것과 대조한 뒤에야 그 저장이 올바르다고 할 수 있습니다.

  5. 고유 링크

    같은 읽기-수정-쓰기 문제가 다른 모양으로도 나타납니다. 레지스터 맵을 구조체로 쓰는 경우가 흔하기 때문입니다. volatile 구조체의 비트 필드 멤버에 저장하는 것은 그 멤버를 담은 저장 단위에 대한 접근이므로, 실제로 컴파일러는 그 단위를 읽고 비트를 바꾼 뒤 다시 씁니다. 그 단위 안의 쓰기 1로 지우는 플래그도 함께 지워집니다. 표준은 또한 한 단위 안에서 비트 필드가 배치되는 순서를 구현 정의로 둡니다. 이것이 레지스터 정의에서 비트 필드를 빼고 volatile uint32_t에 마스크를 쓰는 두 번째 이유입니다.

같은 질문의 다른 갈래

  1. C 포인터에 .?를 쓰는 것은 단언이다

    C에서 돌아온 포인터에 쓰는 .?는 검사가 아니라 단언입니다. 여기서 널은 불가능하다고 말하는 것입니다. 안전 검사 빌드에서 널이면 패닉이 나고, ReleaseFast나 ReleaseSmall에서는 정의되지 않은 동작입니다.…

    답글 5zig
  2. C 콜백에서는 설정해 둔 아레나가 사라진다

    C 라이브러리가 내 Odin 코드로 콜백을 부르는데, 콜백은 등록 전에 설정해 둔 context를 보지 못한다. proc "c"에는 암묵적 context가 없어서 직접 만들어야 하고, runtime.default_contex…

    답글 8odinc
  3. -O2 리스트에서 카나리 검사 읽기

    작은 C 함수 세 개를 x86-64 Linux에서 gcc -O2 -fstack-protector-strong -S -masm=intel로 빌드했다. read_line과 read_count는 같은 프롤로그를 받는다. mov r…