回答編集履歴

3

追加調査事項を追記

2023/12/19 23:37

投稿

退会済みユーザー
test CHANGED
@@ -168,3 +168,122 @@
168
168
  あとは`fputwc()`から`_write()`が呼ばれることを実証できれば因果関係が確認できます。そのために`test_fputwc.c`を生成、ビルドして、`gdb`というデバッガで実行しています。このデバッガはコマンドで動くタイプなので、コマンドをhere documentで直に記述しています。
169
169
 
170
170
  やっていることは、一度実行してDLLを読み込ませ、`_write()`にブレークポイントをしかけて実行し、止まったらコールスタックを表示し、引数を見るためにレジスタを表示し(第1引数がrcx, 第2引数がrdx, 第3引数がr8)、第2引数はポインタなのでメモリのダンプをしているだけです。あとはブレークポイントを解除して残りの処理を実行させて終わりです。
171
+
172
+ ---
173
+
174
+ #### 追記
175
+ 若干調査してわかったことがあるので追記しておきます。
176
+ ```bash
177
+ locales=("C" ".932" ".UTF-8" ".65001" ".20932")
178
+ first=("\\x96" "\\x96" "\\xe6" "\\xe6" "\\xcb")
179
+ second=("\\x7b" "\\x7b" "\\x9c" "\\x9c" "\\xdc")
180
+ third=("" "" "\\xac" "\\xac" "")
181
+ for i in `seq 0 4`;do
182
+ loc=${locales[$i]}
183
+ fbase="test_write_locale_${loc}"
184
+ cat >${fbase}.c <<EOF
185
+ #include <stdio.h>
186
+ #include <locale.h>
187
+ #include <io.h>
188
+ #include <errno.h>
189
+ void test(char* s, size_t len) {
190
+ int r = _write(_fileno(stdout), s, len);
191
+ if (r != -1) {
192
+ printf("r=%d\\n", r);
193
+ } else {
194
+ perror("test_write:");
195
+ printf("r=%d errno=%d\\n", r, errno);
196
+ }
197
+ }
198
+ int main() {
199
+ if (setlocale(LC_CTYPE, "${loc}") == NULL) {
200
+ fprintf(stderr, "setlocale fail!\\n");
201
+ return 1;
202
+ }
203
+ printf("[_write(1, \\"\\${first[$i]}\\", 1)]\\n");
204
+ test("${first[$i]}", 1); // 本の1バイト目だけ
205
+ EOF
206
+ if [ "${second[$i]}" != "" ]; then
207
+ cat >>${fbase}.c <<EOF
208
+ printf("[_write(1, \\"\\${first[$i]}\\${second[$i]}\\", 2)]\\n");
209
+ test("${first[$i]}${second[$i]}", 2); // 本の2バイト目まで
210
+ EOF
211
+ fi
212
+ if [ "${third[$i]}" != "" ]; then
213
+ cat >>${fbase}.c <<EOF
214
+ printf("[_write(1, \\"\\${first[$i]}\\${second[$i]}\\${third[$i]}\\", 3)]\\n");
215
+ test("${first[$i]}${second[$i]}${third[$i]}", 3); // 本の3バイト目まで
216
+ EOF
217
+ fi
218
+ cat >>${fbase}.c <<EOF
219
+ return 0;
220
+ }
221
+ EOF
222
+ gcc -g -Wall ${fbase}.c -o ${fbase}
223
+ echo "=== ${fbase} ==="
224
+ ./${fbase}
225
+ done
226
+ ```
227
+ スクリプトを修正して他のロケールでどうなるか?UCRT64でどうなるか?を見てみました。
228
+
229
+ **MINGW64での結果**
230
+ ```bash
231
+ === test_write_locale_C ===
232
+ [_write(1, "\x96", 1)]
233
+ 睦=1
234
+ [_write(1, "\x96\x7b", 2)]
235
+ 本r=2
236
+ === test_write_locale_.932 ===
237
+ [_write(1, "\x96", 1)]
238
+ test_write:: No space left on device
239
+ r=-1 errno=28
240
+ [_write(1, "\x96\x7b", 2)]
241
+ 本r=2
242
+ === test_write_locale_.UTF-8 ===
243
+ setlocale fail!
244
+ === test_write_locale_.65001 ===
245
+ setlocale fail!
246
+ === test_write_locale_.20932 ===
247
+ [_write(1, "\xcb", 1)]
248
+ test_write:: No space left on device
249
+ r=-1 errno=28
250
+ [_write(1, "\xcb\xdc", 2)]
251
+ 本r=2
252
+ ```
253
+
254
+ **UCRT64の結果**
255
+ ```bash
256
+ === test_write_locale_C ===
257
+ [_write(1, "\x96", 1)]
258
+ 睦=1
259
+ [_write(1, "\x96\x7b", 2)]
260
+ 本r=2
261
+ === test_write_locale_.932 ===
262
+ [_write(1, "\x96", 1)]
263
+ 睦=1
264
+ [_write(1, "\x96\x7b", 2)]
265
+ 本r=2
266
+ === test_write_locale_.UTF-8 ===
267
+ [_write(1, "\xe6", 1)]
268
+ [_write(1, "\xe6\x9c", 2)]
269
+ [_write(1, "\xe6\x9c\xac", 3)]
270
+ 本r=3
271
+ === test_write_locale_.65001 ===
272
+ [_write(1, "\xe6", 1)]
273
+ [_write(1, "\xe6\x9c", 2)]
274
+ [_write(1, "\xe6\x9c\xac", 3)]
275
+ 本r=3
276
+ === test_write_locale_.20932 ===
277
+ [_write(1, "\xcb", 1)]
278
+ ?=1
279
+ [_write(1, "\xcb\xdc", 2)]
280
+ 本r=2
281
+ ```
282
+
283
+ **わかったこと**
284
+
285
+ - "C"ロケールでは問題が発生しないこと
286
+ - UCRT64では問題が発生しないこと
287
+ - MINGW64(MSVCRT)ではコードページ20932(EUC-JP)でも同様の問題が発生していること
288
+ - EUC-JPで正しく表示されていることからbreakpointで仕掛けてみたところ、**`_write()`呼出中**に`MultiByteToWideChar()`と`WideCharToMultiByte()`が呼ばれていた。つまり**MSVCRTでも端末のコードページに合わせてエンコーディングの変換をかけている**。
289
+

2

これ以上追えないので、回答としての体裁を整えて終わらせた。

2023/12/17 06:52

投稿

退会済みユーザー
test CHANGED
@@ -1,10 +1,23 @@
1
- ### 回答ではありません
1
+ ### 回答
2
2
 
3
- 調査段階のメモ
3
+ 現状は個人的に**不可能**と言わざるを得ません
4
4
 
5
- 調査の結果、stdoutにfputwcした際、_writeにCP932で2バイト分も文字を1バイト分しか書き込もうとしなかったときに現象が発生してることがわかりました。多少は追い詰められそうです。_writeの挙動としては仕様に明記されていない動きではありますが、動きを見てるとそういうチェックをしているように見えます。
5
+ #### 原因
6
6
 
7
+ `setlocale()`で設定されたロケールに従いって端末出力する場合、`_write()`が2バイト文字の1バイト目のみを出力しようとすると、`errno`=`ENOSPC`なエラーとなるから。`fputwc()`は2バイト文字の1バイト目を出力するために`_write()`を使用する。
8
+
9
+ https://learn.microsoft.com/ja-jp/cpp/c-runtime-library/reference/write?view=msvc-140
10
+
11
+ を見る限り、`_write()`にそのような仕様はない。
12
+
13
+ #### 対策
14
+
15
+ VS2015がなく、この挙動がMINGW64によるビルドの影響なのか、MSVCRT.dllの実装のせいなのか、切り分けられないので、個人的には対処のしようがない。
16
+ →(個人的には)**不可能**
17
+
18
+ ### 調査詳細
19
+
7
- #### 再現スクリプト
20
+ #### 調査用スクリプト
8
21
 
9
22
  ```bash
10
23
  cat >test_write.c <<EOF
@@ -140,3 +153,18 @@
140
153
  (gdb)
141
154
  ```
142
155
 
156
+ #### 簡単な解説
157
+
158
+ まずは`_write()`の挙動を確認するためのCファイル`test_write.c`をビルドして実行しています。実行結果の
159
+
160
+ > ```bash
161
+ > [_write(1, "\x96", 1)]
162
+ > test_write:: No space left on device
163
+ > r=-1 errno=28
164
+ > ```
165
+
166
+ という部分から、`_write()`でCP932の2バイト文字を1バイト分しか書き込もうとしなかったときに、`fputwc()`と同じ現象が発生してることが確認できます。
167
+
168
+ あとは`fputwc()`から`_write()`が呼ばれることを実証できれば因果関係が確認できます。そのために`test_fputwc.c`を生成、ビルドして、`gdb`というデバッガで実行しています。このデバッガはコマンドで動くタイプなので、コマンドをhere documentで直に記述しています。
169
+
170
+ やっていることは、一度実行してDLLを読み込ませ、`_write()`にブレークポイントをしかけて実行し、止まったらコールスタックを表示し、引数を見るためにレジスタを表示し(第1引数がrcx, 第2引数がrdx, 第3引数がr8)、第2引数はポインタなのでメモリのダンプをしているだけです。あとはブレークポイントを解除して残りの処理を実行させて終わりです。

1

実行ログも添えた

2023/12/17 02:44

投稿

退会済みユーザー
test CHANGED
@@ -64,3 +64,79 @@
64
64
  q
65
65
  EOF
66
66
  ```
67
+ バージョンアップなどで将来挙動が変わる可能性もあるので、ログも控えておきます。
68
+ ```bash
69
+ $ bash test_write.sh
70
+ [_write(1, "\x96", 1)]
71
+ test_write:: No space left on device
72
+ r=-1 errno=28
73
+ [_write(1, "\x96\x7b", 2)]
74
+ 本r=2
75
+ GNU gdb (GDB) 13.1
76
+ Copyright (C) 2023 Free Software Foundation, Inc.
77
+ License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
78
+ This is free software: you are free to change and redistribute it.
79
+ There is NO WARRANTY, to the extent permitted by law.
80
+ Type "show copying" and "show warranty" for details.
81
+ This GDB was configured as "x86_64-w64-mingw32".
82
+ Type "show configuration" for configuration details.
83
+ For bug reporting instructions, please see:
84
+ <https://www.gnu.org/software/gdb/bugs/>.
85
+ Find the GDB manual and other documentation resources online at:
86
+ <http://www.gnu.org/software/gdb/documentation/>.
87
+
88
+ For help, type "help".
89
+ Type "apropos word" to search for commands related to "word"...
90
+ Reading symbols from ./test_fputwc...
91
+ (gdb) Starting program: C:\msys64\home\user\mingw\test_fputwc.exe
92
+ [New Thread 16188.0x4b88]
93
+ test_fputwc: No space left on device
94
+ failed! errno=28 r=65535
95
+ [Thread 16188.0x4b88 exited with code 0]
96
+ [Inferior 1 (process 16188) exited normally]
97
+ (gdb) Breakpoint 1 at 0x7ffe8fbcfaeb
98
+ (gdb) Starting program: C:\msys64\home\user\mingw\test_fputwc.exe
99
+ [New Thread 17372.0x62bc]
100
+
101
+ Thread 1 hit Breakpoint 1, 0x00007ffe8fbcfaeb in msvcrt!_write ()
102
+ from C:\WINDOWS\System32\msvcrt.dll
103
+ (gdb) #0 0x00007ffe8fbcfaeb in msvcrt!_write () from C:\WINDOWS\System32\msvcrt.dll
104
+ #1 0x00007ffe8fbf719e in msvcrt!_flsbuf () from C:\WINDOWS\System32\msvcrt.dll
105
+ #2 0x00007ffe8fbfd521 in msvcrt!fputs () from C:\WINDOWS\System32\msvcrt.dll
106
+ #3 0x00007ffe8fbfd5ed in msvcrt!fputwc () from C:\WINDOWS\System32\msvcrt.dll
107
+ #4 0x00007ff74ccd14c4 in test (fp=0x7ffe8fc3fa30 <msvcrt!_iob+48>) at test_fputwc.c:6
108
+ #5 0x00007ff74ccd155e in main (argc=1, argv=0xd4870) at test_fputwc.c:16
109
+ (gdb) rax 0x40 64
110
+ rbx 0x7ffe8fc3fa30 140731310406192
111
+ rcx 0x1 1
112
+ rdx 0x5ffd90 6290832
113
+ rsi 0x1 1
114
+ rdi 0x0 0
115
+ rbp 0x1 0x1
116
+ rsp 0x5ffd00 0x5ffd00
117
+ r8 0x1 1
118
+ r9 0x5ffdb6 6290870
119
+ r10 0x0 0
120
+ r11 0x5ffd40 6290752
121
+ r12 0xd48b0 870576
122
+ r13 0x0 0
123
+ r14 0x5ffdb4 6290868
124
+ r15 0x0 0
125
+ rip 0x7ffe8fbcfaeb 0x7ffe8fbcfaeb <msvcrt!_write+27>
126
+ eflags 0x206 [ PF IF ]
127
+ cs 0x33 51
128
+ ss 0x2b 43
129
+ ds 0x2b 43
130
+ es 0x2b 43
131
+ fs 0x53 83
132
+ gs 0x2b 43
133
+ (gdb) 0x5ffd90: 0x96 0xff 0xff 0xff 0x00 0x00 0x00 0x00
134
+ 0x5ffd98: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
135
+ (gdb) (gdb) Continuing.
136
+ test_fputwc: No space left on device
137
+ failed! errno=28 r=65535
138
+ [Thread 17372.0x62bc exited with code 0]
139
+ [Inferior 1 (process 17372) exited normally]
140
+ (gdb)
141
+ ```
142
+